use std::collections::HashMap;
use std::sync::Arc;
use super::ir::nodes::{
Aggregate, AggregateBuilder, FileType, Filter, Load, Operator, Project, ScanFile, ScanJson,
ScanParquet, SemiJoin, UnionAll, Values,
};
use super::ir::plan::{Plan, PlanNode};
use crate::expressions::{ColumnName, ExpressionRef, PredicateRef, Scalar};
use crate::schema::SchemaRef;
use crate::struct_patch::ProjectionStructPatchBuilder;
use crate::utils::CollectInto;
use crate::{DeltaResult, Error};
#[derive(Debug)]
struct BuilderNode {
op: Operator,
inputs: Vec<BuilderNodeRef>,
schema: SchemaRef,
}
type BuilderNodeRef = Arc<BuilderNode>;
#[derive(Clone, Debug)]
enum PlanBuilderRoot {
Present(BuilderNodeRef),
Absent(SchemaRef),
}
#[derive(Clone, Debug)]
#[must_use]
pub struct PlanBuilder(PlanBuilderRoot);
impl PlanBuilder {
fn present(schema: SchemaRef, op: impl Into<Operator>, inputs: Vec<BuilderNodeRef>) -> Self {
PlanBuilder(PlanBuilderRoot::Present(Arc::new(BuilderNode {
op: op.into(),
inputs,
schema,
})))
}
fn absent(schema: SchemaRef) -> Self {
PlanBuilder(PlanBuilderRoot::Absent(schema))
}
fn schema(&self) -> &SchemaRef {
match &self.0 {
PlanBuilderRoot::Present(node) => &node.schema,
PlanBuilderRoot::Absent(schema) => schema,
}
}
fn unary_op_or_absent(self, schema: SchemaRef, op: impl Into<Operator>) -> Self {
match self.0 {
PlanBuilderRoot::Present(node) => Self::present(schema, op, vec![node]),
PlanBuilderRoot::Absent(_) => Self::absent(schema),
}
}
pub fn scan_parquet(
files: impl IntoIterator<Item = impl Into<ScanFile>>,
file_constant_columns: &[&str],
schema: impl Into<SchemaRef>,
) -> DeltaResult<Self> {
Self::scan_source(FileType::Parquet, files, file_constant_columns, schema)
}
pub fn scan_json(
files: impl IntoIterator<Item = impl Into<ScanFile>>,
file_constant_columns: &[&str],
schema: impl Into<SchemaRef>,
) -> DeltaResult<Self> {
Self::scan_source(FileType::Json, files, file_constant_columns, schema)
}
fn scan_source(
file_type: FileType,
files: impl IntoIterator<Item = impl Into<ScanFile>>,
file_constant_columns: &[&str],
schema: impl Into<SchemaRef>,
) -> DeltaResult<Self> {
let schema = schema.into();
let files = Vec::from_iter(files.into_iter().map(Into::into));
let cols = Vec::from_iter(file_constant_columns.iter().map(|&c| c.to_string()));
for (i, file) in files.iter().enumerate() {
if file.file_constants.len() != cols.len() {
return Err(Error::generic(format!(
"scan: file {i} has {} constant value(s) {:?} for {} file-constant column(s) {:?}",
file.file_constants.len(),
file.file_constants,
cols.len(),
cols,
)));
}
}
check_file_constant_columns(&schema, &cols, "scan file_constant_columns")?;
if files.is_empty() {
return Ok(Self::absent(schema));
}
let file_constant_columns = cols;
let op = match file_type {
FileType::Parquet => Operator::from(ScanParquet {
files,
file_constant_columns,
schema: Arc::clone(&schema),
}),
FileType::Json => Operator::from(ScanJson {
files,
file_constant_columns,
schema: Arc::clone(&schema),
}),
};
Ok(Self::present(schema, op, vec![]))
}
pub fn values(schema: impl Into<SchemaRef>, rows: Vec<Vec<Scalar>>) -> DeltaResult<Self> {
let schema = schema.into();
let width = schema.fields().count();
for (i, row) in rows.iter().enumerate() {
if row.len() != width {
return Err(Error::generic(format!(
"values: row {i} has {} value(s) {row:?} but schema has {width} field(s) {:?}",
row.len(),
Vec::from_iter(schema.fields().map(|f| f.name())),
)));
}
}
if rows.is_empty() {
return Ok(Self::absent(schema));
}
let op = Values::new(schema, rows);
Ok(Self::present(op.schema.clone(), op, vec![]))
}
pub fn filter(self, predicate: impl Into<PredicateRef>) -> DeltaResult<Self> {
let predicate = predicate.into();
check_columns_resolve(self.schema(), predicate.references(), "filter")?;
let schema = Arc::clone(self.schema());
Ok(self.unary_op_or_absent(schema, Filter { predicate }))
}
pub fn project(
self,
expr: impl Into<ExpressionRef>,
schema: impl Into<SchemaRef>,
) -> DeltaResult<Self> {
let schema = schema.into();
let expr = expr.into();
check_columns_resolve(self.schema(), expr.references(), "project")?;
Ok(self.unary_op_or_absent(Arc::clone(&schema), Project { expr, schema }))
}
pub fn project_patch(
self,
patch: impl FnOnce(ProjectionStructPatchBuilder<'_>) -> ProjectionStructPatchBuilder<'_>,
) -> DeltaResult<Self> {
let (out, expr) = patch(ProjectionStructPatchBuilder::new(self.schema())).build()?;
self.project(expr, out)
}
pub fn load(self, load: Load) -> DeltaResult<Self> {
let meta = [
&load.file_meta.path_column,
&load.file_meta.file_size_column,
&load.file_meta.num_records_column,
&load.dv_column,
];
check_columns_resolve(self.schema(), meta, "load")?;
check_file_constant_columns(
self.schema(),
&load.file_constant_columns,
"load file_constant source",
)?;
check_file_constant_columns(
&load.schema,
&load.file_constant_columns,
"load file_constant",
)?;
Ok(self.unary_op_or_absent(Arc::clone(&load.schema), load))
}
pub fn aggregate(self, aggregate: impl TryInto<Aggregate, Error = Error>) -> DeltaResult<Self> {
let aggregate = aggregate.try_into()?;
let schema = Arc::clone(&aggregate.schema);
match self.0 {
PlanBuilderRoot::Present(node) => Ok(Self::present(schema, aggregate, vec![node])),
PlanBuilderRoot::Absent(_) if !aggregate.group_by.is_empty() => {
Ok(Self::absent(schema))
}
PlanBuilderRoot::Absent(input_schema) => {
let input = Self::present(
Arc::clone(&input_schema),
Values::new(input_schema, vec![]),
vec![],
);
Ok(input.unary_op_or_absent(schema, aggregate))
}
}
}
pub fn aggregate_by(
self,
keys: impl CollectInto<Vec<ColumnName>>,
aggs: impl FnOnce(AggregateBuilder) -> AggregateBuilder,
) -> DeltaResult<Self> {
let builder = Aggregate::group_by(Arc::clone(self.schema()), keys);
self.aggregate(aggs(builder))
}
pub fn semi_join(
self,
build: PlanBuilder,
probe_keys: impl IntoIterator<Item = ColumnName>,
build_keys: impl IntoIterator<Item = ColumnName>,
) -> DeltaResult<Self> {
self.semi_join_impl(build, false, probe_keys, build_keys)
}
pub fn anti_join(
self,
build: PlanBuilder,
probe_keys: impl IntoIterator<Item = ColumnName>,
build_keys: impl IntoIterator<Item = ColumnName>,
) -> DeltaResult<Self> {
self.semi_join_impl(build, true, probe_keys, build_keys)
}
fn semi_join_impl(
self,
build: PlanBuilder,
inverted: bool,
probe_keys: impl IntoIterator<Item = ColumnName>,
build_keys: impl IntoIterator<Item = ColumnName>,
) -> DeltaResult<Self> {
let probe_keys = Vec::from_iter(probe_keys);
let build_keys = Vec::from_iter(build_keys);
if probe_keys.len() != build_keys.len() {
return Err(Error::generic(format!(
"join: {} probe key(s) {probe_keys:?} but {} build key(s) {build_keys:?}; \
they must match in length.",
probe_keys.len(),
build_keys.len(),
)));
}
check_columns_resolve(self.schema(), &probe_keys, "join probe")?;
check_columns_resolve(build.schema(), &build_keys, "join build")?;
let PlanBuilderRoot::Present(probe) = self.0 else {
return Ok(self);
};
let build = match build.0 {
PlanBuilderRoot::Present(build) => build,
PlanBuilderRoot::Absent(_) if inverted => {
return Ok(PlanBuilder(PlanBuilderRoot::Present(probe)))
}
PlanBuilderRoot::Absent(_) => return Ok(Self::absent(Arc::clone(&probe.schema))),
};
let node = SemiJoin {
inverted,
probe_keys,
build_keys,
};
let schema = Arc::clone(&probe.schema);
Ok(Self::present(schema, node, vec![probe, build]))
}
pub fn union_all(inputs: impl IntoIterator<Item = PlanBuilder>) -> DeltaResult<Self> {
let inputs = Vec::from_iter(inputs);
let Some(schema) = inputs.first().map(|b| Arc::clone(b.schema())) else {
return Err(Error::generic("union_all: requires at least one input"));
};
if let Some(i) = inputs.iter().position(|b| b.schema() != &schema) {
return Err(Error::generic(format!(
"union_all: input {i} has a schema differing from input 0: {:?} vs {:?}",
inputs[i].schema(),
schema,
)));
}
let mut present = Vec::from_iter(inputs.into_iter().filter_map(|b| match b.0 {
PlanBuilderRoot::Present(node) => Some(node),
PlanBuilderRoot::Absent(_) => None,
}));
Ok(match present.len() {
0 => Self::absent(schema),
1 => PlanBuilder(PlanBuilderRoot::Present(present.swap_remove(0))),
_ => Self::present(schema, UnionAll, present),
})
}
pub fn build(&self) -> DeltaResult<Plan> {
Ok(match &self.0 {
PlanBuilderRoot::Present(root) => Self::build_plan(root),
PlanBuilderRoot::Absent(schema) => Plan {
nodes: vec![PlanNode::new(
Values::new(Arc::clone(schema), vec![]),
vec![],
)],
},
})
}
pub fn build_opt(&self) -> DeltaResult<Option<Plan>> {
Ok(match &self.0 {
PlanBuilderRoot::Present(root) => Some(Self::build_plan(root)),
PlanBuilderRoot::Absent(_) => None,
})
}
fn build_plan(root: &BuilderNodeRef) -> Plan {
fn emit(
node: &BuilderNodeRef,
nodes: &mut Vec<PlanNode>,
emitted: &mut HashMap<*const BuilderNode, usize>,
) -> usize {
let key = Arc::as_ptr(node);
if let Some(&index) = emitted.get(&key) {
return index;
}
let inputs = Vec::from_iter(node.inputs.iter().map(|i| emit(i, nodes, emitted)));
*emitted.entry(key).or_insert_with(|| {
nodes.push(PlanNode::new(node.op.clone(), inputs));
nodes.len() - 1
})
}
let mut nodes = Vec::new();
emit(root, &mut nodes, &mut HashMap::new());
Plan { nodes }
}
}
fn check_columns_resolve<'a>(
schema: &SchemaRef,
cols: impl IntoIterator<Item = &'a ColumnName>,
ctx: &str,
) -> DeltaResult<()> {
for col in cols {
schema.field_at(col).map_err(|_| {
Error::generic(format!(
"{ctx}: column `{col}` not found; schema has {:?}",
Vec::from_iter(schema.fields().map(|f| f.name())),
))
})?;
}
Ok(())
}
fn check_file_constant_columns<'a>(
schema: &SchemaRef,
names: impl IntoIterator<Item = &'a String>,
ctx: &str,
) -> DeltaResult<()> {
for name in names {
let Some(field) = schema.field(name.as_str()) else {
return Err(Error::generic(format!(
"{ctx}: column `{name}` not found; schema has {:?}",
Vec::from_iter(schema.fields().map(|f| f.name())),
)));
};
if field.is_metadata_column() {
return Err(Error::generic(format!(
"{ctx}: column `{name}` is a metadata column"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expressions::{col, column_name, lit, Expression};
use crate::plans::ir::nodes::{FileType, LoadColumnFileMeta};
use crate::schema::{DataType, StructField, StructType};
use crate::FileMeta;
fn scan(schema: SchemaRef) -> PlanBuilder {
PlanBuilder::scan_parquet([test_file("file:///a.parquet")], &[], schema).unwrap()
}
fn test_file(path: &str) -> FileMeta {
FileMeta {
location: url::Url::parse(path).unwrap(),
last_modified: 0,
size: 0,
}
}
fn id_schema() -> SchemaRef {
Arc::new(StructType::new_unchecked([StructField::nullable(
"id",
DataType::STRING,
)]))
}
fn x_schema() -> SchemaRef {
Arc::new(StructType::new_unchecked([StructField::nullable(
"x",
DataType::LONG,
)]))
}
fn vals(schema: SchemaRef) -> PlanBuilder {
let row = Vec::from_iter(schema.fields().map(|f| Scalar::null(f.data_type().clone())));
PlanBuilder::values(schema, vec![row]).unwrap()
}
fn absent_over(schema: SchemaRef) -> PlanBuilder {
PlanBuilder::values(schema, vec![]).unwrap()
}
fn absent_src() -> PlanBuilder {
absent_over(id_schema())
}
fn op_tag(op: &Operator) -> &'static str {
match op {
Operator::ScanParquet(_) => "scan_parquet",
Operator::ScanJson(_) => "scan_json",
Operator::Values(_) => "values",
Operator::Filter(_) => "filter",
Operator::Project(_) => "project",
Operator::Load(_) => "load",
Operator::Aggregate(_) => "aggregate",
Operator::SemiJoin(_) => "semi_join",
Operator::UnionAll(_) => "union_all",
}
}
fn assert_plan(builder: PlanBuilder, expected: &[(&[usize], &str)]) -> Plan {
let plan = builder.build().unwrap();
assert_eq!(plan.nodes.len(), expected.len(), "node count");
for (i, (node, &(inputs, op))) in plan.nodes.iter().zip(expected).enumerate() {
assert_eq!(node.inputs.as_slice(), inputs, "node {i} inputs");
assert_eq!(op_tag(&node.op), op, "node {i} op");
}
plan
}
#[test]
fn source_builds_single_node() {
let src = scan(id_schema());
assert_eq!(src.schema(), &id_schema());
assert_plan(src, &[(&[], "scan_parquet")]);
}
fn part_schema() -> SchemaRef {
Arc::new(StructType::new_unchecked([
StructField::nullable("id", DataType::STRING),
StructField::nullable("part", DataType::STRING),
]))
}
fn one_constant_file(path: &str) -> ScanFile {
ScanFile {
meta: test_file(path),
file_constants: vec!["p1".into()],
}
}
#[test]
fn scans_record_file_constant_columns() -> DeltaResult<()> {
let part = vec!["part".to_string()];
let parquet = PlanBuilder::scan_parquet(
vec![one_constant_file("file:///a.parquet")],
&["part"],
part_schema(),
)?;
assert_eq!(parquet.schema(), &part_schema());
let plan = assert_plan(parquet, &[(&[], "scan_parquet")]);
let Operator::ScanParquet(node) = &plan.nodes[0].op else {
panic!("expected ScanParquet");
};
assert_eq!(node.file_constant_columns, part);
let json = PlanBuilder::scan_json(
vec![one_constant_file("file:///a.json")],
&["part"],
part_schema(),
)?;
let plan = assert_plan(json, &[(&[], "scan_json")]);
let Operator::ScanJson(node) = &plan.nodes[0].op else {
panic!("expected ScanJson");
};
assert_eq!(node.file_constant_columns, part);
Ok(())
}
#[test]
fn filter_preserves_schema() -> DeltaResult<()> {
let filtered = vals(id_schema()).filter(col!("id").is_not_null())?;
assert_eq!(filtered.schema(), &id_schema());
Ok(())
}
#[test]
fn monadic_chain_is_topological() -> DeltaResult<()> {
assert_plan(
scan(id_schema())
.filter(col!("id").is_not_null())?
.filter(col!("id").is_null())?,
&[(&[], "scan_parquet"), (&[0], "filter"), (&[1], "filter")],
);
Ok(())
}
#[test]
fn shared_source_emitted_once() -> DeltaResult<()> {
let a = vals(id_schema());
let b = a.clone().filter(col!("id").is_not_null())?;
let c = a.filter(col!("id").is_null())?;
assert_plan(
PlanBuilder::union_all([b, c])?,
&[
(&[], "values"),
(&[0], "filter"),
(&[0], "filter"),
(&[1, 2], "union_all"),
],
);
Ok(())
}
#[test]
fn unreachable_nodes_are_dropped() -> DeltaResult<()> {
let src = vals(id_schema());
let _dead = src.clone().filter(col!("id").is_null())?; let kept = src.filter(col!("id").is_not_null())?;
assert_plan(kept, &[(&[], "values"), (&[0], "filter")]);
Ok(())
}
#[test]
fn shared_intermediate_emitted_once_diamond() -> DeltaResult<()> {
let mid = vals(id_schema()).filter(col!("id").is_not_null())?;
let left = mid.clone().filter(col!("id").is_not_null())?;
let right = mid.filter(col!("id").is_null())?;
assert_plan(
PlanBuilder::union_all([left, right])?,
&[
(&[], "values"),
(&[0], "filter"),
(&[1], "filter"),
(&[1], "filter"),
(&[2, 3], "union_all"),
],
);
Ok(())
}
#[test]
fn union_all_records_present_inputs() -> DeltaResult<()> {
let inputs = Vec::from_iter((0..3).map(|_| vals(id_schema())));
assert_plan(
PlanBuilder::union_all(inputs)?,
&[
(&[], "values"),
(&[], "values"),
(&[], "values"),
(&[0, 1, 2], "union_all"),
],
);
Ok(())
}
#[test]
fn union_all_of_one_forwards() -> DeltaResult<()> {
assert_plan(
PlanBuilder::union_all([vals(id_schema())])?,
&[(&[], "values")],
);
Ok(())
}
#[rstest::rstest]
#[case::empty_parquet(PlanBuilder::scan_parquet(Vec::<FileMeta>::new(), &[], id_schema()))]
#[case::empty_json(PlanBuilder::scan_json(Vec::<FileMeta>::new(), &[], id_schema()))]
#[case::empty_values(PlanBuilder::values(id_schema(), vec![]))]
#[case::filter(absent_src().filter(col!("id").is_not_null()))]
#[case::project(absent_src().project(Expression::struct_from([col!("id")]), id_schema()))]
#[case::project_patch(absent_src().project_patch(|p| p.drop("id")))]
#[case::load(absent_over(load_input_schema()).load(load_node(load_output_schema())))]
#[case::grouped_aggregate(absent_src().aggregate(
Aggregate::group_by(part_schema(), [column_name!("id")]).max(column_name!("part"))))]
#[case::semi_join_absent_build(
vals(id_schema()).semi_join(absent_src(), [column_name!("id")], [column_name!("id")]))]
#[case::semi_join_absent_probe(
absent_src().semi_join(vals(x_schema()), [column_name!("id")], [column_name!("x")]))]
#[case::anti_join_absent_probe(
absent_src().anti_join(vals(x_schema()), [column_name!("id")], [column_name!("x")]))]
#[case::union_all_of_absent(PlanBuilder::union_all([absent_src(), absent_src()]))]
fn collapses_to_absent(#[case] builder: DeltaResult<PlanBuilder>) -> DeltaResult<()> {
assert!(builder?.build_opt()?.is_none()); Ok(())
}
#[test]
fn absent_carries_schema_and_builds_empty_values() -> DeltaResult<()> {
let absent = absent_src();
assert_eq!(absent.schema(), &id_schema());
let plan = assert_plan(absent, &[(&[], "values")]);
let Operator::Values(values) = &plan.nodes[0].op else {
panic!("expected Values");
};
assert!(values.rows.is_empty());
assert_eq!(values.schema, id_schema());
Ok(())
}
#[test]
fn union_all_forwards_lone_present_arm() -> DeltaResult<()> {
assert_plan(
PlanBuilder::union_all([absent_src(), vals(id_schema())])?,
&[(&[], "values")],
);
Ok(())
}
#[test]
fn anti_join_over_absent_build_forwards_probe() -> DeltaResult<()> {
let anti = vals(id_schema()).anti_join(
absent_src(),
[column_name!("id")],
[column_name!("id")],
)?;
assert_plan(anti, &[(&[], "values")]);
Ok(())
}
#[test]
fn aggregate_outputs_keys_then_aggs() -> DeltaResult<()> {
let agg = vals(part_schema()).aggregate(
Aggregate::group_by(part_schema(), [column_name!("id")])
.max_non_null_by(column_name!("part"), column_name!("id")),
)?;
assert_eq!(agg.schema(), &part_schema());
assert_plan(agg, &[(&[], "values"), (&[0], "aggregate")]);
Ok(())
}
#[test]
fn aggregate_by_infers_input_schema() -> DeltaResult<()> {
let agg = vals(part_schema()).aggregate_by([column_name!("id")], |a| {
a.max_non_null_by(column_name!("part"), column_name!("id"))
})?;
assert_eq!(agg.schema(), &part_schema());
assert_plan(agg, &[(&[], "values"), (&[0], "aggregate")]);
Ok(())
}
#[test]
fn aggregate_global_over_absent_aggregates_empty_values() -> DeltaResult<()> {
let agg = absent_src().aggregate(
Aggregate::group_by(id_schema(), Vec::<ColumnName>::new()).max(column_name!("id")),
)?;
let plan = assert_plan(agg, &[(&[], "values"), (&[0], "aggregate")]);
let Operator::Values(values) = &plan.nodes[0].op else {
panic!("expected Values input");
};
assert!(values.rows.is_empty());
Ok(())
}
#[test]
fn aggregate_global_over_present_outputs_aggs_only() -> DeltaResult<()> {
let agg = vals(part_schema()).aggregate(
Aggregate::group_by(part_schema(), Vec::<ColumnName>::new()).max(column_name!("part")),
)?;
let schema = agg.schema();
assert_eq!(schema.fields().count(), 1);
assert!(schema.field("part").is_some());
let plan = assert_plan(agg, &[(&[], "values"), (&[0], "aggregate")]);
let Operator::Aggregate(node) = &plan.nodes[1].op else {
panic!("expected Aggregate");
};
assert!(node.group_by.is_empty());
Ok(())
}
fn nested_ab_c() -> SchemaRef {
Arc::new(StructType::new_unchecked([
StructField::nullable(
"outer",
StructType::new_unchecked([
StructField::nullable("a", DataType::LONG),
StructField::nullable("b", DataType::STRING),
]),
),
StructField::nullable("c", DataType::LONG),
]))
}
#[test]
fn project_patch_edits_track_schema() -> DeltaResult<()> {
let patched = vals(nested_ab_c()).project_patch(|p| {
p.replace_at(
["outer"],
"b",
StructField::nullable("b2", DataType::LONG),
lit(0i64),
)
.append(StructField::nullable("d", DataType::LONG), lit(1i64))
.drop("c")
})?;
let expected: SchemaRef = Arc::new(StructType::new_unchecked([
StructField::nullable(
"outer",
StructType::new_unchecked([
StructField::nullable("a", DataType::LONG),
StructField::nullable("b2", DataType::LONG),
]),
),
StructField::nullable("d", DataType::LONG),
]));
assert_eq!(patched.schema(), &expected);
assert_plan(patched, &[(&[], "values"), (&[0], "project")]);
Ok(())
}
#[test]
fn project_patch_accepts_shared_fn() -> DeltaResult<()> {
fn drop_c(p: ProjectionStructPatchBuilder<'_>) -> ProjectionStructPatchBuilder<'_> {
p.drop("c")
}
let a = vals(nested_ab_c()).project_patch(drop_c)?;
let b = vals(nested_ab_c()).project_patch(drop_c)?;
assert_eq!(a.schema(), b.schema());
Ok(())
}
#[test]
fn project_records_declared_schema() -> DeltaResult<()> {
let out = x_schema();
let p = vals(id_schema()).project(Expression::struct_from([col!("id")]), out.clone())?;
assert_eq!(p.schema(), &out);
assert_plan(p, &[(&[], "values"), (&[0], "project")]);
Ok(())
}
#[rstest::rstest]
fn join_mirrors_probe_and_orders_inputs(
#[values(false, true)] inverted: bool,
) -> DeltaResult<()> {
let probe = vals(id_schema());
let build = vals(x_schema());
let joined = if inverted {
probe.anti_join(build, [column_name!("id")], [column_name!("x")])?
} else {
probe.semi_join(build, [column_name!("id")], [column_name!("x")])?
};
assert_eq!(joined.schema(), &id_schema());
let plan = assert_plan(
joined,
&[(&[], "values"), (&[], "values"), (&[0, 1], "semi_join")],
);
let Operator::SemiJoin(node) = &plan.nodes[2].op else {
panic!("expected SemiJoin");
};
assert_eq!(node.inverted, inverted);
Ok(())
}
fn load_node(out_schema: SchemaRef) -> Load {
let file_meta = LoadColumnFileMeta::new(
column_name!("path"),
column_name!("size"),
column_name!("num_records"),
);
Load::new(out_schema, FileType::Parquet, file_meta, column_name!("dv"))
.with_base_url(url::Url::parse("memory:///").unwrap())
.with_file_constant_columns(["version"])
}
fn load_input_schema() -> SchemaRef {
Arc::new(StructType::new_unchecked([
StructField::not_null("path", DataType::STRING),
StructField::nullable("size", DataType::LONG),
StructField::nullable("num_records", DataType::LONG),
StructField::nullable("dv", DataType::STRING),
StructField::nullable("version", DataType::LONG),
]))
}
fn load_output_schema() -> SchemaRef {
Arc::new(StructType::new_unchecked([
StructField::nullable("id", DataType::STRING),
StructField::nullable("version", DataType::LONG),
]))
}
#[test]
fn load_sets_output_schema_and_records_input() -> DeltaResult<()> {
let out = load_output_schema();
let loaded = vals(load_input_schema()).load(load_node(out.clone()))?;
assert_eq!(loaded.schema(), &out);
assert_plan(loaded, &[(&[], "values"), (&[0], "load")]);
Ok(())
}
fn load_input_missing(omit: &str) -> SchemaRef {
let fields = ["path", "size", "num_records", "dv", "version"]
.into_iter()
.filter(|c| *c != omit)
.map(|c| StructField::nullable(c, DataType::STRING));
Arc::new(StructType::new_unchecked(fields))
}
#[rstest::rstest]
#[case::scan_file_constant_arity("constant value",
|| PlanBuilder::scan_parquet(vec![one_constant_file("file:///a.parquet")], &[], part_schema()))]
#[case::scan_unknown_file_constant("`nope` not found",
|| PlanBuilder::scan_parquet(vec![one_constant_file("file:///a.parquet")], &["nope"], part_schema()))]
#[case::values_row_width("schema has 1 field",
|| PlanBuilder::values(id_schema(), vec![vec!["a".into(), "b".into()]]))]
#[case::filter_unknown_column("`nope` not found",
|| vals(id_schema()).filter(col!("nope").is_not_null()))]
#[case::project_unknown_column("`nope` not found",
|| vals(id_schema()).project(Expression::struct_from([col!("nope")]), id_schema()))]
#[case::project_patch_missing_field("does not exist",
|| vals(id_schema()).project_patch(|p| p.drop("nope")))]
#[case::aggregate_unknown_value("not found in schema",
|| vals(id_schema()).aggregate(Aggregate::group_by(id_schema(), Vec::<ColumnName>::new()).max(column_name!("nope"))))]
#[case::aggregate_unknown_group_by("not found in schema",
|| vals(id_schema()).aggregate(Aggregate::group_by(id_schema(), [column_name!("nope")]).max(column_name!("id"))))]
#[case::union_schema_disagrees("differing",
|| PlanBuilder::union_all([vals(id_schema()), vals(x_schema())]))]
#[case::union_empty("at least one input", || PlanBuilder::union_all([]))]
#[case::join_unknown_probe_key("join probe: column `nope`",
|| vals(id_schema()).semi_join(vals(x_schema()), [column_name!("nope")], [column_name!("x")]))]
#[case::join_unknown_build_key("join build: column `nope`",
|| vals(id_schema()).anti_join(vals(x_schema()), [column_name!("id")], [column_name!("nope")]))]
#[case::join_key_arity("must match",
|| vals(id_schema()).semi_join(vals(x_schema()), [column_name!("id")], [column_name!("x"), column_name!("x")]))]
#[case::load_missing_path("`path`", || vals(load_input_missing("path")).load(load_node(load_output_schema())))]
#[case::load_missing_size("`size`", || vals(load_input_missing("size")).load(load_node(load_output_schema())))]
#[case::load_missing_num_records("`num_records`", || vals(load_input_missing("num_records")).load(load_node(load_output_schema())))]
#[case::load_missing_dv("`dv`", || vals(load_input_missing("dv")).load(load_node(load_output_schema())))]
#[case::load_missing_file_constant_source("`version`", || vals(load_input_missing("version")).load(load_node(load_output_schema())))]
#[case::load_file_constant_absent_from_output("`version`", || vals(load_input_schema()).load(load_node(id_schema())))]
fn rejects(#[case] needle: &str, #[case] make: impl Fn() -> DeltaResult<PlanBuilder>) {
let err = make().unwrap_err();
assert!(err.to_string().contains(needle), "got: {err}");
}
}