An exemption cannot name the rows it covers, only the columns #151

Closed
opened 2026-08-16 15:13:38 +02:00 by Claude · 2 comments
Collaborator

Exemption matches a table and its columns. It cannot say which rows, so an
entry that covers two rows hides the column for all of them.

The case that raised it, from #110's library tile: the tile this server draws
agrees with upstream on path, type and 960×540, and its Blurhash cannot agree,
because upstream draws the library's name across the tile at 112 points in the
host's default typeface — so no two upstream installs agree either. Writing that
entry today means naming Blurhash across the whole of BaseItemImageInfos,
hiding the column for the ~120 fetched pictures whose hashes do agree in order
to cover two CollectionFolder rows.

No entry was opened. The reason went into crates/jellyfin-harness/README.md
instead, because no run has produced the difference: both snapshots already
carry upstream's tiles, and either server leaves a row that already has a picture
alone, so producing it takes a library neither side has drawn yet, scanned by
both. An entry for a difference no run has shown is the adds-only smell the risk
table names.

Where to start

A rows predicate on Exemption — enough to name a row by the column values
that identify it, the way mask names a field inside a blob. The two existing
scoping mechanisms (scope: "value" and mask) are the precedent for how narrow
an entry is expected to be.

How to know it is done

The library-tile entry can be written honestly when a run produces the
difference: Blurhash hidden on CollectionFolder rows and diffed on every
other row of the same table. Until then this is a harness feature with one
waiting customer, which is why it is filed rather than built.

House rules

Tests run under cargo nextest. crates/jellyfin-harness/README.md says what a
diff means and what an entry has to carry — every entry needs a reason and an
expiry, and an adds-only history is a smell.

`Exemption` matches a table and its columns. It cannot say **which rows**, so an entry that covers two rows hides the column for all of them. The case that raised it, from #110's library tile: the tile this server draws agrees with upstream on path, type and 960×540, and its `Blurhash` cannot agree, because upstream draws the library's name across the tile at 112 points in the host's default typeface — so no two upstream installs agree either. Writing that entry today means naming `Blurhash` across the whole of `BaseItemImageInfos`, hiding the column for the ~120 fetched pictures whose hashes *do* agree in order to cover two `CollectionFolder` rows. No entry was opened. The reason went into `crates/jellyfin-harness/README.md` instead, because **no run has produced the difference**: both snapshots already carry upstream's tiles, and either server leaves a row that already has a picture alone, so producing it takes a library neither side has drawn yet, scanned by both. An entry for a difference no run has shown is the adds-only smell the risk table names. ## Where to start A `rows` predicate on `Exemption` — enough to name a row by the column values that identify it, the way `mask` names a field inside a blob. The two existing scoping mechanisms (`scope: "value"` and `mask`) are the precedent for how narrow an entry is expected to be. ## How to know it is done The library-tile entry can be written honestly when a run produces the difference: `Blurhash` hidden on `CollectionFolder` rows and diffed on every other row of the same table. Until then this is a harness feature with one waiting customer, which is why it is filed rather than built. ### House rules Tests run under `cargo nextest`. `crates/jellyfin-harness/README.md` says what a diff means and what an entry has to carry — every entry needs a reason and an expiry, and an adds-only history is a smell.
Author
Collaborator

Batch D produced no evidence for this, so it stays filed.

The batch's plan was that #150 might answer it — if ours sorted Data keys
everywhere, the difference would have been row-scoped and would have needed an
entry that could name rows. It does not: two writers took their top-level
object from serde_json, and both are fixed rather than exempted (0afe805).
The AncestorIds half is a row one side files and the other does not, which an
exemption could not cover anyway — it hides columns, not rows.

So the customer is still the library tile alone, and producing its difference
still takes a library neither side has drawn yet, scanned by both. An entry for
a difference no run has shown is the adds-only smell.

One thing worth writing down for whoever builds it: the motivating rows cannot
be named by a type. BaseItemImageInfos carries ItemId, Path, Blurhash,
ImageType and the dimensions — nothing that says CollectionFolder. Naming
the two library tiles inside that table means naming them by Path, so a
rows predicate that only compares column values for equality will not reach
them; it needs at least a prefix or glob. That is a constraint on the design,
not a reason to change the priority.

