fix(mem-wal): backport tombstone delete support to v8.0#7519
Merged
BubbleCal merged 5 commits intoJun 30, 2026
Conversation
Contributor
|
ACTION NEEDED The PR title and description are used as the merge commit message. Please update your PR title and description to match the specification. For details on the error please inspect the "PR Title Check" action. |
BubbleCal
added a commit
that referenced
this pull request
Jun 30, 2026
## Summary This PR fixes CI-only issues on the `release/v8.0` branch so the v8 backport validation PRs can run against a healthy baseline. ## What changed - Backports #7384 to replace deprecated NumPy 2.5 `array.shape = -1` assignments with `reshape(-1)`. - Pins the Rust `linux-build` coverage job to `nightly-2026-06-24`, matching the nightly that passed for `v8.0.0-rc.2`. ## Why - The Python failures on #7519 match the existing `release/v8.0` rc2 failure mode: NumPy 2.5 emits `DeprecationWarning` for setting `array.shape`, and pytest treats warnings as errors. - The Rust `linux-build` failure on #7519 reproduces after rerun and is a nightly rustc ICE with `nightly-2026-06-30`; rc2 passed with `nightly-2026-06-24`. ## Validation - `cargo fmt --all` - `git diff --check origin/release/v8.0...HEAD` - `cargo fmt --all --check` Local Python targeted validation was attempted with `uv run pytest python/tests/test_indices.py -q`, but it stalled in the editable `pylance` build step before reaching pytest output. The PR CI will run the full Python matrix. --------- Co-authored-by: YueZhang <[email protected]>
## What Adds `ShardWriter::put_no_wait`, a variant of `put` that performs the visible in-memory insert and triggers the durable WAL flush, then returns the `BatchDurableWatcher` **without** awaiting it. A thin wrapper restores `put`'s behavior (await the watcher), so existing callers are unchanged. `put_memtable` is split into: - `put_memtable_no_wait` — the in-memory critical section (insert under `state_lock` + `track_batch_for_wal` + flush triggers) followed by `trigger_flush`, returning `(WriteResult, Option<BatchDurableWatcher>)`. - `put_memtable` — calls the above, then `watcher.wait()`. `BatchDurableWatcher` and `WriteResult` are re-exported from `mem_wal`. ## Why Lets an external caller hold its own serialization lock across only the in-memory read-merge-insert critical section and await durability **after** releasing it, so concurrent durable flushes still coalesce. The in-memory insert stays guarded by the writer's `state_lock`, so `BatchStore`'s single-writer invariant holds regardless of the external lock — `state_lock` is intentionally **not** skipped. This is the lance-side primitive for sophon's WAL partial-column-update path (read fresh tier → merge → insert under a per-bucket lock, durability awaited outside it). ## Tests `test_put_no_wait_durable_visible_then_durable` (row visible before durability, watcher resolves) and `test_put_no_wait_non_durable_returns_no_watcher`. --------- Co-authored-by: Lance Release Bot <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
## 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]>
## What Adds `LsmPointLookupPlanner::lookup_keep_tombstone` and `lookup_many_keep_tombstone` — point-lookup variants that carry the `_tombstone` marker through instead of filtering deleted keys out. ## Why The existing `lookup` / `lookup_many` collapse two distinct states — **deleted in the fresh tier** and **never written** — into a single `None`. That's correct for a normal reader, but a caller doing a read-on-write merge needs to tell them apart: a fresh-deleted PK must be treated as *absent* (so a later partial update resurrects it from carried columns + NULLs), while a never-written PK falls back to the base row. These variants return the deleted row with `_tombstone = true`; absent keys still return `None`. Implemented by refactoring `plan_lookup` to share a `plan_lookup_coalesced` helper and skipping the post-coalesce tombstone filter, so the hot read path is unchanged. ## Tests - `test_lookup_keep_tombstone_returns_deleted_row_with_marker` - `test_lookup_many_keep_tombstone_includes_tombstoned_keys` ## Context Prerequisite for a downstream WAL partial-column-update resurrection fix: a partial update arriving after a delete must resurrect carried columns + NULL untouched, never stale pre-delete base data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
## What Adds `ShardWriter::delete_no_wait`, the delete analog of `put_no_wait`: it inserts the tombstone into the in-memory tier (visible to reads on this writer the instant it returns) and hands back the durability watcher *without* awaiting it. `delete` becomes a thin wrapper that awaits the watcher. ## Why Lets a caller hold an external lock across only the in-memory tombstone insert and await durability after releasing it — matching how `put_no_wait` is already used so flushes still coalesce. A downstream WAL serializes deletes against partial-update merges under a per-bucket lock; with the blocking `delete` it would hold that lock across durability, and `delete_no_wait` lets it release first. ## Tests - `test_shard_writer_delete_no_wait_visible_before_durability` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
#7489) ## What The WAL fresh-tier vector and FTS search arms apply a per-generation recency filter (`NewestPkFilterExec`) to every in-memory memtable — but they only apply the cross-generation block-list (`PkBlockFilterExec`) to flushed/base sources, never to in-memory ones. This PR applies the block-list to in-memory arms too. ## Why `LsmDataSourceCollector::in_memory_sources` emits **both** the live active memtable **and** every frozen-awaiting-flush memtable as `ActiveMemTable` sources, each with its own `index_store`. The recency filter queries only *that generation's* PK index, so it is purely within-generation. When a PK is inserted in one generation and later deleted or updated in a **newer** in-memory generation, the older (frozen) generation's arm keeps the stale matching row: the newer generation's tombstone/update lives in a different `index_store` the frozen arm never consults. This happens whenever an insert and its later delete/update straddle a freeze boundary — common under a short flush interval, which freezes memtables continuously. The cross-generation block-list (`NEWER(G)`) is the only mechanism that resolves this, and `compute_source_block_lists` already computes it for in-memory sources (snapshot-bounded by each generation's `max_visible_row`, so a not-yet-visible write can't over-block). The arms just discarded it. ## Change - `vector_search.rs`: after `NewestPkFilterExec`, also wrap in `PkBlockFilterExec` when the source has a non-empty block set. - `fts_search.rs`: unify the dedup loop so every source applies the block-list when blocked; the within-generation recency filter is already applied inside `build_source_plan` for in-memory sources. The newest memtable per shard has no newer generation, so its block set is empty (`None`) and it is unaffected — only older frozen generations are now filtered. Each in-memory generation gets both within-generation recency and cross-generation supersession. ## Tests - `cross_gen_stale_version_blocked_by_newer_memtable` (vector): a PK at a near vector in a frozen generation and at a far vector in the active generation. Before the fix the stale near copy leaks alongside the newest far version; after, only the newest survives. - `cross_gen_stale_update_blocked_by_newer_memtable` (FTS): a PK matching "alpha" in a frozen generation, updated to "beta" in the active generation; an "alpha" search must not return the stale frozen row. Both fail before this change and pass after. The existing single-generation recency tests (`keeps_only_the_newest_visible_position_per_pk`, `active_stale_update_predicate_crossing_leaks`) still pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
BubbleCal
force-pushed
the
backport/pr-7362-and-more-to-release-v8.0
branch
from
June 30, 2026 09:15
e96fce1 to
b85eee1
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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.
Backport of the following PRs:
This PR backports the changes from the original PRs to the release/v8.0 branch.