Skip to content
13 changes: 12 additions & 1 deletion docs/src/format/index/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ or updated. These should be filtered out during query execution.
</figcaption>
</figure>

There are three situations to consider:
There are four situations to consider:

1. **A fragment has some deleted rows.** A few of the rows in the fragment have been marked
as deleted, but some of the rows are still present. The row addresses from the deletion
Expand All @@ -188,6 +188,17 @@ There are three situations to consider:
3. **A fragment has had the indexed column updated in place.** This cannot be detected just
by examining metadata. To prevent reading invalid data, the engine should filter out any
row addresses that are not in the index's current `fragment_bitmap`.
4. **A fragment has an updated value in an [overlay file](../table/data_overlay_file.md).**
This can be detected by checking if any of the fragments in the index's `fragment_bitmap`
have overlay files. For each overlay whose `committed_version` is greater than the index
segment's `dataset_version`, the overlay carries updated values not reflected in the index,
so its covered rows must be excluded from index results. Excluded rows are re-evaluated
against their current (overlaid) values on the flat path — dropping them without
re-evaluation would silently lose rows that match under the new value. Exclusion is
field-aware: only overlays covering the indexed field matter. You may exclude just the
affected rows or the whole fragment; the latter is simpler and safer but re-evaluates more
rows than necessary. See [Data Overlay Files](../table/data_overlay_file.md#index-integration)
for the exclusion set, re-evaluation, and correctness invariant.

## Compaction and remapping

Expand Down
1 change: 1 addition & 0 deletions docs/src/format/table/.pages
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ nav:
- Layout: layout.md
- Branch & Tag: branch_tag.md
- Row ID & Lineage: row_id_lineage.md
- Data Overlay Files: data_overlay_file.md
- MemTable & WAL: mem_wal.md
388 changes: 388 additions & 0 deletions docs/src/format/table/data_overlay_file.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions docs/src/format/table/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,29 @@ However, this invalidates row addresses and requires rebuilding indices, which c

</details>

## Data Overlay Files

!!! warning "Experimental"

This feature is currently experimental and not yet supported in any library.

<!-- TODO: When overlay file support is implemented, update this note to state
the released version that first supports the feature. -->

!!! note "Overlay files require feature flag 64 (data overlay files)"

Overlay files supply new values for a subset of cells within
a fragment without rewriting the base data files. They make updates cheap when only
a small percentage of rows and/or columns change: a writer appends a small file
carrying just the changed cells instead of rewriting whole columns or moving rows
to a new fragment.

For the full specification — coverage and resolution rules, dense vs. sparse layout,
versioning, index integration, compaction, and a worked example — see the
[Data Overlay Files Specification](data_overlay_file.md).



## Related Specifications

### Storage Layout
Expand Down
47 changes: 47 additions & 0 deletions docs/src/format/table/transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,53 @@ The following operations are retryable conflicts with DataReplacement:
A concurrent Delete or Update that only adds a deletion vector to a target fragment (without removing it) is compatible: the
positional column file stays aligned and the rebase preserves the deletion vector.

### DataOverlay

Attaches [overlay files](data_overlay_file.md) to fragments, supplying new values
for a subset of `(row offset, field)` cells without rewriting the fragments' base
data files. The overlays are appended to each fragment's existing `overlays` list,
so overlays written by concurrent commits are preserved. Each overlay's
`committed_version` is stamped to the new dataset version at commit time (and
re-stamped on retry), like the created-at / last-updated-at version sequences.

<details>
<summary>DataOverlay protobuf message</summary>

```protobuf
%%% proto.message.DataOverlay %%%

%%% proto.message.DataOverlayGroup %%%
```

</details>

#### DataOverlay Compatibility

A DataOverlay operation only changes cells within existing fragments and preserves
physical row addresses, so — like DataReplacement — it is intentionally permissive.
Because overlays stack and the higher `committed_version` wins each covered cell,
independent backfills never conflict, and a concurrent Delete simply makes the
overlay value for a deleted offset inert. Here are the operations that conflict
with DataOverlay:

- Overwrite
- Restore
- UpdateMemWalState

The following operations are retryable conflicts with DataOverlay:

- Rewrite (only if overlapping fragments) — row-rewriting compaction or an
overlay→base fold changes physical row addresses or consumes the overlays, so
the overlay's offsets are no longer valid; the writer must re-read the new
fragment, recompute, and retry.
- Merge (always)

DataOverlay is compatible with another DataOverlay (any fields), Append, Delete,
and DataReplacement or a column rewrite (Update with `REWRITE_COLUMNS`) of the same
field, because all of these preserve physical row addresses: overlay offsets stay
valid, the overlay is newer and wins its covered cells, and the version gate
excludes those cells from any rebuilt index.

### UpdateMemWalState

Updates the state of MemWal indices (write-ahead log based indices).
Expand Down
72 changes: 72 additions & 0 deletions protos/table.proto
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ message Manifest {
// * 1 << 3: table config is present
// * 1 << 4: dataset uses multiple base paths
// * 1 << 5: transaction file writes are disabled
// * 1 << 6: data overlay files are present (see DataOverlayFile). Readers that do
// not understand overlays must refuse the dataset, since ignoring an overlay
// would silently return stale base values.
Comment thread
westonpace marked this conversation as resolved.
uint64 reader_feature_flags = 9;

// Feature flags for writers.
Expand Down Expand Up @@ -313,6 +316,15 @@ message DataFragment {

repeated DataFile files = 2;

// Optional overlay files for this fragment, which supply new values for a
// subset of cells without rewriting the base data files. This MUST be empty
// if the data overlay files feature flag (64) is not set in the manifest.
//
// Order is significant: a later entry is newer than an earlier one. When two
// overlays cover the same (offset, field) and share a `committed_version`, the
// later entry wins. See DataOverlayFile for the full resolution rules.
repeated DataOverlayFile overlays = 11;

// File that indicates which rows, if any, should be considered deleted.
DeletionFile deletion_file = 3;

Expand Down Expand Up @@ -435,6 +447,66 @@ message DataFile {
optional uint32 base_id = 7;
} // DataFile

// An overlay file supplies new values for a subset of (row offset, field) cells
// within a fragment, without rewriting the fragment's base data files. It is
// used for efficient updates when only a small fraction of rows and/or columns
// change.
//
// On read, a cell is resolved by consulting the fragment's overlays from newest
// to oldest: the first overlay that covers that (offset, field) wins; if none
// cover it, the value falls through to the base data file. Because deletions
// take precedence over overlays, an overlay value for an offset that is also
// marked deleted is dead and is ignored.
//
// The overlay's data file does NOT store a row-offset key column. Within a value
// column, the position of a covered offset's value is the rank (0-based count of
// set bits below it) of that offset within the field's coverage bitmap. Because
// fields may cover different offset sets, the value columns of a single overlay
// data file may have different lengths (which the Lance file format permits).
message DataOverlayFile {
// The data file storing the overlay's new cell values, one value column per
// field in `data_file.fields`. No row-offset key column is stored.
DataFile data_file = 1;

// Which (offset, field) cells this overlay provides values for.
oneof coverage {
// A single 32-bit Roaring bitmap of physical row offsets that applies to
// every field in `data_file.fields` (a "dense" / rectangular overlay).
// Every covered offset has a value for every field. This is the common case
// for a plain UPDATE, where one SET list is applied to one set of rows.
bytes shared_offset_bitmap = 2;
// Per-field coverage for a "sparse" overlay, used when different fields cover
// different offset sets (e.g. a MERGE with multiple WHEN MATCHED branches).
FieldCoverage field_coverage = 4;
}

// The dataset version at which this overlay became effective: the version of
// the commit that introduced it, NOT the version it was read from. It is
// stamped at commit time and re-stamped if the commit is retried, in the same
// way as the created-at / last-updated-at version sequences.
//
// This drives two orderings:
// * Versus index builds: an index whose `dataset_version` >= this value
// already incorporates this overlay. Otherwise the overlay's covered cells
// are excluded from index results for the affected fields and re-evaluated
// against their current values (see the Data Overlay Files specification).
// * Versus other overlays: when two overlays cover the same (offset, field),
// the one with the higher `committed_version` wins. Overlays that share a
// `committed_version` are ordered by their position in
// `DataFragment.overlays`, where a later entry is newer and wins.
uint64 committed_version = 3;
}

// Per-field coverage for a sparse overlay.
message FieldCoverage {
// One entry per field in the overlay's `data_file.fields`, in the same order.
// Each is a 32-bit Roaring bitmap of the physical row offsets covered for that
// field. An offset present in a field's bitmap but mapped to a NULL value
// means the cell is overridden to NULL (distinct from an offset that is absent,
// which falls through to the base data file).
repeated bytes offset_bitmaps = 1;
}

// Deletion File
//
// The path of the deletion file is constructed as:
Expand Down
23 changes: 23 additions & 0 deletions protos/transaction.proto
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,28 @@ message Transaction {
repeated DataReplacementGroup replacements = 1;
}

// Overlay files to append to a single fragment, in order (the last entry is
// newest). The overlays are appended to the fragment's existing `overlays`
// list; they do not replace it, so overlays written by concurrent commits are
// preserved.
message DataOverlayGroup {
uint64 fragment_id = 1;
// Each DataOverlayFile.committed_version is left 0 by the writer and stamped
// to the new dataset version at commit time (re-stamped on retry), in the
// same way as the created-at / last-updated-at version sequences. The fields
// touched are read from each overlay's `data_file.fields`.
repeated DataOverlayFile overlays = 2;
}

// Attach overlay files to fragments, supplying new values for a subset of
// (row offset, field) cells without rewriting the fragments' base data files.
// See the DataOverlayFile message in table.proto for resolution, coverage, and
// versioning rules, and the Data Overlay Files and Transactions specifications
// for the (intentionally permissive) conflict semantics.
message DataOverlay {
repeated DataOverlayGroup groups = 1;
}

// Update the merged generations in MemWAL index.
// This operation is used during merge-insert to atomically record which
// generations have been merged to the base table.
Expand Down Expand Up @@ -346,6 +368,7 @@ message Transaction {
UpdateMemWalState update_mem_wal_state = 112;
Clone clone = 113;
UpdateBases update_bases = 114;
DataOverlay data_overlay = 115;
}

// Fields 200/202 (`blob_append` / `blob_overwrite`) previously represented blob dataset ops.
Expand Down
2 changes: 2 additions & 0 deletions rust/lance-table/benches/manifest_intern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn make_uniform_pb_fragments(n: u64, num_fields: usize) -> Vec<pb::DataFragment>
file_size_bytes: 0,
base_id: None,
}],
overlays: vec![],
deletion_file: None,
row_id_sequence: None,
physical_rows: 1000,
Expand Down Expand Up @@ -135,6 +136,7 @@ fn make_diverse_pb_fragments(
file_size_bytes: 0,
base_id: None,
}],
overlays: vec![],
deletion_file: None,
row_id_sequence: None,
physical_rows: 1000,
Expand Down
4 changes: 4 additions & 0 deletions rust/lance-table/src/format/fragment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,10 @@ impl From<&Fragment> for pb::DataFragment {
Self {
id: f.id,
files: f.files.iter().map(pb::DataFile::from).collect(),
// Overlay files are not produced by this version of the library; a
// dataset that uses them sets reader feature flag 64, which is
// rejected at the feature-flag layer (see lance-table feature_flags).
overlays: vec![],
deletion_file,
row_id_sequence,
physical_rows: f.physical_rows.unwrap_or_default() as u64,
Expand Down
28 changes: 28 additions & 0 deletions rust/lance/src/dataset/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3348,6 +3348,16 @@ impl TryFrom<pb::Transaction> for Transaction {
})) => Operation::UpdateBases {
new_bases: new_bases.into_iter().map(BasePath::from).collect(),
},
Some(pb::transaction::Operation::DataOverlay(_)) => {
// Overlay files are not supported by this version of the library.
// A dataset that uses them sets reader feature flag 64, which is
// already rejected at the feature-flag layer; reject here too so a
// transaction referencing the operation can never be applied.
return Err(Error::not_supported(
"data overlay files are not supported by this version of Lance \
(reader feature flag 64)",
));
}
None => {
return Err(Error::internal(
"Transaction message did not contain an operation".to_string(),
Expand Down Expand Up @@ -6186,4 +6196,22 @@ mod tests {
assert!(!left.modifies_same_metadata(&different_key));
assert!(left.modifies_same_metadata(&replace));
}

#[test]
fn test_data_overlay_operation_rejected() {
// Overlay files are not supported by this version of the library. A
// transaction carrying the DataOverlay operation must be rejected rather
// than silently ignored, mirroring the feature-flag-64 rejection.
let message = pb::Transaction {
read_version: 1,
uuid: Uuid::new_v4().to_string(),
operation: Some(pb::transaction::Operation::DataOverlay(
pb::transaction::DataOverlay { groups: vec![] },
)),
..Default::default()
};

let result = Transaction::try_from(message);
assert!(matches!(result, Err(Error::NotSupported { .. })));
}
Comment on lines +6200 to +6216

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the expected error message as well as the variant.

The test currently verifies only Error::NotSupported, so an unrelated NotSupported error could pass. Assert that the message identifies unsupported data overlays or reader feature flag 64.

As per coding guidelines: “Assert on both the error variant and the message content in tests; do not check only is_err().”

Proposed test assertion
-        let result = Transaction::try_from(message);
-        assert!(matches!(result, Err(Error::NotSupported { .. })));
+        let err = match Transaction::try_from(message) {
+            Err(err) => err,
+            Ok(_) => panic!("expected data overlay transaction to be rejected"),
+        };
+        assert!(matches!(&err, Error::NotSupported { .. }));
+        assert!(err
+            .to_string()
+            .contains("data overlay files are not supported"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn test_data_overlay_operation_rejected() {
// Overlay files are not supported by this version of the library. A
// transaction carrying the DataOverlay operation must be rejected rather
// than silently ignored, mirroring the feature-flag-64 rejection.
let message = pb::Transaction {
read_version: 1,
uuid: Uuid::new_v4().to_string(),
operation: Some(pb::transaction::Operation::DataOverlay(
pb::transaction::DataOverlay { groups: vec![] },
)),
..Default::default()
};
let result = Transaction::try_from(message);
assert!(matches!(result, Err(Error::NotSupported { .. })));
}
#[test]
fn test_data_overlay_operation_rejected() {
// Overlay files are not supported by this version of the library. A
// transaction carrying the DataOverlay operation must be rejected rather
// than silently ignored, mirroring the feature-flag-64 rejection.
let message = pb::Transaction {
read_version: 1,
uuid: Uuid::new_v4().to_string(),
operation: Some(pb::transaction::Operation::DataOverlay(
pb::transaction::DataOverlay { groups: vec![] },
)),
..Default::default()
};
let err = match Transaction::try_from(message) {
Err(err) => err,
Ok(_) => panic!("expected data overlay transaction to be rejected"),
};
assert!(matches!(&err, Error::NotSupported { .. }));
assert!(err
.to_string()
.contains("data overlay files are not supported"));
}
🤖 Prompt for 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.

In `@rust/lance/src/dataset/transaction.rs` around lines 6200 - 6216, Update
test_data_overlay_operation_rejected to capture the returned error and assert it
is Error::NotSupported with a message containing either “data overlay” or the
reader feature flag 64 identifier, ensuring the test validates the specific
rejection reason.

Source: Coding guidelines

}
Loading