Batch D produced no evidence for this, so it stays filed. The batch's plan was that #150 might answer it — if ours sorted `Data` keys everywhere, the difference would have been row-scoped and would have needed an entry that could name rows. It does not: two writers took their top-level object from `serde_json`, and both are fixed rather than exempted (`0afe805`). The `AncestorIds` half is a row one side files and the other does not, which an exemption could not cover anyway — it hides columns, not rows. So the customer is still the library tile alone, and producing its difference still takes a library neither side has drawn yet, scanned by both. An entry for a difference no run has shown is the adds-only smell. One thing worth writing down for whoever builds it: the motivating rows cannot be named by a type. `BaseItemImageInfos` carries `ItemId`, `Path`, `Blurhash`, `ImageType` and the dimensions — nothing that says `CollectionFolder`. Naming the two library tiles inside that table means naming them by `Path`, so a `rows` predicate that only compares column values for equality will not reach them; it needs at least a prefix or glob. That is a constraint on the design, not a reason to change the priority.
Author
Collaborator

Landed in f345ac6, with review fixes in 611fa42.

"rows" narrows an exemption to the rows it is about, as {"column": "glob"}.
GLOB rather than equality, because this issue's own comment recorded that the
motivating rows cannot be named by value: BaseItemImageInfos carries no column
saying CollectionFolder, so the library tiles inside it have to be named by a
path prefix. Every column given has to match.

scope keeps its meaning within the rows named — a value-scoped entry over them
still reports a column one side wrote and the other left null — and a
field-scoped entry can no longer drop the column from the projection, because
the other rows are still being diffed, so it collapses the value on the rows it
names instead.

No entry uses it. The library tile is still the one waiting customer and its
difference still takes a library neither side has drawn yet, scanned by both. An
entry for a difference no run has shown is the adds-only smell.

Three ways the first cut could have widened an entry silently

All found by review, all fixed and each with a test that fails against the old
shape:

  • An entry naming rows and no column read as a whole-table exemption — the
    row predicate discarded, every column of every row hidden. Refused at load
    now, as is a column two entries name, where only the first was ever read.
  • The markers a hidden column read as were the words exempt and set.
    That is safe only while every row is substituted, and naming rows ends it: a
    column upstream wrote a hash into and this server wrote exempt into read as
    agreeing. They are blobs now — quote renders a blob as X'…' and text as
    '…', so neither is a value either side can hold.
  • A glob that reached no row hid nothing while the run reported a clean
    sweep.
    Those are counted and named in the output, which is what the response
    list's accounting already does for an entry nobody sees.

Two GLOB behaviours are written down beside the field, because both will be
met: it is case-sensitive, unlike the table and column names around it, and a
null in the column matches nothing — {"Path": "*"} is every row that has a
path. Both fail closed.

Landed in f345ac6, with review fixes in 611fa42. `"rows"` narrows an exemption to the rows it is about, as `{"column": "glob"}`. `GLOB` rather than equality, because this issue's own comment recorded that the motivating rows cannot be named by value: `BaseItemImageInfos` carries no column saying `CollectionFolder`, so the library tiles inside it have to be named by a path prefix. Every column given has to match. `scope` keeps its meaning within the rows named — a value-scoped entry over them still reports a column one side wrote and the other left null — and a field-scoped entry can no longer drop the column from the projection, because the other rows are still being diffed, so it collapses the value on the rows it names instead. **No entry uses it.** The library tile is still the one waiting customer and its difference still takes a library neither side has drawn yet, scanned by both. An entry for a difference no run has shown is the adds-only smell. ## Three ways the first cut could have widened an entry silently All found by review, all fixed and each with a test that fails against the old shape: - **An entry naming rows and no column read as a whole-table exemption** — the row predicate discarded, every column of every row hidden. Refused at load now, as is a column two entries name, where only the first was ever read. - **The markers a hidden column read as were the words `exempt` and `set`.** That is safe only while *every* row is substituted, and naming rows ends it: a column upstream wrote a hash into and this server wrote `exempt` into read as agreeing. They are blobs now — `quote` renders a blob as `X'…'` and text as `'…'`, so neither is a value either side can hold. - **A glob that reached no row hid nothing while the run reported a clean sweep.** Those are counted and named in the output, which is what the response list's accounting already does for an entry nobody sees. Two `GLOB` behaviours are written down beside the field, because both will be met: it is case-sensitive, unlike the table and column names around it, and a null in the column matches nothing — `{"Path": "*"}` is every row that *has* a path. Both fail closed.
Sign in to join this conversation.
No description provided.