Skip to content

refactor(table): move transaction helpers down into lance-table - #8053

Draft
wjones127 wants to merge 5 commits into
mainfrom
refactor/transaction-prereqs
Draft

refactor(table): move transaction helpers down into lance-table#8053
wjones127 wants to merge 5 commits into
mainfrom
refactor/transaction-prereqs

Conversation

@wjones127

Copy link
Copy Markdown
Contributor

lance/src/dataset/transaction.rs is 6767 lines and is the next thing we want to move down into lance-table. Its production code turns out to have no dependency on Dataset, Session, DataFusion, or async I/O — only six couplings to code above lance-table. This PR clears five of them so the move itself can be a plain file rename.

Each commit moves one self-contained piece down to the layer that already owns the types it touches, and leaves a re-export behind so no caller changes:

  • is_system_index compared an IndexMetadata name against two constants that already live in lance-table's system_index module. It now sits next to them; lance-index re-exports it.
  • The key existence filter (KeyExistenceFilter and friends, previously merge_insert/inserted_rows.rs) depends only on arrow, lance-core's bloom filter, and the transaction protobuf. It is serialized into that protobuf, so it moves to lance-table.
  • Overlay staleness checks decide which rows an overlay makes stale with respect to an index, reading only coverage bitmaps, the overlay committed_version, and indexed field ids. lance::dataset::overlay keeps the read-resolution half.
  • MemWAL index metadata helpers read and write the MemWAL index's IndexMetadata entry. Every type they touch was already in lance-table, so they join the data structures they serialize.
  • ManifestBuildConfig is new. build_manifest took ManifestWriteConfig, whose timestamp field is the lance crate's mockable SystemTime — and that mock is cfg(test) of the lance crate, so resolving the timestamp inside a lower crate would silently un-mock it. The new config carries the timestamp already resolved to nanoseconds, and ManifestWriteConfig converts into it at the call sites, keeping the clock mockable.

The sixth coupling, ManifestWriteConfig itself, deliberately stays in lance for that reason.

Not included

The move of transaction.rs into lance-table, and its split into a module tree, come as two follow-up PRs stacked on this one. Splitting them keeps the cross-crate move reviewable as a detected rename rather than a 6767-line add/delete pair.

io/commit/conflict_resolver.rs is the other half of the transaction story and a natural later target, but it depends on Dataset and DatasetIndexExt, so it stays put.

Testing

The one behavioral question here is whether the mock clock still works, since that is what ManifestBuildConfig exists to protect. Verified locally: the MockClock-based suites (dataset::cleanup, dataset::delta, dataset::tests::dataset_versioning) pass, 67 tests. The to_build_config() conversion is called inside the two commit retry loops rather than hoisted above them, so each retry still resolves its own timestamp as before.

wjones127 and others added 5 commits July 28, 2026 12:38
The function only compares an IndexMetadata name against the two system
index name constants, both of which already live in lance-table's
system_index module. Moving it there puts it next to the constants it
reads; lance-index re-exports it so callers are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
KeyExistenceFilter and friends were defined in the lance crate but depend
only on arrow, lance-core's bloom filter, and the transaction protobuf in
lance-table. The filter is serialized into that protobuf, so the table
layer is where it belongs.

lance::dataset::write::merge_insert::inserted_rows becomes a re-export, so
callers are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deciding which rows an overlay makes stale with respect to an index reads
only fragment and index metadata: the coverage bitmaps, the overlay
committed_version, and the indexed field ids. None of that needs the read
path, so it moves to lance-table alongside the overlay format itself.

lance::dataset::overlay keeps the read-resolution half and re-exports the
three functions its callers use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
load_mem_wal_index_details, open_mem_wal_index, new_mem_wal_index_meta and
update_mem_wal_index_compacted_sstables read and write the MemWAL index's
IndexMetadata entry. Every type they touch already lives in lance-table's
system_index module, so they join the data structures they serialize.

lance::index::mem_wal keeps the dataset-level operations and re-exports the
four helpers at their previous visibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build_manifest and restore_old_manifest took ManifestWriteConfig, whose
timestamp field is the lance crate's mockable SystemTime. That mock is
cfg(test) of the lance crate, so resolving the timestamp inside a lower
crate would silently un-mock it.

This adds lance_table::format::ManifestBuildConfig, which carries the
timestamp already resolved to nanoseconds, and has ManifestWriteConfig
convert into it at the call sites. The conversion stays in the lance crate,
so the clock is still mockable. Conversion happens per attempt inside the
commit retry loops, matching the previous behavior of resolving the
timestamp on each build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the A-index Vector index, linalg, tokenizer label Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

@geruh geruh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Will, did a pass here and looks like a straightforward refactor with everything moved and everything is lined up. Also, checked out locally and ran some tests. Just a left a few small nits, nothing blocking. Let me know what you think.

);
}
}
pub use lance_table::format::key_existence::*;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we do a named import here? like what you're doing in /index/mem_wal.rs?

}

// A missing `fragment_bitmap` means the index predates fragment-bitmap tracking; treat it as
// covering every fragment (matching `DatasetPreFilter::new`) so overlay-stale rows can't slip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the DatasetPreFilter location moved, so should we update to lance::index::prefilter::DatasetPreFilter::new

//! Writers no longer update the index on every write. Instead, they update
//! shard manifests directly. This module provides functions to:
//! - Load the MemWAL index
//! - Update compacted SSTables (called during merge-insert commits)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like these comments were dropped in the merge. Are they no longer needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants