Skip to main content

buoyant_kernel/plans/proto/
convert.rs

1//! Conversions from the kernel plan IR into the prost-generated proto wire types.
2
3use super::plan::agg as proto_agg;
4use super::schema::data_type::Kind as DataTypeKind;
5use super::schema::metadata_value::Value as MetadataValueKind;
6use super::schema::primitive_type::Kind as PrimitiveTypeKind;
7use super::schema::SimplePrimitiveType as Simple;
8use super::{
9    expressions as proto_expr, operation as proto_op, plan as proto_plan, schema as proto_schema,
10};
11use crate::expressions::{
12    ArrayData, BinaryExpression, BinaryExpressionOp, BinaryPredicate, BinaryPredicateOp,
13    ColumnName, DecimalData, Expression, ExpressionFieldPatch, ExpressionStructPatch,
14    JunctionPredicate, JunctionPredicateOp, MapData, MapToStructExpression, OpaqueExpression,
15    OpaquePredicate, ParseJsonExpression, Predicate, Scalar, StructData, UnaryExpression,
16    UnaryExpressionOp, UnaryPredicate, UnaryPredicateOp, VariadicExpression, VariadicExpressionOp,
17};
18use crate::plans::ir::nodes::{
19    Agg, Aggregate, FileType, Filter, Load, LoadColumnFileMeta, Operator, Project, ScanFile,
20    ScanJson, ScanParquet, SemiJoin, Values,
21};
22use crate::plans::ir::plan::{Plan, PlanNode};
23use crate::plans::{IoOperation, Operation};
24use crate::schema::{
25    ArrayType, DataType, DecimalType, MapType, MetadataValue, PrimitiveType, StructField,
26    StructType,
27};
28use crate::{DeltaResult, Error, FileMeta, FileSlice};
29
30// === Helpers ===
31
32/// Converts each element of `items` into proto via [`From`].
33fn convert_vec<'a, T, U>(items: &'a [T]) -> Vec<U>
34where
35    U: From<&'a T>,
36{
37    items.iter().map(U::from).collect()
38}
39
40/// Converts a slice of [`Expression`] references into proto.
41fn convert_expr_vec<E>(items: &[E]) -> Vec<proto_expr::Expression>
42where
43    E: AsRef<Expression>,
44{
45    items.iter().map(|e| e.as_ref().into()).collect()
46}
47
48// === Operation / IoOperation to Proto ===
49
50impl From<&Operation> for proto_op::Operation {
51    fn from(op: &Operation) -> Self {
52        let op = match op {
53            Operation::IoOperation(io) => proto_op::operation::Op::Io(io.into()),
54            Operation::QueryPlan(plan) => proto_op::operation::Op::QueryPlan(plan.into()),
55        };
56        proto_op::Operation { op: Some(op) }
57    }
58}
59
60impl From<&IoOperation> for proto_op::IoOperation {
61    fn from(io: &IoOperation) -> Self {
62        use proto_op::io_operation::Op;
63        let op = match io {
64            IoOperation::FileListing { url } => Op::FileListing(proto_op::FileListing {
65                url: url.to_string(),
66            }),
67            IoOperation::ReadBytes { files } => Op::ReadBytes(proto_op::ReadBytes {
68                files: convert_vec(files),
69            }),
70            IoOperation::WriteBytes {
71                url,
72                data,
73                overwrite,
74            } => Op::WriteBytes(proto_op::WriteBytes {
75                url: url.to_string(),
76                data: data.to_vec(),
77                overwrite: *overwrite,
78            }),
79            IoOperation::HeadFile { url } => Op::HeadFile(proto_op::HeadFile {
80                url: url.to_string(),
81            }),
82            IoOperation::AtomicCopy {
83                source,
84                destination,
85            } => Op::AtomicCopy(proto_op::AtomicCopy {
86                source: source.to_string(),
87                destination: destination.to_string(),
88            }),
89            IoOperation::ParquetFooter { file } => Op::ParquetFooter(proto_op::ParquetFooter {
90                file: Some(file.into()),
91            }),
92        };
93        proto_op::IoOperation { op: Some(op) }
94    }
95}
96
97impl From<&FileSlice> for proto_op::FileSlice {
98    fn from(slice: &FileSlice) -> Self {
99        let (url, range) = slice;
100        proto_op::FileSlice {
101            url: url.to_string(),
102            range_start: range.as_ref().map(|r| r.start),
103            range_end: range.as_ref().map(|r| r.end),
104        }
105    }
106}
107
108impl From<&FileMeta> for proto_plan::FileMeta {
109    fn from(meta: &FileMeta) -> Self {
110        proto_plan::FileMeta {
111            location: meta.location.to_string(),
112            size: meta.size,
113            last_modified: meta.last_modified,
114        }
115    }
116}
117
118// === Plan / nodes to Proto ===
119
120impl From<&Plan> for proto_plan::Plan {
121    fn from(plan: &Plan) -> Self {
122        proto_plan::Plan {
123            nodes: convert_vec(&plan.nodes),
124        }
125    }
126}
127
128impl From<&PlanNode> for proto_plan::PlanNode {
129    fn from(node: &PlanNode) -> Self {
130        proto_plan::PlanNode {
131            op: Some((&node.op).into()),
132            inputs: node.inputs.iter().map(|&i| i as u32).collect(),
133        }
134    }
135}
136
137impl From<&Operator> for proto_plan::Operator {
138    fn from(op: &Operator) -> Self {
139        use proto_plan::operator::Op;
140        let op = match op {
141            Operator::ScanParquet(n) => Op::ScanParquet(n.into()),
142            Operator::ScanJson(n) => Op::ScanJson(n.into()),
143            Operator::Values(n) => Op::Values(n.into()),
144            Operator::Project(n) => Op::Project(n.into()),
145            Operator::Filter(n) => Op::Filter(n.into()),
146            Operator::Load(n) => Op::Load(n.into()),
147            Operator::Aggregate(n) => Op::Aggregate(n.into()),
148            Operator::SemiJoin(n) => Op::SemiJoin(n.into()),
149            Operator::UnionAll(_) => Op::UnionAll(proto_plan::UnionAllNode {}),
150        };
151        proto_plan::Operator { op: Some(op) }
152    }
153}
154
155impl From<&ScanFile> for proto_plan::ScanFile {
156    fn from(file: &ScanFile) -> Self {
157        proto_plan::ScanFile {
158            meta: Some((&file.meta).into()),
159            file_constants: convert_vec(&file.file_constants),
160        }
161    }
162}
163
164impl From<&ScanParquet> for proto_plan::ScanParquetNode {
165    fn from(node: &ScanParquet) -> Self {
166        proto_plan::ScanParquetNode {
167            files: convert_vec(&node.files),
168            file_constant_columns: node.file_constant_columns.clone(),
169            schema: Some(node.schema.as_ref().into()),
170        }
171    }
172}
173
174impl From<&ScanJson> for proto_plan::ScanJsonNode {
175    fn from(node: &ScanJson) -> Self {
176        proto_plan::ScanJsonNode {
177            files: convert_vec(&node.files),
178            file_constant_columns: node.file_constant_columns.clone(),
179            schema: Some(node.schema.as_ref().into()),
180        }
181    }
182}
183
184impl From<&Values> for proto_plan::ValuesNode {
185    fn from(node: &Values) -> Self {
186        let rows = node
187            .rows
188            .iter()
189            .map(|row| proto_plan::ValuesRow {
190                values: convert_vec(row),
191            })
192            .collect();
193        proto_plan::ValuesNode {
194            schema: Some(node.schema.as_ref().into()),
195            rows,
196        }
197    }
198}
199
200impl From<&Project> for proto_plan::ProjectNode {
201    fn from(node: &Project) -> Self {
202        proto_plan::ProjectNode {
203            expr: Some(node.expr.as_ref().into()),
204            schema: Some(node.schema.as_ref().into()),
205        }
206    }
207}
208
209impl From<&Filter> for proto_plan::FilterNode {
210    fn from(node: &Filter) -> Self {
211        proto_plan::FilterNode {
212            predicate: Some(node.predicate.as_ref().into()),
213        }
214    }
215}
216
217impl From<&Load> for proto_plan::LoadNode {
218    fn from(node: &Load) -> Self {
219        proto_plan::LoadNode {
220            schema: Some(node.schema.as_ref().into()),
221            file_type: proto_plan::FileType::from(node.file_type) as i32,
222            base_url: node.base_url.as_ref().map(ToString::to_string),
223            file_constant_columns: node.file_constant_columns.clone(),
224            file_meta: Some((&node.file_meta).into()),
225            dv_column: Some((&node.dv_column).into()),
226        }
227    }
228}
229
230impl From<&LoadColumnFileMeta> for proto_plan::LoadColumnFileMeta {
231    fn from(meta: &LoadColumnFileMeta) -> Self {
232        proto_plan::LoadColumnFileMeta {
233            path_column: Some((&meta.path_column).into()),
234            file_size_column: Some((&meta.file_size_column).into()),
235            num_records_column: Some((&meta.num_records_column).into()),
236        }
237    }
238}
239
240impl From<&Aggregate> for proto_plan::AggregateNode {
241    fn from(node: &Aggregate) -> Self {
242        proto_plan::AggregateNode {
243            group_by: convert_vec(&node.group_by),
244            aggs: convert_vec(&node.aggs),
245            schema: Some(node.schema.as_ref().into()),
246        }
247    }
248}
249
250impl From<&Agg> for proto_plan::Agg {
251    fn from(agg: &Agg) -> Self {
252        let func = match agg {
253            Agg::Min { value } => proto_agg::Func::Min(proto_plan::MinAgg {
254                value: Some(value.into()),
255            }),
256            Agg::Max { value } => proto_agg::Func::Max(proto_plan::MaxAgg {
257                value: Some(value.into()),
258            }),
259            Agg::MinNonNullBy { value, key } => {
260                proto_agg::Func::MinNonNullBy(proto_plan::MinNonNullByAgg {
261                    value: Some(value.into()),
262                    key: Some(key.into()),
263                })
264            }
265            Agg::MaxNonNullBy { value, key } => {
266                proto_agg::Func::MaxNonNullBy(proto_plan::MaxNonNullByAgg {
267                    value: Some(value.into()),
268                    key: Some(key.into()),
269                })
270            }
271        };
272        proto_plan::Agg { func: Some(func) }
273    }
274}
275
276impl From<&SemiJoin> for proto_plan::SemiJoinNode {
277    fn from(node: &SemiJoin) -> Self {
278        proto_plan::SemiJoinNode {
279            inverted: node.inverted,
280            probe_keys: convert_vec(&node.probe_keys),
281            build_keys: convert_vec(&node.build_keys),
282        }
283    }
284}
285
286impl From<FileType> for proto_plan::FileType {
287    fn from(file_type: FileType) -> Self {
288        match file_type {
289            FileType::Parquet => proto_plan::FileType::Parquet,
290            FileType::Json => proto_plan::FileType::Json,
291        }
292    }
293}
294
295// === Expressions to Proto ===
296
297impl From<&Expression> for proto_expr::Expression {
298    fn from(expr: &Expression) -> Self {
299        use proto_expr::expression::Kind;
300        let kind = match expr {
301            Expression::Literal(scalar) => Kind::Literal(scalar.into()),
302            Expression::Column(column) => Kind::Column(column.into()),
303            Expression::Predicate(pred) => Kind::Predicate(Box::new(pred.as_ref().into())),
304            Expression::Struct(exprs, nullability) => {
305                let nullability_predicate =
306                    nullability.as_ref().map(|n| Box::new(n.as_ref().into()));
307                Kind::StructExpr(Box::new(proto_expr::StructExpression {
308                    exprs: convert_expr_vec(exprs),
309                    nullability_predicate,
310                }))
311            }
312            Expression::StructPatch(patch) => Kind::Transform(patch.into()),
313            Expression::Unary(unary) => Kind::Unary(Box::new(unary.into())),
314            Expression::Binary(binary) => Kind::Binary(Box::new(binary.into())),
315            Expression::Variadic(variadic) => Kind::Variadic(variadic.into()),
316            Expression::Opaque(opaque) => Kind::Opaque(opaque.into()),
317            Expression::Unknown(name) => Kind::Unknown(name.clone()),
318            Expression::ParseJson(parse_json) => Kind::ParseJson(Box::new(parse_json.into())),
319            Expression::MapToStruct(map_to_struct) => {
320                Kind::MapToStruct(Box::new(map_to_struct.into()))
321            }
322        };
323        proto_expr::Expression { kind: Some(kind) }
324    }
325}
326
327impl From<&Predicate> for proto_expr::Predicate {
328    fn from(pred: &Predicate) -> Self {
329        use proto_expr::predicate::Kind;
330        let kind = match pred {
331            Predicate::BooleanExpression(expr) => Kind::BooleanExpression(Box::new(expr.into())),
332            Predicate::Not(inner) => Kind::Not(Box::new(inner.as_ref().into())),
333            Predicate::Unary(unary) => Kind::Unary(Box::new(unary.into())),
334            Predicate::Binary(binary) => Kind::Binary(Box::new(binary.into())),
335            Predicate::Junction(junction) => Kind::Junction(junction.into()),
336            Predicate::Opaque(opaque) => Kind::Opaque(opaque.into()),
337            Predicate::Unknown(name) => Kind::Unknown(name.clone()),
338        };
339        proto_expr::Predicate { kind: Some(kind) }
340    }
341}
342
343impl From<&ColumnName> for proto_expr::ColumnName {
344    fn from(column: &ColumnName) -> Self {
345        proto_expr::ColumnName {
346            path: column.path().to_vec(),
347        }
348    }
349}
350
351impl From<&UnaryExpression> for proto_expr::UnaryExpression {
352    fn from(unary: &UnaryExpression) -> Self {
353        proto_expr::UnaryExpression {
354            op: proto_expr::UnaryExpressionOp::from(unary.op) as i32,
355            expr: Some(Box::new(unary.expr.as_ref().into())),
356        }
357    }
358}
359
360impl From<&BinaryExpression> for proto_expr::BinaryExpression {
361    fn from(binary: &BinaryExpression) -> Self {
362        proto_expr::BinaryExpression {
363            op: proto_expr::BinaryExpressionOp::from(binary.op) as i32,
364            left: Some(Box::new(binary.left.as_ref().into())),
365            right: Some(Box::new(binary.right.as_ref().into())),
366        }
367    }
368}
369
370impl From<&VariadicExpression> for proto_expr::VariadicExpression {
371    fn from(variadic: &VariadicExpression) -> Self {
372        proto_expr::VariadicExpression {
373            op: proto_expr::VariadicExpressionOp::from(variadic.op) as i32,
374            exprs: convert_vec(&variadic.exprs),
375        }
376    }
377}
378
379impl From<&OpaqueExpression> for proto_expr::OpaqueExpression {
380    fn from(opaque: &OpaqueExpression) -> Self {
381        proto_expr::OpaqueExpression {
382            name: opaque.op.name().to_string(),
383            exprs: convert_vec(&opaque.exprs),
384        }
385    }
386}
387
388impl From<&ParseJsonExpression> for proto_expr::ParseJsonExpression {
389    fn from(parse_json: &ParseJsonExpression) -> Self {
390        proto_expr::ParseJsonExpression {
391            json_expr: Some(Box::new(parse_json.json_expr.as_ref().into())),
392            output_schema: Some(parse_json.output_schema.as_ref().into()),
393        }
394    }
395}
396
397impl From<&MapToStructExpression> for proto_expr::MapToStructExpression {
398    fn from(map_to_struct: &MapToStructExpression) -> Self {
399        proto_expr::MapToStructExpression {
400            map_expr: Some(Box::new(map_to_struct.map_expr.as_ref().into())),
401        }
402    }
403}
404
405impl From<&UnaryPredicate> for proto_expr::UnaryPredicate {
406    fn from(unary: &UnaryPredicate) -> Self {
407        proto_expr::UnaryPredicate {
408            op: proto_expr::UnaryPredicateOp::from(unary.op) as i32,
409            expr: Some(Box::new(unary.expr.as_ref().into())),
410        }
411    }
412}
413
414impl From<&BinaryPredicate> for proto_expr::BinaryPredicate {
415    fn from(binary: &BinaryPredicate) -> Self {
416        proto_expr::BinaryPredicate {
417            op: proto_expr::BinaryPredicateOp::from(binary.op) as i32,
418            left: Some(Box::new(binary.left.as_ref().into())),
419            right: Some(Box::new(binary.right.as_ref().into())),
420        }
421    }
422}
423
424impl From<&JunctionPredicate> for proto_expr::JunctionPredicate {
425    fn from(junction: &JunctionPredicate) -> Self {
426        proto_expr::JunctionPredicate {
427            op: proto_expr::JunctionPredicateOp::from(junction.op) as i32,
428            preds: convert_vec(&junction.preds),
429        }
430    }
431}
432
433impl From<&OpaquePredicate> for proto_expr::OpaquePredicate {
434    fn from(opaque: &OpaquePredicate) -> Self {
435        proto_expr::OpaquePredicate {
436            name: opaque.op.name().to_string(),
437            exprs: convert_vec(&opaque.exprs),
438        }
439    }
440}
441
442// The proto `Transform` struct mirrors `ExpressionStructPatch`.
443impl From<&ExpressionStructPatch> for proto_expr::Transform {
444    fn from(patch: &ExpressionStructPatch) -> Self {
445        let field_transforms = patch
446            .field_patches
447            .iter()
448            .map(|(name, field_patch)| (name.clone(), field_patch.into()))
449            .collect();
450        proto_expr::Transform {
451            input_path: patch.input_path.as_ref().map(Into::into),
452            field_transforms,
453            prepended_fields: convert_expr_vec(&patch.prepended_fields),
454            appended_fields: convert_expr_vec(&patch.appended_fields),
455        }
456    }
457}
458
459// The proto `FieldTransform` struct mirrors `ExpressionFieldPatch`.
460impl From<&ExpressionFieldPatch> for proto_expr::FieldTransform {
461    fn from(field_patch: &ExpressionFieldPatch) -> Self {
462        proto_expr::FieldTransform {
463            exprs: convert_expr_vec(&field_patch.insertions),
464            is_replace: !field_patch.keep_input,
465            optional: field_patch.optional,
466        }
467    }
468}
469
470impl From<UnaryExpressionOp> for proto_expr::UnaryExpressionOp {
471    fn from(op: UnaryExpressionOp) -> Self {
472        match op {
473            UnaryExpressionOp::ToJson => proto_expr::UnaryExpressionOp::ToJson,
474        }
475    }
476}
477
478impl From<BinaryExpressionOp> for proto_expr::BinaryExpressionOp {
479    fn from(op: BinaryExpressionOp) -> Self {
480        match op {
481            BinaryExpressionOp::Plus => proto_expr::BinaryExpressionOp::Plus,
482            BinaryExpressionOp::Minus => proto_expr::BinaryExpressionOp::Minus,
483            BinaryExpressionOp::Multiply => proto_expr::BinaryExpressionOp::Multiply,
484            BinaryExpressionOp::Divide => proto_expr::BinaryExpressionOp::Divide,
485        }
486    }
487}
488
489impl From<VariadicExpressionOp> for proto_expr::VariadicExpressionOp {
490    fn from(op: VariadicExpressionOp) -> Self {
491        match op {
492            VariadicExpressionOp::Coalesce => proto_expr::VariadicExpressionOp::Coalesce,
493            VariadicExpressionOp::Array => proto_expr::VariadicExpressionOp::Array,
494        }
495    }
496}
497
498impl From<UnaryPredicateOp> for proto_expr::UnaryPredicateOp {
499    fn from(op: UnaryPredicateOp) -> Self {
500        match op {
501            UnaryPredicateOp::IsNull => proto_expr::UnaryPredicateOp::IsNull,
502        }
503    }
504}
505
506impl From<BinaryPredicateOp> for proto_expr::BinaryPredicateOp {
507    fn from(op: BinaryPredicateOp) -> Self {
508        match op {
509            BinaryPredicateOp::LessThan => proto_expr::BinaryPredicateOp::LessThan,
510            BinaryPredicateOp::GreaterThan => proto_expr::BinaryPredicateOp::GreaterThan,
511            BinaryPredicateOp::Equal => proto_expr::BinaryPredicateOp::Equal,
512            BinaryPredicateOp::Distinct => proto_expr::BinaryPredicateOp::Distinct,
513            BinaryPredicateOp::In => proto_expr::BinaryPredicateOp::In,
514        }
515    }
516}
517
518impl From<JunctionPredicateOp> for proto_expr::JunctionPredicateOp {
519    fn from(op: JunctionPredicateOp) -> Self {
520        match op {
521            JunctionPredicateOp::And => proto_expr::JunctionPredicateOp::And,
522            JunctionPredicateOp::Or => proto_expr::JunctionPredicateOp::Or,
523        }
524    }
525}
526
527// === Scalars to Proto ===
528
529impl From<&Scalar> for proto_expr::Scalar {
530    fn from(scalar: &Scalar) -> Self {
531        use proto_expr::scalar::Value;
532        let value = match scalar {
533            Scalar::Integer(v) => Value::Integer(*v),
534            Scalar::Long(v) => Value::Long(*v),
535            Scalar::Short(v) => Value::Short(*v as i32),
536            Scalar::Byte(v) => Value::Byte(*v as i32),
537            Scalar::Float(v) => Value::Float(*v),
538            Scalar::Double(v) => Value::Double(*v),
539            Scalar::String(v) => Value::String(v.clone()),
540            Scalar::Boolean(v) => Value::Boolean(*v),
541            Scalar::Timestamp(v) => Value::Timestamp(*v),
542            Scalar::TimestampNtz(v) => Value::TimestampNtz(*v),
543            #[cfg(feature = "nanosecond-timestamps")]
544            Scalar::TimestampNanos(v) => Value::TimestampNanos(*v),
545            #[cfg(feature = "nanosecond-timestamps")]
546            Scalar::TimestampNanosNtz(v) => Value::TimestampNanosNtz(*v),
547            Scalar::Date(v) => Value::Date(*v),
548            Scalar::Binary(v) => Value::Binary(v.clone()),
549            Scalar::Decimal(decimal) => Value::Decimal(decimal.into()),
550            Scalar::Null(data_type) => Value::Null(data_type.into()),
551            Scalar::Struct(struct_data) => Value::Struct(struct_data.into()),
552            Scalar::Array(array_data) => Value::Array(array_data.into()),
553            Scalar::Map(map_data) => Value::Map(map_data.into()),
554        };
555        proto_expr::Scalar { value: Some(value) }
556    }
557}
558
559impl From<&DecimalData> for proto_expr::DecimalData {
560    fn from(decimal: &DecimalData) -> Self {
561        proto_expr::DecimalData {
562            bits: decimal.bits().to_be_bytes().to_vec(),
563            decimal_type: Some((*decimal.ty()).into()),
564        }
565    }
566}
567
568impl From<&StructData> for proto_expr::StructData {
569    fn from(struct_data: &StructData) -> Self {
570        proto_expr::StructData {
571            fields: convert_vec(struct_data.fields()),
572            values: convert_vec(struct_data.values()),
573        }
574    }
575}
576
577impl From<&ArrayData> for proto_expr::ArrayData {
578    fn from(array_data: &ArrayData) -> Self {
579        proto_expr::ArrayData {
580            array_type: Some(array_data.array_type().into()),
581            elements: convert_vec(array_data.array_elements()),
582        }
583    }
584}
585
586impl From<&MapData> for proto_expr::MapData {
587    fn from(map_data: &MapData) -> Self {
588        let pairs = map_data
589            .pairs()
590            .iter()
591            .map(|(key, value)| proto_expr::MapEntry {
592                key: Some(key.into()),
593                value: Some(value.into()),
594            })
595            .collect();
596        proto_expr::MapData {
597            map_type: Some(map_data.map_type().into()),
598            pairs,
599        }
600    }
601}
602
603// === Schema to Proto ===
604
605impl From<&DataType> for proto_schema::DataType {
606    fn from(data_type: &DataType) -> Self {
607        let kind = match data_type {
608            DataType::Primitive(primitive) => DataTypeKind::Primitive(primitive.into()),
609            DataType::Array(array) => DataTypeKind::Array(Box::new(array.as_ref().into())),
610            DataType::Struct(struct_type) => DataTypeKind::Struct(struct_type.as_ref().into()),
611            DataType::Map(map) => DataTypeKind::Map(Box::new(map.as_ref().into())),
612            // The proto `VariantType` is intentionally empty: variants are opaque on the wire.
613            DataType::Variant(_) => DataTypeKind::Variant(proto_schema::VariantType {}),
614        };
615        proto_schema::DataType { kind: Some(kind) }
616    }
617}
618
619impl From<&PrimitiveType> for proto_schema::PrimitiveType {
620    fn from(primitive: &PrimitiveType) -> Self {
621        let kind = match primitive {
622            PrimitiveType::String => Kind::Simple(Simple::String as i32),
623            PrimitiveType::Long => Kind::Simple(Simple::Long as i32),
624            PrimitiveType::Integer => Kind::Simple(Simple::Integer as i32),
625            PrimitiveType::Short => Kind::Simple(Simple::Short as i32),
626            PrimitiveType::Byte => Kind::Simple(Simple::Byte as i32),
627            PrimitiveType::Float => Kind::Simple(Simple::Float as i32),
628            PrimitiveType::Double => Kind::Simple(Simple::Double as i32),
629            PrimitiveType::Boolean => Kind::Simple(Simple::Boolean as i32),
630            PrimitiveType::Binary => Kind::Simple(Simple::Binary as i32),
631            PrimitiveType::Date => Kind::Simple(Simple::Date as i32),
632            PrimitiveType::Timestamp => Kind::Simple(Simple::Timestamp as i32),
633            PrimitiveType::TimestampNtz => Kind::Simple(Simple::TimestampNtz as i32),
634            #[cfg(feature = "nanosecond-timestamps")]
635            PrimitiveType::TimestampNanos => Kind::Simple(Simple::TimestampNanos as i32),
636            #[cfg(feature = "nanosecond-timestamps")]
637            PrimitiveType::TimestampNanosNtz => Kind::Simple(Simple::TimestampNanosNtz as i32),
638            PrimitiveType::Decimal(decimal) => Kind::Decimal((*decimal).into()),
639            PrimitiveType::Void => Kind::Simple(Simple::Void as i32),
640            PrimitiveType::IntervalYearMonth => Kind::Simple(Simple::IntervalYearMonth as i32),
641            PrimitiveType::IntervalDayTime => Kind::Simple(Simple::IntervalDayTime as i32),
642        };
643        proto_schema::PrimitiveType { kind: Some(kind) }
644    }
645}
646
647impl From<DecimalType> for proto_schema::DecimalType {
648    fn from(decimal: DecimalType) -> Self {
649        proto_schema::DecimalType {
650            precision: u32::from(decimal.precision()),
651            scale: u32::from(decimal.scale()),
652        }
653    }
654}
655
656impl From<&ArrayType> for proto_schema::ArrayType {
657    fn from(array: &ArrayType) -> Self {
658        proto_schema::ArrayType {
659            element_type: Some(Box::new(array.element_type().into())),
660            contains_null: array.contains_null(),
661        }
662    }
663}
664
665impl From<&MapType> for proto_schema::MapType {
666    fn from(map: &MapType) -> Self {
667        proto_schema::MapType {
668            key_type: Some(Box::new(map.key_type().into())),
669            value_type: Some(Box::new(map.value_type().into())),
670            value_contains_null: map.value_contains_null(),
671        }
672    }
673}
674
675impl From<&StructType> for proto_schema::StructType {
676    fn from(struct_type: &StructType) -> Self {
677        proto_schema::StructType {
678            fields: struct_type.fields().map(Into::into).collect(),
679        }
680    }
681}
682
683impl From<&StructField> for proto_schema::StructField {
684    fn from(field: &StructField) -> Self {
685        let metadata = field
686            .metadata
687            .iter()
688            .map(|(key, value)| (key.clone(), value.into()))
689            .collect();
690        proto_schema::StructField {
691            name: field.name.clone(),
692            data_type: Some((&field.data_type).into()),
693            nullable: field.nullable,
694            metadata,
695        }
696    }
697}
698
699impl From<&MetadataValue> for proto_schema::MetadataValue {
700    fn from(metadata: &MetadataValue) -> Self {
701        let value = match metadata {
702            MetadataValue::Number(n) => MetadataValueKind::Number(*n),
703            MetadataValue::String(s) => MetadataValueKind::String(s.clone()),
704            MetadataValue::Boolean(b) => MetadataValueKind::Boolean(*b),
705            MetadataValue::Other(json) => MetadataValueKind::OtherJson(json.to_string()),
706        };
707        proto_schema::MetadataValue { value: Some(value) }
708    }
709}
710
711// === Schema from Proto ===
712
713impl TryFrom<proto_schema::StructType> for StructType {
714    type Error = Error;
715    fn try_from(proto: proto_schema::StructType) -> DeltaResult<Self> {
716        let fields = proto
717            .fields
718            .into_iter()
719            .map(StructField::try_from)
720            .collect::<DeltaResult<Vec<_>>>()?;
721        StructType::try_new(fields)
722    }
723}
724
725impl TryFrom<proto_schema::StructField> for StructField {
726    type Error = Error;
727
728    fn try_from(proto: proto_schema::StructField) -> DeltaResult<Self> {
729        let data_type = proto
730            .data_type
731            .ok_or_else(|| Error::schema("StructField proto missing data_type"))?;
732        let metadata = proto
733            .metadata
734            .into_iter()
735            .map(|(key, value)| Ok::<_, Error>((key, MetadataValue::try_from(value)?)))
736            .collect::<DeltaResult<std::collections::HashMap<_, _>>>()?;
737        Ok(StructField {
738            name: proto.name,
739            data_type: DataType::try_from(data_type)?,
740            nullable: proto.nullable,
741            metadata,
742        })
743    }
744}
745
746impl TryFrom<proto_schema::DataType> for DataType {
747    type Error = Error;
748    fn try_from(proto: proto_schema::DataType) -> DeltaResult<Self> {
749        let kind = proto
750            .kind
751            .ok_or_else(|| Error::schema("DataType proto missing kind"))?;
752        let data_type = match kind {
753            DataTypeKind::Primitive(primitive) => DataType::Primitive(primitive.try_into()?),
754            DataTypeKind::Array(array) => DataType::Array(Box::new((*array).try_into()?)),
755            DataTypeKind::Struct(struct_type) => {
756                DataType::Struct(Box::new(struct_type.try_into()?))
757            }
758            DataTypeKind::Map(map) => DataType::Map(Box::new((*map).try_into()?)),
759            // Kernel does not support shredded variants, so always decode as unshredded.
760            DataTypeKind::Variant(_) => DataType::unshredded_variant(),
761        };
762        Ok(data_type)
763    }
764}
765
766impl TryFrom<proto_schema::PrimitiveType> for PrimitiveType {
767    type Error = Error;
768    fn try_from(proto: proto_schema::PrimitiveType) -> DeltaResult<Self> {
769        let kind = proto
770            .kind
771            .ok_or_else(|| Error::schema("PrimitiveType proto missing kind"))?;
772        let primitive = match kind {
773            PrimitiveTypeKind::Simple(simple) => {
774                let simple = Simple::try_from(simple).map_err(|_| {
775                    Error::schema(format!("unknown SimplePrimitiveType value: {simple}"))
776                })?;
777                match simple {
778                    Simple::String => PrimitiveType::String,
779                    Simple::Long => PrimitiveType::Long,
780                    Simple::Integer => PrimitiveType::Integer,
781                    Simple::Short => PrimitiveType::Short,
782                    Simple::Byte => PrimitiveType::Byte,
783                    Simple::Float => PrimitiveType::Float,
784                    Simple::Double => PrimitiveType::Double,
785                    Simple::Boolean => PrimitiveType::Boolean,
786                    Simple::Binary => PrimitiveType::Binary,
787                    Simple::Date => PrimitiveType::Date,
788                    Simple::Timestamp => PrimitiveType::Timestamp,
789                    Simple::TimestampNtz => PrimitiveType::TimestampNtz,
790                    Simple::Void => PrimitiveType::Void,
791                    Simple::IntervalYearMonth => PrimitiveType::IntervalYearMonth,
792                    Simple::IntervalDayTime => PrimitiveType::IntervalDayTime,
793                    Simple::Unspecified => {
794                        return Err(Error::schema("SimplePrimitiveType is unspecified"))
795                    }
796                }
797            }
798            PrimitiveTypeKind::Decimal(decimal) => PrimitiveType::Decimal(decimal.try_into()?),
799        };
800        Ok(primitive)
801    }
802}
803
804impl TryFrom<proto_schema::DecimalType> for DecimalType {
805    type Error = Error;
806    fn try_from(proto: proto_schema::DecimalType) -> DeltaResult<Self> {
807        let precision = u8::try_from(proto.precision).map_err(|_| {
808            Error::invalid_decimal(format!("precision out of range: {}", proto.precision))
809        })?;
810        let scale = u8::try_from(proto.scale)
811            .map_err(|_| Error::invalid_decimal(format!("scale out of range: {}", proto.scale)))?;
812        DecimalType::try_new(precision, scale)
813    }
814}
815
816impl TryFrom<proto_schema::ArrayType> for ArrayType {
817    type Error = Error;
818    fn try_from(proto: proto_schema::ArrayType) -> DeltaResult<Self> {
819        let element_type = proto
820            .element_type
821            .ok_or_else(|| Error::schema("ArrayType proto missing element_type"))?;
822        Ok(ArrayType::new(
823            DataType::try_from(*element_type)?,
824            proto.contains_null,
825        ))
826    }
827}
828
829impl TryFrom<proto_schema::MapType> for MapType {
830    type Error = Error;
831    fn try_from(proto: proto_schema::MapType) -> DeltaResult<Self> {
832        let key_type = proto
833            .key_type
834            .ok_or_else(|| Error::schema("MapType proto missing key_type"))?;
835        let value_type = proto
836            .value_type
837            .ok_or_else(|| Error::schema("MapType proto missing value_type"))?;
838        Ok(MapType::new(
839            DataType::try_from(*key_type)?,
840            DataType::try_from(*value_type)?,
841            proto.value_contains_null,
842        ))
843    }
844}
845
846impl TryFrom<proto_schema::MetadataValue> for MetadataValue {
847    type Error = Error;
848    fn try_from(proto: proto_schema::MetadataValue) -> DeltaResult<Self> {
849        let value = proto
850            .value
851            .ok_or_else(|| Error::schema("MetadataValue proto missing value"))?;
852        let metadata = match value {
853            MetadataValueKind::Number(n) => MetadataValue::Number(n),
854            MetadataValueKind::String(s) => MetadataValue::String(s),
855            MetadataValueKind::Boolean(b) => MetadataValue::Boolean(b),
856            MetadataValueKind::OtherJson(json) => {
857                MetadataValue::Other(serde_json::from_str(&json)?)
858            }
859        };
860        Ok(metadata)
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use std::sync::Arc;
867
868    use bytes::Bytes;
869    use rstest::rstest;
870    use url::Url;
871
872    use crate::expressions::{
873        lit, ArrayData, BinaryExpressionOp, BinaryPredicateOp, ColumnName, DecimalData, Expression,
874        ExpressionStructPatchBuilder, JunctionPredicateOp, MapData, OpaqueExpressionOp,
875        OpaquePredicateOp, Predicate, Scalar, ScalarExpressionEvaluator, StructData,
876        UnaryExpressionOp, UnaryPredicateOp, VariadicExpressionOp,
877    };
878    use crate::kernel_predicates::{
879        DirectDataSkippingPredicateEvaluator, DirectPredicateEvaluator,
880        IndirectDataSkippingPredicateEvaluator,
881    };
882    use crate::plans::ir::nodes::{
883        Agg, Aggregate, FileType, Filter, Load, LoadColumnFileMeta, Operator, Project, ScanFile,
884        ScanJson, ScanParquet, SemiJoin, UnionAll, Values,
885    };
886    use crate::plans::ir::plan::{Plan, PlanNode};
887    use crate::plans::proto::{
888        expressions as proto_expr, operation as proto_op, plan as proto_plan,
889        schema as proto_schema,
890    };
891    use crate::plans::{IoOperation, Operation};
892    use crate::schema::{
893        ArrayType, DataType, DecimalType, MapType, MetadataValue, PrimitiveType, SchemaRef,
894        StructField, StructType,
895    };
896    use crate::{DeltaResult, FileMeta, FileSlice};
897
898    // === Test helpers ===
899
900    #[derive(Debug, PartialEq)]
901    struct TestOpaqueExprOp;
902
903    impl OpaqueExpressionOp for TestOpaqueExprOp {
904        fn name(&self) -> &str {
905            "test_opaque_expr"
906        }
907        fn eval_expr_scalar(
908            &self,
909            _eval_expr: &ScalarExpressionEvaluator<'_>,
910            _exprs: &[Expression],
911        ) -> DeltaResult<Scalar> {
912            Ok(Scalar::Integer(0))
913        }
914    }
915
916    #[derive(Debug, PartialEq)]
917    struct TestOpaquePredOp;
918
919    impl OpaquePredicateOp for TestOpaquePredOp {
920        fn name(&self) -> &str {
921            "test_opaque_pred"
922        }
923        fn eval_pred_scalar(
924            &self,
925            _eval_expr: &ScalarExpressionEvaluator<'_>,
926            _eval_pred: &DirectPredicateEvaluator<'_>,
927            _exprs: &[Expression],
928            _inverted: bool,
929        ) -> DeltaResult<Option<bool>> {
930            Ok(Some(true))
931        }
932        fn eval_as_data_skipping_predicate(
933            &self,
934            _evaluator: &DirectDataSkippingPredicateEvaluator<'_>,
935            _exprs: &[Expression],
936            _inverted: bool,
937        ) -> Option<bool> {
938            None
939        }
940        fn as_data_skipping_predicate(
941            &self,
942            _evaluator: &IndirectDataSkippingPredicateEvaluator<'_>,
943            _exprs: &[Expression],
944            _inverted: bool,
945        ) -> Option<Predicate> {
946            None
947        }
948    }
949
950    fn sample_file_meta() -> FileMeta {
951        FileMeta {
952            location: Url::parse("memory:///table/part-0.parquet").unwrap(),
953            last_modified: 123,
954            size: 456,
955        }
956    }
957
958    fn sample_schema() -> SchemaRef {
959        Arc::new(StructType::try_new(vec![StructField::nullable("id", DataType::INTEGER)]).unwrap())
960    }
961
962    fn decode(op: &Operation) -> proto_op::Operation {
963        let bytes = op.to_proto_bytes();
964        prost::Message::decode(bytes.as_slice()).expect("decode succeeds")
965    }
966
967    fn io_op(op: &Operation) -> proto_op::io_operation::Op {
968        let Some(proto_op::operation::Op::Io(io)) = decode(op).op else {
969            panic!("expected an IoOperation");
970        };
971        io.op.expect("io op present")
972    }
973
974    fn expr_kind_of(expr: Expression) -> proto_expr::expression::Kind {
975        proto_expr::Expression::from(&expr)
976            .kind
977            .expect("expression kind present")
978    }
979
980    fn pred_kind_of(pred: Predicate) -> proto_expr::predicate::Kind {
981        proto_expr::Predicate::from(&pred)
982            .kind
983            .expect("predicate kind present")
984    }
985
986    fn scalar_value_of(scalar: Scalar) -> proto_expr::scalar::Value {
987        proto_expr::Scalar::from(&scalar)
988            .value
989            .expect("scalar value present")
990    }
991
992    // === Operation / IoOperation ===
993
994    #[rstest]
995    #[case(
996        Operation::IoOperation(IoOperation::head_file(Url::parse("memory:///h").unwrap())),
997        "io"
998    )]
999    #[case(Operation::QueryPlan(Plan { nodes: vec![] }), "query_plan")]
1000    fn from_operation(#[case] op: Operation, #[case] expected: &str) {
1001        use proto_op::operation::Op;
1002        let kind = match decode(&op).op.unwrap() {
1003            Op::Io(_) => "io",
1004            Op::QueryPlan(_) => "query_plan",
1005        };
1006        assert_eq!(kind, expected);
1007    }
1008
1009    #[test]
1010    fn from_io_operation_file_listing() {
1011        let url = "memory:///table/";
1012        let op = Operation::IoOperation(IoOperation::file_listing(Url::parse(url).unwrap()));
1013        let proto_op::io_operation::Op::FileListing(file_listing) = io_op(&op) else {
1014            panic!("expected FileListing");
1015        };
1016        assert_eq!(file_listing.url, url);
1017    }
1018
1019    #[test]
1020    fn from_io_operation_read_bytes() {
1021        let files = vec![
1022            (Url::parse("memory:///a").unwrap(), Some(0u64..10u64)),
1023            (Url::parse("memory:///b").unwrap(), None),
1024        ];
1025        let op = Operation::IoOperation(IoOperation::read_bytes(files));
1026        let proto_op::io_operation::Op::ReadBytes(read_bytes) = io_op(&op) else {
1027            panic!("expected ReadBytes");
1028        };
1029        assert_eq!(read_bytes.files.len(), 2);
1030        assert_eq!(read_bytes.files[0].url, "memory:///a");
1031    }
1032
1033    #[test]
1034    fn from_io_operation_write_bytes() {
1035        let op = Operation::IoOperation(IoOperation::write_bytes(
1036            Url::parse("memory:///out").unwrap(),
1037            Bytes::from_static(b"hello"),
1038            true,
1039        ));
1040        let proto_op::io_operation::Op::WriteBytes(write_bytes) = io_op(&op) else {
1041            panic!("expected WriteBytes");
1042        };
1043        assert_eq!(write_bytes.url, "memory:///out");
1044        assert_eq!(write_bytes.data, b"hello");
1045        assert!(write_bytes.overwrite);
1046    }
1047
1048    #[test]
1049    fn from_io_operation_head_file() {
1050        let op = Operation::IoOperation(IoOperation::head_file(
1051            Url::parse("memory:///head").unwrap(),
1052        ));
1053        let proto_op::io_operation::Op::HeadFile(head_file) = io_op(&op) else {
1054            panic!("expected HeadFile");
1055        };
1056        assert_eq!(head_file.url, "memory:///head");
1057    }
1058
1059    #[test]
1060    fn from_io_operation_atomic_copy() {
1061        let op = Operation::IoOperation(IoOperation::atomic_copy(
1062            Url::parse("memory:///src").unwrap(),
1063            Url::parse("memory:///dst").unwrap(),
1064        ));
1065        let proto_op::io_operation::Op::AtomicCopy(atomic_copy) = io_op(&op) else {
1066            panic!("expected AtomicCopy");
1067        };
1068        assert_eq!(atomic_copy.source, "memory:///src");
1069        assert_eq!(atomic_copy.destination, "memory:///dst");
1070    }
1071
1072    #[test]
1073    fn from_io_operation_parquet_footer() {
1074        let op = Operation::IoOperation(IoOperation::parquet_footer(sample_file_meta()));
1075        let proto_op::io_operation::Op::ParquetFooter(parquet_footer) = io_op(&op) else {
1076            panic!("expected ParquetFooter");
1077        };
1078        let file = parquet_footer.file.expect("file meta present");
1079        assert_eq!(file.location, "memory:///table/part-0.parquet");
1080        assert_eq!(file.size, 456);
1081        assert_eq!(file.last_modified, 123);
1082    }
1083
1084    #[rstest]
1085    #[case(Some(0u64..10u64), Some(0), Some(10))]
1086    #[case(None, None, None)]
1087    fn from_file_slice(
1088        #[case] range: Option<std::ops::Range<u64>>,
1089        #[case] expected_start: Option<u64>,
1090        #[case] expected_end: Option<u64>,
1091    ) {
1092        let slice: FileSlice = (Url::parse("memory:///a").unwrap(), range);
1093        let proto = proto_op::FileSlice::from(&slice);
1094        assert_eq!(proto.url, "memory:///a");
1095        assert_eq!(proto.range_start, expected_start);
1096        assert_eq!(proto.range_end, expected_end);
1097    }
1098
1099    #[test]
1100    fn from_file_meta() {
1101        let proto = proto_plan::FileMeta::from(&sample_file_meta());
1102        assert_eq!(proto.location, "memory:///table/part-0.parquet");
1103        assert_eq!(proto.size, 456);
1104        assert_eq!(proto.last_modified, 123);
1105    }
1106
1107    // === Plan / nodes ===
1108
1109    #[test]
1110    fn from_plan() {
1111        let schema = Arc::new(
1112            StructType::try_new(vec![
1113                StructField::nullable("id", DataType::INTEGER),
1114                StructField::not_null("name", DataType::STRING),
1115            ])
1116            .unwrap(),
1117        );
1118        let plan = Plan {
1119            nodes: vec![
1120                PlanNode {
1121                    op: Operator::ScanParquet(ScanParquet {
1122                        files: vec![ScanFile::new(sample_file_meta())],
1123                        file_constant_columns: vec![],
1124                        schema: schema.clone(),
1125                    }),
1126                    inputs: vec![],
1127                },
1128                PlanNode {
1129                    op: Operator::Filter(Filter {
1130                        predicate: Arc::new(Predicate::gt(
1131                            Expression::Column(ColumnName::new(["id"])),
1132                            lit(5i32),
1133                        )),
1134                    }),
1135                    inputs: vec![0],
1136                },
1137            ],
1138        };
1139
1140        let Some(proto_op::operation::Op::QueryPlan(plan)) = decode(&Operation::QueryPlan(plan)).op
1141        else {
1142            panic!("expected a QueryPlan");
1143        };
1144        assert_eq!(plan.nodes.len(), 2);
1145
1146        // Source node: ScanParquet at index 0, no inputs.
1147        let source = &plan.nodes[0];
1148        assert!(source.inputs.is_empty());
1149        let Some(proto_plan::operator::Op::ScanParquet(scan)) = &source.op.as_ref().unwrap().op
1150        else {
1151            panic!("expected ScanParquet");
1152        };
1153        assert_eq!(scan.files.len(), 1);
1154        assert_eq!(scan.schema.as_ref().unwrap().fields.len(), 2);
1155
1156        // Filter node at index 1: consumes node 0, carries a `id > 5` binary predicate.
1157        let filter_node = &plan.nodes[1];
1158        assert_eq!(filter_node.inputs.len(), 1);
1159        assert_eq!(filter_node.inputs[0], 0);
1160        let Some(proto_plan::operator::Op::Filter(filter)) = &filter_node.op.as_ref().unwrap().op
1161        else {
1162            panic!("expected Filter");
1163        };
1164        let predicate = filter.predicate.as_ref().unwrap();
1165        let Some(proto_expr::predicate::Kind::Binary(binary)) = &predicate.kind else {
1166            panic!("expected a binary predicate");
1167        };
1168        assert_eq!(binary.op, proto_expr::BinaryPredicateOp::GreaterThan as i32);
1169        assert!(matches!(
1170            binary.left.as_ref().unwrap().kind,
1171            Some(proto_expr::expression::Kind::Column(_))
1172        ));
1173    }
1174
1175    #[rstest]
1176    #[case(
1177        Operator::ScanParquet(ScanParquet {
1178            files: vec![],
1179            file_constant_columns: vec![],
1180            schema: sample_schema(),
1181        }),
1182        "scan_parquet"
1183    )]
1184    #[case(
1185        Operator::ScanJson(ScanJson {
1186            files: vec![],
1187            file_constant_columns: vec![],
1188            schema: sample_schema(),
1189        }),
1190        "scan_json"
1191    )]
1192    #[case(Operator::Values(Values { schema: sample_schema(), rows: vec![] }), "values")]
1193    #[case(
1194        Operator::Project(Project {
1195            expr: Arc::new(Expression::struct_from([lit(1)])),
1196            schema: sample_schema(),
1197        }),
1198        "project"
1199    )]
1200    #[case(Operator::Filter(Filter { predicate: Arc::new(Predicate::literal(true)) }), "filter")]
1201    #[case(
1202        Operator::Load(Load {
1203            schema: sample_schema(),
1204            file_type: FileType::Parquet,
1205            base_url: None,
1206            file_constant_columns: vec![],
1207            file_meta: sample_load_column_file_meta(),
1208            dv_column: ColumnName::new(["dv"]),
1209        }),
1210        "load"
1211    )]
1212    #[case(
1213        Operator::Aggregate(Aggregate {
1214            group_by: vec![],
1215            aggs: vec![],
1216            schema: sample_schema(),
1217        }),
1218        "aggregate"
1219    )]
1220    #[case(
1221        Operator::SemiJoin(SemiJoin { inverted: false, probe_keys: vec![], build_keys: vec![] }),
1222        "semi_join"
1223    )]
1224    #[case(Operator::UnionAll(UnionAll), "union_all")]
1225    fn from_operator(#[case] op: Operator, #[case] expected: &str) {
1226        use proto_plan::operator::Op;
1227        let kind = match proto_plan::Operator::from(&op).op.unwrap() {
1228            Op::ScanParquet(_) => "scan_parquet",
1229            Op::ScanJson(_) => "scan_json",
1230            Op::Values(_) => "values",
1231            Op::Project(_) => "project",
1232            Op::Filter(_) => "filter",
1233            Op::Load(_) => "load",
1234            Op::Aggregate(_) => "aggregate",
1235            Op::SemiJoin(_) => "semi_join",
1236            Op::UnionAll(_) => "union_all",
1237        };
1238        assert_eq!(kind, expected);
1239    }
1240
1241    #[test]
1242    fn from_scan_file() {
1243        let scan_file = ScanFile {
1244            meta: sample_file_meta(),
1245            file_constants: vec![Scalar::Integer(1), Scalar::String("x".into())],
1246        };
1247        let proto = proto_plan::ScanFile::from(&scan_file);
1248        assert!(proto.meta.is_some());
1249        assert_eq!(proto.file_constants.len(), 2);
1250    }
1251
1252    #[test]
1253    fn from_scan_parquet() {
1254        let node = ScanParquet {
1255            files: vec![ScanFile::new(sample_file_meta())],
1256            file_constant_columns: vec!["c".to_string()],
1257            schema: sample_schema(),
1258        };
1259        let proto = proto_plan::ScanParquetNode::from(&node);
1260        assert_eq!(proto.files.len(), 1);
1261        assert_eq!(proto.file_constant_columns.len(), 1);
1262        assert!(proto.schema.is_some());
1263    }
1264
1265    #[test]
1266    fn from_scan_json() {
1267        let node = ScanJson {
1268            files: vec![ScanFile::new(sample_file_meta())],
1269            file_constant_columns: vec!["c".to_string()],
1270            schema: sample_schema(),
1271        };
1272        let proto = proto_plan::ScanJsonNode::from(&node);
1273        assert_eq!(proto.files.len(), 1);
1274        assert_eq!(proto.file_constant_columns.len(), 1);
1275        assert!(proto.schema.is_some());
1276    }
1277
1278    #[test]
1279    fn from_values() {
1280        let node = Values {
1281            schema: sample_schema(),
1282            rows: vec![vec![Scalar::Integer(1)], vec![Scalar::Integer(2)]],
1283        };
1284        let proto = proto_plan::ValuesNode::from(&node);
1285        assert!(proto.schema.is_some());
1286        assert_eq!(proto.rows.len(), 2);
1287        assert_eq!(proto.rows[0].values.len(), 1);
1288    }
1289
1290    #[test]
1291    fn from_project() {
1292        let node = Project {
1293            expr: Arc::new(Expression::struct_from([lit(1)])),
1294            schema: sample_schema(),
1295        };
1296        let proto = proto_plan::ProjectNode::from(&node);
1297        assert!(proto.expr.is_some());
1298        assert!(proto.schema.is_some());
1299    }
1300
1301    #[test]
1302    fn from_filter() {
1303        let node = Filter {
1304            predicate: Arc::new(Predicate::literal(true)),
1305        };
1306        let proto = proto_plan::FilterNode::from(&node);
1307        assert!(proto.predicate.is_some());
1308    }
1309
1310    fn sample_load_column_file_meta() -> LoadColumnFileMeta {
1311        LoadColumnFileMeta {
1312            path_column: ColumnName::new(["path"]),
1313            file_size_column: ColumnName::new(["size"]),
1314            num_records_column: ColumnName::new(["num_records"]),
1315        }
1316    }
1317
1318    #[rstest]
1319    #[case(
1320        FileType::Json,
1321        Some(Url::parse("memory:///base/").unwrap()),
1322        Some("memory:///base/")
1323    )]
1324    #[case(FileType::Parquet, None, None)]
1325    fn from_load(
1326        #[case] file_type: FileType,
1327        #[case] base_url: Option<Url>,
1328        #[case] expected_base_url: Option<&str>,
1329    ) {
1330        let node = Load {
1331            schema: sample_schema(),
1332            file_type,
1333            base_url,
1334            file_constant_columns: vec!["c".to_string()],
1335            file_meta: sample_load_column_file_meta(),
1336            dv_column: ColumnName::new(["dv"]),
1337        };
1338        let proto = proto_plan::LoadNode::from(&node);
1339        assert!(proto.schema.is_some());
1340        assert_eq!(
1341            proto.file_type,
1342            proto_plan::FileType::from(file_type) as i32
1343        );
1344        assert_eq!(proto.base_url.as_deref(), expected_base_url);
1345        assert_eq!(proto.file_constant_columns.len(), 1);
1346        assert!(proto.dv_column.is_some());
1347
1348        let file_meta = proto.file_meta.expect("file_meta present");
1349        assert!(file_meta.path_column.is_some());
1350        assert!(file_meta.file_size_column.is_some());
1351        assert!(file_meta.num_records_column.is_some());
1352    }
1353
1354    #[test]
1355    fn from_aggregate() {
1356        let node = Aggregate {
1357            group_by: vec![ColumnName::new(["g"])],
1358            aggs: vec![Agg::max(ColumnName::new(["a"]))],
1359            schema: sample_schema(),
1360        };
1361        let proto = proto_plan::AggregateNode::from(&node);
1362        assert_eq!(proto.group_by.len(), 1);
1363        assert_eq!(proto.aggs.len(), 1);
1364        assert!(proto.aggs[0].func.is_some());
1365        assert!(proto.schema.is_some());
1366    }
1367
1368    #[rstest]
1369    #[case(Agg::min(ColumnName::new(["a"])), "min")]
1370    #[case(Agg::max(ColumnName::new(["a"])), "max")]
1371    #[case(Agg::min_non_null_by(ColumnName::new(["a"]), ColumnName::new(["k"])), "min_non_null_by")]
1372    #[case(Agg::max_non_null_by(ColumnName::new(["a"]), ColumnName::new(["k"])), "max_non_null_by")]
1373    fn from_agg(#[case] agg: Agg, #[case] expected: &str) {
1374        use proto_plan::agg::Func;
1375        let proto = proto_plan::Agg::from(&agg);
1376        let kind = match proto.func.unwrap() {
1377            Func::Min(_) => "min",
1378            Func::Max(_) => "max",
1379            Func::MinNonNullBy(_) => "min_non_null_by",
1380            Func::MaxNonNullBy(_) => "max_non_null_by",
1381        };
1382        assert_eq!(kind, expected);
1383    }
1384
1385    #[rstest]
1386    #[case(true)]
1387    #[case(false)]
1388    fn from_semi_join(#[case] inverted: bool) {
1389        let node = SemiJoin {
1390            inverted,
1391            probe_keys: vec![ColumnName::new(["p"])],
1392            build_keys: vec![ColumnName::new(["b"])],
1393        };
1394        let proto = proto_plan::SemiJoinNode::from(&node);
1395        assert_eq!(proto.inverted, inverted);
1396        assert_eq!(proto.probe_keys.len(), 1);
1397        assert_eq!(proto.build_keys.len(), 1);
1398    }
1399
1400    #[rstest]
1401    #[case(FileType::Parquet, proto_plan::FileType::Parquet)]
1402    #[case(FileType::Json, proto_plan::FileType::Json)]
1403    fn from_file_type(#[case] value: FileType, #[case] expected: proto_plan::FileType) {
1404        assert_eq!(proto_plan::FileType::from(value) as i32, expected as i32);
1405    }
1406
1407    // === Expressions ===
1408
1409    #[rstest]
1410    #[case(lit(1), "literal")]
1411    #[case(Expression::Column(ColumnName::new(["a"])), "column")]
1412    #[case(Expression::Predicate(Box::new(Predicate::literal(true))), "predicate")]
1413    #[case(Expression::struct_from([lit(1)]), "struct_expr")]
1414    #[case(
1415        Expression::StructPatch(
1416            ExpressionStructPatchBuilder::new().append(lit(1)).build().unwrap(),
1417        ),
1418        "transform"
1419    )]
1420    #[case(Expression::unary(UnaryExpressionOp::ToJson, lit(1)), "unary")]
1421    #[case(Expression::binary(BinaryExpressionOp::Plus, lit(1), lit(2)), "binary")]
1422    #[case(Expression::coalesce([lit(1), lit(2)]), "variadic")]
1423    #[case(Expression::opaque(TestOpaqueExprOp, [lit(1)]), "opaque")]
1424    #[case(Expression::parse_json(lit("{}"), sample_schema()), "parse_json")]
1425    #[case(Expression::map_to_struct(Expression::Column(ColumnName::new(["m"]))), "map_to_struct")]
1426    #[case(Expression::unknown("x"), "unknown")]
1427    fn from_expression(#[case] expr: Expression, #[case] expected: &str) {
1428        use proto_expr::expression::Kind;
1429        let kind = match proto_expr::Expression::from(&expr).kind.unwrap() {
1430            Kind::Literal(_) => "literal",
1431            Kind::Column(_) => "column",
1432            Kind::Predicate(_) => "predicate",
1433            Kind::StructExpr(_) => "struct_expr",
1434            Kind::Transform(_) => "transform",
1435            Kind::Unary(_) => "unary",
1436            Kind::Binary(_) => "binary",
1437            Kind::Variadic(_) => "variadic",
1438            Kind::IfExpr(_) => "if_expr",
1439            Kind::Opaque(_) => "opaque",
1440            Kind::ParseJson(_) => "parse_json",
1441            Kind::MapToStruct(_) => "map_to_struct",
1442            Kind::Unknown(_) => "unknown",
1443        };
1444        assert_eq!(kind, expected);
1445    }
1446
1447    #[rstest]
1448    #[case(Predicate::literal(true), "boolean_expression")]
1449    #[case(Predicate::not(Predicate::literal(true)), "not")]
1450    #[case(Predicate::is_null(lit(1)), "unary")]
1451    #[case(Predicate::gt(lit(1), lit(2)), "binary")]
1452    #[case(
1453        Predicate::and(Predicate::literal(true), Predicate::literal(false)),
1454        "junction"
1455    )]
1456    #[case(Predicate::opaque(TestOpaquePredOp, [lit(1)]), "opaque")]
1457    #[case(Predicate::Unknown("x".to_string()), "unknown")]
1458    fn from_predicate(#[case] pred: Predicate, #[case] expected: &str) {
1459        use proto_expr::predicate::Kind;
1460        let kind = match proto_expr::Predicate::from(&pred).kind.unwrap() {
1461            Kind::BooleanExpression(_) => "boolean_expression",
1462            Kind::Not(_) => "not",
1463            Kind::Unary(_) => "unary",
1464            Kind::Binary(_) => "binary",
1465            Kind::Junction(_) => "junction",
1466            Kind::Opaque(_) => "opaque",
1467            Kind::Unknown(_) => "unknown",
1468        };
1469        assert_eq!(kind, expected);
1470    }
1471
1472    #[test]
1473    fn from_column_name() {
1474        let proto = proto_expr::ColumnName::from(&ColumnName::new(["a", "b", "c"]));
1475        assert_eq!(proto.path, vec!["a", "b", "c"]);
1476    }
1477
1478    #[test]
1479    fn from_struct_expression() {
1480        let proto_expr::expression::Kind::StructExpr(plain) =
1481            expr_kind_of(Expression::struct_from([lit(1), lit(2)]))
1482        else {
1483            panic!("expected a struct expression");
1484        };
1485        assert_eq!(plain.exprs.len(), 2);
1486        assert!(plain.nullability_predicate.is_none());
1487
1488        let proto_expr::expression::Kind::StructExpr(guarded) = expr_kind_of(
1489            Expression::struct_with_nullability_from([lit(1)], lit(true)),
1490        ) else {
1491            panic!("expected a struct expression");
1492        };
1493        assert_eq!(guarded.exprs.len(), 1);
1494        assert!(guarded.nullability_predicate.is_some());
1495    }
1496
1497    #[test]
1498    fn from_unary_expression() {
1499        let proto_expr::expression::Kind::Unary(unary) =
1500            expr_kind_of(Expression::unary(UnaryExpressionOp::ToJson, lit(1)))
1501        else {
1502            panic!("expected a unary expression");
1503        };
1504        assert_eq!(unary.op, proto_expr::UnaryExpressionOp::ToJson as i32);
1505        assert!(unary.expr.is_some());
1506    }
1507
1508    #[test]
1509    fn from_binary_expression() {
1510        let proto_expr::expression::Kind::Binary(binary) =
1511            expr_kind_of(Expression::binary(BinaryExpressionOp::Plus, lit(1), lit(2)))
1512        else {
1513            panic!("expected a binary expression");
1514        };
1515        assert_eq!(binary.op, proto_expr::BinaryExpressionOp::Plus as i32);
1516        assert!(binary.left.is_some());
1517        assert!(binary.right.is_some());
1518    }
1519
1520    #[test]
1521    fn from_variadic_expression() {
1522        let proto_expr::expression::Kind::Variadic(variadic) =
1523            expr_kind_of(Expression::coalesce([lit(1), lit(2), lit(3)]))
1524        else {
1525            panic!("expected a variadic expression");
1526        };
1527        assert_eq!(
1528            variadic.op,
1529            proto_expr::VariadicExpressionOp::Coalesce as i32
1530        );
1531        assert_eq!(variadic.exprs.len(), 3);
1532    }
1533
1534    #[test]
1535    fn from_opaque_expression() {
1536        let proto_expr::expression::Kind::Opaque(opaque) =
1537            expr_kind_of(Expression::opaque(TestOpaqueExprOp, [lit(1), lit(2)]))
1538        else {
1539            panic!("expected an opaque expression");
1540        };
1541        assert_eq!(opaque.name, "test_opaque_expr");
1542        assert_eq!(opaque.exprs.len(), 2);
1543    }
1544
1545    #[test]
1546    fn from_parse_json_expression() {
1547        let proto_expr::expression::Kind::ParseJson(parse_json) =
1548            expr_kind_of(Expression::parse_json(lit("{}"), sample_schema()))
1549        else {
1550            panic!("expected a parse_json expression");
1551        };
1552        assert!(parse_json.json_expr.is_some());
1553        assert!(parse_json.output_schema.is_some());
1554    }
1555
1556    #[test]
1557    fn from_map_to_struct_expression() {
1558        let proto_expr::expression::Kind::MapToStruct(map_to_struct) = expr_kind_of(
1559            Expression::map_to_struct(Expression::Column(ColumnName::new(["m"]))),
1560        ) else {
1561            panic!("expected a map_to_struct expression");
1562        };
1563        assert!(map_to_struct.map_expr.is_some());
1564    }
1565
1566    #[test]
1567    fn from_unary_predicate() {
1568        let proto_expr::predicate::Kind::Unary(unary) = pred_kind_of(Predicate::is_null(lit(1)))
1569        else {
1570            panic!("expected a unary predicate");
1571        };
1572        assert_eq!(unary.op, proto_expr::UnaryPredicateOp::IsNull as i32);
1573        assert!(unary.expr.is_some());
1574    }
1575
1576    #[test]
1577    fn from_binary_predicate() {
1578        let proto_expr::predicate::Kind::Binary(binary) =
1579            pred_kind_of(Predicate::gt(lit(1), lit(2)))
1580        else {
1581            panic!("expected a binary predicate");
1582        };
1583        assert_eq!(binary.op, proto_expr::BinaryPredicateOp::GreaterThan as i32);
1584        assert!(binary.left.is_some());
1585        assert!(binary.right.is_some());
1586    }
1587
1588    #[test]
1589    fn from_junction_predicate() {
1590        let proto_expr::predicate::Kind::Junction(junction) = pred_kind_of(Predicate::and(
1591            Predicate::literal(true),
1592            Predicate::literal(false),
1593        )) else {
1594            panic!("expected a junction predicate");
1595        };
1596        assert_eq!(junction.op, proto_expr::JunctionPredicateOp::And as i32);
1597        assert_eq!(junction.preds.len(), 2);
1598    }
1599
1600    #[test]
1601    fn from_opaque_predicate() {
1602        let proto_expr::predicate::Kind::Opaque(opaque) =
1603            pred_kind_of(Predicate::opaque(TestOpaquePredOp, [lit(1), lit(2)]))
1604        else {
1605            panic!("expected an opaque predicate");
1606        };
1607        assert_eq!(opaque.name, "test_opaque_pred");
1608        assert_eq!(opaque.exprs.len(), 2);
1609    }
1610
1611    #[test]
1612    fn from_struct_patch() {
1613        let patch = ExpressionStructPatchBuilder::new()
1614            .replace("a", lit(1))
1615            .insert_after("b", lit(2))
1616            .drop_if_exists("c")
1617            .prepend(lit(0))
1618            .append(lit(3))
1619            .build()
1620            .unwrap();
1621
1622        let transform = proto_expr::Transform::from(&patch);
1623
1624        // `replace` drops the input field, so `is_replace` is true and it is not optional.
1625        assert!(transform.field_transforms["a"].is_replace);
1626        assert!(!transform.field_transforms["a"].optional);
1627        // `insert_after` keeps the input field, so `is_replace` is false.
1628        assert!(!transform.field_transforms["b"].is_replace);
1629        // `drop_if_exists` drops the input field optionally.
1630        assert!(transform.field_transforms["c"].is_replace);
1631        assert!(transform.field_transforms["c"].optional);
1632        // A top-level transform carries no input path.
1633        assert!(transform.input_path.is_none());
1634        assert_eq!(transform.prepended_fields.len(), 1);
1635        assert_eq!(transform.appended_fields.len(), 1);
1636    }
1637
1638    // === Expression and predicate operators ===
1639
1640    #[rstest]
1641    #[case(UnaryExpressionOp::ToJson, proto_expr::UnaryExpressionOp::ToJson)]
1642    fn from_unary_expression_op(
1643        #[case] op: UnaryExpressionOp,
1644        #[case] expected: proto_expr::UnaryExpressionOp,
1645    ) {
1646        assert_eq!(
1647            proto_expr::UnaryExpressionOp::from(op) as i32,
1648            expected as i32
1649        );
1650    }
1651
1652    #[rstest]
1653    #[case(BinaryExpressionOp::Plus, proto_expr::BinaryExpressionOp::Plus)]
1654    #[case(BinaryExpressionOp::Minus, proto_expr::BinaryExpressionOp::Minus)]
1655    #[case(BinaryExpressionOp::Multiply, proto_expr::BinaryExpressionOp::Multiply)]
1656    #[case(BinaryExpressionOp::Divide, proto_expr::BinaryExpressionOp::Divide)]
1657    fn from_binary_expression_op(
1658        #[case] op: BinaryExpressionOp,
1659        #[case] expected: proto_expr::BinaryExpressionOp,
1660    ) {
1661        assert_eq!(
1662            proto_expr::BinaryExpressionOp::from(op) as i32,
1663            expected as i32
1664        );
1665    }
1666
1667    #[rstest]
1668    #[case(
1669        VariadicExpressionOp::Coalesce,
1670        proto_expr::VariadicExpressionOp::Coalesce
1671    )]
1672    #[case(VariadicExpressionOp::Array, proto_expr::VariadicExpressionOp::Array)]
1673    fn from_variadic_expression_op(
1674        #[case] op: VariadicExpressionOp,
1675        #[case] expected: proto_expr::VariadicExpressionOp,
1676    ) {
1677        assert_eq!(
1678            proto_expr::VariadicExpressionOp::from(op) as i32,
1679            expected as i32
1680        );
1681    }
1682
1683    #[rstest]
1684    #[case(UnaryPredicateOp::IsNull, proto_expr::UnaryPredicateOp::IsNull)]
1685    fn from_unary_predicate_op(
1686        #[case] op: UnaryPredicateOp,
1687        #[case] expected: proto_expr::UnaryPredicateOp,
1688    ) {
1689        assert_eq!(
1690            proto_expr::UnaryPredicateOp::from(op) as i32,
1691            expected as i32
1692        );
1693    }
1694
1695    #[rstest]
1696    #[case(BinaryPredicateOp::LessThan, proto_expr::BinaryPredicateOp::LessThan)]
1697    #[case(
1698        BinaryPredicateOp::GreaterThan,
1699        proto_expr::BinaryPredicateOp::GreaterThan
1700    )]
1701    #[case(BinaryPredicateOp::Equal, proto_expr::BinaryPredicateOp::Equal)]
1702    #[case(BinaryPredicateOp::Distinct, proto_expr::BinaryPredicateOp::Distinct)]
1703    #[case(BinaryPredicateOp::In, proto_expr::BinaryPredicateOp::In)]
1704    fn from_binary_predicate_op(
1705        #[case] op: BinaryPredicateOp,
1706        #[case] expected: proto_expr::BinaryPredicateOp,
1707    ) {
1708        assert_eq!(
1709            proto_expr::BinaryPredicateOp::from(op) as i32,
1710            expected as i32
1711        );
1712    }
1713
1714    #[rstest]
1715    #[case(JunctionPredicateOp::And, proto_expr::JunctionPredicateOp::And)]
1716    #[case(JunctionPredicateOp::Or, proto_expr::JunctionPredicateOp::Or)]
1717    fn from_junction_predicate_op(
1718        #[case] op: JunctionPredicateOp,
1719        #[case] expected: proto_expr::JunctionPredicateOp,
1720    ) {
1721        assert_eq!(
1722            proto_expr::JunctionPredicateOp::from(op) as i32,
1723            expected as i32
1724        );
1725    }
1726
1727    // === Scalars ===
1728
1729    #[rstest]
1730    #[case(Scalar::Integer(7), "integer")]
1731    #[case(Scalar::Long(7), "long")]
1732    #[case(Scalar::Short(7), "short")]
1733    #[case(Scalar::Byte(7), "byte")]
1734    #[case(Scalar::Float(1.5), "float")]
1735    #[case(Scalar::Double(1.5), "double")]
1736    #[case(Scalar::String("hi".into()), "string")]
1737    #[case(Scalar::Boolean(true), "boolean")]
1738    #[case(Scalar::Timestamp(9), "timestamp")]
1739    #[case(Scalar::TimestampNtz(9), "timestamp_ntz")]
1740    #[case(Scalar::Date(9), "date")]
1741    #[case(Scalar::Binary(vec![1, 2, 3]), "binary")]
1742    #[case(
1743        Scalar::Decimal(
1744            DecimalData::try_new(1234i128, DecimalType::try_new(10, 2).unwrap()).unwrap(),
1745        ),
1746        "decimal"
1747    )]
1748    #[case(Scalar::Null(DataType::LONG), "null")]
1749    #[case(
1750        Scalar::Struct(
1751            StructData::try_new(
1752                vec![StructField::nullable("a", DataType::INTEGER)],
1753                vec![Scalar::Integer(1)],
1754            )
1755            .unwrap(),
1756        ),
1757        "struct"
1758    )]
1759    #[case(
1760        Scalar::Array(
1761            ArrayData::try_new(ArrayType::new(DataType::INTEGER, false), [1, 2, 3]).unwrap(),
1762        ),
1763        "array"
1764    )]
1765    #[case(
1766        Scalar::Map(
1767            MapData::try_new(MapType::new(DataType::STRING, DataType::INTEGER, false), [("k", 1)])
1768                .unwrap(),
1769        ),
1770        "map"
1771    )]
1772    fn from_scalar(#[case] value: Scalar, #[case] expected: &str) {
1773        use proto_expr::scalar::Value;
1774        let kind = match proto_expr::Scalar::from(&value).value.unwrap() {
1775            Value::Integer(_) => "integer",
1776            Value::Long(_) => "long",
1777            Value::Short(_) => "short",
1778            Value::Byte(_) => "byte",
1779            Value::Float(_) => "float",
1780            Value::Double(_) => "double",
1781            Value::String(_) => "string",
1782            Value::Boolean(_) => "boolean",
1783            Value::Timestamp(_) => "timestamp",
1784            Value::TimestampNtz(_) => "timestamp_ntz",
1785            Value::TimestampNanos(_) => "timestamp_nanos",
1786            Value::TimestampNanosNtz(_) => "timestamp_nanos_ntz",
1787            Value::Date(_) => "date",
1788            Value::Binary(_) => "binary",
1789            Value::Decimal(_) => "decimal",
1790            Value::Null(_) => "null",
1791            Value::Struct(_) => "struct",
1792            Value::Array(_) => "array",
1793            Value::Map(_) => "map",
1794        };
1795        assert_eq!(kind, expected);
1796    }
1797
1798    #[test]
1799    fn from_decimal_data() {
1800        let decimal = DecimalData::try_new(1234i128, DecimalType::try_new(10, 2).unwrap()).unwrap();
1801        let proto_expr::scalar::Value::Decimal(decimal) = scalar_value_of(Scalar::Decimal(decimal))
1802        else {
1803            panic!("expected a decimal scalar");
1804        };
1805        assert_eq!(decimal.bits, 1234i128.to_be_bytes().to_vec());
1806        let decimal_type = decimal.decimal_type.expect("decimal type present");
1807        assert_eq!((decimal_type.precision, decimal_type.scale), (10, 2));
1808    }
1809
1810    #[test]
1811    fn from_struct_data() {
1812        let struct_data = StructData::try_new(
1813            vec![StructField::nullable("a", DataType::INTEGER)],
1814            vec![Scalar::Integer(1)],
1815        )
1816        .unwrap();
1817        let proto_expr::scalar::Value::Struct(struct_data) =
1818            scalar_value_of(Scalar::Struct(struct_data))
1819        else {
1820            panic!("expected a struct scalar");
1821        };
1822        assert_eq!(struct_data.fields.len(), 1);
1823        assert_eq!(struct_data.values.len(), 1);
1824        assert_eq!(struct_data.fields[0].name, "a");
1825    }
1826
1827    #[test]
1828    fn from_array_data() {
1829        let array_data =
1830            ArrayData::try_new(ArrayType::new(DataType::INTEGER, false), [1, 2, 3]).unwrap();
1831        let proto_expr::scalar::Value::Array(array_data) =
1832            scalar_value_of(Scalar::Array(array_data))
1833        else {
1834            panic!("expected an array scalar");
1835        };
1836        assert!(array_data.array_type.is_some());
1837        assert_eq!(array_data.elements.len(), 3);
1838    }
1839
1840    #[test]
1841    fn from_map_data() {
1842        let map_data = MapData::try_new(
1843            MapType::new(DataType::STRING, DataType::INTEGER, false),
1844            [("k", 1)],
1845        )
1846        .unwrap();
1847        let proto_expr::scalar::Value::Map(map_data) = scalar_value_of(Scalar::Map(map_data))
1848        else {
1849            panic!("expected a map scalar");
1850        };
1851        assert!(map_data.map_type.is_some());
1852        assert_eq!(map_data.pairs.len(), 1);
1853        assert!(map_data.pairs[0].key.is_some());
1854        assert!(map_data.pairs[0].value.is_some());
1855    }
1856
1857    // === Schema ===
1858
1859    #[rstest]
1860    #[case(DataType::INTEGER, "primitive")]
1861    #[case(ArrayType::new(DataType::INTEGER, true).into(), "array")]
1862    #[case(
1863        StructType::try_new(vec![StructField::nullable("a", DataType::INTEGER)])
1864            .unwrap()
1865            .into(),
1866        "struct"
1867    )]
1868    #[case(MapType::new(DataType::STRING, DataType::INTEGER, true).into(), "map")]
1869    #[case(DataType::unshredded_variant(), "variant")]
1870    fn from_data_type(#[case] value: DataType, #[case] expected: &str) {
1871        use proto_schema::data_type::Kind;
1872        let kind = match proto_schema::DataType::from(&value).kind.unwrap() {
1873            Kind::Primitive(_) => "primitive",
1874            Kind::Array(_) => "array",
1875            Kind::Struct(_) => "struct",
1876            Kind::Map(_) => "map",
1877            Kind::Variant(_) => "variant",
1878        };
1879        assert_eq!(kind, expected);
1880    }
1881
1882    #[rstest]
1883    #[case(PrimitiveType::String, proto_schema::SimplePrimitiveType::String)]
1884    #[case(PrimitiveType::Long, proto_schema::SimplePrimitiveType::Long)]
1885    #[case(PrimitiveType::Integer, proto_schema::SimplePrimitiveType::Integer)]
1886    #[case(PrimitiveType::Short, proto_schema::SimplePrimitiveType::Short)]
1887    #[case(PrimitiveType::Byte, proto_schema::SimplePrimitiveType::Byte)]
1888    #[case(PrimitiveType::Float, proto_schema::SimplePrimitiveType::Float)]
1889    #[case(PrimitiveType::Double, proto_schema::SimplePrimitiveType::Double)]
1890    #[case(PrimitiveType::Boolean, proto_schema::SimplePrimitiveType::Boolean)]
1891    #[case(PrimitiveType::Binary, proto_schema::SimplePrimitiveType::Binary)]
1892    #[case(PrimitiveType::Date, proto_schema::SimplePrimitiveType::Date)]
1893    #[case(PrimitiveType::Timestamp, proto_schema::SimplePrimitiveType::Timestamp)]
1894    #[case(
1895        PrimitiveType::TimestampNtz,
1896        proto_schema::SimplePrimitiveType::TimestampNtz
1897    )]
1898    #[case(PrimitiveType::Void, proto_schema::SimplePrimitiveType::Void)]
1899    #[case(
1900        PrimitiveType::IntervalYearMonth,
1901        proto_schema::SimplePrimitiveType::IntervalYearMonth
1902    )]
1903    #[case(
1904        PrimitiveType::IntervalDayTime,
1905        proto_schema::SimplePrimitiveType::IntervalDayTime
1906    )]
1907    fn from_primitive_type(
1908        #[case] primitive: PrimitiveType,
1909        #[case] expected: proto_schema::SimplePrimitiveType,
1910    ) {
1911        let Some(proto_schema::primitive_type::Kind::Simple(simple)) =
1912            proto_schema::PrimitiveType::from(&primitive).kind
1913        else {
1914            panic!("expected a simple primitive type");
1915        };
1916        assert_eq!(simple, expected as i32);
1917    }
1918
1919    #[test]
1920    fn from_decimal_type() {
1921        let primitive = PrimitiveType::decimal(10, 2).unwrap();
1922        let Some(proto_schema::primitive_type::Kind::Decimal(decimal)) =
1923            proto_schema::PrimitiveType::from(&primitive).kind
1924        else {
1925            panic!("expected a decimal primitive type");
1926        };
1927        assert_eq!((decimal.precision, decimal.scale), (10, 2));
1928    }
1929
1930    #[rstest]
1931    #[case(true)]
1932    #[case(false)]
1933    fn from_array_type(#[case] contains_null: bool) {
1934        let proto =
1935            proto_schema::ArrayType::from(&ArrayType::new(DataType::INTEGER, contains_null));
1936        assert!(proto.element_type.is_some());
1937        assert_eq!(proto.contains_null, contains_null);
1938    }
1939
1940    #[rstest]
1941    #[case(true)]
1942    #[case(false)]
1943    fn from_map_type(#[case] value_contains_null: bool) {
1944        let proto = proto_schema::MapType::from(&MapType::new(
1945            DataType::STRING,
1946            DataType::INTEGER,
1947            value_contains_null,
1948        ));
1949        assert!(proto.key_type.is_some());
1950        assert!(proto.value_type.is_some());
1951        assert_eq!(proto.value_contains_null, value_contains_null);
1952    }
1953
1954    #[test]
1955    fn from_struct_type() {
1956        let struct_type = StructType::try_new(vec![
1957            StructField::nullable("a", DataType::INTEGER),
1958            StructField::not_null("b", DataType::STRING),
1959        ])
1960        .unwrap();
1961        let proto = proto_schema::StructType::from(&struct_type);
1962        assert_eq!(proto.fields.len(), 2);
1963        assert!(proto.fields[0].nullable);
1964        assert!(!proto.fields[1].nullable);
1965    }
1966
1967    #[test]
1968    fn from_struct_field() {
1969        let field = StructField::nullable("a", DataType::INTEGER)
1970            .with_metadata([("k", MetadataValue::Number(7))]);
1971        let proto = proto_schema::StructField::from(&field);
1972        assert_eq!(proto.name, "a");
1973        assert!(proto.nullable);
1974        assert!(proto.data_type.is_some());
1975        assert_eq!(
1976            proto.metadata["k"].value,
1977            Some(proto_schema::metadata_value::Value::Number(7))
1978        );
1979    }
1980
1981    #[rstest]
1982    #[case(
1983        MetadataValue::Number(5),
1984        proto_schema::metadata_value::Value::Number(5)
1985    )]
1986    #[case(
1987        MetadataValue::String("s".to_string()),
1988        proto_schema::metadata_value::Value::String("s".to_string())
1989    )]
1990    #[case(
1991        MetadataValue::Boolean(true),
1992        proto_schema::metadata_value::Value::Boolean(true)
1993    )]
1994    #[case(
1995        MetadataValue::Other(serde_json::json!([1, 2])),
1996        proto_schema::metadata_value::Value::OtherJson("[1,2]".to_string())
1997    )]
1998    fn from_metadata_value(
1999        #[case] metadata: MetadataValue,
2000        #[case] expected: proto_schema::metadata_value::Value,
2001    ) {
2002        assert_eq!(
2003            proto_schema::MetadataValue::from(&metadata).value.unwrap(),
2004            expected
2005        );
2006    }
2007
2008    // === Schema decode (proto -> kernel) ===
2009
2010    /// Round-trips a [`DataType`] through its proto form and back, asserting it is unchanged.
2011    fn assert_data_type_round_trips(data_type: DataType) {
2012        let decoded = DataType::try_from(proto_schema::DataType::from(&data_type));
2013        assert_eq!(decoded.expect("decode succeeds"), data_type);
2014    }
2015
2016    /// Round-trips a [`StructType`] through its proto form and back, asserting it is unchanged.
2017    fn assert_schema_round_trips(schema: StructType) {
2018        let decoded = StructType::try_from(proto_schema::StructType::from(&schema));
2019        assert_eq!(decoded.expect("decode succeeds"), schema);
2020    }
2021
2022    #[rstest]
2023    fn round_trip_primitive(
2024        #[values(
2025            PrimitiveType::String,
2026            PrimitiveType::Long,
2027            PrimitiveType::Integer,
2028            PrimitiveType::Short,
2029            PrimitiveType::Byte,
2030            PrimitiveType::Float,
2031            PrimitiveType::Double,
2032            PrimitiveType::Boolean,
2033            PrimitiveType::Binary,
2034            PrimitiveType::Date,
2035            PrimitiveType::Timestamp,
2036            PrimitiveType::TimestampNtz,
2037            PrimitiveType::Void,
2038            PrimitiveType::IntervalYearMonth,
2039            PrimitiveType::IntervalDayTime
2040        )]
2041        primitive: PrimitiveType,
2042    ) {
2043        assert_data_type_round_trips(DataType::Primitive(primitive));
2044    }
2045
2046    #[rstest]
2047    #[case(1, 0)]
2048    #[case(10, 2)]
2049    #[case(38, 0)]
2050    #[case(38, 38)]
2051    fn round_trip_decimal(#[case] precision: u8, #[case] scale: u8) {
2052        assert_data_type_round_trips(DataType::Primitive(
2053            PrimitiveType::decimal(precision, scale).unwrap(),
2054        ));
2055    }
2056
2057    #[rstest]
2058    #[case(DataType::from(ArrayType::new(DataType::INTEGER, true)))]
2059    #[case(DataType::from(ArrayType::new(DataType::STRING, false)))]
2060    #[case(DataType::from(MapType::new(DataType::STRING, DataType::LONG, true)))]
2061    #[case(DataType::from(MapType::new(DataType::INTEGER, DataType::BOOLEAN, false)))]
2062    #[case(DataType::from(ArrayType::new(
2063        MapType::new(DataType::STRING, DataType::LONG, true),
2064        true
2065    )))]
2066    #[case(DataType::from(StructType::try_new(vec![
2067        StructField::nullable("a", DataType::INTEGER),
2068        StructField::not_null("b", DataType::STRING),
2069    ]).unwrap()))]
2070    fn round_trip_composite(#[case] data_type: DataType) {
2071        assert_data_type_round_trips(data_type);
2072    }
2073
2074    #[rstest]
2075    #[case(MetadataValue::Number(7))]
2076    #[case(MetadataValue::String("hello".to_string()))]
2077    #[case(MetadataValue::Boolean(true))]
2078    #[case(MetadataValue::Other(serde_json::json!({"nested": [1, 2, 3]})))]
2079    fn round_trip_field_metadata(#[case] value: MetadataValue) {
2080        let field = StructField::nullable("f", DataType::INTEGER).with_metadata([("k", value)]);
2081        let decoded = StructField::try_from(proto_schema::StructField::from(&field));
2082        assert_eq!(decoded.expect("decode succeeds"), field);
2083    }
2084
2085    #[test]
2086    fn round_trip_full_schema() {
2087        let schema = StructType::try_new(vec![
2088            StructField::nullable("id", DataType::LONG)
2089                .with_metadata([("k", MetadataValue::Number(7))]),
2090            StructField::not_null("name", DataType::STRING),
2091            StructField::nullable("scores", ArrayType::new(DataType::INTEGER, true)),
2092            StructField::nullable(
2093                "attrs",
2094                MapType::new(DataType::STRING, DataType::LONG, true),
2095            ),
2096            StructField::nullable(
2097                "price",
2098                DataType::Primitive(PrimitiveType::decimal(10, 2).unwrap()),
2099            ),
2100            StructField::nullable(
2101                "nested",
2102                StructType::try_new(vec![StructField::not_null("inner", DataType::BOOLEAN)])
2103                    .unwrap(),
2104            ),
2105        ])
2106        .unwrap();
2107        assert_schema_round_trips(schema);
2108    }
2109
2110    /// The proto `VariantType` is currently opaque, so any variant (even a shredded one)
2111    /// decodes to the canonical unshredded form rather than round-tripping its inner struct.
2112    #[test]
2113    fn variant_decodes_to_unshredded() {
2114        let shredded = DataType::Variant(Box::new(
2115            StructType::try_new(vec![
2116                StructField::not_null("metadata", DataType::BINARY),
2117                StructField::not_null("value", DataType::BINARY),
2118                StructField::nullable("typed_value", DataType::INTEGER),
2119            ])
2120            .unwrap(),
2121        ));
2122        let decoded = DataType::try_from(proto_schema::DataType::from(&shredded));
2123        assert_eq!(
2124            decoded.expect("decode succeeds"),
2125            DataType::unshredded_variant()
2126        );
2127    }
2128
2129    // Builds a proto `DataType::Primitive` with the given (possibly malformed) primitive kind.
2130    fn primitive_data_type(
2131        kind: Option<proto_schema::primitive_type::Kind>,
2132    ) -> proto_schema::DataType {
2133        proto_schema::DataType {
2134            kind: Some(proto_schema::data_type::Kind::Primitive(
2135                proto_schema::PrimitiveType { kind },
2136            )),
2137        }
2138    }
2139
2140    fn simple_primitive_data_type(value: i32) -> proto_schema::DataType {
2141        primitive_data_type(Some(proto_schema::primitive_type::Kind::Simple(value)))
2142    }
2143
2144    fn decimal_data_type(precision: u32, scale: u32) -> proto_schema::DataType {
2145        primitive_data_type(Some(proto_schema::primitive_type::Kind::Decimal(
2146            proto_schema::DecimalType { precision, scale },
2147        )))
2148    }
2149
2150    #[rstest]
2151    #[case::missing_data_type_kind(proto_schema::DataType { kind: None })]
2152    #[case::missing_primitive_kind(primitive_data_type(None))]
2153    #[case::unspecified_primitive(simple_primitive_data_type(
2154        proto_schema::SimplePrimitiveType::Unspecified as i32
2155    ))]
2156    #[case::unknown_primitive(simple_primitive_data_type(9999))]
2157    #[case::decimal_zero_precision(decimal_data_type(0, 0))]
2158    #[case::decimal_precision_too_large(decimal_data_type(39, 0))]
2159    #[case::decimal_scale_exceeds_precision(decimal_data_type(5, 6))]
2160    #[case::decimal_precision_out_of_u8_range(decimal_data_type(256, 0))]
2161    #[case::array_missing_element_type(proto_schema::DataType {
2162        kind: Some(proto_schema::data_type::Kind::Array(Box::new(proto_schema::ArrayType {
2163            element_type: None,
2164            contains_null: true,
2165        }))),
2166    })]
2167    #[case::map_missing_key_type(proto_schema::DataType {
2168        kind: Some(proto_schema::data_type::Kind::Map(Box::new(proto_schema::MapType {
2169            key_type: None,
2170            value_type: Some(Box::new(proto_schema::DataType::from(&DataType::STRING))),
2171            value_contains_null: true,
2172        }))),
2173    })]
2174    #[case::map_missing_value_type(proto_schema::DataType {
2175        kind: Some(proto_schema::data_type::Kind::Map(Box::new(proto_schema::MapType {
2176            key_type: Some(Box::new(proto_schema::DataType::from(&DataType::STRING))),
2177            value_type: None,
2178            value_contains_null: true,
2179        }))),
2180    })]
2181    fn data_type_decode_rejects_invalid(#[case] proto: proto_schema::DataType) {
2182        assert!(DataType::try_from(proto).is_err());
2183    }
2184
2185    #[test]
2186    fn struct_field_decode_rejects_missing_data_type() {
2187        let proto = proto_schema::StructField {
2188            name: "f".to_string(),
2189            data_type: None,
2190            nullable: true,
2191            metadata: Default::default(),
2192        };
2193        assert!(StructField::try_from(proto).is_err());
2194    }
2195
2196    #[rstest]
2197    #[case::missing_value(proto_schema::MetadataValue { value: None })]
2198    #[case::invalid_other_json(proto_schema::MetadataValue {
2199        value: Some(proto_schema::metadata_value::Value::OtherJson("not json".to_string())),
2200    })]
2201    fn metadata_value_decode_rejects_invalid(#[case] proto: proto_schema::MetadataValue) {
2202        assert!(MetadataValue::try_from(proto).is_err());
2203    }
2204}