use crate::format::Manifest;
use lance_core::{Error, Result};
pub const FLAG_DELETION_FILES: u64 = 1 << 0;
pub const FLAG_STABLE_ROW_IDS: u64 = 1 << 1;
pub const FLAG_USE_V2_FORMAT_DEPRECATED: u64 = 1 << 2;
pub const FLAG_TABLE_CONFIG: u64 = 1 << 3;
pub const FLAG_BASE_PATHS: u64 = 1 << 4;
pub const FLAG_DISABLE_TRANSACTION_FILE: u64 = 1 << 5;
pub const FLAG_UNSTABLE_DATA_OVERLAY_FILES: u64 = 1 << 6;
pub const FLAG_COVERED_INDEX_METADATA: u64 = 1 << 7;
pub const FLAG_MIXED_DATA_FILE_VERSIONS: u64 = 1 << 8;
pub const FLAG_UNKNOWN: u64 = 1 << 9;
const _: () = assert!(FLAG_COVERED_INDEX_METADATA < FLAG_UNKNOWN);
const _: () = assert!(FLAG_COVERED_INDEX_METADATA >= 1 << 7);
const _: () = assert!(FLAG_MIXED_DATA_FILE_VERSIONS < FLAG_UNKNOWN);
pub(crate) const STICKY_PAIRED_FLAGS: u64 = FLAG_MIXED_DATA_FILE_VERSIONS;
pub const ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV: &str = "LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES";
pub fn apply_feature_flags(
manifest: &mut Manifest,
enable_stable_row_id: bool,
disable_transaction_file: bool,
) -> Result<()> {
let covered_index_metadata = (manifest.reader_feature_flags | manifest.writer_feature_flags)
& FLAG_COVERED_INDEX_METADATA;
let sticky_paired_flags = validated_sticky_paired_flags(manifest)?;
manifest.reader_feature_flags = 0;
manifest.writer_feature_flags = 0;
let has_deletion_files = manifest
.fragments
.iter()
.any(|frag| frag.deletion_file.is_some());
if has_deletion_files {
manifest.reader_feature_flags |= FLAG_DELETION_FILES;
manifest.writer_feature_flags |= FLAG_DELETION_FILES;
}
let has_row_ids = manifest
.fragments
.iter()
.any(|frag| frag.row_id_meta.is_some());
if has_row_ids || enable_stable_row_id {
if !manifest
.fragments
.iter()
.all(|frag| frag.row_id_meta.is_some())
{
return Err(Error::invalid_input("All fragments must have row ids"));
}
manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
manifest.writer_feature_flags |= FLAG_STABLE_ROW_IDS;
}
if !manifest.config.is_empty() {
manifest.writer_feature_flags |= FLAG_TABLE_CONFIG;
}
if !manifest.base_paths.is_empty() {
manifest.reader_feature_flags |= FLAG_BASE_PATHS;
manifest.writer_feature_flags |= FLAG_BASE_PATHS;
}
let has_overlays = manifest
.fragments
.iter()
.any(|frag| !frag.overlays.is_empty());
if has_overlays {
manifest.reader_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
manifest.writer_feature_flags |= FLAG_UNSTABLE_DATA_OVERLAY_FILES;
}
if disable_transaction_file {
manifest.writer_feature_flags |= FLAG_DISABLE_TRANSACTION_FILE;
}
manifest.reader_feature_flags |= covered_index_metadata;
manifest.writer_feature_flags |= covered_index_metadata;
manifest.reader_feature_flags |= sticky_paired_flags;
manifest.writer_feature_flags |= sticky_paired_flags;
Ok(())
}
pub fn inherit_sticky_feature_flags(destination: &mut Manifest, source: &Manifest) -> Result<()> {
let sticky_flags = validated_sticky_paired_flags(source)?;
destination.reader_feature_flags |= sticky_flags;
destination.writer_feature_flags |= sticky_flags;
Ok(())
}
fn data_overlay_files_enabled() -> bool {
cfg!(debug_assertions) || std::env::var_os(ENABLE_UNSTABLE_DATA_OVERLAY_FILES_ENV).is_some()
}
fn mark_supported(flags: &mut u64, flag: u64, feature_enabled: bool) {
if !feature_enabled {
*flags &= !flag;
}
}
fn supported_flags_when(overlay_enabled: bool) -> u64 {
let mut supported = FLAG_UNKNOWN - 1;
mark_supported(
&mut supported,
FLAG_UNSTABLE_DATA_OVERLAY_FILES,
overlay_enabled,
);
supported
}
fn supported_flags() -> u64 {
supported_flags_when(data_overlay_files_enabled())
}
pub fn can_read_dataset(reader_flags: u64) -> bool {
reader_flags & !supported_flags() == 0
}
pub fn can_write_dataset(writer_flags: u64) -> bool {
writer_flags & !supported_flags() == 0
}
pub fn ensure_can_read_manifest(manifest: &Manifest) -> Result<()> {
validate_paired_feature_flags(manifest)?;
if !can_read_dataset(manifest.reader_feature_flags) {
return Err(Error::not_supported_source(
format!(
"This dataset cannot be read by this version of Lance. Please upgrade \
Lance to read this dataset. Flags: {}",
manifest.reader_feature_flags
)
.into(),
));
}
Ok(())
}
pub fn ensure_can_write_manifest(manifest: &Manifest) -> Result<()> {
validate_paired_feature_flags(manifest)?;
if !can_write_dataset(manifest.writer_feature_flags) {
return Err(Error::not_supported_source(
format!(
"This dataset cannot be written by this version of Lance. Please upgrade \
Lance to write this dataset. Flags: {}",
manifest.writer_feature_flags
)
.into(),
));
}
Ok(())
}
pub fn has_deprecated_v2_feature_flag(writer_flags: u64) -> bool {
writer_flags & FLAG_USE_V2_FORMAT_DEPRECATED != 0
}
pub fn validate_paired_feature_flags(manifest: &Manifest) -> Result<()> {
let reader = manifest.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS != 0;
let writer = manifest.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS != 0;
if reader != writer {
return Err(Error::corrupt_file_named(
"manifest",
"Manifest has only one of the mixed data-file-version reader and writer feature bits set, \
so its semantics are undefined",
));
}
Ok(())
}
fn validated_sticky_paired_flags(manifest: &Manifest) -> Result<u64> {
validate_paired_feature_flags(manifest)?;
Ok(manifest.reader_feature_flags & STICKY_PAIRED_FLAGS)
}
#[cfg(test)]
mod tests {
#[test]
fn test_covered_index_metadata_fences_older_builds_only() {
assert_eq!(
FLAG_COVERED_INDEX_METADATA, 128,
"the fence must sit on the boundary the released build shipped with"
);
assert!(
can_read_dataset(FLAG_COVERED_INDEX_METADATA),
"this build implements covering, so it must accept its own datasets"
);
assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
}
use super::*;
use crate::format::BasePath;
#[test]
fn test_read_check() {
assert!(can_read_dataset(0));
assert!(can_read_dataset(super::FLAG_DELETION_FILES));
assert!(can_read_dataset(super::FLAG_STABLE_ROW_IDS));
assert!(can_read_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
assert!(can_read_dataset(super::FLAG_TABLE_CONFIG));
assert!(can_read_dataset(super::FLAG_BASE_PATHS));
assert!(can_read_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
assert_eq!(
can_read_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
data_overlay_files_enabled()
);
assert!(can_read_dataset(
super::FLAG_DELETION_FILES
| super::FLAG_STABLE_ROW_IDS
| super::FLAG_USE_V2_FORMAT_DEPRECATED
));
assert!(!can_read_dataset(super::FLAG_UNKNOWN));
}
#[test]
fn test_data_overlay_flag_release_gating() {
let supported = supported_flags_when(false);
assert_eq!(supported & FLAG_UNSTABLE_DATA_OVERLAY_FILES, 0);
assert_eq!(FLAG_DELETION_FILES & !supported, 0);
assert_ne!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
let supported = supported_flags_when(true);
assert_eq!(FLAG_UNSTABLE_DATA_OVERLAY_FILES & !supported, 0);
}
#[test]
fn test_apply_feature_flags_sets_overlay_flag() {
use crate::format::overlay::{DataOverlayFile, OverlayCoverage};
use crate::format::{DataFile, DataStorageFormat, Fragment};
use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
use lance_core::datatypes::Schema;
use roaring::RoaringBitmap;
use std::collections::HashMap;
use std::sync::Arc;
let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
"id",
arrow_schema::DataType::Int64,
false,
)]);
let schema = Schema::try_from(&arrow_schema).unwrap();
let mut fragment = Fragment::new(0);
fragment.overlays = vec![DataOverlayFile {
data_file: DataFile::new_legacy_from_fields("o.lance", vec![0], None),
coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
committed_version: 1,
}];
let mut manifest = Manifest::new(
schema,
Arc::new(vec![fragment]),
DataStorageFormat::default(),
HashMap::new(),
);
apply_feature_flags(&mut manifest, false, false).unwrap();
assert_ne!(
manifest.reader_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
0
);
assert_ne!(
manifest.writer_feature_flags & FLAG_UNSTABLE_DATA_OVERLAY_FILES,
0
);
}
#[test]
fn test_write_check() {
assert!(can_write_dataset(0));
assert!(can_write_dataset(super::FLAG_DELETION_FILES));
assert!(can_write_dataset(super::FLAG_STABLE_ROW_IDS));
assert!(can_write_dataset(super::FLAG_USE_V2_FORMAT_DEPRECATED));
assert!(can_write_dataset(super::FLAG_TABLE_CONFIG));
assert!(can_write_dataset(super::FLAG_BASE_PATHS));
assert!(can_write_dataset(super::FLAG_DISABLE_TRANSACTION_FILE));
assert_eq!(
can_write_dataset(super::FLAG_UNSTABLE_DATA_OVERLAY_FILES),
data_overlay_files_enabled()
);
assert!(can_write_dataset(
super::FLAG_DELETION_FILES
| super::FLAG_STABLE_ROW_IDS
| super::FLAG_USE_V2_FORMAT_DEPRECATED
| super::FLAG_TABLE_CONFIG
| super::FLAG_BASE_PATHS
));
assert!(!can_write_dataset(super::FLAG_UNKNOWN));
}
#[test]
fn test_base_paths_feature_flags() {
use crate::format::{DataStorageFormat, Manifest};
use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
use lance_core::datatypes::Schema;
use std::collections::HashMap;
use std::sync::Arc;
let arrow_schema = ArrowSchema::new(vec![ArrowField::new(
"test_field",
arrow_schema::DataType::Int64,
false,
)]);
let schema = Schema::try_from(&arrow_schema).unwrap();
let mut normal_manifest = Manifest::new(
schema.clone(),
Arc::new(vec![]),
DataStorageFormat::default(),
HashMap::new(), );
apply_feature_flags(&mut normal_manifest, false, false).unwrap();
assert_eq!(normal_manifest.reader_feature_flags & FLAG_BASE_PATHS, 0);
assert_eq!(normal_manifest.writer_feature_flags & FLAG_BASE_PATHS, 0);
let mut base_paths: HashMap<u32, BasePath> = HashMap::new();
base_paths.insert(
1,
BasePath::new(
1,
"file:///path/to/original".to_string(),
Some("test_ref".to_string()),
true,
),
);
let mut multi_base_manifest = Manifest::new(
schema,
Arc::new(vec![]),
DataStorageFormat::default(),
base_paths,
);
apply_feature_flags(&mut multi_base_manifest, false, false).unwrap();
assert_ne!(
multi_base_manifest.reader_feature_flags & FLAG_BASE_PATHS,
0
);
assert_ne!(
multi_base_manifest.writer_feature_flags & FLAG_BASE_PATHS,
0
);
}
#[test]
fn inheriting_carries_sticky_paired_bits_from_the_source() {
let mut source = empty_manifest();
source.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
source.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
let mut destination = empty_manifest();
inherit_sticky_feature_flags(&mut destination, &source).unwrap();
assert_ne!(
destination.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
0
);
assert_ne!(
destination.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
0
);
}
#[test]
fn inheriting_refuses_a_half_set_source() {
for (reader, writer) in [
(FLAG_MIXED_DATA_FILE_VERSIONS, 0),
(0, FLAG_MIXED_DATA_FILE_VERSIONS),
] {
let mut source = empty_manifest();
source.reader_feature_flags = reader;
source.writer_feature_flags = writer;
let mut destination = empty_manifest();
let err = inherit_sticky_feature_flags(&mut destination, &source).unwrap_err();
assert!(err.to_string().contains("only one of"), "{err}");
}
}
#[test]
fn apply_feature_flags_carries_sticky_paired_bits_across_its_reset() {
let mut manifest = empty_manifest();
manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
apply_feature_flags(&mut manifest, false, false).unwrap();
assert_ne!(
manifest.reader_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
0
);
assert_ne!(
manifest.writer_feature_flags & FLAG_MIXED_DATA_FILE_VERSIONS,
0
);
}
#[test]
fn apply_feature_flags_rejects_half_set_sticky_bits() {
let mut manifest = empty_manifest();
manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
let err = apply_feature_flags(&mut manifest, false, false).unwrap_err();
assert!(matches!(err, Error::CorruptFile { .. }));
assert!(err.to_string().contains("only one of"), "{err}");
}
#[test]
fn paired_validation_rejects_half_set_mixed_version_capability() {
for (reader, writer) in [
(FLAG_MIXED_DATA_FILE_VERSIONS, 0),
(0, FLAG_MIXED_DATA_FILE_VERSIONS),
] {
let mut manifest = empty_manifest();
manifest.reader_feature_flags = reader;
manifest.writer_feature_flags = writer;
let err = validate_paired_feature_flags(&manifest).unwrap_err();
assert!(err.to_string().contains("mixed data-file-version"), "{err}");
}
}
#[test]
fn writer_gate_accepts_mixed_capability_and_rejects_unknown_flags() {
let mut manifest = empty_manifest();
manifest.reader_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
manifest.writer_feature_flags = FLAG_MIXED_DATA_FILE_VERSIONS;
ensure_can_write_manifest(&manifest).unwrap();
manifest.writer_feature_flags |= FLAG_UNKNOWN;
let err = ensure_can_write_manifest(&manifest).unwrap_err();
assert!(matches!(err, Error::NotSupported { .. }));
assert!(err.to_string().contains("cannot be written"), "{err}");
}
fn empty_manifest() -> Manifest {
use crate::format::DataStorageFormat;
use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
use lance_core::datatypes::Schema;
use std::collections::HashMap;
use std::sync::Arc;
let arrow_schema = ArrowSchema::new(vec![ArrowField::new("i", DataType::Int32, false)]);
Manifest::new(
Schema::try_from(&arrow_schema).unwrap(),
Arc::new(vec![]),
DataStorageFormat::default(),
HashMap::new(),
)
}
#[test]
fn mixed_capability_is_below_the_unknown_boundary() {
assert!(can_read_dataset(FLAG_COVERED_INDEX_METADATA));
assert!(can_write_dataset(FLAG_COVERED_INDEX_METADATA));
assert!(can_read_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
assert!(can_write_dataset(FLAG_MIXED_DATA_FILE_VERSIONS));
assert!(!can_read_dataset(FLAG_UNKNOWN));
}
}