perf(changes): derive prunable commit intervals in O(delta) - #522
perf(changes): derive prunable commit intervals in O(delta)#522ragnorc wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a592fe228
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
d5cc430 to
29fcf18
Compare
|
Rebased onto the updated The base branch replaced the No behavior change to the pruning logic. Verified against the rebased base: engine builds, and |
da7ef79 to
8d870a4
Compare
First stage of the candidate-pruning optimization. changes::candidate_scan classifies whether a changed table interval can be derived in O(delta): every transaction in (begin, end] must be Append or a RewriteRows merge Update (row-set-preserving), with same branch/identity, an advancing bounded version interval, pinned handles, and active stable row IDs. Any doubt returns Ok(false) so the caller falls back to the exact ordered merge; it never errors on a normal miss (cleaned history). The Operation match is exhaustive with no wildcard, so a new Lance variant is a compile error that forces review (§9). Not yet wired into the enumerator (#[allow(dead_code)] until the wiring stage). Unit-tested over synthesized operations.
The CDC candidate-pruning classifier treats every persisted Operation::Update as row-set-preserving, so it can derive the change feed without a delete pass. That is sound only because OmniGraph's merge_insert never deletes an unmatched-by-source row. Lock that floor with a source-walk guard in forbidden_apis.rs: a by-source merge arm (WhenNotMatchedBySource / when_not_matched_by_source) must be absent from engine source, so introducing one forces the classifier to be re-gated first. The invariant holds today (neither symbol appears in src).
…C-030 §4.2) Make the per-commit change enumerator O(delta), not O(table), for the common insert/update/no-delete commit. Per changed interval, changes::candidate_scan proves whether the commit's effect is row-set-preserving (every transaction in the interval is Append or a RewriteRows merge Update); if so it derives the changes by scanning only the commit's changed fragments (the child fragments absent from the parent, from the manifest diff) plus a batched exact-id BTREE probe of the parent for before-images, classifying via the same typed rows_equal / emitted_image the full merge uses. Any unproven op (delete, overwrite, restore, compaction, unknown) falls back to the exact ordered merge. A prunable interval has zero logical deletes (one transaction per commit + the D2 rule + no delete-capable merge arm), so the pruned path needs no delete pass. The EmitSource seam yields the same id-ordered Emit stream as next_emit, so the streaming / ContinuationKey / budgeting contract is unchanged — the full changes and point_in_time suites pass identically with pruning enabled. Flip the changes_cost tripwire from assert_grows to assert_flat (data_reads 10 -> 9 across the extent sweep vs the old 11 -> 23) and add a fallback tripwire that keeps the unproven-op path honestly pinned as growing. The cost fixture now reconciles the id BTREE so the parent probe is an index lookup (the production steady state).
…rite RFC-030 §9 L0 guard: an overwrite that drops one logical id and changes another must still surface the delete. The classifier rejects Operation::Overwrite, so the enumerator falls back to the exact ordered merge and reports both the delete of the dropped id and the update — a candidate scan of the child's new fragments alone would never see the dropped id and would silently lose the delete. Passes with the optimization enabled, guarding against a future mis-prune of a row-removing op.
Move the row-version candidate pruning + no-delete proof from deferred to shipped in RFC-030 §14, and update the changes_cost.rs testing-map row to describe the flat pruned tripwire + the growing fallback tripwire.
…ites OmniGraph's keyed merge_insert never uses a delete-capable by-source arm, so every keyed-write Operation::Update removes no unmatched rows. Stamp a durable omnigraph.no_by_source_delete transaction property at the single general keyed merge chokepoint (staged_keyed_merge_result) so a downstream reader can trust a *persisted* Update was delete-free — the op shape plus the source-walk guard prove only that current engine code builds no such arm, not that a persisted transaction (e.g. one adopted from an external merge via repair --force) is. The marker is read-advisory: stamped unconditionally, it survives commit and recovery (it lives in Lance's committed manifest; recovery reuses the landed version), and a missing marker only costs an optimization. It is distinct from the RFC-023 insert_absence certificate (minted only for pure inserts), so a real update-bearing upsert carries the marker but not the certificate.
… Update
The candidate-pruning classifier trusted any Operation::Update{RewriteRows} as
row-set-preserving, but its child-only fragment scan has no delete pass. An
external Lance merge with a delete-capable by-source arm — adopted as uncovered
drift via repair --force --confirm — persists that exact shape, and its removed
rows would be silently dropped from the diff and feed.
Gate Update pruning on a durable OmniGraph provenance proof:
transaction_is_row_set_preserving requires the no_by_source_delete marker or the
insert_absence certificate for a RewriteRows Update; Append stays unconditional.
A marker-less external Update falls back to the exact ordered merge, which
reports the deletes. The op-shape classifier is retained (its exhaustive match
still fails a new Lance variant into review) and the forbidden_apis source guard
stays as defense-in-depth.
The new unit test pins that a marker-less Update{RewriteRows} is not
row-set-preserving while a marked/certified one is; the existing upsert-prune
cost and image tests stay green as live end-to-end proof that the write path
stamps the marker and the classifier honors it.
Rewrite RFC-030 §14's candidate-pruning justification — a RewriteRows Update is trusted delete-free only with a durable per-transaction provenance marker, not the source-walk guard alone; flip the deferred per-write certificate to shipped. Touch §4.3 and §9 L0 to require the marker, and note the write-path stamp cell and classifier gate test in testing.md.
The classifier checked only the dataset-level uses_stable_row_ids() flag, but the pruned path's correctness rests on each CHANGED FRAGMENT's _row_last_updated_at_version sequence: pinned Lance 10 silently fills the column with 1 when a fragment's sequence is missing or fails to load (the stream reader's 'Default to version 1 if sequence not provided' arm, which also swallows a failed load_sequence()). For any interval with begin > 1 those rows fall outside the candidate window and real updates vanish without an error. Before returning the changed set, require every changed fragment to carry present, decodable last-updated metadata (a manifest-level check, no data reads); any gap is a normal miss that falls back to the exact ordered merge, which never consumes the version column. Unit test pins the missing-metadata fragment as not loadable; the pruned cost/image tests staying flat/green are the live positive proof that real keyed-write fragments pass the gate.
RFC-030 §11 claimed the C0-C4 core persists nothing, which the no_by_source_delete transaction property made stale. Record the marker in the format audit with the explicit no-format-bump conclusion: it is read-advisory in every direction (missing marker = exact-merge fallback, older binaries ignore unknown properties, recovery and publication never consult it), an optimization-eligibility proof rather than a stored watermark or tombstone. Precision fixes: the marker is stamped by every GENERAL keyed MergeInsert update — proven strict inserts carry insert_absence instead — corrected in §14, the module docs, and the constant's doc comment. §14 also records the new per-fragment row-version-metadata loadability gate. Status honesty: the header no longer reads as unqualified shipped — it names the two recorded open obligations gating full acceptance (the §4.4 ordered-scan memory bound and bounded client auto-pagination), and the C2 phasing row annotates the aggregating helpers as open. §3.3 records name-only type filtering as a deliberate v1 scope decision with the sanctioned ID-filter extension path.
8d870a4 to
5c87662
Compare
What & why
The per-commit change enumerator derived one commit's entity changes by a full ordered-by-id merge of BOTH pinned table versions for every changed interval — O(table extent), not O(delta). A one-row update on a large table read the whole table on both sides. This was honestly pinned as a GROWING cost tripwire; RFC-030 §4.2/§4.3 specified the sanctioned fix (row-version candidate pruning + a transaction no-delete proof) and §14 recorded it as deferred. This PR ships it: the common insert/update/no-delete commit is now derived in O(delta).
The design
Per changed interval,
changes::candidate_scanproves whether the commit's effect is row-set-preserving — every Lance transaction in the interval isAppendor aRewriteRowsmergeUpdate(an exhaustive, wildcard-freeOperationmatch, so a new Lance variant compile-errors into review). When proven:Scanner::with_fragmentsto exactly the fragments the parent lacks (the child-minus-parent manifest diff — pure metadata, no data reads), with the_row_last_updated_at_version ∈ (begin, end]window dropping carried-over rows a fragment rewrite pulled along.id IN (chunk)index probe of the parent, reusing the same typedrows_equal/emitted_imagethe full merge uses (so it inherits the recent Blob descriptor-collision fix).Any unproven op (delete, overwrite, restore, compaction, a branch/lineage change, a non-advancing/oversized interval, a missing/cleaned transaction) falls back to the exact ordered merge, which classifies deletes correctly. The
EmitSourceseam yields the same id-orderedEmitstream as the oldnext_emit, so the streaming /ContinuationKey/ budgeting contract is unchanged.Correctness floor (guarded)
The classifier accepts every
Operation::Updateas non-deleting because OmniGraph'smerge_insertnever sets a by-source delete arm. Aforbidden_apis.rssource guard locks that (WhenNotMatchedBySource/when_not_matched_by_sourcemust be absent from engine source), so introducing one forces the classifier to be re-gated first.Backing RFC
Local verification
changes(38) andpoint_in_time(15) suites pass with pruning enabled — the candidate path produces the same results as the exact merge.changes_cost.rs::changes_page_opens_and_data_reads_are_bounded_by_deltaflipped fromassert_growstoassert_flat(data reads 10→9 across the extent sweep vs the old 11→23), with a companionchanges_page_unproven_op_scan_term_grows_with_table_extentkeeping the fallback honestly pinned as growing. The fixture reconciles theidBTREE (the production steady state, where the parent probe is an index lookup).commit_changes_falls_back_for_overwrite_that_removes_an_id— an overwrite that drops one id and changes another still surfaces the delete (pruning would miss it).forbidden_apis(18, updated.dataset()registrations for the new emitter),lance_surface_guards(32).cargo fmt --all --checkand both clippy graphs (-D warnings) clean; full failpoint-superset workspace test.Notes for reviewers
change-feed(feat(changes): commit entity diffs and a durable change feed #519) — this builds on the CDC enumerator + the Blob descriptor-collision fix. Retarget tomainonce feat(changes): commit entity diffs and a durable change feed #519 merges.idBTREE. On a never-optimized graph the parent probe full-scans (correct, slower); the feed's realistic steady state is periodically optimized.Note
Medium Risk
Touches CDC correctness for the change feed, but unproven intervals always use the exact merge and new pruning requires durable transaction proofs plus integration/cost guards.
Overview
RFC-030 §4.2/§4.3 candidate pruning replaces always doing a full ordered parent/child merge for every changed table interval. New
changes::candidate_scanclassifies each interval; when every Lance transaction in(begin, end]isAppendor a proven non-deletingRewriteRowsUpdate, enumeration scans only new child fragments (manifest diff), filters by_row_last_updated_at_version, and classifies rows with batchedid IN (chunk)parent probes—sameEmitstream and pagination as before viaEmitSource.Safety gates:
RewriteRowsupdates prune only withomnigraph.no_by_source_delete(stamped at the keyedmerge_insertchokepoint) orinsert_absence; marker-less external merges and removing ops (delete, overwrite, etc.) fall back to the exact merge. Fragment row-version metadata must be loadable or the interval falls back.OrderedRows::open_scanadds fragment scope and composed filters for this path.Tests/docs: cost tripwire flips to flat for pruned upserts and stays growing for deletes/overwrites; overwrite delete visibility;
forbidden_apisblocks by-source delete merge arms; RFC-030 §14 updated as shipped.Reviewed by Cursor Bugbot for commit 5c87662. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
The PR adds an O(delta) change-feed derivation path for proven insert/update-only transactions while retaining the exact full-table merge as the fallback.
idchunks to classify inserts and updates and preserve before-images.Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains in the eligible follow-up scope.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Changed table interval] --> B{Identity, branch, versions, metadata, and transactions proven safe?} B -- No --> C[Exact ordered parent-child merge] B -- Yes --> D[Compute child-minus-parent fragments] D --> E[Scan rows updated within version window] E --> F[Probe parent by batched IDs] F --> G[Classify insert or update] C --> H[Ordered Emit stream] G --> H H --> I[Existing budgeting and continuation logic]Reviews (7): Last reviewed commit: "docs(changes): audit the persisted marke..." | Re-trigger Greptile
Context used: