Skip to main content

lance_table/
feature_flags.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Feature flags
5
6use crate::format::Manifest;
7use lance_core::{Error, Result};
8
9/// Fragments may contain deletion files, which record the tombstones of
10/// soft-deleted rows.
11pub const FLAG_DELETION_FILES: u64 = 1;
12/// Row ids are stable for both moves and updates. Fragments contain an index
13/// mapping row ids to row addresses.
14pub const FLAG_STABLE_ROW_IDS: u64 = 2;
15/// Files are written with the new v2 format (this flag is no longer used)
16pub const FLAG_USE_V2_FORMAT_DEPRECATED: u64 = 4;
17/// Table config is present
18pub const FLAG_TABLE_CONFIG: u64 = 8;
19/// Dataset uses multiple base paths (for shallow clones or multi-base datasets)
20pub const FLAG_BASE_PATHS: u64 = 16;
21/// Disable writing transaction file under _transaction/, this flag is set when we only want to write inline transaction in manifest
22pub const FLAG_DISABLE_TRANSACTION_FILE: u64 = 32;
23/// Fragments contain data overlay files, which supply new values for a subset of
24/// cells without rewriting base data files. A reader that does not understand
25/// overlays must refuse the dataset, since ignoring an overlay would silently
26/// return stale base values.
27///
28/// Data overlay files are not yet a released feature: in release builds this flag
29/// is treated as unknown (so a release reader/writer refuses an overlay dataset)
30/// unless [`ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV`] is set, which lets benchmarks opt in.
31/// Debug builds always understand it so tests exercise the path.
32pub const FLAG_UNSTABLE_DATA_OVERLAY_FILES: u64 = 64;
33/// Some index declares covering columns: `IndexMetadata.covering_fields` names
34/// columns the index carries values for but is not keyed on.
35///
36/// Covering makes `fields` mean "keyed columns followed by carried columns"
37/// rather than "the columns this index is searched on". A reader without this
38/// bit still selects a vector index by testing membership of `fields`, so it
39/// would answer a query on a merely-carried column with an index keyed on a
40/// different column and return wrong neighbours with no error. A writer without
41/// it would maintain the index as though every entry of `fields` were keyed.
42/// Both must refuse the table.
43///
44/// This takes the bit reclaimed from the retired MemWAL index-catchup flag
45/// (<https://github.com/lance-format/lance/pull/8680>), which is the boundary the
46/// current released build treats as unknown -- so that build refuses a covering
47/// dataset without needing a change of its own. Builds from the window where the
48/// bit was allocated to index catch-up (v11.0.0-beta.4 through beta.17) still
49/// count it as supported and will open a covering dataset rather than refuse it;
50/// that exposure comes with the reclamation and is inherited by whichever flag
51/// takes the bit.
52pub const FLAG_COVERED_INDEX_METADATA: u64 = 128;
53/// The first bit that is unknown as a feature flag
54pub const FLAG_UNKNOWN: u64 = 256;
55
56// The highest flag allocated must stay below the unknown boundary, or
57// `supported_flags` would refuse a bit this code claims to understand. The next
58// flag takes 256, so it has to move the boundary to 512 with it.
59const _: () = assert!(FLAG_COVERED_INDEX_METADATA < FLAG_UNKNOWN);
60// The fence needs a bit the current released build already refuses, which means
61// at or above the boundary that build shipped with (128).
62const _: () = assert!(FLAG_COVERED_INDEX_METADATA >= 128);
63
64/// Environment variable that opts a release build into reading and writing data
65/// overlay files before the feature is generally released.
66pub const ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV: &str = "LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES";
67
68/// Set the reader and writer feature flags in the manifest based on the contents of the manifest.
69pub fn apply_feature_flags(
70    manifest: &mut Manifest,
71    enable_stable_row_id: bool,
72    disable_transaction_file: bool,
73) -> Result<()> {
74    // Carried across the reset: a `Manifest` only points at its index section,
75    // so whether any index declares covering columns is not visible here. `build_manifest` decides it from the index list it is
76    // committing and sets the bit after calling this; without the carry the
77    // second call, from `write_manifest_file`, would clear that decision
78    // immediately before the write.
79    let covered_index_metadata = (manifest.reader_feature_flags | manifest.writer_feature_flags)
80        & FLAG_COVERED_INDEX_METADATA;
81
82    // Reset flags
83    manifest.reader_feature_flags = 0;
84    manifest.writer_feature_flags = 0;
85
86    let has_deletion_files = manifest
87        .fragments
88        .iter()
89        .any(|frag| frag.deletion_file.is_some());
90    if has_deletion_files {
91        // Both readers and writers need to be able to read deletion files
92        manifest.reader_feature_flags |= FLAG_DELETION_FILES;
93        manifest.writer_feature_flags |= FLAG_DELETION_FILES;
94    }
95
96    // If any fragment has row ids, they must all have row ids.
97    let has_row_ids = manifest
98        .fragments
99        .iter()
100        .any(|frag| frag.row_id_meta.is_some());
101    if has_row_ids || enable_stable_row_id {
102        if !manifest
103            .fragments
104            .iter()
105            .all(|frag| frag.row_id_meta.is_some())
106        {
107            return Err(Error::invalid_input("All fragments must have row ids"));
108        }
109        manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
110        manifest.writer_feature_flags |= FLAG_STABLE_ROW_IDS;
111    }
112
113    // Test whether any table metadata has been set
114    if !manifest.config.is_empty() {
115        manifest.writer_feature_flags |= FLAG_TABLE_CONFIG;
116    }
117
118    // Check if this dataset uses multiple base paths (for shallow clones or multi-base datasets)
119    if !manifest.base_paths.is_empty() {
120        manifest.reader_feature_flags |= FLAG_BASE_PATHS;
121        manifest.writer_feature_flags |= FLAG_BASE_PATHS;
122    }
123
124    // Overlay files change cell values on read, so a reader that ignores them
125    // would return stale base values. Both readers and writers must understand
126    // them.
127    let has_overlays = manifest
128        .fragments
129        .iter()
130        .any(|frag| !frag.overlays.is_empty());
131    if has_overlays {
132        manifest.reader_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
133        manifest.writer_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
134    }
135
136    if disable_transaction_file {
137        manifest.writer_feature_flags |= FLAG_DISABLE_TRANSACTION_FILE;
138    }
139
140    manifest.reader_feature_flags |= covered_index_metadata;
141    manifest.writer_feature_flags |= covered_index_metadata;
142
143    Ok(())
144}
145
146/// Whether this build understands data overlay files: always in debug builds,
147/// and in release builds only when [`ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV`] is set.
148fn data_overlay_files_enabled() -> bool {
149    cfg!(debug_assertions) || std::env::var_os(ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV).is_some()
150}
151
152/// Clear `flag` from `flags` when its gating feature is not enabled in this
153/// build; leave it set otherwise. One call per unstable flag, so support for
154/// several unstable features chains cleanly.
155fn mark_supported(flags: &mut u64, flag: u64, feature_enabled: bool) {
156    if !feature_enabled {
157        *flags &= !flag;
158    }
159}
160
161/// The feature-flag bits this build understands, given whether overlay support
162/// is enabled. Split out from [`supported_flags`] so the policy is testable
163/// without toggling the build profile or environment.
164fn supported_flags_when(overlay_enabled: bool) -> u64 {
165    let mut supported = FLAG_UNKNOWN - 1;
166    mark_supported(
167        &mut supported,
168        FLAG_UNSTABLE_DATA_OVERLAY_FILES,
169        overlay_enabled,
170    );
171    supported
172}
173
174fn supported_flags() -> u64 {
175    supported_flags_when(data_overlay_files_enabled())
176}
177
178pub fn can_read_dataset(reader_flags: u64) -> bool {
179    reader_flags & !supported_flags() == 0
180}
181
182pub fn can_write_dataset(writer_flags: u64) -> bool {
183    writer_flags & !supported_flags() == 0
184}
185
186pub fn has_deprecated_v2_feature_flag(writer_flags: u64) -> bool {
187    writer_flags & FLAG_USE_V2_FORMAT_DEPRECATED != 0
188}
189
190#[cfg(test)]
191mod tests {
192    /// The covering fence only works if the bit is one the current released
193    /// build already rejects. That build's unknown boundary is 128, so the bit
194    /// has to be 128 and this build has to have moved its own boundary past it
195    /// -- otherwise either that build accepts a covering dataset, or we refuse
196    /// our own.
197    #[test]
198    fn test_covered_index_metadata_fences_older_builds_only() {
199        assert_eq!(
200            FLAG_COVERED_INDEX_METADATA, 128,
201            "the fence must sit on the boundary the released build shipped with"
202        );
203        assert!(
204            can_read_dataset(FLAG_COVERED_INDEX_METADATA),
205            "this build implements covering, so it must accept its own datasets"
206        );
207        assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
208        // A build whose boundary is still 128 refuses the bit, which is the fence;
209        // the module-level `const _` assertion keeps it at or above that boundary.
210    }
211
212    use super::*;
213    use crate::format::BasePath;
214
215    #[test]
216    fn test_read_check() {
217        assert!(can_read_dataset(0));
218        assert!(can_read_dataset(super::FLAG_DELETION_FILES));
219        assert!(can_read_dataset(super::FLAG_STABLE_ROW_IDS));
220        assert!(can_read_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
221        assert!(can_read_dataset(super::FLAG_TABLE_CONFIG));
222        assert!(can_read_dataset(super::FLAG_BASE_PATHS));
223        assert!(can_read_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
224        // Overlay support is gated on the build profile / env opt-in, so the
225        // flag is readable exactly when overlays are enabled (see
226        // test_data_overlay_flag_release_gating for the full policy).
227        assert_eq!(
228            can_read_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
229            data_overlay_files_enabled()
230        );
231        assert!(can_read_dataset(
232            super::FLAG_DELETION_FILES
233                | super::FLAG_STABLE_ROW_IDS
234                | super::FLAG_USE_V2_FORMAT_DEPRECATED
235        ));
236        assert!(!can_read_dataset(super::FLAG_UNKNOWN));
237    }
238
239    #[test]
240    fn test_data_overlay_flag_release_gating() {
241        // Release default (overlays disabled): the overlay flag is treated as
242        // unknown so the dataset is refused, while other known flags still pass.
243        let supported = supported_flags_when(false);
244        assert_eq!(supported & FLAG_UNSTABLE_DATA_OVERLAY_FILES, 0);
245        assert_eq!(FLAG_DELETION_FILES & !supported, 0);
246        assert_ne!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
247        // Enabled (debug or env opt-in): the overlay flag is understood.
248        let supported = supported_flags_when(true);
249        assert_eq!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
250    }
251
252    #[test]
253    fn test_apply_feature_flags_sets_overlay_flag() {
254        use crate::format::overlay::{DataOverlayFile, OverlayCoverage};
255        use crate::format::{DataFile, DataStorageFormat, Fragment};
256        use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
257        use lance_core::datatypes::Schema;
258        use roaring::RoaringBitmap;
259        use std::collections::HashMap;
260        use std::sync::Arc;
261
262        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
263            "id",
264            arrow_schema::DataType::Int64,
265            false,
266        )]);
267        let schema = Schema::try_from(&arrow_schema).unwrap();
268        let mut fragment = Fragment::new(0);
269        fragment.overlays = vec![DataOverlayFile {
270            data_file: DataFile::new_legacy_from_fields("o.lance", vec![0], None),
271            coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
272            committed_version: 1,
273        }];
274        let mut manifest = Manifest::new(
275            schema,
276            Arc::new(vec![fragment]),
277            DataStorageFormat::default(),
278            HashMap::new(),
279        );
280        apply_feature_flags(&mut manifest, false, false).unwrap();
281        assert_ne!(
282            manifest.reader_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
283            0
284        );
285        assert_ne!(
286            manifest.writer_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
287            0
288        );
289    }
290
291    #[test]
292    fn test_write_check() {
293        assert!(can_write_dataset(0));
294        assert!(can_write_dataset(super::FLAG_DELETION_FILES));
295        assert!(can_write_dataset(super::FLAG_STABLE_ROW_IDS));
296        assert!(can_write_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
297        assert!(can_write_dataset(super::FLAG_TABLE_CONFIG));
298        assert!(can_write_dataset(super::FLAG_BASE_PATHS));
299        assert!(can_write_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
300        // Overlay support is gated on the build profile / env opt-in, so the
301        // flag is writable exactly when overlays are enabled (see
302        // test_data_overlay_flag_release_gating for the full policy).
303        assert_eq!(
304            can_write_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
305            data_overlay_files_enabled()
306        );
307        assert!(can_write_dataset(
308            super::FLAG_DELETION_FILES
309                | super::FLAG_STABLE_ROW_IDS
310                | super::FLAG_USE_V2_FORMAT_DEPRECATED
311                | super::FLAG_TABLE_CONFIG
312                | super::FLAG_BASE_PATHS
313        ));
314        assert!(!can_write_dataset(super::FLAG_UNKNOWN));
315    }
316
317    #[test]
318    fn test_base_paths_feature_flags() {
319        use crate::format::{DataStorageFormat, Manifest};
320        use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
321        use lance_core::datatypes::Schema;
322        use std::collections::HashMap;
323        use std::sync::Arc;
324        // Create a basic schema for testing
325        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
326            "test_field",
327            arrow_schema::DataType::Int64,
328            false,
329        )]);
330        let schema = Schema::try_from(&arrow_schema).unwrap();
331        // Test 1: Normal dataset (no base_paths) should not have FLAG_BASE_PATHS
332        let mut normal_manifest = Manifest::new(
333            schema.clone(),
334            Arc::new(vec![]),
335            DataStorageFormat::default(),
336            HashMap::new(), // Empty base_paths
337        );
338        apply_feature_flags(&mut normal_manifest, false, false).unwrap();
339        assert_eq!(normal_manifest.reader_feature_flags & FLAG_BASE_PATHS, 0);
340        assert_eq!(normal_manifest.writer_feature_flags & FLAG_BASE_PATHS, 0);
341        // Test 2: Dataset with base_paths (shallow clone or multi-base) should have FLAG_BASE_PATHS
342        let mut base_paths: HashMap<u32, BasePath> = HashMap::new();
343        base_paths.insert(
344            1,
345            BasePath::new(
346                1,
347                "file:///path/to/original".to_string(),
348                Some("test_ref".to_string()),
349                true,
350            ),
351        );
352        let mut multi_base_manifest = Manifest::new(
353            schema,
354            Arc::new(vec![]),
355            DataStorageFormat::default(),
356            base_paths,
357        );
358        apply_feature_flags(&mut multi_base_manifest, false, false).unwrap();
359        assert_ne!(
360            multi_base_manifest.reader_feature_flags & FLAG_BASE_PATHS,
361            0
362        );
363        assert_ne!(
364            multi_base_manifest.writer_feature_flags & FLAG_BASE_PATHS,
365            0
366        );
367    }
368}