1use crate::format::Manifest;
7use lance_core::{Error, Result};
8
9pub const FLAG_DELETION_FILES: u64 = 1 << 0;
12pub const FLAG_STABLE_ROW_IDS: u64 = 1 << 1;
15pub const FLAG_USE_V2_FORMAT_DEPRECATED: u64 = 1 << 2;
17pub const FLAG_TABLE_CONFIG: u64 = 1 << 3;
19pub const FLAG_BASE_PATHS: u64 = 1 << 4;
21pub const FLAG_DISABLE_TRANSACTION_FILE: u64 = 1 << 5;
23pub const FLAG_UNSTABLE_DATA_OVERLAY_FILES: u64 = 1 << 6;
33pub const FLAG_COVERED_INDEX_METADATA: u64 = 1 << 7;
53pub const FLAG_MIXED_DATA_FILE_VERSIONS: u64 = 1 << 8;
57pub const FLAG_UNKNOWN: u64 = 1 << 9;
59
60const _: () = assert!(FLAG_COVERED_INDEX_METADATA < FLAG_UNKNOWN);
61const _: () = assert!(FLAG_COVERED_INDEX_METADATA >= 1 << 7);
64const _: () = assert!(FLAG_MIXED_DATA_FILE_VERSIONS < FLAG_UNKNOWN);
65
66pub(crate) const STICKY_PAIRED_FLAGS: u64 = FLAG_MIXED_DATA_FILE_VERSIONS;
67
68pub const ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV: &str = "LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES";
71
72pub fn apply_feature_flags(
74 manifest: &mut Manifest,
75 enable_stable_row_id: bool,
76 disable_transaction_file: bool,
77) -> Result<()> {
78 let covered_index_metadata = (manifest.reader_feature_flags | manifest.writer_feature_flags)
84 & FLAG_COVERED_INDEX_METADATA;
85 let sticky_paired_flags = validated_sticky_paired_flags(manifest)?;
86
87 manifest.reader_feature_flags = 0;
89 manifest.writer_feature_flags = 0;
90
91 let has_deletion_files = manifest
92 .fragments
93 .iter()
94 .any(|frag| frag.deletion_file.is_some());
95 if has_deletion_files {
96 manifest.reader_feature_flags |= FLAG_DELETION_FILES;
98 manifest.writer_feature_flags |= FLAG_DELETION_FILES;
99 }
100
101 let has_row_ids = manifest
103 .fragments
104 .iter()
105 .any(|frag| frag.row_id_meta.is_some());
106 if has_row_ids || enable_stable_row_id {
107 if !manifest
108 .fragments
109 .iter()
110 .all(|frag| frag.row_id_meta.is_some())
111 {
112 return Err(Error::invalid_input("All fragments must have row ids"));
113 }
114 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
115 manifest.writer_feature_flags |= FLAG_STABLE_ROW_IDS;
116 }
117
118 if !manifest.config.is_empty() {
120 manifest.writer_feature_flags |= FLAG_TABLE_CONFIG;
121 }
122
123 if !manifest.base_paths.is_empty() {
125 manifest.reader_feature_flags |= FLAG_BASE_PATHS;
126 manifest.writer_feature_flags |= FLAG_BASE_PATHS;
127 }
128
129 let has_overlays = manifest
133 .fragments
134 .iter()
135 .any(|frag| !frag.overlays.is_empty());
136 if has_overlays {
137 manifest.reader_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
138 manifest.writer_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
139 }
140
141 if disable_transaction_file {
142 manifest.writer_feature_flags |= FLAG_DISABLE_TRANSACTION_FILE;
143 }
144
145 manifest.reader_feature_flags |= covered_index_metadata;
146 manifest.writer_feature_flags |= covered_index_metadata;
147 manifest.reader_feature_flags |= sticky_paired_flags;
148 manifest.writer_feature_flags |= sticky_paired_flags;
149
150 Ok(())
151}
152
153pub fn inherit_sticky_feature_flags(destination: &mut Manifest, source: &Manifest) -> Result<()> {
164 let sticky_flags = validated_sticky_paired_flags(source)?;
165 destination.reader_feature_flags |= sticky_flags;
166 destination.writer_feature_flags |= sticky_flags;
167 Ok(())
168}
169
170fn data_overlay_files_enabled() -> bool {
173 cfg!(debug_assertions) || std::env::var_os(ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV).is_some()
174}
175
176fn mark_supported(flags: &mut u64, flag: u64, feature_enabled: bool) {
180 if !feature_enabled {
181 *flags &= !flag;
182 }
183}
184
185fn supported_flags_when(overlay_enabled: bool) -> u64 {
189 let mut supported = FLAG_UNKNOWN - 1;
190 mark_supported(
191 &mut supported,
192 FLAG_UNSTABLE_DATA_OVERLAY_FILES,
193 overlay_enabled,
194 );
195 supported
196}
197
198fn supported_flags() -> u64 {
199 supported_flags_when(data_overlay_files_enabled())
200}
201
202pub fn can_read_dataset(reader_flags: u64) -> bool {
203 reader_flags & !supported_flags() == 0
204}
205
206pub fn can_write_dataset(writer_flags: u64) -> bool {
207 writer_flags & !supported_flags() == 0
208}
209
210pub fn ensure_can_read_manifest(manifest: &Manifest) -> Result<()> {
213 validate_paired_feature_flags(manifest)?;
214 if !can_read_dataset(manifest.reader_feature_flags) {
215 return Err(Error::not_supported_source(
216 format!(
217 "This dataset cannot be read by this version of Lance. Please upgrade \
218 Lance to read this dataset. Flags: {}",
219 manifest.reader_feature_flags
220 )
221 .into(),
222 ));
223 }
224 Ok(())
225}
226
227pub fn ensure_can_write_manifest(manifest: &Manifest) -> Result<()> {
230 validate_paired_feature_flags(manifest)?;
231 if !can_write_dataset(manifest.writer_feature_flags) {
232 return Err(Error::not_supported_source(
233 format!(
234 "This dataset cannot be written by this version of Lance. Please upgrade \
235 Lance to write this dataset. Flags: {}",
236 manifest.writer_feature_flags
237 )
238 .into(),
239 ));
240 }
241 Ok(())
242}
243
244pub fn has_deprecated_v2_feature_flag(writer_flags: u64) -> bool {
245 writer_flags & FLAG_USE_V2_FORMAT_DEPRECATED != 0
246}
247
248pub fn validate_paired_feature_flags(manifest: &Manifest) -> Result<()> {
255 let reader = manifest.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS != 0;
256 let writer = manifest.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS != 0;
257 if reader != writer {
258 return Err(Error::corrupt_file_named(
259 "manifest",
260 "Manifest has only one of the mixed data-file-version reader and writer feature bits set, \
261 so its semantics are undefined",
262 ));
263 }
264 Ok(())
265}
266
267fn validated_sticky_paired_flags(manifest: &Manifest) -> Result<u64> {
268 validate_paired_feature_flags(manifest)?;
269 Ok(manifest.reader_feature_flags & STICKY_PAIRED_FLAGS)
270}
271
272#[cfg(test)]
273mod tests {
274 #[test]
280 fn test_covered_index_metadata_fences_older_builds_only() {
281 assert_eq!(
282 FLAG_COVERED_INDEX_METADATA, 128,
283 "the fence must sit on the boundary the released build shipped with"
284 );
285 assert!(
286 can_read_dataset(FLAG_COVERED_INDEX_METADATA),
287 "this build implements covering, so it must accept its own datasets"
288 );
289 assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
290 }
293
294 use super::*;
295 use crate::format::BasePath;
296
297 #[test]
298 fn test_read_check() {
299 assert!(can_read_dataset(0));
300 assert!(can_read_dataset(super::FLAG_DELETION_FILES));
301 assert!(can_read_dataset(super::FLAG_STABLE_ROW_IDS));
302 assert!(can_read_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
303 assert!(can_read_dataset(super::FLAG_TABLE_CONFIG));
304 assert!(can_read_dataset(super::FLAG_BASE_PATHS));
305 assert!(can_read_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
306 assert_eq!(
310 can_read_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
311 data_overlay_files_enabled()
312 );
313 assert!(can_read_dataset(
314 super::FLAG_DELETION_FILES
315 | super::FLAG_STABLE_ROW_IDS
316 | super::FLAG_USE_V2_FORMAT_DEPRECATED
317 ));
318 assert!(!can_read_dataset(super::FLAG_UNKNOWN));
319 }
320
321 #[test]
322 fn test_data_overlay_flag_release_gating() {
323 let supported = supported_flags_when(false);
326 assert_eq!(supported & FLAG_UNSTABLE_DATA_OVERLAY_FILES, 0);
327 assert_eq!(FLAG_DELETION_FILES & !supported, 0);
328 assert_ne!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
329 let supported = supported_flags_when(true);
331 assert_eq!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
332 }
333
334 #[test]
335 fn test_apply_feature_flags_sets_overlay_flag() {
336 use crate::format::overlay::{DataOverlayFile, OverlayCoverage};
337 use crate::format::{DataFile, DataStorageFormat, Fragment};
338 use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
339 use lance_core::datatypes::Schema;
340 use roaring::RoaringBitmap;
341 use std::collections::HashMap;
342 use std::sync::Arc;
343
344 let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
345 "id",
346 arrow_schema::DataType::Int64,
347 false,
348 )]);
349 let schema = Schema::try_from(&arrow_schema).unwrap();
350 let mut fragment = Fragment::new(0);
351 fragment.overlays = vec![DataOverlayFile {
352 data_file: DataFile::new_legacy_from_fields("o.lance", vec![0], None),
353 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
354 committed_version: 1,
355 }];
356 let mut manifest = Manifest::new(
357 schema,
358 Arc::new(vec![fragment]),
359 DataStorageFormat::default(),
360 HashMap::new(),
361 );
362 apply_feature_flags(&mut manifest, false, false).unwrap();
363 assert_ne!(
364 manifest.reader_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
365 0
366 );
367 assert_ne!(
368 manifest.writer_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
369 0
370 );
371 }
372
373 #[test]
374 fn test_write_check() {
375 assert!(can_write_dataset(0));
376 assert!(can_write_dataset(super::FLAG_DELETION_FILES));
377 assert!(can_write_dataset(super::FLAG_STABLE_ROW_IDS));
378 assert!(can_write_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
379 assert!(can_write_dataset(super::FLAG_TABLE_CONFIG));
380 assert!(can_write_dataset(super::FLAG_BASE_PATHS));
381 assert!(can_write_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
382 assert_eq!(
386 can_write_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
387 data_overlay_files_enabled()
388 );
389 assert!(can_write_dataset(
390 super::FLAG_DELETION_FILES
391 | super::FLAG_STABLE_ROW_IDS
392 | super::FLAG_USE_V2_FORMAT_DEPRECATED
393 | super::FLAG_TABLE_CONFIG
394 | super::FLAG_BASE_PATHS
395 ));
396 assert!(!can_write_dataset(super::FLAG_UNKNOWN));
397 }
398
399 #[test]
400 fn test_base_paths_feature_flags() {
401 use crate::format::{DataStorageFormat, Manifest};
402 use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
403 use lance_core::datatypes::Schema;
404 use std::collections::HashMap;
405 use std::sync::Arc;
406 let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
408 "test_field",
409 arrow_schema::DataType::Int64,
410 false,
411 )]);
412 let schema = Schema::try_from(&arrow_schema).unwrap();
413 let mut normal_manifest = Manifest::new(
415 schema.clone(),
416 Arc::new(vec![]),
417 DataStorageFormat::default(),
418 HashMap::new(), );
420 apply_feature_flags(&mut normal_manifest, false, false).unwrap();
421 assert_eq!(normal_manifest.reader_feature_flags & FLAG_BASE_PATHS, 0);
422 assert_eq!(normal_manifest.writer_feature_flags & FLAG_BASE_PATHS, 0);
423 let mut base_paths: HashMap<u32, BasePath> = HashMap::new();
425 base_paths.insert(
426 1,
427 BasePath::new(
428 1,
429 "file:///path/to/original".to_string(),
430 Some("test_ref".to_string()),
431 true,
432 ),
433 );
434 let mut multi_base_manifest = Manifest::new(
435 schema,
436 Arc::new(vec![]),
437 DataStorageFormat::default(),
438 base_paths,
439 );
440 apply_feature_flags(&mut multi_base_manifest, false, false).unwrap();
441 assert_ne!(
442 multi_base_manifest.reader_feature_flags & FLAG_BASE_PATHS,
443 0
444 );
445 assert_ne!(
446 multi_base_manifest.writer_feature_flags & FLAG_BASE_PATHS,
447 0
448 );
449 }
450 #[test]
451 fn inheriting_carries_sticky_paired_bits_from_the_source() {
452 let mut source = empty_manifest();
453 source.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
454 source.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
455 let mut destination = empty_manifest();
457
458 inherit_sticky_feature_flags(&mut destination, &source).unwrap();
459
460 assert_ne!(
461 destination.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
462 0
463 );
464 assert_ne!(
465 destination.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
466 0
467 );
468 }
469
470 #[test]
471 fn inheriting_refuses_a_half_set_source() {
472 for (reader, writer) in [
473 (FLAG_MIXED_DATA_FILE_VERSIONS, 0),
474 (0, FLAG_MIXED_DATA_FILE_VERSIONS),
475 ] {
476 let mut source = empty_manifest();
477 source.reader_feature_flags = reader;
478 source.writer_feature_flags = writer;
479 let mut destination = empty_manifest();
480
481 let err = inherit_sticky_feature_flags(&mut destination, &source).unwrap_err();
482
483 assert!(err.to_string().contains("only one of"), "{err}");
484 }
485 }
486
487 #[test]
488 fn apply_feature_flags_carries_sticky_paired_bits_across_its_reset() {
489 let mut manifest = empty_manifest();
490 manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
491 manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
492
493 apply_feature_flags(&mut manifest, false, false).unwrap();
494
495 assert_ne!(
496 manifest.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
497 0
498 );
499 assert_ne!(
500 manifest.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
501 0
502 );
503 }
504
505 #[test]
506 fn apply_feature_flags_rejects_half_set_sticky_bits() {
507 let mut manifest = empty_manifest();
508 manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
509
510 let err = apply_feature_flags(&mut manifest, false, false).unwrap_err();
511
512 assert!(matches!(err, Error::CorruptFile { .. }));
513 assert!(err.to_string().contains("only one of"), "{err}");
514 }
515
516 #[test]
517 fn paired_validation_rejects_half_set_mixed_version_capability() {
518 for (reader, writer) in [
519 (FLAG_MIXED_DATA_FILE_VERSIONS, 0),
520 (0, FLAG_MIXED_DATA_FILE_VERSIONS),
521 ] {
522 let mut manifest = empty_manifest();
523 manifest.reader_feature_flags = reader;
524 manifest.writer_feature_flags = writer;
525
526 let err = validate_paired_feature_flags(&manifest).unwrap_err();
527
528 assert!(err.to_string().contains("mixed data-file-version"), "{err}");
529 }
530 }
531
532 #[test]
533 fn writer_gate_accepts_mixed_capability_and_rejects_unknown_flags() {
534 let mut manifest = empty_manifest();
535 manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
536 manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
537 ensure_can_write_manifest(&manifest).unwrap();
538
539 manifest.writer_feature_flags |= FLAG_UNKNOWN;
540 let err = ensure_can_write_manifest(&manifest).unwrap_err();
541 assert!(matches!(err, Error::NotSupported { .. }));
542 assert!(err.to_string().contains("cannot be written"), "{err}");
543 }
544
545 fn empty_manifest() -> Manifest {
546 use crate::format::DataStorageFormat;
547 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
548 use lance_core::datatypes::Schema;
549 use std::collections::HashMap;
550 use std::sync::Arc;
551
552 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("i", DataType::Int32, false)]);
553 Manifest::new(
554 Schema::try_from(&arrow_schema).unwrap(),
555 Arc::new(vec![]),
556 DataStorageFormat::default(),
557 HashMap::new(),
558 )
559 }
560
561 #[test]
562 fn mixed_capability_is_below_the_unknown_boundary() {
563 assert!(can_read_dataset(FLAG_COVERED_INDEX_METADATA));
564 assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
565 assert!(can_read_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
566 assert!(can_write_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
567 assert!(!can_read_dataset(FLAG_UNKNOWN));
568 }
569}