mod api;
mod hnsw;
pub mod index;
mod manifest;
pub mod memtable;
pub mod observer;
pub(crate) mod reconcile;
pub mod scanner;
pub mod sharding;
#[cfg(test)]
pub(crate) mod test_util;
pub mod util;
mod wal;
pub mod write;
use std::sync::Arc;
use lance_core::Result;
use lance_core::datatypes::{Field, LANCE_FIELD_ID_KEY, Schema};
use arrow_array::RecordBatch;
use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
use object_store::path::Path;
use uuid::Uuid;
use self::util::{generate_random_hash, shard_base_path};
use crate::dataset::fragment::write::generate_random_filename;
pub const TOMBSTONE: &str = "_tombstone";
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct MemTableDataTarget {
pub generation: u64,
pub generation_dir: String,
pub data_file_name: String,
pub creator_epoch: u64,
pub batch_capacity: usize,
}
impl MemTableDataTarget {
pub fn new(generation: u64, creator_epoch: u64, batch_capacity: usize) -> Self {
let random_hash = generate_random_hash();
Self {
generation,
generation_dir: format!("{random_hash}_gen_{generation}"),
data_file_name: format!("{}.lance", generate_random_filename()),
creator_epoch,
batch_capacity,
}
}
pub fn generation_path(&self, base_path: &Path, shard_id: &Uuid) -> Path {
shard_base_path(base_path, shard_id).join(self.generation_dir.as_str())
}
pub fn data_file_key(&self) -> &str {
self.data_file_name
.strip_suffix(".lance")
.unwrap_or(&self.data_file_name)
}
fn validate(&self) -> Result<()> {
let simple_component = |value: &str| {
!value.is_empty()
&& value != "."
&& value != ".."
&& !value.contains('/')
&& !value.contains('\\')
};
if !simple_component(&self.generation_dir)
|| !simple_component(&self.data_file_name)
|| !self.data_file_name.ends_with(".lance")
|| self.batch_capacity == 0
|| !self
.generation_dir
.ends_with(format!("_gen_{}", self.generation).as_str())
{
return Err(lance_core::Error::io(format!(
"invalid MemTable data target for generation {}: directory {:?}, file {:?}",
self.generation, self.generation_dir, self.data_file_name
)));
}
Ok(())
}
}
pub fn tombstone_field() -> ArrowField {
ArrowField::new(TOMBSTONE, DataType::Boolean, false)
}
pub fn relax_non_pk_nullability(
logical_schema: &ArrowSchema,
pk_columns: &[String],
) -> Arc<ArrowSchema> {
let fields: Vec<ArrowField> = logical_schema
.fields()
.iter()
.map(|field| {
let keep = field.is_nullable()
|| field.name() == TOMBSTONE
|| pk_columns.iter().any(|c| c == field.name());
let field = field.as_ref().clone();
if keep {
field
} else {
field.with_nullable(true)
}
})
.collect();
Arc::new(ArrowSchema::new_with_metadata(
fields,
logical_schema.metadata().clone(),
))
}
pub fn arrow_schema_with_field_ids(schema: &Schema) -> ArrowSchema {
let arrow: ArrowSchema = schema.into();
let fields: Vec<ArrowField> = arrow
.fields()
.iter()
.map(|field| stamp_field_id(field, &schema.fields))
.collect();
ArrowSchema::new_with_metadata(fields, arrow.metadata().clone())
}
fn stamp_field_id(field: &ArrowField, among: &[Field]) -> ArrowField {
let Some(source) = among.iter().find(|f| f.name == *field.name()) else {
return field.clone();
};
let field = match source.id {
id if id >= 0 => {
let mut metadata = field.metadata().clone();
metadata.insert(LANCE_FIELD_ID_KEY.to_string(), id.to_string());
field.clone().with_metadata(metadata)
}
_ => field.clone(),
};
match field.data_type() {
DataType::Struct(children) => {
let children: Vec<ArrowField> = children
.iter()
.map(|child| stamp_field_id(child, &source.children))
.collect();
field.with_data_type(DataType::Struct(children.into()))
}
DataType::List(element) => {
let element = stamp_field_id(element, &source.children);
field.with_data_type(DataType::List(Arc::new(element)))
}
DataType::LargeList(element) => {
let element = stamp_field_id(element, &source.children);
field.with_data_type(DataType::LargeList(Arc::new(element)))
}
DataType::FixedSizeList(element, size) => {
let size = *size;
let element = stamp_field_id(element, &source.children);
field.with_data_type(DataType::FixedSizeList(Arc::new(element), size))
}
DataType::Map(entries, sorted) => {
let sorted = *sorted;
let entries = stamp_field_id(entries, &source.children);
field.with_data_type(DataType::Map(Arc::new(entries), sorted))
}
_ => field,
}
}
pub fn schema_with_tombstone(base: &ArrowSchema) -> Arc<ArrowSchema> {
if base.column_with_name(TOMBSTONE).is_some() {
return Arc::new(base.clone());
}
let mut fields: Vec<ArrowField> = base.fields().iter().map(|f| f.as_ref().clone()).collect();
fields.push(tombstone_field());
Arc::new(ArrowSchema::new_with_metadata(
fields,
base.metadata().clone(),
))
}
pub fn reconcile_batches(
source_schema: &ArrowSchema,
target_schema: &Arc<ArrowSchema>,
pk_columns: &[String],
batches: Vec<RecordBatch>,
) -> lance_core::Result<Vec<RecordBatch>> {
let source = ArrowSchema::new_with_metadata(
source_schema
.fields()
.iter()
.map(|field| {
match field.name() != TOMBSTONE && !lance_core::is_system_column(field.name()) {
true => field.as_ref().clone(),
false => reconcile::without_field_id(field),
}
})
.collect::<Vec<_>>(),
source_schema.metadata().clone(),
);
let plan =
reconcile::Plan::resolve(&source, target_schema, pk_columns)?.emitting_plain_schema();
if plan.is_identity() {
return Ok(batches);
}
batches.iter().map(|batch| plan.apply(batch)).collect()
}
pub use api::{DatasetMemWalExt, InitializeMemWalBuilder, validate_maintained_indexes};
pub use index::{MemIndexKind, MemTableVisibility};
pub use manifest::ShardManifestStore;
pub use memtable::scanner::MemTableScanner;
pub use scanner::{LsmDataSource, LsmGeneration, LsmScanner, ShardSnapshot};
pub use sharding::{
evaluate_sharding_spec, evaluate_sharding_spec_with_embedded_columns,
evaluate_sharding_spec_with_source_columns,
};
pub use wal::{BatchDurableWatcher, WalAppendResult, WalAppender, WalReadEntry, WalTailer};
pub use write::SealFence;
pub use write::ShardWriter;
pub use write::ShardWriterConfig;
pub use write::WriteResult;
#[cfg(test)]
mod tests {
use super::*;
use arrow_schema::Fields;
fn logical() -> ArrowSchema {
ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, false),
ArrowField::new("count", DataType::Int64, false),
ArrowField::new("note", DataType::Utf8, true),
])
}
fn stamped(name: &str, data_type: DataType, id: i32) -> ArrowField {
ArrowField::new(name, data_type, true).with_metadata(
[(LANCE_FIELD_ID_KEY.to_string(), id.to_string())]
.into_iter()
.collect(),
)
}
#[test]
fn a_generations_tombstone_does_not_answer_for_a_column_sharing_its_id() {
let source = ArrowSchema::new(vec![
stamped("id", DataType::Int64, 0),
stamped(TOMBSTONE, DataType::Boolean, 1),
]);
let target = Arc::new(ArrowSchema::new(vec![
stamped("id", DataType::Int64, 0),
stamped("extra", DataType::Int64, 1),
ArrowField::new(TOMBSTONE, DataType::Boolean, true),
]));
let batch = RecordBatch::try_new(
Arc::new(source.clone()),
vec![
Arc::new(arrow_array::Int64Array::from(vec![1])),
Arc::new(arrow_array::BooleanArray::from(vec![false])),
],
)
.expect("a batch under the source schema");
let out = reconcile_batches(&source, &target, &["id".to_string()], vec![batch])
.expect("the tombstone's id must not be honoured");
let out = &out[0];
assert!(
out.column_by_name("extra").expect("extra").is_null(0),
"the added column has no value in a generation sealed before it"
);
let tombstone = out
.column_by_name(TOMBSTONE)
.expect("_tombstone")
.as_any()
.downcast_ref::<arrow_array::BooleanArray>()
.expect("boolean");
assert!(!tombstone.value(0), "and the row is still live");
}
#[test]
fn a_pair_of_children_that_swapped_names_follow_their_ids() {
let struct_of = |first: &str, second: &str, ids: (i32, i32)| {
DataType::Struct(Fields::from(vec![
stamped(first, DataType::Int64, ids.0),
stamped(second, DataType::Int64, ids.1),
]))
};
let source = ArrowSchema::new(vec![
stamped("id", DataType::Int64, 0),
stamped("info", struct_of("a", "b", (1, 2)), 3),
]);
let target = Arc::new(ArrowSchema::new(vec![
stamped("id", DataType::Int64, 0),
stamped("info", struct_of("b", "a", (1, 2)), 3),
]));
let info = arrow_array::StructArray::new(
match source.field(1).data_type() {
DataType::Struct(fields) => fields.clone(),
_ => unreachable!("info is a struct"),
},
vec![
Arc::new(arrow_array::Int64Array::from(vec![10])) as arrow_array::ArrayRef,
Arc::new(arrow_array::Int64Array::from(vec![20])),
],
None,
);
let batch = RecordBatch::try_new(
Arc::new(source.clone()),
vec![
Arc::new(arrow_array::Int64Array::from(vec![1])),
Arc::new(info),
],
)
.expect("a batch under the source schema");
let out = reconcile_batches(&source, &target, &["id".to_string()], vec![batch])
.expect("reconcile");
let info = out[0]
.column(1)
.as_any()
.downcast_ref::<arrow_array::StructArray>()
.expect("info is a struct");
let b = info
.column_by_name("b")
.expect("b")
.as_any()
.downcast_ref::<arrow_array::Int64Array>()
.expect("int64");
assert_eq!(b.value(0), 10, "id 1's value follows its id to `b`");
let a = info
.column_by_name("a")
.expect("a")
.as_any()
.downcast_ref::<arrow_array::Int64Array>()
.expect("int64");
assert_eq!(a.value(0), 20, "id 2's value follows its id to `a`");
}
#[test]
fn relax_widens_every_non_pk_field_and_leaves_the_key_alone() {
let relaxed = relax_non_pk_nullability(&logical(), &["id".to_string()]);
assert!(
!relaxed.field(0).is_nullable(),
"the primary key stays strict"
);
assert!(
relaxed.field(1).is_nullable(),
"`count` must accept a tombstone null"
);
assert!(
relaxed.field(2).is_nullable(),
"already-nullable is untouched"
);
}
#[test]
fn relax_leaves_nested_fields_exactly_as_declared() {
let item = Arc::new(ArrowField::new("item", DataType::Float32, false));
let child = ArrowField::new("a", DataType::Int32, false);
let schema = ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, false),
ArrowField::new("vector", DataType::FixedSizeList(item, 4), false),
ArrowField::new("s", DataType::Struct(Fields::from(vec![child])), false),
]);
let relaxed = relax_non_pk_nullability(&schema, &["id".to_string()]);
assert!(relaxed.field(1).is_nullable());
match relaxed.field(1).data_type() {
DataType::FixedSizeList(f, _) => assert!(!f.is_nullable(), "item field untouched"),
other => panic!("expected FixedSizeList, got {other:?}"),
}
match relaxed.field(2).data_type() {
DataType::Struct(fields) => assert!(!fields[0].is_nullable(), "child field untouched"),
other => panic!("expected Struct, got {other:?}"),
}
}
#[test]
fn relax_keeps_tombstone_non_nullable_and_is_idempotent() {
let pk = ["id".to_string()];
let once = relax_non_pk_nullability(&schema_with_tombstone(&logical()), &pk);
let twice = relax_non_pk_nullability(&once, &pk);
let tombstone = once.field_with_name(TOMBSTONE).unwrap();
assert!(
!tombstone.is_nullable(),
"the write path always populates _tombstone"
);
assert_eq!(once, twice);
}
#[test]
fn relax_preserves_schema_and_field_metadata() {
let marked = ArrowField::new("count", DataType::Int64, false)
.with_metadata([("k".to_string(), "v".to_string())].into());
let schema = ArrowSchema::new_with_metadata(
vec![ArrowField::new("id", DataType::Int32, false), marked],
[("s".to_string(), "m".to_string())].into(),
);
let relaxed = relax_non_pk_nullability(&schema, &["id".to_string()]);
assert_eq!(relaxed.metadata().get("s").map(String::as_str), Some("m"));
assert_eq!(
relaxed.field(1).metadata().get("k").map(String::as_str),
Some("v")
);
}
}