RowAddrMaskKey is scoped by {version, restrict_hash}, but the metadata cache it lives in is namespaced by dataset URI alone. A dataset dropped and recreated at the same URI restarts its version history at 1, so a long-lived session can serve the new incarnation the previous incarnation's cached stable-row-id allow-list. Stable row ids restart near 0 in both generations, so every row beyond the old generation's id range silently disappears from every indexed query. No error, no warning, just missing rows.
The sibling key RowIdIndexKey already carries the manifest e-tag for exactly this reason, and RowIdSequenceKey is content-addressed. Only the mask key was left without a generation token.
Reproducer, with one shared Session across both incarnations:
- Write 10 rows with
enable_stable_row_ids: true, then delete("x = 9"). The dataset reaches version 2 and the mask cache holds the allow-list {0..8}.
- Drop the dataset directory.
- Write 20 rows at the same URI, again with stable row ids, then
delete("x = 19"). This incarnation also reaches version 2.
- Ask for the deletion mask. It comes back as
{0..8} instead of {0..18}, so ids 9 through 18 are treated as deleted.
Reproduced on main at ebba5814c.
RowAddrMaskKeyis scoped by{version, restrict_hash}, but the metadata cache it lives in is namespaced by dataset URI alone. A dataset dropped and recreated at the same URI restarts its version history at 1, so a long-lived session can serve the new incarnation the previous incarnation's cached stable-row-id allow-list. Stable row ids restart near 0 in both generations, so every row beyond the old generation's id range silently disappears from every indexed query. No error, no warning, just missing rows.The sibling key
RowIdIndexKeyalready carries the manifest e-tag for exactly this reason, andRowIdSequenceKeyis content-addressed. Only the mask key was left without a generation token.Reproducer, with one shared
Sessionacross both incarnations:enable_stable_row_ids: true, thendelete("x = 9"). The dataset reaches version 2 and the mask cache holds the allow-list{0..8}.delete("x = 19"). This incarnation also reaches version 2.{0..8}instead of{0..18}, so ids 9 through 18 are treated as deleted.Reproduced on
mainatebba5814c.