fix(core): stop serving stale row id sequences after overwrite - #8078
fix(core): stop serving stale row id sequences after overwrite#8078wjones127 wants to merge 2 commits into
Conversation
The `Session` metadata cache entry for a fragment's `RowIdSequence` was keyed only on the fragment id. `WriteMode::Overwrite` restarts fragment ids at 0, so after an overwrite readers were served the previous generation's sequence for the reused fragment id, silently corrupting stable row ids: row ids for rows that no longer exist, row counts that disagree with the manifest, and compaction rechunking more ids than the fragments hold. `RowIdSequenceKey` now also carries the fragment's `row_id_meta`, which is rewritten whenever the fragment's row ids change. Operations that leave row ids alone (deletes, added columns) keep hitting the cache. Fixes lance-format#8075 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
I independently wrote this same fix in #8080 (now closed as a duplicate — this PR was first and its tests are better). Two things from that work worth folding in here. Prior history the reviewer will want. This key has been reported three times from three different directions, and a previous fix in this exact spot was closed in favour of a different design:
So this PR is the lightweight route that #5508 was steered away from. That seems right to me — content-keying is correct independently of whether fragment ids are ever made unique, and it also covers drop + recreate, which the One suggestion on fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_u64(self.fragment_id);
match self.row_id_meta {
RowIdMeta::Inline(data) => {
builder.write_variant(0);
builder.write_bytes(data);
}
RowIdMeta::External(file) => {
builder.write_variant(1);
builder.write_str(&file.path);
builder.write_u64(file.offset);
builder.write_u64(file.size);
}
}
}
|
Fragment ids must be globally unique across a dataset's history because per-fragment objects are cached by id. `WriteMode::Overwrite` restarted the counter at 0, so the fragments it wrote aliased the ones they replaced. Restore was fixed the same way in lance-format#5554; overwrite was the remaining hole. Fragment ids now continue from the manifest's high water mark on overwrite, so the first fragment an overwrite writes takes the next unused id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both suggestions folded in.
The overwrite half of #5540 is also in this PR: fragment ids no longer restart at 0 on overwrite, which is what #5508 was steered toward and what #5554 already did for restore. That is why the regression test for the key itself now lives in |
Fragment ids were reused across generations of a dataset:
WriteMode::Overwriterestarted them at 0. The cached row id sequence for a fragment was looked up by fragment id alone, so with a sharedSessionevery reader after an overwrite was handed the previous generation's sequence for the reused id.That silently corrupts stable row ids: row ids are reported for rows that no longer exist, row counts disagree with the manifest, compaction rechunks more ids than the fragments physically hold, and the same id can look live in two fragments at once.
This PR fixes both halves:
fragment_idandrow_idto be globally unique across the table history" — which fix: restore decrease max_fragment_id in manifest #5554 already established for restore. Overwrite was the remaining hole.A version-scoped key was proposed for this same spot in #5508 and closed in favour of making fragment ids unique; keying on the manifest version would also fix the overwrite case but would discard the cache on every commit.
This does not fully close #7645:
RowIdIndexKeyandManifestKeyare still keyed on(uri, version), and version restarts at 1 for a dataset recreated at the same URI.Breaking changes
Fragment ids no longer restart at 0 after an overwrite — the first fragment written by an overwrite now takes the next id after the dataset's high water mark. Code that assumes a specific fragment id after an overwrite (e.g.
dataset.get_fragment(0)) needs to read the ids from the manifest instead.Fixes #8075