use std::collections::HashMap;
use lance_core::{Error, Result};
use lance_namespace::error::NamespaceError;
pub const READER_FEATURE_FLAGS_KEY: &str = "lance.namespace.manifest.reader_feature_flags";
pub const WRITER_FEATURE_FLAGS_KEY: &str = "lance.namespace.manifest.writer_feature_flags";
const READER_KNOWN_FLAGS: u64 = 0;
const WRITER_KNOWN_FLAGS: u64 = 0;
pub fn can_read_manifest(reader_flags: u64) -> bool {
(reader_flags & !READER_KNOWN_FLAGS) == 0
}
pub fn can_write_manifest(writer_flags: u64) -> bool {
(writer_flags & !WRITER_KNOWN_FLAGS) == 0
}
fn parse_flags(table_metadata: &HashMap<String, String>, key: &str) -> Result<u64> {
match table_metadata.get(key) {
None => Ok(0),
Some(raw) => raw.parse::<u64>().map_err(|e| {
Error::from(NamespaceError::Unsupported {
message: format!(
"The __manifest dataset has an unparsable feature-flag value '{raw}' for \
'{key}': {e}. This likely means it was written by a newer, incompatible \
version of Lance; please upgrade Lance to use this catalog."
),
})
}),
}
}
pub fn reader_flags(table_metadata: &HashMap<String, String>) -> Result<u64> {
parse_flags(table_metadata, READER_FEATURE_FLAGS_KEY)
}
pub fn writer_flags(table_metadata: &HashMap<String, String>) -> Result<u64> {
parse_flags(table_metadata, WRITER_FEATURE_FLAGS_KEY)
}
pub fn ensure_readable(table_metadata: &HashMap<String, String>) -> Result<()> {
let flags = reader_flags(table_metadata)?;
if !can_read_manifest(flags) {
return Err(Error::from(NamespaceError::Unsupported {
message: format!(
"The __manifest dataset was written with reader feature flags {flags}, which this \
version of Lance does not understand (known reader flags: {READER_KNOWN_FLAGS}). \
Please upgrade Lance to read this catalog."
),
}));
}
Ok(())
}
pub fn ensure_writable(table_metadata: &HashMap<String, String>) -> Result<()> {
let flags = writer_flags(table_metadata)?;
if !can_write_manifest(flags) {
return Err(Error::from(NamespaceError::Unsupported {
message: format!(
"The __manifest dataset was written with writer feature flags {flags}, which this \
version of Lance does not understand (known writer flags: {WRITER_KNOWN_FLAGS}). \
Please upgrade Lance to modify this catalog."
),
}));
}
Ok(())
}
pub fn is_incompatible_manifest_error(err: &Error) -> bool {
matches!(
err,
Error::Namespace { source, .. }
if source
.downcast_ref::<NamespaceError>()
.is_some_and(|e| matches!(e, NamespaceError::Unsupported { .. }))
)
}
#[cfg(test)]
mod tests {
use super::*;
fn meta(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn unflagged_is_compatible() {
assert!(can_read_manifest(0));
assert!(can_write_manifest(0));
let empty = HashMap::new();
assert!(ensure_readable(&empty).is_ok());
assert!(ensure_writable(&empty).is_ok());
assert_eq!(reader_flags(&empty).unwrap(), 0);
assert_eq!(writer_flags(&empty).unwrap(), 0);
let zeroed = meta(&[
(READER_FEATURE_FLAGS_KEY, "0"),
(WRITER_FEATURE_FLAGS_KEY, "0"),
]);
assert!(ensure_readable(&zeroed).is_ok());
assert!(ensure_writable(&zeroed).is_ok());
}
#[test]
fn any_unknown_flag_is_refused() {
assert!(!can_read_manifest(1));
assert!(!can_write_manifest(1));
assert!(!can_read_manifest(1 << 30));
assert!(!can_write_manifest(1 << 63));
let reader = meta(&[(READER_FEATURE_FLAGS_KEY, "1")]);
let err = ensure_readable(&reader).unwrap_err();
assert!(err.to_string().to_lowercase().contains("upgrade"));
assert!(is_incompatible_manifest_error(&err));
assert!(ensure_writable(&reader).is_ok());
let writer = meta(&[(WRITER_FEATURE_FLAGS_KEY, "2")]);
let err = ensure_writable(&writer).unwrap_err();
assert!(err.to_string().to_lowercase().contains("upgrade"));
assert!(is_incompatible_manifest_error(&err));
}
#[test]
fn unparsable_value_is_refused() {
let m = meta(&[(READER_FEATURE_FLAGS_KEY, "not-a-number")]);
assert!(reader_flags(&m).is_err());
assert!(ensure_readable(&m).is_err());
}
#[test]
fn unrelated_error_is_not_an_incompatibility() {
let other = Error::from(NamespaceError::TableNotFound {
message: "x".to_string(),
});
assert!(!is_incompatible_manifest_error(&other));
}
}