use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef};
use datafusion::common::DFSchema;
use datafusion::common::tree_node::{Transformed, TreeNode};
use datafusion::execution::context::ExecutionProps;
use datafusion::physical_plan::ExecutionPlan;
use datafusion::physical_plan::filter::FilterExec;
use datafusion::prelude::Expr;
use datafusion_physical_expr::create_physical_expr;
use lance_core::datatypes::format_field_path_minimal;
use lance_core::is_system_column;
use lance_core::{Error, Result};
use super::exec::ReconcileExec;
use super::projection::{is_auto_managed, resolve_data_fields_or_drop};
use crate::dataset::mem_wal::reconcile::{Plan, field_id_of, without_field_id};
use crate::dataset::mem_wal::{TOMBSTONE, arrow_schema_with_field_ids};
pub(super) struct GenerationRead {
stored_schema: Schema,
names: HashMap<String, String>,
stored_columns: HashSet<String>,
fts_names: HashMap<String, String>,
table_schema: SchemaRef,
pk_columns: Vec<String>,
projection: Vec<String>,
}
impl GenerationRead {
pub(super) fn new(
dataset_schema: &lance_core::datatypes::Schema,
table_schema: &SchemaRef,
pk_columns: &[String],
projection: Vec<String>,
) -> Self {
Self::from_stored(
arrow_schema_with_field_ids(dataset_schema),
table_schema,
pk_columns,
projection,
)
}
pub(super) fn for_memtable(
memtable_schema: &Schema,
table_schema: &SchemaRef,
pk_columns: &[String],
projection: Vec<String>,
) -> Self {
Self::from_stored(
memtable_schema.clone(),
table_schema,
pk_columns,
projection,
)
}
fn from_stored(
stored_schema: Schema,
table_schema: &SchemaRef,
pk_columns: &[String],
projection: Vec<String>,
) -> Self {
let (names, fts_names) = stored_names(&stored_schema, table_schema);
let stored_columns = names.keys().cloned().collect::<HashSet<_>>();
let invert = |m: HashMap<String, String>| {
m.into_iter()
.map(|(stored, in_table)| (in_table, stored))
.collect::<HashMap<_, _>>()
};
let (names, fts_names) = (invert(names), invert(fts_names));
let (table_schema, pk_columns) = (Arc::clone(table_schema), pk_columns.to_vec());
Self {
stored_schema,
names,
fts_names,
stored_columns,
table_schema,
pk_columns,
projection,
}
}
pub(super) fn stored_name(&self, column: &str) -> Option<&str> {
self.names.get(column).map(String::as_str)
}
pub(super) fn stored_fts_name(&self, column: &str) -> Option<&str> {
self.fts_names.get(column).map(String::as_str)
}
pub(super) fn stored_pk_columns(&self) -> Result<Vec<String>> {
self.pk_columns
.iter()
.map(|pk| {
self.stored_name(pk).map(str::to_string).ok_or_else(|| {
Error::internal(format!(
"primary key column `{pk}` is not stored by a generation of this table"
))
})
})
.collect()
}
pub(super) fn also_produce(&mut self, column: &str) {
if !self.projection.iter().any(|w| w == column) {
self.projection.push(column.to_string());
}
}
pub(super) fn stored_projection(&self) -> Vec<&str> {
self.projection
.iter()
.filter_map(|name| {
self.stored_name(name)
.or_else(|| self.stored_system_column(name))
})
.collect()
}
fn stored_system_column(&self, name: &str) -> Option<&str> {
(is_system_column(name) || name == TOMBSTONE)
.then(|| self.stored_schema.field_with_name(name).ok())
.flatten()
.map(|f| f.name().as_str())
}
pub(super) fn to_stored(&self, expr: &Expr) -> Option<Expr> {
let pushable = expr
.column_refs()
.iter()
.all(|c| self.stored_as_declared(&c.name));
if !pushable {
return None;
}
expr.clone()
.transform(|e| match e {
Expr::Column(mut c) => {
c.name = self.stored_name(&c.name).expect("checked").to_string();
Ok(Transformed::yes(Expr::Column(c)))
}
other => Ok(Transformed::no(other)),
})
.map(|t| t.data)
.ok()
}
pub(super) fn split_filter(&mut self, filter: Option<&Expr>) -> (Option<Expr>, Option<Expr>) {
let pushed = filter.and_then(|expr| self.to_stored(expr));
let deferred = filter.filter(|_| pushed.is_none()).cloned();
if let Some(expr) = &deferred {
for column in expr.column_refs() {
self.also_produce(&column.name);
}
}
(pushed, deferred)
}
fn stored_as_declared(&self, column: &str) -> bool {
let Some(stored_column) = self.stored_name(column) else {
return false;
};
let (Ok(stored_field), Ok(declared)) = (
self.stored_schema.field_with_name(stored_column),
self.table_schema.field_with_name(column),
) else {
return false;
};
stored_field.data_type() == declared.data_type()
}
pub(super) fn reconcile_above(
&self,
scan: Arc<dyn ExecutionPlan>,
above: &Option<Expr>,
) -> Result<Arc<dyn ExecutionPlan>> {
let reconciled = self.reconcile(scan)?;
match above {
Some(expr) => filter_above(reconciled, expr),
None => Ok(reconciled),
}
}
pub(super) fn reconcile(&self, scan: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> {
let source =
self.with_only_table_field_ids(with_ids_from(&scan.schema(), &self.stored_schema));
let target = self.target(&source)?;
let plan = Plan::resolve(&source, &target, &self.pk_columns)?.emitting_plain_schema();
if plan.is_identity() {
return Ok(scan);
}
Ok(Arc::new(ReconcileExec::new(scan, Arc::new(plan))))
}
fn with_only_table_field_ids(&self, source: Schema) -> Schema {
let fields: Vec<Field> = source
.fields()
.iter()
.map(|field| match self.stored_columns.contains(field.name()) {
true => field.as_ref().clone(),
false => without_field_id(field),
})
.collect();
Schema::new_with_metadata(fields, source.metadata().clone())
}
fn target(&self, source: &Schema) -> Result<SchemaRef> {
let data_names: Vec<String> = self
.projection
.iter()
.filter(|name| !is_auto_managed(name))
.cloned()
.collect();
let resolved = resolve_data_fields_or_drop(&data_names, &self.table_schema)?;
let resolved = with_ids_from(
&Schema::new(
resolved
.iter()
.map(|f| f.as_ref().clone())
.collect::<Vec<_>>(),
),
&self.table_schema,
);
let mut fields: Vec<Field> = resolved
.fields()
.iter()
.map(|field| {
let nullable = self
.stored_name(field.name())
.and_then(|stored_column| source.field_with_name(stored_column).ok())
.is_none_or(|f| f.is_nullable());
field.as_ref().clone().with_nullable(nullable)
})
.collect();
for field in source.fields() {
let is_the_tables = self.stored_columns.contains(field.name());
if !is_the_tables && fields.iter().all(|f| f.name() != field.name()) {
fields.push(field.as_ref().clone());
}
}
Ok(Arc::new(Schema::new(fields)))
}
}
pub(super) fn memtable_matches_table(memtable_schema: &Schema, table_schema: &Schema) -> bool {
let stored = memtable_schema.fields();
let declared = table_schema.fields();
let extra = &stored[declared.len().min(stored.len())..];
stored.len() >= declared.len()
&& extra.iter().all(|f| f.name() == TOMBSTONE)
&& stored
.iter()
.zip(declared.iter())
.all(|(stored, declared)| {
stored.name() == declared.name()
&& match (field_id_of(stored), field_id_of(declared)) {
(Some(stored_id), Some(declared_id)) => {
stored_id == declared_id && stored.data_type() == declared.data_type()
}
_ => stored.data_type().equals_datatype(declared.data_type()),
}
})
}
fn filter_above(plan: Arc<dyn ExecutionPlan>, expr: &Expr) -> Result<Arc<dyn ExecutionPlan>> {
let schema = plan.schema();
let df_schema = DFSchema::try_from(schema.as_ref().clone())
.map_err(|e| Error::internal(format!("build a filter schema for `{expr}`: {e}")))?;
let props = ExecutionProps::new();
let physical = create_physical_expr(expr, &df_schema, &props)
.map_err(|e| Error::internal(format!("plan filter `{expr}`: {e}")))?;
Ok(Arc::new(
FilterExec::try_new(physical, plan).map_err(|e| Error::internal(format!("filter: {e}")))?,
))
}
fn stored_names(
stored_schema: &Schema,
table_schema: &Schema,
) -> (HashMap<String, String>, HashMap<String, String>) {
let table = field_paths(table_schema);
let by_id: HashMap<(i32, bool), &String> = table
.iter()
.filter_map(|(id, elided, path)| id.map(|id| ((id, *elided), path)))
.collect();
if by_id.is_empty() {
let known: std::collections::HashSet<&String> =
table.iter().map(|(_, _, path)| path).collect();
let mut physical = HashMap::new();
let mut fts = HashMap::new();
for (_, elided, path) in field_paths(stored_schema) {
if !known.contains(&path) {
continue;
}
let into = if elided { &mut fts } else { &mut physical };
into.insert(path.clone(), path);
}
return (physical, fts);
}
let mut physical = HashMap::new();
let mut fts = HashMap::new();
for (id, elided, stored_path) in field_paths(stored_schema) {
let Some(id) = id else { continue };
let Some(table_path) = by_id.get(&(id, elided)) else {
continue;
};
let into = if elided { &mut fts } else { &mut physical };
into.insert(stored_path, (*table_path).clone());
}
(physical, fts)
}
fn nested_children(data_type: &DataType) -> Vec<&Arc<Field>> {
match data_type {
DataType::Struct(children) => children.iter().collect(),
DataType::List(element)
| DataType::LargeList(element)
| DataType::FixedSizeList(element, _)
| DataType::Map(element, _) => vec![element],
_ => Vec::new(),
}
}
fn field_paths(schema: &Schema) -> Vec<(Option<i32>, bool, String)> {
fn walk(
field: &Field,
physical: &mut Vec<String>,
elided: &mut Vec<String>,
in_list: bool,
out: &mut Vec<(Option<i32>, bool, String)>,
) {
physical.push(field.name().clone());
if !in_list {
elided.push(field.name().clone());
}
let id = field_id_of(field);
let as_path = |segments: &[String]| {
format_field_path_minimal(&segments.iter().map(String::as_str).collect::<Vec<_>>())
};
let physical_path = as_path(physical);
if !in_list {
out.push((id, true, as_path(elided)));
}
out.push((id, false, physical_path));
let list = matches!(
field.data_type(),
DataType::List(_) | DataType::LargeList(_) | DataType::FixedSizeList(..)
);
for child in nested_children(field.data_type()) {
walk(child, physical, elided, list, out);
}
physical.pop();
if !in_list {
elided.pop();
}
}
let mut out = Vec::new();
let (mut physical, mut elided) = (Vec::new(), Vec::new());
for field in schema
.fields()
.iter()
.filter(|f| f.name() != TOMBSTONE && !is_system_column(f.name()))
{
walk(field, &mut physical, &mut elided, false, &mut out);
}
out
}
fn with_ids_from(schema: &Schema, stored_schema: &Schema) -> Schema {
fn restore(field: &Field, among: &Fields) -> Field {
let Some(source) = among.iter().find(|f| f.name() == field.name()) else {
return field.clone();
};
let mut metadata = field.metadata().clone();
metadata.extend(source.metadata().clone());
let field = field.clone().with_metadata(metadata);
let data_type = field.data_type().clone();
match (&data_type, source.data_type()) {
(DataType::Struct(children), DataType::Struct(source_children)) => {
let children: Vec<Field> = children
.iter()
.map(|child| restore(child, source_children))
.collect();
field.with_data_type(DataType::Struct(children.into()))
}
(DataType::List(element), DataType::List(source_element)) => {
let one: Fields = vec![source_element.as_ref().clone()].into();
field.with_data_type(DataType::List(Arc::new(restore(element, &one))))
}
(DataType::LargeList(element), DataType::LargeList(source_element)) => {
let one: Fields = vec![source_element.as_ref().clone()].into();
field.with_data_type(DataType::LargeList(Arc::new(restore(element, &one))))
}
(
DataType::FixedSizeList(element, size),
DataType::FixedSizeList(source_element, _),
) => {
let size = *size;
let one: Fields = vec![source_element.as_ref().clone()].into();
field.with_data_type(DataType::FixedSizeList(
Arc::new(restore(element, &one)),
size,
))
}
(DataType::Map(entries, sorted), DataType::Map(source_entries, _)) => {
let sorted = *sorted;
let one: Fields = vec![source_entries.as_ref().clone()].into();
field.with_data_type(DataType::Map(Arc::new(restore(entries, &one)), sorted))
}
_ => field,
}
}
let fields: Vec<Field> = schema
.fields()
.iter()
.map(|field| restore(field, stored_schema.fields()))
.collect();
Schema::new_with_metadata(fields, schema.metadata().clone())
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_schema::Fields;
use datafusion::prelude::{col, lit};
use lance_core::datatypes::LANCE_FIELD_ID_KEY;
use lance_core::datatypes::Schema as LanceSchema;
fn with_id(name: &str, data_type: DataType, id: i32) -> Field {
Field::new(name, data_type, true).with_metadata(
[(LANCE_FIELD_ID_KEY.to_string(), id.to_string())]
.into_iter()
.collect(),
)
}
fn schema(fields: Vec<Field>) -> SchemaRef {
Arc::new(Schema::new(fields))
}
fn generation(
stored_schema: SchemaRef,
table_schema: SchemaRef,
projection: &[&str],
) -> GenerationRead {
let lance = LanceSchema::try_from(stored_schema.as_ref()).expect("a lance schema");
GenerationRead::new(
&lance,
&table_schema,
&["id".to_string()],
projection.iter().map(|s| s.to_string()).collect(),
)
}
fn renamed() -> GenerationRead {
generation(
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("value", DataType::Int64, 1),
]),
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("amount", DataType::Int64, 1),
]),
&["id", "amount"],
)
}
#[test]
fn a_struct_predicate_is_pushed_down_only_when_its_children_did_not_move() {
let cases = [
("nothing moved", ("c", 2), ("c", 2), true),
("the child was renamed", ("c", 2), ("d", 2), false),
("the child was replaced", ("c", 2), ("c", 9), false),
];
for (case, stored_child, table_child, pushed_down) in cases {
let info = |(name, id): (&str, i32)| {
with_id(
"info",
DataType::Struct(Fields::from(vec![with_id(name, DataType::Int64, id)])),
1,
)
};
let read = generation(
schema(vec![with_id("id", DataType::Int64, 0), info(stored_child)]),
schema(vec![with_id("id", DataType::Int64, 0), info(table_child)]),
&["id", "info"],
);
let expr = col("info").is_not_null();
let got = read.to_stored(&expr);
assert_eq!(
got,
pushed_down.then_some(expr),
"{case}: the predicate was pushed down when it should not have been, or the reverse"
);
}
}
#[test]
fn a_renamed_column_is_asked_for_under_the_name_the_generation_has() {
assert_eq!(renamed().stored_projection(), vec!["id", "value"]);
assert_eq!(renamed().stored_name("amount"), Some("value"));
}
#[test]
fn a_field_id_taken_back_after_a_drop_is_read_as_the_column_that_took_it() {
let read = generation(
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("retired", DataType::Int64, 1),
]),
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("added", DataType::Int64, 1),
]),
&["id", "added"],
);
assert_eq!(
read.stored_name("added"),
Some("retired"),
"today the reused id pairs them; a fix makes this `None`, and the \
generation contributes null for `added` instead"
);
}
#[test]
fn a_column_the_generation_never_stored_is_left_out_of_the_projection() {
let read = generation(
schema(vec![with_id("id", DataType::Int64, 0)]),
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("added", DataType::Int64, 7),
]),
&["id", "added"],
);
assert_eq!(read.stored_projection(), vec!["id"]);
assert_eq!(read.stored_name("added"), None);
}
#[test]
fn a_system_column_never_answers_for_one_of_the_tables() {
let read = generation(
schema(vec![
with_id("id", DataType::Int64, 0),
with_id(TOMBSTONE, DataType::Boolean, 7),
]),
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("added", DataType::Int64, 7),
]),
&["id", "added", TOMBSTONE],
);
assert_eq!(read.stored_name("added"), None, "not the tombstone's id");
assert_eq!(
read.stored_projection(),
vec!["id", TOMBSTONE],
"the tombstone is still asked for, under its own name"
);
}
#[test]
fn a_predicate_naming_a_renamed_column_is_rewritten_to_the_stored_name() {
let read = renamed();
assert_eq!(
read.to_stored(&col("amount").eq(lit(1i64))),
Some(col("value").eq(lit(1i64))),
);
}
#[test]
fn a_predicate_naming_a_column_that_did_not_move_is_left_alone() {
let read = renamed();
let expr = col("id").eq(lit(1i64));
assert_eq!(read.to_stored(&expr), Some(expr));
}
#[test]
fn a_predicate_naming_a_column_the_generation_lacks_is_not_pushable() {
let read = generation(
schema(vec![with_id("id", DataType::Int64, 0)]),
schema(vec![
with_id("id", DataType::Int64, 0),
with_id("added", DataType::Int64, 7),
]),
&["id", "added"],
);
assert_eq!(read.to_stored(&col("added").eq(lit(1i64))), None);
}
#[test]
fn a_table_without_ids_matches_by_name() {
let stored_schema = Schema::new(vec![
with_id("id", DataType::Int64, 0),
with_id("value", DataType::Int64, 1),
with_id(TOMBSTONE, DataType::Boolean, 2),
]);
let table_schema = Schema::new(vec![
Field::new("id", DataType::Int64, true),
Field::new("value", DataType::Int64, true),
]);
let (names, _) = stored_names(&stored_schema, &table_schema);
assert_eq!(names.get("value"), Some(&"value".to_string()));
assert_eq!(names.get(TOMBSTONE), None, "not one of the table's columns");
}
#[test]
fn a_full_text_alias_resolves_the_current_name_to_the_stored_one() {
let list = |child: &str, id: i32| {
let element = Arc::new(with_id(
"item",
DataType::Struct(vec![Arc::new(with_id(child, DataType::Utf8, id))].into()),
2,
));
Schema::new(vec![with_id("tags", DataType::List(element), 1)])
};
let (_, fts) = stored_names(&list("body", 3), &list("text", 3));
let read = GenerationRead {
names: HashMap::new(),
stored_columns: HashSet::new(),
fts_names: fts
.into_iter()
.map(|(stored, in_table)| (in_table, stored))
.collect(),
stored_schema: Schema::empty(),
table_schema: Arc::new(Schema::empty()),
pk_columns: Vec::new(),
projection: Vec::new(),
};
assert_eq!(
read.stored_fts_name("tags.text"),
Some("tags.body"),
"the renamed child still has to find its stored index name"
);
}
#[test]
fn a_list_element_and_a_child_named_item_do_not_share_a_mapping() {
let child = Arc::new(with_id("item", DataType::Utf8, 3));
let element = Arc::new(with_id(
"item",
DataType::Struct(vec![Arc::clone(&child)].into()),
2,
));
let schema = Schema::new(vec![with_id("tags", DataType::List(element), 1)]);
let (physical, fts) = stored_names(&schema, &schema);
assert_eq!(
physical.get("tags.item").map(String::as_str),
Some("tags.item"),
"the element keeps the path a projection spells out"
);
assert_eq!(
fts.get("tags.item").map(String::as_str),
Some("tags.item"),
"the child keeps the path a full-text query uses"
);
}
}