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, datatypes::Schema};
8
9/// Fragments may contain deletion files, which record the tombstones of
10/// soft-deleted rows.
11pub const FLAG_DELETION_FILES: u64 = 1 << 0;
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 = 1 << 1;
15/// Files are written with the new v2 format (this flag is no longer used)
16pub const FLAG_USE_V2_FORMAT_DEPRECATED: u64 = 1 << 2;
17/// Table config is present
18pub const FLAG_TABLE_CONFIG: u64 = 1 << 3;
19/// Dataset uses multiple base paths (for shallow clones or multi-base datasets)
20pub const FLAG_BASE_PATHS: u64 = 1 << 4;
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 = 1 << 5;
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 = 1 << 6;
33/// Some index declares covering columns: `IndexMetadata.covering_fields` names
34/// columns the index carries values for, whether or not it is also keyed on
35/// them.
36///
37/// Without [`FLAG_INDEPENDENT_COVERING_FIELDS`], the carried columns form the
38/// trailing suffix of `fields` in emission order. A reader without this bit still selects
39/// a vector index by testing membership of `fields`, so it would answer a query
40/// on a merely-carried column with an index keyed on a different column and
41/// return wrong neighbours with no error. A writer without it would maintain the
42/// index as though every entry of `fields` were keyed. 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 = 1 << 7;
53/// A dataset may reference recognized V2 data files with different exact
54/// versions. Readers and writers must both understand the per-file version
55/// contract before either can safely access the dataset.
56pub const FLAG_MIXED_DATA_FILE_VERSIONS: u64 = 1 << 8;
57/// The table uses stable row ids and carries a fragment reuse index.
58///
59/// Understood only by debug builds until support is released (see
60/// [`frag_reuse_with_stable_row_ids_enabled`]).
61pub const FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS: u64 = 1 << 9;
62/// Tagged FRI requires a reader that interprets its mappings and a writer that
63/// preserves them during maintenance. Legacy-only FRI does not set this bit.
64/// Bit 9 is taken by the stable-row-id FRI compatibility flag.
65pub const FLAG_FRAGMENT_REUSE_INDEX: u64 = 1 << 10;
66/// Some fragment stores a row lineage sequence -- its row ids, or its
67/// created-at or last-updated-at versions -- as a hidden column of a data file
68/// rather than inline in the manifest (`RowIdMeta::Column`,
69/// `RowDatasetVersionMeta::Column`).
70///
71/// A reader without this bit sees an unset `row_id_sequence` oneof and would
72/// take the fragment to have no row ids at all, on a table whose manifest says
73/// every fragment has them; a writer without it would carry the fragment
74/// forward and drop the sequence. Both must refuse the table.
75///
76/// Supported in every build. Table configuration controls whether a writer
77/// spills sequences; readers must understand existing spilled sequences even
78/// when spilling is disabled for subsequent writes.
79///
80/// ```
81/// use lance_table::feature_flags::{FLAG_SPILLED_ROW_LINEAGE, can_read_dataset};
82///
83/// assert!(can_read_dataset(FLAG_SPILLED_ROW_LINEAGE));
84/// ```
85pub const FLAG_SPILLED_ROW_LINEAGE: u64 = 1 << 11;
86/// Deprecated name for [`FLAG_SPILLED_ROW_LINEAGE`].
87#[deprecated(since = "14.0.0", note = "use FLAG_SPILLED_ROW_LINEAGE instead")]
88pub const FLAG_UNSTABLE_SPILLED_ROW_LINEAGE: u64 = FLAG_SPILLED_ROW_LINEAGE;
89/// Reserved for fragment trees. Readers and writers reject it until implemented.
90///
91/// Bit 11 is spilled row lineage, so this takes the next free bit.
92pub const FLAG_FRAGMENT_TREE: u64 = 1 << 12;
93/// Index key fields and covering fields are declared independently.
94///
95/// When this flag is set, `IndexMetadata.fields` contains only the columns the
96/// index is keyed on and `IndexMetadata.covering_fields` separately contains
97/// the columns whose values the index carries. A field may occur in both lists.
98/// The flag requires [`FLAG_COVERED_INDEX_METADATA`] in both feature words, and
99/// the two are retained together while the independent bit is set.
100///
101/// Reserved ahead of its implementation. This build masks the bit out in
102/// `supported_flags_when`, so it cannot open a table and apply the legacy
103/// suffix contract to independent declarations.
104pub const FLAG_INDEPENDENT_COVERING_FIELDS: u64 = 1 << 13;
105/// Blob v2 descriptors may independently address Lance-owned objects. Readers
106/// must resolve their explicit bases and writers/GC must preserve those references.
107/// This capability is sticky, including across restore, and requires both words.
108pub const FLAG_MANAGED_BLOBS: u64 = 1 << 14;
109
110/// Schema metadata that explicitly opts writes and compaction into Managed blobs.
111/// Set `lance.blob.managed` to `true` to activate the sticky table capability.
112/// This requires Managed-aware readers and maintenance clients thereafter.
113pub const MANAGED_BLOBS_META_KEY: &str = "lance.blob.managed";
114
115/// Field IDs are allocated from a persistent high-water mark and are never reused.
116/// Writers must understand this allocation contract. It does not change how
117/// readers interpret the schema or data files.
118pub const FLAG_NON_REUSABLE_FIELD_IDS: u64 = 1 << 15;
119/// Required for data files containing version 1 zone-level statistics seeds.
120/// Their min/max exclude NaNs, which are counted separately by sign. Set in
121/// both reader and writer feature flags and preserve across commits.
122///
123/// Reserved: support for these seeds is not implemented yet.
124pub const FLAG_ZONE_WRITE_SEEDS: u64 = 1 << 16;
125/// Writer-only protection for clustering definitions, ID allocation, references,
126/// and generic fragment/index segment metadata maintenance.
127/// Reserved and unsupported until writers implement its maintenance rules.
128/// Ordinary readers can ignore this metadata; all covered features share this one bit.
129pub const FLAG_OBJECT_METADATA: u64 = 1 << 17;
130/// The first bit that is unknown as a feature flag
131pub const FLAG_UNKNOWN: u64 = 1 << 18;
132
133const _: () = assert!(FLAG_COVERED_INDEX_METADATA < FLAG_UNKNOWN);
134// The fence needs a bit the current released build already refuses, which means
135// at or above the boundary that build shipped with (bit 7).
136const _: () = assert!(FLAG_COVERED_INDEX_METADATA >= 1 << 7);
137const _: () = assert!(FLAG_MIXED_DATA_FILE_VERSIONS < FLAG_UNKNOWN);
138const _: () = assert!(FLAG_NON_REUSABLE_FIELD_IDS < FLAG_UNKNOWN);
139// Same fence for this bit: v12.0.0 refuses bit 9 and up.
140const _: () = assert!(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS >= 1 << 9);
141const _: () = assert!(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS < FLAG_UNKNOWN);
142const _: () = assert!(FLAG_FRAGMENT_REUSE_INDEX < FLAG_UNKNOWN);
143const _: () = assert!(FLAG_SPILLED_ROW_LINEAGE < FLAG_UNKNOWN);
144const _: () = assert!(FLAG_FRAGMENT_TREE < FLAG_UNKNOWN);
145const _: () = assert!(FLAG_INDEPENDENT_COVERING_FIELDS < FLAG_UNKNOWN);
146
147const _: () = assert!(FLAG_MANAGED_BLOBS < FLAG_UNKNOWN);
148const _: () = assert!(FLAG_ZONE_WRITE_SEEDS < FLAG_UNKNOWN);
149const _: () = assert!(FLAG_OBJECT_METADATA < FLAG_UNKNOWN);
150
151pub(crate) const STICKY_PAIRED_FLAGS: u64 = FLAG_MIXED_DATA_FILE_VERSIONS
152    | FLAG_FRAGMENT_REUSE_INDEX
153    | FLAG_MANAGED_BLOBS
154    | FLAG_ZONE_WRITE_SEEDS;
155pub(crate) const STICKY_READER_FLAGS: u64 = STICKY_PAIRED_FLAGS;
156pub(crate) const STICKY_WRITER_FLAGS: u64 = STICKY_PAIRED_FLAGS | FLAG_NON_REUSABLE_FIELD_IDS;
157
158/// Environment variable that opts a release build into reading and writing data
159/// overlay files before the feature is generally released.
160pub const ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV: &str = "LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES";
161
162/// Set the reader and writer feature flags in the manifest based on the contents of the manifest.
163pub fn apply_feature_flags(
164    manifest: &mut Manifest,
165    enable_stable_row_id: bool,
166    disable_transaction_file: bool,
167) -> Result<()> {
168    // Carried across the reset: a `Manifest` only points at its index section,
169    // so whether any index declares covering columns is not visible here.
170    // `build_manifest` decides it from the index list it is committing and sets
171    // the bit after calling this; without the carry the second call, from
172    // `write_manifest_file`, would clear that decision immediately before the
173    // write.
174    let covered_index_metadata = (manifest.reader_feature_flags | manifest.writer_feature_flags)
175        & FLAG_COVERED_INDEX_METADATA;
176    let sticky_paired_flags = validated_sticky_paired_flags(manifest)?;
177    let non_reusable_field_ids = manifest.max_allocated_field_id.is_some();
178    if non_reusable_field_ids {
179        manifest.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
180    }
181    validate_non_reusable_field_id_flags(manifest)?;
182    // Reset flags
183    manifest.reader_feature_flags = 0;
184    manifest.writer_feature_flags = 0;
185
186    let has_deletion_files = manifest
187        .fragments
188        .iter()
189        .any(|frag| frag.deletion_file.is_some());
190    if has_deletion_files {
191        // Both readers and writers need to be able to read deletion files
192        manifest.reader_feature_flags |= FLAG_DELETION_FILES;
193        manifest.writer_feature_flags |= FLAG_DELETION_FILES;
194    }
195
196    // If any fragment has row ids, they must all have row ids.
197    let has_row_ids = manifest
198        .fragments
199        .iter()
200        .any(|frag| frag.row_id_meta.is_some());
201    if has_row_ids || enable_stable_row_id {
202        if !manifest
203            .fragments
204            .iter()
205            .all(|frag| frag.row_id_meta.is_some())
206        {
207            return Err(Error::invalid_input("All fragments must have row ids"));
208        }
209        manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
210        manifest.writer_feature_flags |= FLAG_STABLE_ROW_IDS;
211    }
212
213    // Test whether any table metadata has been set
214    if !manifest.config.is_empty() {
215        manifest.writer_feature_flags |= FLAG_TABLE_CONFIG;
216    }
217
218    // Check if this dataset uses multiple base paths (for shallow clones or multi-base datasets)
219    if !manifest.base_paths.is_empty() {
220        manifest.reader_feature_flags |= FLAG_BASE_PATHS;
221        manifest.writer_feature_flags |= FLAG_BASE_PATHS;
222    }
223
224    // Overlay files change cell values on read, so a reader that ignores them
225    // would return stale base values. Both readers and writers must understand
226    // them.
227    let has_overlays = manifest
228        .fragments
229        .iter()
230        .any(|frag| !frag.overlays.is_empty());
231    if has_overlays {
232        manifest.reader_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
233        manifest.writer_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
234    }
235
236    let has_spilled_row_lineage = manifest
237        .fragments
238        .iter()
239        .any(|frag| frag.has_spilled_row_lineage());
240    if has_spilled_row_lineage {
241        manifest.reader_feature_flags |= FLAG_SPILLED_ROW_LINEAGE;
242        manifest.writer_feature_flags |= FLAG_SPILLED_ROW_LINEAGE;
243    }
244
245    if disable_transaction_file {
246        manifest.writer_feature_flags |= FLAG_DISABLE_TRANSACTION_FILE;
247    }
248
249    if non_reusable_field_ids {
250        manifest.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
251    }
252
253    manifest.reader_feature_flags |= covered_index_metadata;
254    manifest.writer_feature_flags |= covered_index_metadata;
255    manifest.reader_feature_flags |= sticky_paired_flags;
256    manifest.writer_feature_flags |= sticky_paired_flags;
257
258    Ok(())
259}
260
261/// Carry sticky capabilities from the manifest a new one is derived from.
262///
263/// [`apply_feature_flags`] carries these bits across its own reset, but it only
264/// ever sees one manifest. Constructors preserve these flags, and this helper
265/// also validates that the source is not half-set before a derived manifest is
266/// committed.
267///
268/// Non-reusable field IDs are activated explicitly and only require writer support.
269pub fn inherit_sticky_feature_flags(destination: &mut Manifest, source: &Manifest) -> Result<()> {
270    let sticky_flags = validated_sticky_paired_flags(source)?;
271    validate_non_reusable_field_id_flags(source)?;
272    destination.reader_feature_flags |= sticky_flags;
273    destination.writer_feature_flags |=
274        sticky_flags | (source.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS);
275    Ok(())
276}
277
278/// Whether a schema explicitly requests Managed blob storage. Absent or `false`
279/// keeps the existing Inline/Packed/Dedicated storage layout, in all build profiles.
280///
281/// ```
282/// use lance_core::datatypes::Schema;
283/// use lance_table::feature_flags::{MANAGED_BLOBS_META_KEY, managed_blobs_enabled};
284/// let mut schema = Schema::default();
285/// assert!(!managed_blobs_enabled(&schema)?);
286/// schema.metadata.insert(MANAGED_BLOBS_META_KEY.to_string(), "true".to_string());
287/// assert!(managed_blobs_enabled(&schema)?);
288/// # Ok::<(), lance_core::Error>(())
289/// ```
290pub fn managed_blobs_enabled(schema: &Schema) -> Result<bool> {
291    match schema
292        .metadata
293        .get(MANAGED_BLOBS_META_KEY)
294        .map(String::as_str)
295    {
296        None | Some("false") => Ok(false),
297        Some("true") => Ok(true),
298        Some(value) => Err(Error::invalid_input(format!(
299            "Schema metadata {MANAGED_BLOBS_META_KEY} must be true or false, got {value:?}"
300        ))),
301    }
302}
303
304/// Whether this build understands data overlay files: always in debug builds,
305/// and in release builds only when [`ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV`] is set.
306fn data_overlay_files_enabled() -> bool {
307    cfg!(debug_assertions) || std::env::var_os(ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV).is_some()
308}
309
310/// Debug builds only, with no environment override, until continuous recording
311/// and retention land. Hidden because it is removed then.
312#[doc(hidden)]
313pub fn frag_reuse_with_stable_row_ids_enabled() -> bool {
314    cfg!(debug_assertions)
315}
316
317/// Clear `flag` from `flags` when its gating feature is not enabled in this
318/// build; leave it set otherwise. One call per unstable flag, so support for
319/// several unstable features chains cleanly.
320fn mark_supported(flags: &mut u64, flag: u64, feature_enabled: bool) {
321    if !feature_enabled {
322        *flags &= !flag;
323    }
324}
325
326/// The feature-flag bits this build understands, given which unstable features
327/// are enabled. Split out from [`supported_flags`] so the policy is testable
328/// without toggling the build profile or environment.
329fn supported_flags_when(
330    overlay_enabled: bool,
331    frag_reuse_with_stable_row_ids_enabled: bool,
332) -> u64 {
333    let mut supported = FLAG_UNKNOWN - 1;
334    mark_supported(
335        &mut supported,
336        FLAG_UNSTABLE_DATA_OVERLAY_FILES,
337        overlay_enabled,
338    );
339    mark_supported(
340        &mut supported,
341        FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS,
342        frag_reuse_with_stable_row_ids_enabled,
343    );
344    // Reserved, not implemented: see the flag's doc comment.
345    mark_supported(&mut supported, FLAG_FRAGMENT_TREE, false);
346    mark_supported(&mut supported, FLAG_INDEPENDENT_COVERING_FIELDS, false);
347    mark_supported(&mut supported, FLAG_ZONE_WRITE_SEEDS, false);
348    mark_supported(&mut supported, FLAG_OBJECT_METADATA, false);
349    supported
350}
351
352fn supported_flags() -> u64 {
353    supported_flags_when(
354        data_overlay_files_enabled(),
355        frag_reuse_with_stable_row_ids_enabled(),
356    )
357}
358
359pub fn can_read_dataset(reader_flags: u64) -> bool {
360    reader_flags & !supported_flags() == 0
361}
362
363pub fn can_write_dataset(writer_flags: u64) -> bool {
364    writer_flags & !supported_flags() == 0
365}
366
367/// Refuse reads from manifests whose required reader features this build does
368/// not support or whose paired capabilities are inconsistent.
369pub fn ensure_can_read_manifest(manifest: &Manifest) -> Result<()> {
370    validate_paired_feature_flags(manifest)?;
371    validate_non_reusable_field_id_flags(manifest)?;
372    if !can_read_dataset(manifest.reader_feature_flags) {
373        return Err(Error::not_supported_source(
374            format!(
375                "This dataset cannot be read by this version of Lance. Please upgrade \
376                 Lance to read this dataset. Flags: {}",
377                manifest.reader_feature_flags
378            )
379            .into(),
380        ));
381    }
382    Ok(())
383}
384
385/// Refuse writes to manifests whose required writer features this build does
386/// not support or whose paired capabilities are inconsistent.
387pub fn ensure_can_write_manifest(manifest: &Manifest) -> Result<()> {
388    validate_paired_feature_flags(manifest)?;
389    validate_non_reusable_field_id_flags(manifest)?;
390    if !can_write_dataset(manifest.writer_feature_flags) {
391        return Err(Error::not_supported_source(
392            format!(
393                "This dataset cannot be written by this version of Lance. Please upgrade \
394                 Lance to write this dataset. Flags: {}",
395                manifest.writer_feature_flags
396            )
397            .into(),
398        ));
399    }
400    Ok(())
401}
402
403pub fn has_deprecated_v2_feature_flag(writer_flags: u64) -> bool {
404    writer_flags & FLAG_USE_V2_FORMAT_DEPRECATED != 0
405}
406
407/// Refuse a manifest whose paired reader and writer capability bits disagree.
408///
409/// One word set and the other not is neither mode: it would let a legacy reader
410/// or a legacy writer through on a table where the other half is enforcing. The
411/// commit path refuses to *produce* this, so seeing it on read means the
412/// manifest was written by something that did not.
413pub fn validate_paired_feature_flags(manifest: &Manifest) -> Result<()> {
414    if (manifest.reader_feature_flags ^ manifest.writer_feature_flags) & FLAG_FRAGMENT_REUSE_INDEX
415        != 0
416    {
417        return Err(Error::corrupt_file_named(
418            "manifest",
419            "FRI requires both reader and writer feature flags",
420        ));
421    }
422
423    for (flag, name) in [
424        (FLAG_MIXED_DATA_FILE_VERSIONS, "mixed data-file-version"),
425        (FLAG_MANAGED_BLOBS, "Managed Blob"),
426        (FLAG_ZONE_WRITE_SEEDS, "zone write seed"),
427    ] {
428        let reader = manifest.reader_feature_flags & flag != 0;
429        let writer = manifest.writer_feature_flags & flag != 0;
430        if reader != writer {
431            return Err(Error::corrupt_file_named(
432                "manifest",
433                format!(
434                    "Manifest has only one of the {name} reader and writer feature bits set, so its semantics are undefined"
435                ),
436            ));
437        }
438    }
439    Ok(())
440}
441
442/// Refuse a manifest whose non-reusable-field-ID marker and required flags disagree.
443///
444/// The high-water mark is the activation marker and always requires the writer
445/// bit. Non-reusable field IDs do not change read semantics, so the reader bit is not
446/// a valid activation mode.
447pub fn validate_non_reusable_field_id_flags(manifest: &Manifest) -> Result<()> {
448    let activated = manifest.max_allocated_field_id.is_some();
449    let reader = manifest.reader_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS != 0;
450    let writer = manifest.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS != 0;
451    if activated != writer {
452        return Err(Error::corrupt_file_named(
453            "manifest",
454            "Manifest non-reusable-field-ID high-water mark and writer feature flag disagree",
455        ));
456    }
457    if reader {
458        return Err(Error::corrupt_file_named(
459            "manifest",
460            "Manifest has a non-reusable-field-ID reader feature flag, but non-reusable field IDs only require writer support",
461        ));
462    }
463    Ok(())
464}
465
466fn validated_sticky_paired_flags(manifest: &Manifest) -> Result<u64> {
467    validate_paired_feature_flags(manifest)?;
468    Ok(manifest.reader_feature_flags & STICKY_PAIRED_FLAGS)
469}
470
471#[cfg(test)]
472mod tests {
473    #[rstest::rstest]
474    #[case::absent(None, false)]
475    #[case::disabled(Some("false"), false)]
476    #[case::enabled(Some("true"), true)]
477    fn managed_blobs_require_explicit_opt_in(#[case] value: Option<&str>, #[case] enabled: bool) {
478        let mut schema = empty_manifest().schema;
479        if let Some(value) = value {
480            schema
481                .metadata
482                .insert(MANAGED_BLOBS_META_KEY.to_string(), value.to_string());
483        }
484        assert_eq!(managed_blobs_enabled(&schema).unwrap(), enabled);
485    }
486
487    #[test]
488    fn managed_blobs_reject_invalid_opt_in() {
489        let mut schema = empty_manifest().schema;
490        schema
491            .metadata
492            .insert(MANAGED_BLOBS_META_KEY.to_string(), "invalid".to_string());
493        let error = managed_blobs_enabled(&schema).unwrap_err();
494        assert!(matches!(error, Error::InvalidInput { .. }));
495        assert!(
496            error
497                .to_string()
498                .contains("lance.blob.managed must be true or false")
499        );
500    }
501
502    #[test]
503    fn tagged_fri_flag_is_supported_and_sticky() {
504        let flag = super::FLAG_FRAGMENT_REUSE_INDEX;
505        assert!(super::can_read_dataset(flag));
506        assert!(super::can_write_dataset(flag));
507        let mut manifest = empty_manifest();
508        manifest.reader_feature_flags = flag;
509        assert!(super::ensure_can_read_manifest(&manifest).is_err());
510        manifest.writer_feature_flags = flag;
511        super::apply_feature_flags(&mut manifest, false, false).unwrap();
512        assert_eq!(manifest.reader_feature_flags & flag, flag);
513        assert_eq!(manifest.writer_feature_flags & flag, flag);
514    }
515
516    /// The covering fence only works if the bit is one the current released
517    /// build already rejects. That build's unknown boundary is 128, so the bit
518    /// has to be 128 and this build has to have moved its own boundary past it
519    /// -- otherwise either that build accepts a covering dataset, or we refuse
520    /// our own.
521    #[test]
522    fn test_covered_index_metadata_fences_older_builds_only() {
523        assert_eq!(
524            FLAG_COVERED_INDEX_METADATA, 128,
525            "the fence must sit on the boundary the released build shipped with"
526        );
527        assert!(
528            can_read_dataset(FLAG_COVERED_INDEX_METADATA),
529            "this build implements covering, so it must accept its own datasets"
530        );
531        assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
532        // A build whose boundary is still 128 refuses the bit, which is the fence;
533        // the module-level `const _` assertion keeps it at or above that boundary.
534    }
535
536    use super::*;
537    use crate::format::BasePath;
538
539    #[test]
540    fn test_object_metadata_flag_fences_writes_without_blocking_reads() {
541        let mut manifest = empty_manifest();
542        manifest.writer_feature_flags = FLAG_OBJECT_METADATA;
543        ensure_can_read_manifest(&manifest).unwrap();
544        let error = ensure_can_write_manifest(&manifest).unwrap_err();
545        assert!(matches!(error, Error::NotSupported { .. }));
546        assert!(error.to_string().contains("cannot be written"));
547        assert!(!can_read_dataset(FLAG_OBJECT_METADATA));
548        assert!(!can_write_dataset(FLAG_OBJECT_METADATA));
549        assert_eq!(FLAG_OBJECT_METADATA, 1 << 17);
550        // Neither reserved bit is supported, and bits above them remain unknown.
551        assert!(!can_read_dataset(FLAG_ZONE_WRITE_SEEDS));
552        assert!(!can_write_dataset(FLAG_ZONE_WRITE_SEEDS));
553        assert_eq!(FLAG_UNKNOWN, 1 << 18);
554        assert!(!can_read_dataset(FLAG_UNKNOWN));
555        assert!(!can_write_dataset(FLAG_UNKNOWN));
556    }
557
558    #[test]
559    fn test_zone_write_seed_flag_is_reserved_and_sticky() {
560        assert_eq!(FLAG_ZONE_WRITE_SEEDS, 1 << 16);
561        assert!(!can_read_dataset(FLAG_ZONE_WRITE_SEEDS));
562        assert!(!can_write_dataset(FLAG_ZONE_WRITE_SEEDS));
563
564        let mut manifest = empty_manifest();
565        manifest.reader_feature_flags = FLAG_ZONE_WRITE_SEEDS;
566        manifest.writer_feature_flags = FLAG_ZONE_WRITE_SEEDS;
567        apply_feature_flags(&mut manifest, false, false).unwrap();
568        assert_eq!(manifest.reader_feature_flags, FLAG_ZONE_WRITE_SEEDS);
569        assert_eq!(manifest.writer_feature_flags, FLAG_ZONE_WRITE_SEEDS);
570
571        let read_error = ensure_can_read_manifest(&manifest).unwrap_err();
572        assert!(matches!(read_error, Error::NotSupported { .. }));
573        assert!(read_error.to_string().contains("Please upgrade"));
574        let write_error = ensure_can_write_manifest(&manifest).unwrap_err();
575        assert!(matches!(write_error, Error::NotSupported { .. }));
576        assert!(write_error.to_string().contains("Please upgrade"));
577
578        let mut destination = empty_manifest();
579        inherit_sticky_feature_flags(&mut destination, &manifest).unwrap();
580        assert_eq!(destination.reader_feature_flags, FLAG_ZONE_WRITE_SEEDS);
581        assert_eq!(destination.writer_feature_flags, FLAG_ZONE_WRITE_SEEDS);
582    }
583
584    #[test]
585    fn test_zone_write_seeds_require_both_feature_bits() {
586        for (reader, writer) in [(true, false), (false, true)] {
587            let mut manifest = empty_manifest();
588            manifest.reader_feature_flags = if reader { FLAG_ZONE_WRITE_SEEDS } else { 0 };
589            manifest.writer_feature_flags = if writer { FLAG_ZONE_WRITE_SEEDS } else { 0 };
590            let error = validate_paired_feature_flags(&manifest).unwrap_err();
591            assert!(matches!(error, Error::CorruptFile { .. }));
592            assert!(error.to_string().contains("zone write seed"));
593        }
594    }
595
596    #[test]
597    fn test_fragment_tree_flag_is_reserved_not_supported() {
598        assert_eq!(FLAG_FRAGMENT_TREE, 4096);
599        assert_eq!(
600            FLAG_FRAGMENT_TREE & (FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS | FLAG_FRAGMENT_REUSE_INDEX),
601            0
602        );
603        assert!(!can_read_dataset(FLAG_FRAGMENT_TREE));
604        assert!(!can_write_dataset(FLAG_FRAGMENT_TREE));
605    }
606
607    #[test]
608    fn test_frag_reuse_with_stable_row_ids_flag_release_gating() {
609        // v12.0.0's unknown boundary, so every released client refuses it.
610        assert_eq!(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS, 512);
611        let without_support = supported_flags_when(true, false);
612        assert_ne!(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS & !without_support, 0);
613        assert_eq!(FLAG_STABLE_ROW_IDS & !without_support, 0);
614        let enabled = supported_flags_when(false, true);
615        assert_eq!(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS & !enabled, 0);
616        for unrelated in [
617            FLAG_UNSTABLE_DATA_OVERLAY_FILES,
618            FLAG_FRAGMENT_TREE,
619            FLAG_UNKNOWN,
620        ] {
621            assert_ne!(unrelated & !enabled, 0);
622        }
623        assert_eq!(
624            can_read_dataset(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS),
625            frag_reuse_with_stable_row_ids_enabled()
626        );
627        assert_eq!(
628            can_write_dataset(FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS),
629            frag_reuse_with_stable_row_ids_enabled()
630        );
631    }
632
633    #[test]
634    fn test_independent_covering_fields_flag_is_reserved() {
635        assert_eq!(FLAG_INDEPENDENT_COVERING_FIELDS, 8192);
636        let flags = FLAG_COVERED_INDEX_METADATA | FLAG_INDEPENDENT_COVERING_FIELDS;
637        assert!(!can_read_dataset(flags));
638        assert!(!can_write_dataset(flags));
639    }
640
641    #[test]
642    fn test_read_check() {
643        assert!(can_read_dataset(0));
644        assert!(can_read_dataset(super::FLAG_DELETION_FILES));
645        assert!(can_read_dataset(super::FLAG_STABLE_ROW_IDS));
646        assert!(can_read_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
647        assert!(can_read_dataset(super::FLAG_TABLE_CONFIG));
648        assert!(can_read_dataset(super::FLAG_BASE_PATHS));
649        assert!(can_read_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
650        assert!(can_read_dataset(super::FLAG_NON_REUSABLE_FIELD_IDS));
651        assert!(can_read_dataset(super::FLAG_MIXED_DATA_FILE_VERSIONS));
652        // Overlay support is gated on the build profile / env opt-in, so the
653        // flag is readable exactly when overlays are enabled (see
654        // test_data_overlay_flag_release_gating for the full policy).
655        assert_eq!(
656            can_read_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
657            data_overlay_files_enabled()
658        );
659        assert!(can_read_dataset(
660            super::FLAG_DELETION_FILES
661                | super::FLAG_STABLE_ROW_IDS
662                | super::FLAG_USE_V2_FORMAT_DEPRECATED
663        ));
664        assert!(!can_read_dataset(super::FLAG_UNKNOWN));
665    }
666
667    #[test]
668    fn test_data_overlay_flag_release_gating() {
669        // Release default (overlays disabled): the overlay flag is treated as
670        // unknown so the dataset is refused, while other known flags still pass.
671        let supported = supported_flags_when(false, false);
672        assert_eq!(supported & FLAG_UNSTABLE_DATA_OVERLAY_FILES, 0);
673        assert_eq!(FLAG_DELETION_FILES & !supported, 0);
674        assert_ne!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
675        // Enabled (debug or env opt-in): the overlay flag is understood.
676        let supported = supported_flags_when(true, false);
677        assert_eq!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
678    }
679
680    #[test]
681    fn test_spilled_row_lineage_flag_is_stable() {
682        assert_eq!(FLAG_SPILLED_ROW_LINEAGE, 2048);
683        // Support must not depend on enabling unrelated experimental formats.
684        let supported = supported_flags_when(false, false);
685        let flags = FLAG_STABLE_ROW_IDS | FLAG_SPILLED_ROW_LINEAGE;
686        assert_eq!(flags & !supported, 0);
687        assert!(can_read_dataset(flags));
688        assert!(can_write_dataset(flags));
689        for unsupported in [
690            FLAG_UNSTABLE_DATA_OVERLAY_FILES,
691            FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS,
692            FLAG_FRAGMENT_TREE,
693            FLAG_UNKNOWN,
694        ] {
695            assert_ne!(unsupported & !supported, 0);
696        }
697    }
698
699    #[test]
700    fn test_apply_feature_flags_sets_overlay_flag() {
701        use crate::format::overlay::{DataOverlayFile, OverlayCoverage};
702        use crate::format::{DataFile, DataStorageFormat, Fragment};
703        use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
704        use lance_core::datatypes::Schema;
705        use roaring::RoaringBitmap;
706        use std::collections::HashMap;
707        use std::sync::Arc;
708
709        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
710            "id",
711            arrow_schema::DataType::Int64,
712            false,
713        )]);
714        let schema = Schema::try_from(&arrow_schema).unwrap();
715        let mut fragment = Fragment::new(0);
716        fragment.overlays = vec![DataOverlayFile {
717            data_file: DataFile::new_legacy_from_fields("o.lance", vec![0], None),
718            coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
719            committed_version: 1,
720        }];
721        let mut manifest = Manifest::new(
722            schema,
723            Arc::new(vec![fragment]),
724            DataStorageFormat::default(),
725            HashMap::new(),
726        );
727        apply_feature_flags(&mut manifest, false, false).unwrap();
728        assert_ne!(
729            manifest.reader_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
730            0
731        );
732        assert_ne!(
733            manifest.writer_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
734            0
735        );
736    }
737
738    #[test]
739    fn test_apply_feature_flags_sets_spilled_row_lineage_flag() {
740        use crate::format::{DataFile, DataStorageFormat, Fragment, ROW_ID_FIELD_ID, RowIdMeta};
741        use crate::rowids::version::RowDatasetVersionMeta;
742        use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
743        use lance_core::datatypes::Schema;
744        use std::collections::HashMap;
745        use std::sync::Arc;
746
747        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
748            "id",
749            arrow_schema::DataType::Int64,
750            false,
751        )]);
752        let schema = Schema::try_from(&arrow_schema).unwrap();
753        let spilled =
754            DataFile::new_legacy_from_fields("lineage.lance", vec![ROW_ID_FIELD_ID], None);
755        // Each of the three sequences on its own is enough to require the flag.
756        let mut spilled_fragments = Vec::new();
757        for kind in 0..3 {
758            let mut fragment = Fragment::new(kind);
759            fragment.files.push(spilled.clone());
760            // Every fragment of a stable-row-id table carries row ids; the
761            // version-only cases keep theirs inline.
762            fragment.row_id_meta = Some(RowIdMeta::Inline(vec![].into()));
763            match kind {
764                0 => fragment.row_id_meta = Some(RowIdMeta::Column),
765                1 => fragment.created_at_version_meta = Some(RowDatasetVersionMeta::Column),
766                _ => fragment.last_updated_at_version_meta = Some(RowDatasetVersionMeta::Column),
767            }
768            spilled_fragments.push(fragment);
769        }
770        for fragment in spilled_fragments {
771            let mut manifest = Manifest::new(
772                schema.clone(),
773                Arc::new(vec![fragment]),
774                DataStorageFormat::default(),
775                HashMap::new(),
776            );
777            apply_feature_flags(&mut manifest, true, false).unwrap();
778            assert_ne!(manifest.reader_feature_flags & FLAG_SPILLED_ROW_LINEAGE, 0);
779            assert_ne!(manifest.writer_feature_flags & FLAG_SPILLED_ROW_LINEAGE, 0);
780        }
781
782        // An inline sequence does not, so a table whose sequences all fit in
783        // the manifest stays readable by builds without the flag.
784        let mut fragment = Fragment::new(0);
785        fragment.row_id_meta = Some(RowIdMeta::Inline(vec![].into()));
786        let mut manifest = Manifest::new(
787            schema,
788            Arc::new(vec![fragment]),
789            DataStorageFormat::default(),
790            HashMap::new(),
791        );
792        apply_feature_flags(&mut manifest, true, false).unwrap();
793        assert_eq!(manifest.reader_feature_flags & FLAG_SPILLED_ROW_LINEAGE, 0);
794    }
795
796    #[test]
797    fn test_write_check() {
798        assert!(can_write_dataset(0));
799        assert!(can_write_dataset(super::FLAG_DELETION_FILES));
800        assert!(can_write_dataset(super::FLAG_STABLE_ROW_IDS));
801        assert!(can_write_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
802        assert!(can_write_dataset(super::FLAG_TABLE_CONFIG));
803        assert!(can_write_dataset(super::FLAG_BASE_PATHS));
804        assert!(can_write_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
805        assert!(can_write_dataset(super::FLAG_NON_REUSABLE_FIELD_IDS));
806        assert!(can_write_dataset(super::FLAG_MIXED_DATA_FILE_VERSIONS));
807        // Overlay support is gated on the build profile / env opt-in, so the
808        // flag is writable exactly when overlays are enabled (see
809        // test_data_overlay_flag_release_gating for the full policy).
810        assert_eq!(
811            can_write_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
812            data_overlay_files_enabled()
813        );
814        assert!(can_write_dataset(
815            super::FLAG_DELETION_FILES
816                | super::FLAG_STABLE_ROW_IDS
817                | super::FLAG_USE_V2_FORMAT_DEPRECATED
818                | super::FLAG_TABLE_CONFIG
819                | super::FLAG_BASE_PATHS
820        ));
821        assert!(!can_write_dataset(super::FLAG_UNKNOWN));
822    }
823
824    #[test]
825    fn test_base_paths_feature_flags() {
826        use crate::format::{DataStorageFormat, Manifest};
827        use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
828        use lance_core::datatypes::Schema;
829        use std::collections::HashMap;
830        use std::sync::Arc;
831        // Create a basic schema for testing
832        let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
833            "test_field",
834            arrow_schema::DataType::Int64,
835            false,
836        )]);
837        let schema = Schema::try_from(&arrow_schema).unwrap();
838        // Test 1: Normal dataset (no base_paths) should not have FLAG_BASE_PATHS
839        let mut normal_manifest = Manifest::new(
840            schema.clone(),
841            Arc::new(vec![]),
842            DataStorageFormat::default(),
843            HashMap::new(), // Empty base_paths
844        );
845        apply_feature_flags(&mut normal_manifest, false, false).unwrap();
846        assert_eq!(normal_manifest.reader_feature_flags & FLAG_BASE_PATHS, 0);
847        assert_eq!(normal_manifest.writer_feature_flags & FLAG_BASE_PATHS, 0);
848        // Test 2: Dataset with base_paths (shallow clone or multi-base) should have FLAG_BASE_PATHS
849        let mut base_paths: HashMap<u32, BasePath> = HashMap::new();
850        base_paths.insert(
851            1,
852            BasePath::new(
853                1,
854                "file:///path/to/original".to_string(),
855                Some("test_ref".to_string()),
856                true,
857            ),
858        );
859        let mut multi_base_manifest = Manifest::new(
860            schema,
861            Arc::new(vec![]),
862            DataStorageFormat::default(),
863            base_paths,
864        );
865        apply_feature_flags(&mut multi_base_manifest, false, false).unwrap();
866        assert_ne!(
867            multi_base_manifest.reader_feature_flags & FLAG_BASE_PATHS,
868            0
869        );
870        assert_ne!(
871            multi_base_manifest.writer_feature_flags & FLAG_BASE_PATHS,
872            0
873        );
874    }
875    #[test]
876    fn inheriting_carries_sticky_paired_bits_from_the_source() {
877        let mut source = empty_manifest();
878        source.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
879        source.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
880        // A fresh destination models any derived manifest before inheritance.
881        let mut destination = empty_manifest();
882
883        inherit_sticky_feature_flags(&mut destination, &source).unwrap();
884
885        assert_ne!(
886            destination.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
887            0
888        );
889        assert_ne!(
890            destination.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
891            0
892        );
893    }
894
895    #[test]
896    fn inheriting_preserves_non_reusable_field_id_writer_gate() {
897        let mut source = empty_manifest();
898        source.activate_non_reusable_field_ids();
899        source.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
900        let mut destination = empty_manifest();
901
902        inherit_sticky_feature_flags(&mut destination, &source).unwrap();
903
904        assert_eq!(
905            destination.reader_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
906            0
907        );
908        assert_ne!(
909            destination.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
910            0
911        );
912    }
913
914    #[test]
915    fn inheriting_refuses_a_half_set_source() {
916        for (reader, writer) in [
917            (FLAG_MIXED_DATA_FILE_VERSIONS, 0),
918            (0, FLAG_MIXED_DATA_FILE_VERSIONS),
919        ] {
920            let mut source = empty_manifest();
921            source.reader_feature_flags = reader;
922            source.writer_feature_flags = writer;
923            let mut destination = empty_manifest();
924
925            let err = inherit_sticky_feature_flags(&mut destination, &source).unwrap_err();
926
927            assert!(err.to_string().contains("only one of"), "{err}");
928        }
929    }
930
931    #[test]
932    fn apply_feature_flags_carries_sticky_paired_bits_across_its_reset() {
933        let mut manifest = empty_manifest();
934        manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
935        manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
936
937        apply_feature_flags(&mut manifest, false, false).unwrap();
938
939        assert_ne!(
940            manifest.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
941            0
942        );
943        assert_ne!(
944            manifest.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
945            0
946        );
947    }
948
949    #[test]
950    fn apply_feature_flags_rejects_half_set_sticky_bits() {
951        let mut manifest = empty_manifest();
952        manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
953
954        let err = apply_feature_flags(&mut manifest, false, false).unwrap_err();
955
956        assert!(matches!(err, Error::CorruptFile { .. }));
957        assert!(err.to_string().contains("only one of"), "{err}");
958    }
959
960    #[test]
961    fn paired_validation_rejects_half_set_mixed_version_capability() {
962        for (reader, writer) in [
963            (FLAG_MIXED_DATA_FILE_VERSIONS, 0),
964            (0, FLAG_MIXED_DATA_FILE_VERSIONS),
965        ] {
966            let mut manifest = empty_manifest();
967            manifest.reader_feature_flags = reader;
968            manifest.writer_feature_flags = writer;
969
970            let err = validate_paired_feature_flags(&manifest).unwrap_err();
971
972            assert!(err.to_string().contains("mixed data-file-version"), "{err}");
973        }
974    }
975
976    #[test]
977    fn writer_gate_accepts_mixed_capability_and_rejects_unknown_flags() {
978        let mut manifest = empty_manifest();
979        manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
980        manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
981        ensure_can_write_manifest(&manifest).unwrap();
982
983        manifest.writer_feature_flags |= FLAG_UNKNOWN;
984        let err = ensure_can_write_manifest(&manifest).unwrap_err();
985        assert!(matches!(err, Error::NotSupported { .. }));
986        assert!(err.to_string().contains("cannot be written"), "{err}");
987    }
988
989    #[rstest::rstest]
990    fn apply_feature_flags_sets_writer_gate_for_explicit_non_reusable_field_id_activation(
991        #[values(false, true)] managed_blobs: bool,
992    ) {
993        let mut manifest = empty_manifest();
994        let managed_blob_flags = if managed_blobs { FLAG_MANAGED_BLOBS } else { 0 };
995        manifest.reader_feature_flags = managed_blob_flags;
996        manifest.writer_feature_flags = managed_blob_flags;
997        manifest.activate_non_reusable_field_ids();
998
999        apply_feature_flags(&mut manifest, false, false).unwrap();
1000
1001        assert_eq!(
1002            manifest.reader_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
1003            0
1004        );
1005        assert_ne!(
1006            manifest.writer_feature_flags & FLAG_NON_REUSABLE_FIELD_IDS,
1007            0
1008        );
1009        assert_eq!(
1010            manifest.reader_feature_flags & FLAG_MANAGED_BLOBS,
1011            managed_blob_flags
1012        );
1013        assert_eq!(
1014            manifest.writer_feature_flags & FLAG_MANAGED_BLOBS,
1015            managed_blob_flags
1016        );
1017    }
1018
1019    #[test]
1020    fn apply_feature_flags_rejects_non_reusable_field_id_reader_flag() {
1021        let mut manifest = empty_manifest();
1022        manifest.activate_non_reusable_field_ids();
1023        manifest.reader_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1024        manifest.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1025
1026        let err = apply_feature_flags(&mut manifest, false, false).unwrap_err();
1027
1028        assert!(
1029            err.to_string().contains("only require writer support"),
1030            "{err}"
1031        );
1032    }
1033
1034    #[test]
1035    fn non_reusable_field_id_marker_and_writer_gate_must_agree() {
1036        let mut activated_without_gate = empty_manifest();
1037        activated_without_gate.activate_non_reusable_field_ids();
1038        assert!(validate_non_reusable_field_id_flags(&activated_without_gate).is_err());
1039
1040        let mut gate_without_marker = empty_manifest();
1041        gate_without_marker.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1042        assert!(validate_non_reusable_field_id_flags(&gate_without_marker).is_err());
1043
1044        let mut writer_only = empty_manifest();
1045        writer_only.activate_non_reusable_field_ids();
1046        writer_only.writer_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1047        validate_non_reusable_field_id_flags(&writer_only).unwrap();
1048
1049        let mut paired = writer_only.clone();
1050        paired.reader_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1051        assert!(validate_non_reusable_field_id_flags(&paired).is_err());
1052
1053        let mut reader_without_activation = empty_manifest();
1054        reader_without_activation.reader_feature_flags |= FLAG_NON_REUSABLE_FIELD_IDS;
1055        assert!(validate_non_reusable_field_id_flags(&reader_without_activation).is_err());
1056    }
1057
1058    #[rstest::rstest]
1059    #[case::reader_only(true, false)]
1060    #[case::writer_only(false, true)]
1061    #[case::paired(true, true)]
1062    fn managed_capability_is_paired_and_sticky(#[case] reader: bool, #[case] writer: bool) {
1063        let mut source = empty_manifest();
1064        source.reader_feature_flags = if reader { FLAG_MANAGED_BLOBS } else { 0 };
1065        source.writer_feature_flags = if writer { FLAG_MANAGED_BLOBS } else { 0 };
1066        if reader != writer {
1067            for error in [
1068                ensure_can_read_manifest(&source).unwrap_err(),
1069                ensure_can_write_manifest(&source).unwrap_err(),
1070            ] {
1071                assert!(matches!(error, Error::CorruptFile { .. }));
1072                assert!(error.to_string().contains("Managed Blob"));
1073            }
1074            return;
1075        }
1076        ensure_can_read_manifest(&source).unwrap();
1077        ensure_can_write_manifest(&source).unwrap();
1078        let mut destination = empty_manifest();
1079        inherit_sticky_feature_flags(&mut destination, &source).unwrap();
1080        apply_feature_flags(&mut destination, false, false).unwrap();
1081        assert!(destination.has_managed_blobs());
1082        assert_eq!(
1083            destination.writer_feature_flags & FLAG_MANAGED_BLOBS,
1084            FLAG_MANAGED_BLOBS
1085        );
1086        // The released v11.0.0 client only accepts bits below 128.
1087        assert_ne!(destination.reader_feature_flags & !(128 - 1), 0);
1088    }
1089
1090    fn empty_manifest() -> Manifest {
1091        use crate::format::DataStorageFormat;
1092        use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1093        use lance_core::datatypes::Schema;
1094        use std::collections::HashMap;
1095        use std::sync::Arc;
1096
1097        let arrow_schema = ArrowSchema::new(vec![ArrowField::new("i", DataType::Int32, false)]);
1098        Manifest::new(
1099            Schema::try_from(&arrow_schema).unwrap(),
1100            Arc::new(vec![]),
1101            DataStorageFormat::default(),
1102            HashMap::new(),
1103        )
1104    }
1105
1106    #[test]
1107    fn paired_capabilities_are_below_the_unknown_boundary() {
1108        assert!(can_read_dataset(FLAG_COVERED_INDEX_METADATA));
1109        assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
1110        assert!(can_read_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
1111        assert!(can_write_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
1112        assert!(can_read_dataset(FLAG_MANAGED_BLOBS));
1113        assert!(can_write_dataset(FLAG_MANAGED_BLOBS));
1114        assert!(!can_read_dataset(FLAG_UNKNOWN));
1115        assert!(!can_write_dataset(FLAG_UNKNOWN));
1116    }
1117}