Skip to content

perf(dataset): read transactions by version without populating session caches#7817

Merged
Xuanwo merged 4 commits into
lance-format:mainfrom
xuanyu-z:fix-read-transaction-by-version-direct
Jul 16, 2026
Merged

perf(dataset): read transactions by version without populating session caches#7817
Xuanwo merged 4 commits into
lance-format:mainfrom
xuanyu-z:fix-read-transaction-by-version-direct

Conversation

@xuanyu-z

@xuanyu-z xuanyu-z commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

read_transaction_by_version delegated to checkout_version + read_transaction. A caller requesting one transaction does not need a historical Dataset, and the checkout path has session-cache side effects: the historical manifest is inserted into the metadata cache, manifest loading opportunistically decodes and caches the IndexSection into the index cache, and the transaction is inserted into the metadata cache. A long-lived process scanning many historical versions fills the shared caches with entries it never reuses (more visible after #7661).

Fixes #7801.

Change

  • read_version_transaction(version) -> VersionTransaction { version, timestamp, transaction } (new): resolves the version through the dataset's current branch and CommitHandler, decodes the manifest transiently (no cache read or write, no IndexSection decode), and reads the inline or external transaction. No historical Dataset is constructed. The compact record carries the manifest timestamp so callers don't have to check out for it.
  • The resolved manifest is validated to belong to the dataset's branch (matching checkout_by_ref), so a branch-insensitive commit handler errors instead of returning another branch's transaction.
  • read_transaction_by_version now delegates to it; signature and error semantics unchanged (missing/cleaned-up version is still an error).
  • read_transaction (current version) is unchanged apart from factoring the storage read into a shared helper; it still checks/populates the transaction cache.
  • checkout_version caching behavior is untouched.

Known trade-off: an inline transaction costs one extra ranged read vs the old path (the manifest tail is read for the timestamp/offsets, then the transaction message separately). Callers scanning history are expected to memoize per-version results; a combined read is a possible follow-up.

Tests

  • test_read_version_transaction_does_not_populate_caches — 20-version dataset with a BTree index; reads every version via the new API on a fresh session, asserts results and timestamps equal checkout_version(v), that the session's index and metadata cache entries/bytes do not grow, and that a missing version errors with Error::NotFound (the version resolves to a manifest path that does not exist).
  • test_read_version_transaction_v1_manifest_naming — V1-named manifests (asserts the naming premise) resolve and match a checkout.
  • test_read_version_transaction_on_branch — versions resolve against the branch chain and match a full branch checkout.
  • test_inline_transaction (existing) extended: the external-transaction-file fallback is asserted for the direct read using the manifest that test already constructs.

read_version_transaction carries a compiling doc example. Missing-version behavior of read_transaction_by_version itself is already covered by the existing test_read_transaction_properties, which now runs through the new implementation.

Summary by CodeRabbit

  • New Features

    • Added an API to retrieve transaction details for a specific dataset version, including the UTC commit timestamp and an optional transaction payload.
  • Bug Fixes

    • Historical transaction reads avoid unnecessary cache/index updates when no transaction is present.
  • Tests

    • Added coverage for inline-versus-external transaction fallback, correct behavior without cache pollution, and accurate handling of historical manifest formats across versions and branches.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Dataset gains a cache-free historical transaction API that resolves manifests on the current branch, supports inline and external transaction storage, returns version metadata, and is covered by tests for caches, branches, manifest naming, and missing versions.

Changes

Historical transaction reads

Layer / File(s) Summary
Transaction storage loading
rust/lance/src/dataset.rs
Adds VersionTransaction, extracts shared inline/external transaction loading, and preserves cache population for current-version reads.
Historical version API
rust/lance/src/dataset.rs
Adds read_version_transaction and updates read_transaction_by_version to use direct historical manifest resolution.
Version transaction validation
rust/lance/src/dataset/tests/dataset_transactions.rs
Adds dataset setup helpers and tests for cache isolation, inline/external transactions, V1 manifests, branches, timestamps, and missing versions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: jackye1995, wjones127

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Dataset
  participant CommitHandler
  participant Storage
  Caller->>Dataset: read_version_transaction(version)
  Dataset->>CommitHandler: resolve version on current branch
  Dataset->>Storage: read manifest and transaction data
  Storage-->>Dataset: return timestamp and optional Transaction
  Dataset-->>Caller: return VersionTransaction
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: reading versioned transactions without populating session caches.
Linked Issues check ✅ Passed The implementation matches the linked issue: it resolves versions via the branch/CommitHandler and avoids historical checkout and session cache writes.
Out of Scope Changes check ✅ Passed The added struct, helper, and tests all support the versioned-transaction refactor and do not introduce unrelated functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@xuanyu-z
xuanyu-z force-pushed the fix-read-transaction-by-version-direct branch from 9a1ec85 to 8803678 Compare July 15, 2026 22:43
@xuanyu-z
xuanyu-z marked this pull request as ready for review July 15, 2026 22:50
@xuanyu-z
xuanyu-z force-pushed the fix-read-transaction-by-version-direct branch from 8803678 to 2c17e72 Compare July 15, 2026 22:50

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset.rs`:
- Around line 1249-1282: Document all three public APIs in
rust/lance/src/dataset.rs:1249-1282 by adding a compiling example for
read_version_transaction, linking VersionTransaction, and defining version as a
dataset version identifier; rust/lance/src/dataset.rs:233-249 by adding an
example that consumes the returned version, timestamp, and optional Transaction;
and rust/lance/src/dataset.rs:1284-1292 by adding a linked example for the
compatibility wrapper and clarifying its relationship to
read_version_transaction.
- Around line 1260-1271: In the checkout flow after resolve_version_location and
read_manifest, validate that the manifest’s branch matches the requested branch
before continuing. Reuse the existing branch-mismatch error behavior from
checkout_by_ref, rejecting differing branches while preserving successful
resolution for the current branch.

In `@rust/lance/src/dataset/tests/dataset_transactions.rs`:
- Around line 512-513: Update the read_version_transaction assertion in the
dataset transaction test to match the specific Error::VersionNotFound variant
for version 9999, and verify the resulting error message includes “9999” rather
than accepting any error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c2e9063-59b6-445b-b071-9805df590407

📥 Commits

Reviewing files that changed from the base of the PR and between 2ba078c and 2c17e72.

📒 Files selected for processing (2)
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/tests/dataset_transactions.rs

Comment thread rust/lance/src/dataset.rs
Comment thread rust/lance/src/dataset.rs Outdated
Comment thread rust/lance/src/dataset/tests/dataset_transactions.rs Outdated
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.84615% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/dataset.rs 73.84% 9 Missing and 8 partials ⚠️

📢 Thoughts on this report? Let us know!

…n caches

`read_transaction_by_version` delegated to `checkout_version`, which
constructs a historical Dataset and has session-cache side effects: the
historical manifest is inserted into the metadata cache, manifest loading
opportunistically decodes and caches the IndexSection into the index cache,
and the transaction is inserted into the metadata cache. A long-lived
process scanning many historical transactions therefore fills the shared
Dataset and index caches with historical state it never reuses. This became
more visible after lance-format#7661 made version checkout populate the session manifest
cache.

Read the transaction directly instead: resolve the version through the
dataset's current branch and CommitHandler, decode the manifest transiently
(no cache read or write, no IndexSection decode), and read the inline or
external transaction. `checkout_version` caching is unchanged.

Also add `read_version_transaction`, returning a compact
`{ version, timestamp, transaction }` so callers that need the commit
timestamp do not have to check out the dataset; `read_transaction_by_version`
delegates to it.

Fixes lance-format#7801.
@xuanyu-z
xuanyu-z force-pushed the fix-read-transaction-by-version-direct branch from 2c17e72 to c3aadbe Compare July 15, 2026 23:31

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset.rs`:
- Around line 1312-1321: Add a compiling Rust doc example to the public method
read_transaction_by_version, showing Dataset usage with an async
Result-returning example, awaiting version 5, and handling the optional
Transaction. Keep the existing link to read_version_transaction and ensure the
example matches the method’s actual signature.
- Around line 1221-1247: The inline transaction path in
read_transaction_from_storage reopens the manifest and performs a second read;
update the historical-read flow around read_version_transaction and
read_transaction_from_storage to reuse the manifest bytes or decoded transaction
from the initial fetch. Preserve external transaction_file handling and return
the inline transaction without reopening manifest_location.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 76a3547a-d785-422a-bbe3-c833d712c2fe

📥 Commits

Reviewing files that changed from the base of the PR and between 2c17e72 and c3aadbe.

📒 Files selected for processing (2)
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/tests/dataset_transactions.rs

Comment thread rust/lance/src/dataset.rs
Comment thread rust/lance/src/dataset.rs
@xuanyu-z
xuanyu-z force-pushed the fix-read-transaction-by-version-direct branch from c3aadbe to 5a89f71 Compare July 15, 2026 23:56

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset.rs`:
- Around line 1265-1269: Remove the println! statements from the Rustdoc example
around read_version_transaction so it consumes the returned version metadata and
optional transaction without stdout output. Apply the same removal to the
Rustdoc example at rust/lance/src/dataset.rs lines 1325-1327, preserving the
examples’ existing API usage while avoiding debug output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2919058f-afda-4387-b77a-97f37b521384

📥 Commits

Reviewing files that changed from the base of the PR and between c3aadbe and 5a89f71.

📒 Files selected for processing (2)
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/tests/dataset_transactions.rs

Comment thread rust/lance/src/dataset.rs Outdated
- Validate the resolved manifest belongs to the dataset's branch, matching
  checkout_by_ref: a branch-insensitive commit handler must error rather
  than hand back another branch's transaction.
- Assert the missing-version error variant (NotFound: the version resolves
  to a manifest path that does not exist) instead of a bare is_err.
- Document the version parameter and add an example to
  read_version_transaction.
@xuanyu-z
xuanyu-z force-pushed the fix-read-transaction-by-version-direct branch from 5a89f71 to af8018a Compare July 16, 2026 00:05
@wjones127
wjones127 self-requested a review July 16, 2026 06:06
Comment thread rust/lance/src/dataset.rs Outdated
let reader = if let Some(size) = manifest_location.size {
self.object_store
.open_with_size(&self.manifest_location.path, size as usize)
.open_with_size(&manifest_location.path, size as usize)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

After read_manifest retries without a stale ManifestLocation.size, this inline transaction path still reuses the original size. A concurrent overwrite that leaves a too-small cached size can therefore decode the manifest successfully and then fail here with file size is too small, regressing the stale-size recovery contract established in #7542.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New revision matches the contract

Comment thread rust/lance/src/dataset.rs
pub async fn read_transaction_by_version(&self, version: u64) -> Result<Option<Transaction>> {
let dataset_version = self.checkout_version(version).await?;
dataset_version.read_transaction().await
Ok(self.read_version_transaction(version).await?.transaction)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Delegating the existing read_transaction_by_version API to this path changes a missing version from Error::DatasetNotFound to Error::NotFound, because the new path bypasses the mapping performed by load_manifest. This is an observable compatibility change for callers matching the existing public error variant, and it is unrelated to the cache-isolation goal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated to have the same error type as before

- read_version_transaction returned raw NotFound for a missing version,
  changing the error variant read_transaction_by_version callers see from
  the checkout-based path's DatasetNotFound. Map it back.
- The inline transaction read trusted manifest_location.size, so a
  concurrent overwrite leaving that size too small decoded the manifest
  (which recovers) but then failed reading the transaction section. Retry
  the transaction read with the true size, matching the manifest reader.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset.rs`:
- Around line 1237-1247: Add regression coverage for the stale-size recovery in
the transaction-reading path containing read_message: construct a
ManifestLocation with an undersized explicit size so the first read fails with
“file size is too small,” then verify reopening the manifest without a size
succeeds and returns the inline transaction. Ensure the test exercises the retry
branch rather than only validating normal reads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2ef2566d-1e77-4fbb-abc0-3485498af84d

📥 Commits

Reviewing files that changed from the base of the PR and between af8018a and 2f71a01.

📒 Files selected for processing (2)
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/tests/dataset_transactions.rs

Comment thread rust/lance/src/dataset.rs
…read

Feed read_transaction_from_storage a ManifestLocation size smaller than the
transaction offset so the first read fails "file size is too small", and
assert the retry at the true size returns the inline transaction.
@Xuanwo
Xuanwo merged commit 74b2822 into lance-format:main Jul 16, 2026
33 checks passed
wjones127 pushed a commit that referenced this pull request Jul 21, 2026
…n caches (#7817)

## Problem

`read_transaction_by_version` delegated to `checkout_version` +
`read_transaction`. A caller requesting one transaction does not need a
historical `Dataset`, and the checkout path has session-cache side
effects: the historical manifest is inserted into the metadata cache,
manifest loading opportunistically decodes and caches the `IndexSection`
into the index cache, and the transaction is inserted into the metadata
cache. A long-lived process scanning many historical versions fills the
shared caches with entries it never reuses (more visible after #7661).

Fixes #7801.

## Change

- `read_version_transaction(version) -> VersionTransaction { version,
timestamp, transaction }` (new): resolves the version through the
dataset's current branch and `CommitHandler`, decodes the manifest
transiently (no cache read or write, no `IndexSection` decode), and
reads the inline or external transaction. No historical `Dataset` is
constructed. The compact record carries the manifest timestamp so
callers don't have to check out for it.
- The resolved manifest is validated to belong to the dataset's branch
(matching `checkout_by_ref`), so a branch-insensitive commit handler
errors instead of returning another branch's transaction.
- `read_transaction_by_version` now delegates to it; signature and error
semantics unchanged (missing/cleaned-up version is still an error).
- `read_transaction` (current version) is unchanged apart from factoring
the storage read into a shared helper; it still checks/populates the
transaction cache.
- `checkout_version` caching behavior is untouched.

Known trade-off: an inline transaction costs one extra ranged read vs
the old path (the manifest tail is read for the timestamp/offsets, then
the transaction message separately). Callers scanning history are
expected to memoize per-version results; a combined read is a possible
follow-up.

## Tests

- `test_read_version_transaction_does_not_populate_caches` — 20-version
dataset with a BTree index; reads every version via the new API on a
fresh session, asserts results and timestamps equal
`checkout_version(v)`, that the session's index and metadata cache
entries/bytes do not grow, and that a missing version errors with
`Error::NotFound` (the version resolves to a manifest path that does not
exist).
- `test_read_version_transaction_v1_manifest_naming` — V1-named
manifests (asserts the naming premise) resolve and match a checkout.
- `test_read_version_transaction_on_branch` — versions resolve against
the branch chain and match a full branch checkout.
- `test_inline_transaction` (existing) extended: the
external-transaction-file fallback is asserted for the direct read using
the manifest that test already constructs.

`read_version_transaction` carries a compiling doc example.
Missing-version behavior of `read_transaction_by_version` itself is
already covered by the existing `test_read_transaction_properties`,
which now runs through the new implementation.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an API to retrieve transaction details for a specific dataset
version, including the UTC commit timestamp and an optional transaction
payload.

* **Bug Fixes**
* Historical transaction reads avoid unnecessary cache/index updates
when no transaction is present.

* **Tests**
* Added coverage for inline-versus-external transaction fallback,
correct behavior without cache pollution, and accurate handling of
historical manifest formats across versions and branches.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 74b2822)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

read_transaction_by_version should not populate Dataset Session caches

2 participants