feat(mem_wal): row-level delete via tombstone rows#7417
Merged
Conversation
Add row-level DELETE to the in-memory WAL. A delete is a tombstone (sentinel) row: an append carrying the primary key plus `_tombstone = true` and null elsewhere. The tombstone wins newest-per-PK resolution (suppressing the older real row) and is then dropped from query results. - `_tombstone`: lance-owned, non-nullable Boolean appended to the memtable/generation schema (not the base table). `ShardWriter::open` extends the caller's base schema; `put()` injects `false`; new `ShardWriter::delete(keys)` builds the tombstone batch. No caller names the column. Legacy WAL replay back-fills it. - Fix mem_wal HNSW to skip null-vector rows instead of erroring (a pre-existing bug — mem_wal could not handle a nullable vector column at all). Survivors keep their original offsets in `row_ids`. - Read paths: filtered read folds `NOT _tombstone` into the predicate per WAL arm (only when the source carries the column); point lookup carries `_tombstone` through and filters after `CoalesceFirstExec`, with the plan-free BTree fast path short-circuiting a tombstone hit to not-found. Vector/FTS need no filter — a null-payload tombstone is never an index candidate; recency rides the existing block-list / NewestPkFilter. 19 new tests (read paths, point-lookup resurrection guards, HNSW null-vector handling, write path); full mem_wal suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
# Conflicts: # rust/lance/src/dataset/mem_wal/write.rs
put_no_wait routed batches straight to insert_batches_only without the _tombstone = false injection that put applies, so base-shaped batches failed the extended memtable schema check. Mirror put's normalization. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Two correctness fixes for WAL row-level delete, surfaced by the wallop fuzz: - merge_insert took the scalar-index-scan join whenever ANY join column was indexed. For a composite primary key with only one indexed column (the WAL base table: BTree on `id`, PK `(id, shard_key)`), that partial-index path under-matched, so a key-only `when_matched_delete` silently deleted nothing. Compaction then trimmed the WAL tombstone while the base row survived and the deleted row resurfaced. The indexed-scan path is now used only when every join column is indexed; a partially-indexed composite key falls back to the correct full path. - Flushing a generation with no indexable vectors (all tombstones — delete nulls the vector column and the HNSW skips nulls) errored with "HnswMemIndex is empty". It now skips the empty index and still flushes the data; an index-only (fast_search) vector query over that generation returns empty, with no brute-force scan. Tests: indexed composite-key delete, all-tombstone flush, and delete+flush (same-generation / cross-generation / cross-generation-indexed) regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
hamersaw
marked this pull request as ready for review
June 23, 2026 15:24
jackye1995
approved these changes
Jun 23, 2026
jackye1995
left a comment
Contributor
There was a problem hiding this comment.
don't have much time to review for now, mostly looks good, will probably do some post-merge review to catch up
BubbleCal
pushed a commit
that referenced
this pull request
Jun 30, 2026
## Summary Adds row-level **DELETE** to the in-memory WAL (`mem_wal`), expressed as **tombstone (sentinel) rows**: a delete appends the primary key plus `_tombstone = true` (null elsewhere). A tombstone is the newest value for its PK — it wins newest-per-PK resolution (suppressing the older real row) and is then silently dropped from query results. This is the lance (OSS) half of the WAL delete feature. The sophon half (compaction hard-delete + serving) consumes this API separately. ## What's here - **`_tombstone` column** — lance-owned, non-nullable `Boolean` appended to the memtable/generation schema only (never the base table). `ShardWriter::open` extends the caller's base schema internally; callers keep passing the base schema and never name the column. - **Write path** — `ShardWriter::put` injects `_tombstone = false`; new **`ShardWriter::delete(keys)`** builds the tombstone batch from a key-only input. Legacy WAL entries (written before this change) are back-filled on replay. - **HNSW null-vector fix** — mem_wal's HNSW rejected null vectors outright, so it couldn't handle a nullable vector column at all (a pre-existing bug, independent of deletes). `append_batch` now skips null rows like the base-table index, compacting survivors while preserving their original offsets in `row_ids` so search still resolves to the right row. - **Read paths** - *Filtered read*: fold `NOT _tombstone` into the predicate per WAL arm (only when the source carries the column), so it runs before the per-source pushdown limit and post-dedup in the active arm. - *Point lookup*: carry `_tombstone` through and filter **after** `CoalesceFirstExec` (per-source filtering would let the coalesce fall through and resurrect the row); the plan-free BTree fast path short-circuits a tombstone hit to not-found. - *Vector / FTS*: no filter needed — a null-payload tombstone is never an index candidate; the deleted real row is suppressed by the existing block-list (cross-gen) / `NewestPkFilter` (within active). ## Notable design points - Fold/carry `_tombstone` **only when the source schema has the column** — back-compat for legacy generations and existing tests for free. - `_tombstone` is non-nullable so the point-lookup base arm can synthesize a matching `Literal(false)` for `CoalesceFirstExec`'s exact-schema check. - Delete requires non-PK columns to be nullable (a tombstone nulls them); surfaced as a clear error otherwise. - Caveat documented in code: a delete-heavy burst between compactions can under-deliver a `LIMIT`/top-k query (uncompensated block-list drops) — recall degradation, not wrong content; self-limiting as compaction drains tombstones. ## Testing 19 new tests; full `mem_wal` suite green (417 passed, 0 failed). `cargo fmt` + `cargo clippy -p lance --tests` clean. Coverage: filtered-read masking + filter + limit, point-lookup resurrection guards (fast + plan paths), delete-then-reinsert, delete-of-absent, HNSW null-vector handling (mid-batch + all-null), write-path injection/round-trip and validation. Draft — opening for early review. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
BubbleCal
pushed a commit
that referenced
this pull request
Jun 30, 2026
Backport of the following PRs: - #7362 - #7417 - #7482 - #7483 - #7489 This PR backports the changes from the original PRs to the release/v8.0 branch. --------- Co-authored-by: Dan Rammer <[email protected]> Co-authored-by: Lance Release Bot <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds row-level DELETE to the in-memory WAL (
mem_wal), expressed as tombstone (sentinel) rows: a delete appends the primary key plus_tombstone = true(null elsewhere). A tombstone is the newest value for its PK — it wins newest-per-PK resolution (suppressing the older real row) and is then silently dropped from query results.This is the lance (OSS) half of the WAL delete feature. The sophon half (compaction hard-delete + serving) consumes this API separately.
What's here
_tombstonecolumn — lance-owned, non-nullableBooleanappended to the memtable/generation schema only (never the base table).ShardWriter::openextends the caller's base schema internally; callers keep passing the base schema and never name the column.ShardWriter::putinjects_tombstone = false; newShardWriter::delete(keys)builds the tombstone batch from a key-only input. Legacy WAL entries (written before this change) are back-filled on replay.append_batchnow skips null rows like the base-table index, compacting survivors while preserving their original offsets inrow_idsso search still resolves to the right row.NOT _tombstoneinto the predicate per WAL arm (only when the source carries the column), so it runs before the per-source pushdown limit and post-dedup in the active arm._tombstonethrough and filter afterCoalesceFirstExec(per-source filtering would let the coalesce fall through and resurrect the row); the plan-free BTree fast path short-circuits a tombstone hit to not-found.NewestPkFilter(within active).Notable design points
_tombstoneonly when the source schema has the column — back-compat for legacy generations and existing tests for free._tombstoneis non-nullable so the point-lookup base arm can synthesize a matchingLiteral(false)forCoalesceFirstExec's exact-schema check.LIMIT/top-k query (uncompensated block-list drops) — recall degradation, not wrong content; self-limiting as compaction drains tombstones.Testing
19 new tests; full
mem_walsuite green (417 passed, 0 failed).cargo fmt+cargo clippy -p lance --testsclean. Coverage: filtered-read masking + filter + limit, point-lookup resurrection guards (fast + plan paths), delete-then-reinsert, delete-of-absent, HNSW null-vector handling (mid-batch + all-null), write-path injection/round-trip and validation.Draft — opening for early review.
🤖 Generated with Claude Code