Context
#201 changed PersistenceBackend::query from a prefix-glob contract to the
MQTT-style grammar subscriptions already use: * covers exactly one
dot-separated segment, # covers zero or more. The implementation and the
CHANGELOGs landed with the PR; the documentation and tests that teach the old
grammar did not.
Raised in review as
thread 23,
where it was agreed as non-blocking for that PR but required before release —
the affected files are published crate documentation for a grammar that now
fails silently. This issue is what that thread is waiting on.
Line references are as of b790143.
The problem is the record keys, not just the patterns
The obvious read is "replace ::* with .#". That is not sufficient, and doing
only that would leave the examples incoherent.
Every persistence example keys its records with :: as the separator —
accuracy::vienna, my_record::key, sensor::a. A wildcard must be its own
whole dot-separated segment, so accuracy::vienna is a single segment and no
pattern can partition that key space at all. accuracy::* is likewise one
segment containing a literal *, so it is a literal key that matches nothing:
// aimdb-persistence/src/pattern.rs:66
fn a_wildcard_must_be_its_own_segment() {
assert_eq!(literal_prefix("sensors*"), "sensors*");
aimdb-persistence-sqlite/CHANGELOG.md:20 already records the consequence
("Patterns over keys that are not dot-separated ("temp::*") no longer
wildcard") — but every example still uses exactly such keys.
So the sweep is: migrate the example keys to dot-separated, then fix the
patterns over them. A reader who copies today's README gets a query that
returns zero rows and no error.
The sections below are commit-sized. §1–§3 are pure documentation; §4 is the
test change and is the only one that can fail CI. §5 is a separate documentation
defect in aimdb-core/aimdb-client with the same cause — a #201 contract
change the prose did not follow — folded in rather than tracked on its own.
1. aimdb-persistence/README.md
Keys:
:17 — .persist("my_record::key") in the API table.
:67-71 — configure::<Accuracy>("accuracy::vienna") / ("accuracy::berlin")
and their matching reg.persist(...) calls.
:137-138 — configure::<MyRecord>("my_record::key") + reg.persist(...).
:140 — the commented-out format!("my_record::{}", city).
Patterns over them:
:77 — query_latest("accuracy::*", 10), captioned "Latest 10 per city".
:153, :162, :167, :182 — "my_record::*" across query_latest,
query_range, query_raw and the error-handling example.
:152 — the prose "pattern supports * wildcard", which now needs to name
both wildcards and say they are whole segments.
With keys migrated to accuracy.vienna / accuracy.berlin, the right pattern at
:77 is accuracy.*, not accuracy.# — both keys are exactly one segment
below accuracy, and * says so. Reserve # for the cases where depth varies;
query_raw at :167 is the one that feeds the AimX record.query handler, so
# is the honest choice there (see remote::QUERY_ALL_PATTERN). Worth one line
of prose contrasting the two, since choosing between them is the thing the old
grammar never made anyone think about.
2. aimdb-persistence-sqlite/README.md
Same treatment, smaller surface: :54-55 (configure + persist on
accuracy::vienna) and :60 (query_latest("accuracy::*", 5)).
Do not touch CHANGELOG.md:20 — its "temp::*" is a historical entry
describing the break, and is correct as written.
3. Rustdoc
aimdb-persistence/src/query_ext.rs:19-21 — query_latest's "Pattern
support: "accuracy::*" returns latest N from each matching record. Single
record: "accuracy::vienna"". Both halves are wrong now: the first matches
nothing, and the second is no longer contrastive since it is just a
wildcard-free pattern. query_range and query_raw below it say "pattern"
without defining it and can point at the canonical statement instead of
restating it.
aimdb-persistence/src/backend.rs:19 — StoredValue::record_name's example
value is "accuracy::vienna". This is the type the backend contract is
written against, so the stale key sits directly beside the corrected grammar.
backend.rs:52-61 (PersistenceBackend::query) and pattern.rs are already
correct and are the wording the rest should defer to. Nothing to change there.
4. aimdb-persistence/tests/query_skip_invalid.rs
Keys at :100, :105, :110, :115, :163, :168, :173, :199, :204,
:258, :279, :284, :289; patterns at :133, :182, :213, :237,
:266, :298; the doc table at :91-96.
These tests pass today and would keep passing after a careless key rename,
which is the actual gap the review named:
// :50 — Returns all pre-configured rows unconditionally (params ignored).
fn query<'a>(&'a self, _record_pattern: &'a str, _params: QueryParams)
MockBackend::query ignores the pattern entirely, so no test in the crate proves
a pattern ever reaches a backend, let alone that it is interpreted under the new
grammar. The six tests here exercise the filter_map in AimDbQueryExt and are
right to keep that focus — but the mock should honor record_pattern via the
re-exported topic_matches so the suite stops being blind to the grammar, and
one case should pin the behavior that motivated the change: a sensor.* query
must not return sensor.deep.nested.
aimdb-persistence-sqlite has real coverage of this against SQLite
(src/lib.rs:266 filters rows through topic_matches); what is missing is
coverage at the backend-contract level, which is where a third-party backend
author would look.
5. The snapshot_end contract is documented as stronger than it is
Different crate, same root cause: #201 changed a contract and some of the prose
did not follow. Folded in here rather than tracked separately, since it is the
same pre-release documentation pass and the same failure mode — a doc that sends
a reader down a path the code no longer supports.
aimdb-client/src/engine.rs:78-81 tells a consumer the marker always arrives:
The engine reserves a sink slot for this update, so it arrives even from a
burst that overran — a subscription over static records may never fire an
event, and a consumer still has to tell a complete initial state from a
truncated one.
It does not always arrive. The marker rides the final snapshot's frame
(aimdb-core/src/session/server.rs:242-245), so there is none when there is no
final snapshot:
- Empty burst. The pattern matched nothing, or matched only records that have
not produced a value yet — snapshots() filters through try_latest_as_json
(aimdb-core/src/session/aimx/dispatch.rs:147-149). total == 0, the loop
body never runs, nothing is sent (server.rs:230-234). Ordinary on a fresh
database, not an edge case.
- Tail failed to encode.
server.rs:251-267 logs and continues, and the
last flag goes with the skipped frame — the comment at :257-260 already
says so.
- Exact-topic subscribe. Never produces snapshots at all (
dispatch.rs:142),
correctly.
So a consumer following the current doc treats "no snapshot_end yet" as "the
burst is still arriving" and waits indefinitely. What the absence actually means
is one of four things — three benign, one silent snapshot loss.
This is a documentation fix, not a protocol change. Making the marker
unconditional needs a record-independent terminator frame; that was considered
and rejected as disproportionate. It would cost a new frame kind, SubUpdate::data
becoming Option<Payload>, and a reservation refactor — and still could not
promise delivery, because an unrecognized or malformed frame is skipped by design
(aimx/codec.rs:334 → client.rs:732), which is the same property that keeps
new frame kinds backward-compatible. State the real contract instead.
Sites:
aimdb-client/src/engine.rs:74-81 — the paragraph above. It should say the
marker is present when the burst produced at least one encodable snapshot, that
its absence carries no information, and that skipped remains the loss signal.
aimdb-core/src/session/mod.rs:83 — SubUpdate::snapshot_end's one-liner
("Set on the last update of the late-join snapshot burst") has the same gap in
shorter form.
aimdb-core/src/session/client.rs:390-393 — Delivery::BurstEnd's "guaranteed
delivery into the slot Delivery::BurstBody kept free" holds only when a
BurstBody actually ran to reserve it. On an empty burst nothing reserves
anything, and the delivery succeeds because the sink was created moments
earlier (client.rs:809) and the server sends snapshots before registering the
event pump. That is an ordering property, not the mechanism the doc names.
Internal, but it is what a future reader would build on.
6. Out of scope — but confirm before closing
:: keys also appear in examples/remote-access-demo/src/server.rs:136-181 and
aimdb-wasm-adapter/tests/transform_join_integration_tests.rs:42-48. Neither is
broken: the demo never persists and subscribes to one exact key
(client.rs:179), and exact-match lookup is unaffected by the grammar. They are
only unwildcardable, which nothing there tries to do.
Leaving them is defensible. Changing them is also defensible, as the ::
convention is what led the persistence docs into this in the first place. Make it
an explicit decision rather than an oversight, and if they stay, they should not
be cited anywhere as examples of a pattern-friendly key.
Acceptance criteria
grep -rn '::\*' aimdb-persistence/ aimdb-persistence-sqlite/ returns only
CHANGELOG.md entries.
- No persistence example, rustdoc or test registers a record key that a
wildcard cannot address — i.e. no :: in any key that an accompanying
pattern is meant to match.
MockBackend::query in query_skip_invalid.rs honors record_pattern
through topic_matches, and a regression asserts sensor.* excludes
sensor.deep.nested.
- Prose that mentions patterns names both
* (exactly one segment) and #
(zero or more) and states that a wildcard is a whole segment; the
crate-level statement stays in backend.rs with others referring to it
rather than restating it.
- No
snapshot_end doc claims unconditional delivery. Each states the
condition under which the marker appears and says explicitly that its
absence is not a signal.
make check green.
Context
#201 changed
PersistenceBackend::queryfrom a prefix-glob contract to theMQTT-style grammar subscriptions already use:
*covers exactly onedot-separated segment,
#covers zero or more. The implementation and theCHANGELOGs landed with the PR; the documentation and tests that teach the old
grammar did not.
Raised in review as
thread 23,
where it was agreed as non-blocking for that PR but required before release —
the affected files are published crate documentation for a grammar that now
fails silently. This issue is what that thread is waiting on.
Line references are as of
b790143.The problem is the record keys, not just the patterns
The obvious read is "replace
::*with.#". That is not sufficient, and doingonly that would leave the examples incoherent.
Every persistence example keys its records with
::as the separator —accuracy::vienna,my_record::key,sensor::a. A wildcard must be its ownwhole dot-separated segment, so
accuracy::viennais a single segment and nopattern can partition that key space at all.
accuracy::*is likewise onesegment containing a literal
*, so it is a literal key that matches nothing:aimdb-persistence-sqlite/CHANGELOG.md:20already records the consequence("Patterns over keys that are not dot-separated (
"temp::*") no longerwildcard") — but every example still uses exactly such keys.
So the sweep is: migrate the example keys to dot-separated, then fix the
patterns over them. A reader who copies today's README gets a query that
returns zero rows and no error.
The sections below are commit-sized. §1–§3 are pure documentation; §4 is the
test change and is the only one that can fail CI. §5 is a separate documentation
defect in
aimdb-core/aimdb-clientwith the same cause — a #201 contractchange the prose did not follow — folded in rather than tracked on its own.
1.
aimdb-persistence/README.mdKeys:
:17—.persist("my_record::key")in the API table.:67-71—configure::<Accuracy>("accuracy::vienna")/("accuracy::berlin")and their matching
reg.persist(...)calls.:137-138—configure::<MyRecord>("my_record::key")+reg.persist(...).:140— the commented-outformat!("my_record::{}", city).Patterns over them:
:77—query_latest("accuracy::*", 10), captioned "Latest 10 per city".:153,:162,:167,:182—"my_record::*"acrossquery_latest,query_range,query_rawand the error-handling example.:152— the prose "pattern supports*wildcard", which now needs to nameboth wildcards and say they are whole segments.
With keys migrated to
accuracy.vienna/accuracy.berlin, the right pattern at:77isaccuracy.*, notaccuracy.#— both keys are exactly one segmentbelow
accuracy, and*says so. Reserve#for the cases where depth varies;query_rawat:167is the one that feeds the AimXrecord.queryhandler, so#is the honest choice there (seeremote::QUERY_ALL_PATTERN). Worth one lineof prose contrasting the two, since choosing between them is the thing the old
grammar never made anyone think about.
2.
aimdb-persistence-sqlite/README.mdSame treatment, smaller surface:
:54-55(configure+persistonaccuracy::vienna) and:60(query_latest("accuracy::*", 5)).Do not touch
CHANGELOG.md:20— its"temp::*"is a historical entrydescribing the break, and is correct as written.
3. Rustdoc
aimdb-persistence/src/query_ext.rs:19-21—query_latest's "Patternsupport:
"accuracy::*"returns latest N from each matching record. Singlerecord:
"accuracy::vienna"". Both halves are wrong now: the first matchesnothing, and the second is no longer contrastive since it is just a
wildcard-free pattern.
query_rangeandquery_rawbelow it say "pattern"without defining it and can point at the canonical statement instead of
restating it.
aimdb-persistence/src/backend.rs:19—StoredValue::record_name's examplevalue is
"accuracy::vienna". This is the type the backend contract iswritten against, so the stale key sits directly beside the corrected grammar.
backend.rs:52-61(PersistenceBackend::query) andpattern.rsare alreadycorrect and are the wording the rest should defer to. Nothing to change there.
4.
aimdb-persistence/tests/query_skip_invalid.rsKeys at
:100,:105,:110,:115,:163,:168,:173,:199,:204,:258,:279,:284,:289; patterns at:133,:182,:213,:237,:266,:298; the doc table at:91-96.These tests pass today and would keep passing after a careless key rename,
which is the actual gap the review named:
MockBackend::queryignores the pattern entirely, so no test in the crate provesa pattern ever reaches a backend, let alone that it is interpreted under the new
grammar. The six tests here exercise the
filter_mapinAimDbQueryExtand areright to keep that focus — but the mock should honor
record_patternvia there-exported
topic_matchesso the suite stops being blind to the grammar, andone case should pin the behavior that motivated the change: a
sensor.*querymust not return
sensor.deep.nested.aimdb-persistence-sqlitehas real coverage of this against SQLite(
src/lib.rs:266filters rows throughtopic_matches); what is missing iscoverage at the backend-contract level, which is where a third-party backend
author would look.
5. The
snapshot_endcontract is documented as stronger than it isDifferent crate, same root cause: #201 changed a contract and some of the prose
did not follow. Folded in here rather than tracked separately, since it is the
same pre-release documentation pass and the same failure mode — a doc that sends
a reader down a path the code no longer supports.
aimdb-client/src/engine.rs:78-81tells a consumer the marker always arrives:It does not always arrive. The marker rides the final snapshot's frame
(
aimdb-core/src/session/server.rs:242-245), so there is none when there is nofinal snapshot:
not produced a value yet —
snapshots()filters throughtry_latest_as_json(
aimdb-core/src/session/aimx/dispatch.rs:147-149).total == 0, the loopbody never runs, nothing is sent (
server.rs:230-234). Ordinary on a freshdatabase, not an edge case.
server.rs:251-267logs andcontinues, and thelastflag goes with the skipped frame — the comment at:257-260alreadysays so.
dispatch.rs:142),correctly.
So a consumer following the current doc treats "no
snapshot_endyet" as "theburst is still arriving" and waits indefinitely. What the absence actually means
is one of four things — three benign, one silent snapshot loss.
This is a documentation fix, not a protocol change. Making the marker
unconditional needs a record-independent terminator frame; that was considered
and rejected as disproportionate. It would cost a new frame kind,
SubUpdate::databecoming
Option<Payload>, and a reservation refactor — and still could notpromise delivery, because an unrecognized or malformed frame is skipped by design
(
aimx/codec.rs:334→client.rs:732), which is the same property that keepsnew frame kinds backward-compatible. State the real contract instead.
Sites:
aimdb-client/src/engine.rs:74-81— the paragraph above. It should say themarker is present when the burst produced at least one encodable snapshot, that
its absence carries no information, and that
skippedremains the loss signal.aimdb-core/src/session/mod.rs:83—SubUpdate::snapshot_end's one-liner("Set on the last update of the late-join snapshot burst") has the same gap in
shorter form.
aimdb-core/src/session/client.rs:390-393—Delivery::BurstEnd's "guaranteeddelivery into the slot
Delivery::BurstBodykept free" holds only when aBurstBodyactually ran to reserve it. On an empty burst nothing reservesanything, and the delivery succeeds because the sink was created moments
earlier (
client.rs:809) and the server sends snapshots before registering theevent pump. That is an ordering property, not the mechanism the doc names.
Internal, but it is what a future reader would build on.
6. Out of scope — but confirm before closing
::keys also appear inexamples/remote-access-demo/src/server.rs:136-181andaimdb-wasm-adapter/tests/transform_join_integration_tests.rs:42-48. Neither isbroken: the demo never persists and subscribes to one exact key
(
client.rs:179), and exact-match lookup is unaffected by the grammar. They areonly unwildcardable, which nothing there tries to do.
Leaving them is defensible. Changing them is also defensible, as the
::convention is what led the persistence docs into this in the first place. Make it
an explicit decision rather than an oversight, and if they stay, they should not
be cited anywhere as examples of a pattern-friendly key.
Acceptance criteria
grep -rn '::\*' aimdb-persistence/ aimdb-persistence-sqlite/returns onlyCHANGELOG.mdentries.wildcard cannot address — i.e. no
::in any key that an accompanyingpattern is meant to match.
MockBackend::queryinquery_skip_invalid.rshonorsrecord_patternthrough
topic_matches, and a regression assertssensor.*excludessensor.deep.nested.*(exactly one segment) and#(zero or more) and states that a wildcard is a whole segment; the
crate-level statement stays in
backend.rswith others referring to itrather than restating it.
snapshot_enddoc claims unconditional delivery. Each states thecondition under which the marker appears and says explicitly that its
absence is not a signal.
make checkgreen.