feat(overlay): gate overlay writes on an explicit dataset setting - #8018
Merged
Merged
Conversation
Contributor
|
Important This PR touches the Lance format specification. Substantive changes to the format specification — the If this is a meaningful format change:
|
wjones127
force-pushed
the
will/oss-overlay-explicit-enable
branch
from
July 27, 2026 23:05
4e66c4d to
a24f253
Compare
Data overlay files were permitted on any dataset, which leaves nowhere to record that a table wants overlays before its first one exists. That is what a high-level writer (update, merge_insert) needs to consult when deciding whether to store an update as an overlay. Add `lance.overlays.enabled`, a table config key resolved as: an explicit boolean wins; otherwise a dataset already carrying overlays counts as enabled (so datasets written before the key existed stay writable); otherwise the library default, currently off. Enablement lives in config rather than in feature flag 64 so it survives writers that predate overlays, and so an enabled-but-overlay-free dataset stays open to pre-overlay readers. The flag keeps its existing meaning: overlays are present. Disabling is a two-way door, allowed only while no fragment carries an overlay. Both that check and the converse -- no overlay commit while disabled -- are one invariant enforced in `build_manifest`, which re-runs on conflict rebase, so a disable that races a concurrent `DataOverlay` is rejected rather than silently committed. Surfaces: `WriteParams::enable_overlays` (an `Option<bool>`, since the default is expected to flip and a bare bool could not distinguish "off" from "unspecified"), `Dataset::set_overlays_enabled`, and `Dataset::remove_overlays`, which compacts overlaid fragments and nothing else via a new `CompactionOptions::overlays_only`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127
force-pushed
the
will/oss-overlay-explicit-enable
branch
from
July 27, 2026 23:08
a24f253 to
b1e7a7a
Compare
Contributor
Author
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.
Stacked on #8016 — review the last commit only.
Overlays were permitted on any dataset, so there was nowhere to record that a table wants overlays before its first one exists. That's the fact a high-level writer (
update,merge_insert) needs to consult when deciding whether to store an update as an overlay.Where enablement lives
lance.overlays.enabled, a table config key — not feature flag 64. Two reasons:Manifest::new_from_previousclonesconfigverbatim, so a config key round-trips through writers that have never heard of it.reader_feature_flagsis a hard refusal gate. An enabled-but-overlay-free dataset is byte-for-byte readable by a pre-overlay reader, and setting bit 64 on it would exclude them for no correctness reason. This matters more once the default flips: enablement-as-flag would set bit 64 on every newly created table at creation.Flag 64 keeps its existing meaning — overlays are present.
Resolution, and the coming default flip
true/falsewins.DEFAULT_OVERLAYS_ENABLED, currentlyfalse.The plan is to flip step 3 to
truein ~6 months so existing tables get the optimization, which is a one-line change plus deleting clause 2's rationale. Two consequences are baked in deliberately:WriteParams::enable_overlaysisOption<bool>, notbool.rust/CLAUDE.mdsays to name booleans sofalse/Default::default()is the desired default, which makesenable_*: boolwrong once the default is on — and renaming a public field todisable_*later is a break.falseexplicitly rather than relying on absence, since absence is the thing designed to change.Not in this PR, and I'd treat it as a prerequisite for the flip: a session-level default, so a deployment can opt out fleet-wide before upgrading rather than table-by-table after. Resolution order would become explicit config → session default → library default;
overlays_enabled_withis the single place it slots into.The disable guard
One invariant — "disabled implies no overlays" — covers both directions: disabling while overlays remain, and committing a
DataOverlaywhile disabled. It lives inbuild_manifestrather than at the API boundary becausebuild_manifestre-runs on conflict rebase.check_update_config_txnputsUpdateConfigvsDataOverlayin the compatible bucket, so a disable racing an overlay write gets rebased and replayed against the newer manifest, and the check fires.test_disable_racing_a_concurrent_overlay_is_rejectedcovers exactly that path.One subtlety worth a look: enablement is resolved against the pre-commit fragment list (
overlays_enabled_with(&manifest.config, was_overlaid)). Resolving against post-commit fragments would let aDataOverlayauthorize itself — the overlays it's adding would satisfy clause 2.API
WriteParams::enable_overlays: Option<bool>— honored on Create and Overwrite; append leaves the setting alone.Dataset::overlays_enabled()/Dataset::set_overlays_enabled(bool).Dataset::remove_overlays()— compacts overlaid fragments and nothing else. This needed a newCompactionOptions::overlays_only:max_overlays_per_fragment: Some(0)alone still bins small neighbors together and materializes deletions, and the obvious workaround of zeroingtarget_rows_per_fragmentis unsafe because it doubles asmax_rows_per_filefor the rewrite.test_without_overlays_only_the_size_trigger_still_appliespins the contrast.Test churn
Every existing overlay test committed overlays against datasets that never enabled them, so ~80 now correctly fail without opting in. Fixed by setting
enable_overlays: Some(true)in each module's base-dataset helper (fragment.rsoverlay_read,optimize.rs,dataset_overlay_index_masking.rs) and addingoverlay_enabled_manifest()for the onetransaction.rsunit test. That churn is the feature working.New tests: 8 resolution cases +
parse_overlays_enabledin lance-table; 7 e2e indataset_overlay_feature_flag.rs(default-off, enable-at-creation, enablement surviving flag clearing, disable rejected with overlays, remove-then-disable, the rebase race, unparsable value, and a dataset with overlays but no key staying writable); 3 inoptimize.rsforoverlays_only.Testing
cargo fmt --allandcargo clippy --all --tests --benches -- -D warningsclean. Fullcargo test -p lance --libgreen (2619 passed) andcargo test -p lance-table --libgreen (135).cargo checkclean for the python and java crates.🤖 Generated with Claude Code