Skip to main content

reifydb_evaluate/expression/
compile.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::mem::discriminant;
5
6use arrow_array::{Array, ArrayRef};
7use arrow_schema::FieldRef;
8use reifydb_core::{
9	expression::VariableExpression,
10	value::{
11		batch::{is_scalar, scalar_value, scalar_value_type},
12		column::{
13			arith::{length_mismatch, option::binary_op_unwrap_option},
14			builder::ColumnBuilder,
15			cast::error::CastError,
16			factory,
17		},
18	},
19};
20use reifydb_value::{
21	error::{BinaryOp, Error, IntoDiagnostic, RuntimeErrorKind, TypeError},
22	fragment::Fragment,
23	value::{
24		Value,
25		column_view::{ColumnView, ViewData},
26		constraint::{Constraint, TypeConstraint, precision::Precision, scale::Scale},
27		system_columns::user_columns,
28		value_type::ValueType,
29	},
30};
31
32use crate::{
33	Result,
34	expression::{
35		compare::{Equal, compare_columns},
36		context::EvalContext,
37	},
38	stack::Variable,
39};
40
41pub(crate) fn variable_column(
42	ctx: &EvalContext,
43	expr: &VariableExpression,
44	row_count: usize,
45) -> Result<(FieldRef, ArrayRef)> {
46	let variable_name = expr.name();
47
48	if variable_name == "env" {
49		return Err(TypeError::Runtime {
50			kind: RuntimeErrorKind::VariableIsDataframe {
51				name: variable_name.to_string(),
52			},
53			message: format!(
54				"Variable '{}' contains a dataframe and cannot be used directly in scalar expressions",
55				variable_name
56			),
57		}
58		.into());
59	}
60
61	match ctx.symbols.get(variable_name) {
62		Some(Variable::Columns {
63			batch,
64		}) if is_scalar(batch) => {
65			let value = match scalar_value(batch)? {
66				Value::Any(inner)
67					if matches!(
68						inner.as_ref(),
69						Value::List(items)
70							if items.iter().all(|v| matches!(v, Value::Record(_)))
71					) =>
72				{
73					*inner
74				}
75				other => other,
76			};
77			if matches!(
78				value,
79				Value::None {
80					inner: ValueType::Any
81				}
82			) {
83				return Ok(factory::none(variable_name, row_count));
84			}
85			let mut data = ColumnBuilder::with_capacity(scalar_value_type(batch, &value)?, row_count);
86			for _ in 0..row_count {
87				data.push_value(value.clone());
88			}
89			Ok(data.finish(variable_name))
90		}
91		Some(Variable::Columns {
92			..
93		})
94		| Some(Variable::ForIterator {
95			..
96		})
97		| Some(Variable::Closure(_)) => Err(TypeError::Runtime {
98			kind: RuntimeErrorKind::VariableIsDataframe {
99				name: variable_name.to_string(),
100			},
101			message: format!(
102				"Variable '{}' contains a dataframe and cannot be used directly in scalar expressions",
103				variable_name
104			),
105		}
106		.into()),
107		None => {
108			if let Some(value) = ctx.params.get_named(variable_name) {
109				let mut data = ColumnBuilder::with_capacity(value.get_type(), row_count);
110				for _ in 0..row_count {
111					data.push_value(value.clone());
112				}
113				return Ok(data.finish(variable_name));
114			}
115			Err(TypeError::Runtime {
116				kind: RuntimeErrorKind::VariableNotFound {
117					fragment: expr.fragment.clone(),
118				},
119				message: format!("Variable '{}' is not defined", variable_name),
120			}
121			.into())
122		}
123	}
124}
125
126pub(crate) fn variable_field_column(
127	ctx: &EvalContext,
128	variable: &VariableExpression,
129	field_name: &str,
130	row_count: usize,
131) -> Result<(FieldRef, ArrayRef)> {
132	let variable_name = variable.name();
133	match ctx.symbols.get(variable_name) {
134		Some(Variable::Columns {
135			batch,
136		}) if !is_scalar(batch) => {
137			let found = user_columns(batch).find(|(field, _)| field.name() == field_name);
138			match found {
139				Some((field, array)) => {
140					let value = ColumnView::try_from((array, field.as_ref()))?.get_value(0);
141					let mut data = ColumnBuilder::with_capacity(value.get_type(), row_count);
142					for _ in 0..row_count {
143						data.push_value(value.clone());
144					}
145					Ok(data.finish(field_name))
146				}
147				None => {
148					let available: Vec<String> = user_columns(batch)
149						.map(|(field, _)| field.name().to_string())
150						.collect();
151					Err(TypeError::Runtime {
152						kind: RuntimeErrorKind::FieldNotFound {
153							variable: variable_name.to_string(),
154							field: field_name.to_string(),
155							available,
156						},
157						message: format!(
158							"Field '{}' not found on variable '{}'",
159							field_name, variable_name
160						),
161					}
162					.into())
163				}
164			}
165		}
166		Some(Variable::Columns {
167			..
168		})
169		| Some(Variable::Closure(_)) => Err(TypeError::Runtime {
170			kind: RuntimeErrorKind::FieldNotFound {
171				variable: variable_name.to_string(),
172				field: field_name.to_string(),
173				available: vec![],
174			},
175			message: format!("Field '{}' not found on variable '{}'", field_name, variable_name),
176		}
177		.into()),
178		Some(Variable::ForIterator {
179			..
180		}) => Err(TypeError::Runtime {
181			kind: RuntimeErrorKind::VariableIsDataframe {
182				name: variable_name.to_string(),
183			},
184			message: format!(
185				"Variable '{}' contains a dataframe and cannot be used directly in scalar expressions",
186				variable_name
187			),
188		}
189		.into()),
190		None => Err(TypeError::Runtime {
191			kind: RuntimeErrorKind::VariableNotFound {
192				fragment: variable.fragment.clone(),
193			},
194			message: format!("Variable '{}' is not defined", variable_name),
195		}
196		.into()),
197	}
198}
199
200pub(crate) fn type_column(row_count: usize, ty: &ValueType, fragment: &Fragment) -> (FieldRef, ArrayRef) {
201	let values: Vec<Value> = (0..row_count).map(|_| Value::Type(ty.clone())).collect();
202	factory::any(fragment.text(), values)
203}
204
205pub(crate) fn combine_bool_columns(
206	left: (FieldRef, ArrayRef),
207	right: (FieldRef, ArrayRef),
208	fragment: Fragment,
209	combine_fn: fn(bool, bool) -> bool,
210) -> Result<(FieldRef, ArrayRef)> {
211	if left.1.len() != right.1.len() {
212		return Err(length_mismatch(left.1.len(), right.1.len(), &fragment));
213	}
214
215	binary_op_unwrap_option(&left, &right, fragment.clone(), |left, right| {
216		let (left, right) = (ColumnView::try_from(left)?, ColumnView::try_from(right)?);
217		match (&left.data, &right.data) {
218			(ViewData::Bool(l), ViewData::Bool(r)) => {
219				let len = l.len();
220				let mut data = Vec::with_capacity(len);
221				let mut bitvec = Vec::with_capacity(len);
222
223				for i in 0..len {
224					data.push(combine_fn(l.value(i), r.value(i)));
225					bitvec.push(true);
226				}
227
228				Ok(factory::bool_with_bitvec(fragment.text(), data, bitvec))
229			}
230			_ => {
231				unreachable!("combine_bool_columns should only be called with boolean columns")
232			}
233		}
234	})
235}
236
237fn list_items_contain(items: &[Value], element: &Value, fragment: &Fragment) -> bool {
238	if items.iter().any(|item| item == element) {
239		return true;
240	}
241	if items.is_empty() {
242		return false;
243	}
244
245	if let Some(items_col) = build_homogeneous_buffer(items, fragment.text()) {
246		let elems_col = factory::from_many(fragment.text(), element.clone(), items.len());
247		return compare_columns::<Equal>(&items_col, &elems_col, fragment.clone(), |f, l, r| {
248			TypeError::BinaryOperatorNotApplicable {
249				operator: BinaryOp::Equal,
250				left: l,
251				right: r,
252				fragment: f,
253			}
254			.into_diagnostic()
255		})
256		.and_then(|c| bool_column_has_true(&c))
257		.unwrap_or(false);
258	}
259
260	list_items_contain_per_item(items, element, fragment)
261}
262
263fn list_items_contain_per_item(items: &[Value], element: &Value, fragment: &Fragment) -> bool {
264	items.iter().any(|item| {
265		let item_col = factory::from_one(fragment.text(), item.clone());
266		let elem_col = factory::from_one(fragment.text(), element.clone());
267		compare_columns::<Equal>(&item_col, &elem_col, fragment.clone(), |f, l, r| {
268			TypeError::BinaryOperatorNotApplicable {
269				operator: BinaryOp::Equal,
270				left: l,
271				right: r,
272				fragment: f,
273			}
274			.into_diagnostic()
275		})
276		.and_then(|c| {
277			let view = ColumnView::try_from(&c)?;
278			Ok(match &view.data {
279				ViewData::Bool(b) if view.logical_nulls().is_none() => b.value(0),
280				_ => false,
281			})
282		})
283		.unwrap_or(false)
284	})
285}
286
287fn bool_column_has_true(col: &(FieldRef, ArrayRef)) -> Result<bool> {
288	let view = ColumnView::try_from(col)?;
289	Ok(match &view.data {
290		ViewData::Bool(b) => match view.logical_nulls() {
291			Some(nulls) => (nulls.inner() & b.values()).has_true(),
292			None => b.values().has_true(),
293		},
294		_ => false,
295	})
296}
297
298fn build_homogeneous_buffer(items: &[Value], name: &str) -> Option<(FieldRef, ArrayRef)> {
299	let first = items.first()?;
300	let first_disc = discriminant(first);
301	if !items.iter().all(|v| discriminant(v) == first_disc) {
302		return None;
303	}
304
305	macro_rules! collect {
306		($variant:ident, $constructor:ident $([$($arg:expr),*])?, |$x:ident| $convert:expr) => {{
307			let data: Vec<_> = items
308				.iter()
309				.map(|v| match v {
310					Value::$variant($x) => $convert,
311					_ => unreachable!("homogeneous check guarantees variant"),
312				})
313				.collect();
314			Some(factory::$constructor(name, $($($arg,)*)? data))
315		}};
316	}
317
318	match first {
319		Value::Boolean(_) => collect!(Boolean, bool, |x| *x),
320		Value::Float4(_) => collect!(Float4, float4, |x| x.value()),
321		Value::Float8(_) => collect!(Float8, float8, |x| x.value()),
322		Value::Int1(_) => collect!(Int1, int1, |x| *x),
323		Value::Int2(_) => collect!(Int2, int2, |x| *x),
324		Value::Int4(_) => collect!(Int4, int4, |x| *x),
325		Value::Int8(_) => collect!(Int8, int8, |x| *x),
326		Value::Int16(_) => collect!(Int16, int16, |x| *x),
327		Value::Uint1(_) => collect!(Uint1, uint1, |x| *x),
328		Value::Uint2(_) => collect!(Uint2, uint2, |x| *x),
329		Value::Uint4(_) => collect!(Uint4, uint4, |x| *x),
330		Value::Uint8(_) => collect!(Uint8, uint8, |x| *x),
331		Value::Uint16(_) => collect!(Uint16, uint16, |x| *x),
332		Value::Utf8(_) => collect!(Utf8, utf8, |x| x.clone()),
333		Value::Date(_) => collect!(Date, date, |x| *x),
334		Value::DateTime(_) => collect!(DateTime, datetime, |x| *x),
335		Value::Time(_) => collect!(Time, time, |x| *x),
336		Value::Duration(_) => collect!(Duration, duration, |x| *x),
337		Value::Uuid4(_) => collect!(Uuid4, uuid4, |x| *x),
338		Value::Uuid7(_) => collect!(Uuid7, uuid7, |x| *x),
339		Value::IdentityId(_) => collect!(IdentityId, identity_id, |x| *x),
340		Value::Blob(_) => collect!(Blob, blob, |x| x.clone()),
341		Value::Decimal(_) => {
342			let scale = items
343				.iter()
344				.filter_map(|v| match v {
345					Value::Decimal(d) => Some(d.scale()),
346					_ => None,
347				})
348				.max()?;
349			if !items.iter().all(|v| matches!(v, Value::Decimal(d) if d.rescale(scale).is_some())) {
350				return None;
351			}
352			collect!(Decimal, decimal[Precision::MAX, Scale::new(scale)], |x| x.clone())
353		}
354		Value::DictionaryId(_) => collect!(DictionaryId, dictionary_id, |x| *x),
355
356		_ => None,
357	}
358}
359
360pub(crate) fn list_contains_element(
361	list_col: &(FieldRef, ArrayRef),
362	element_col: &(FieldRef, ArrayRef),
363	fragment: &Fragment,
364) -> Result<(FieldRef, ArrayRef)> {
365	let (list_view, element_view) = (ColumnView::try_from(list_col)?, ColumnView::try_from(element_col)?);
366	let len = list_col.1.len();
367	let mut data = Vec::with_capacity(len);
368
369	for i in 0..len {
370		let list_value = list_view.get_value(i);
371		let element_value = element_view.get_value(i);
372
373		let contained = match &list_value {
374			Value::List(items) => list_items_contain(items, &element_value, fragment),
375			Value::Tuple(items) => list_items_contain(items, &element_value, fragment),
376			Value::Any(boxed) => match boxed.as_ref() {
377				Value::List(items) => list_items_contain(items, &element_value, fragment),
378				Value::Tuple(items) => list_items_contain(items, &element_value, fragment),
379				_ => false,
380			},
381			_ => false,
382		};
383		data.push(contained);
384	}
385
386	Ok(factory::bool(fragment.text(), data))
387}
388
389pub(crate) fn is_truthy(value: &Value) -> bool {
390	match value {
391		Value::Boolean(true) => true,
392		Value::Boolean(false) => false,
393		Value::None {
394			..
395		} => false,
396		Value::Int1(0) | Value::Int2(0) | Value::Int4(0) | Value::Int8(0) | Value::Int16(0) => false,
397		Value::Uint1(0) | Value::Uint2(0) | Value::Uint4(0) | Value::Uint8(0) | Value::Uint16(0) => false,
398		Value::Int1(_) | Value::Int2(_) | Value::Int4(_) | Value::Int8(_) | Value::Int16(_) => true,
399		Value::Uint1(_) | Value::Uint2(_) | Value::Uint4(_) | Value::Uint8(_) | Value::Uint16(_) => true,
400		Value::Utf8(s) => !s.is_empty(),
401		_ => true,
402	}
403}
404
405pub(crate) fn enforce_cast_constraint(
406	column: &(FieldRef, ArrayRef),
407	target: &ValueType,
408	constraint: &Option<Constraint>,
409	fragment: &Fragment,
410) -> Result<()> {
411	let Some(constraint) = constraint else {
412		return Ok(());
413	};
414	let checked = TypeConstraint::with_constraint(target.clone(), constraint.clone());
415	let view = ColumnView::try_from(column)?;
416	for row in (0..view.len()).filter(|&row| !view.none_at(row)) {
417		if let Err(mut error) = checked.coerce(&mut view.get_value(row)) {
418			error.0.fragment = fragment.clone();
419			return Err(error);
420		}
421	}
422	Ok(())
423}
424
425pub(crate) fn wrap_cast_error(err: Error, fragment: Fragment, target: &ValueType) -> Error {
426	if err.0.code.starts_with("CAST_") {
427		return err;
428	}
429	let cause = err.diagnostic();
430	let wrapped = if target.is_bool() {
431		CastError::InvalidBoolean {
432			fragment,
433			cause,
434		}
435	} else if target.is_temporal() {
436		CastError::InvalidTemporal {
437			fragment,
438			target: target.clone(),
439			cause,
440		}
441	} else if target.is_uuid() || *target == ValueType::IdentityId {
442		CastError::InvalidUuid {
443			fragment,
444			target: target.clone(),
445			cause,
446		}
447	} else {
448		CastError::InvalidNumber {
449			fragment,
450			target: target.clone(),
451			cause,
452		}
453	};
454	Error::from(wrapped)
455}
456
457#[cfg(test)]
458mod tests {
459	use std::str::FromStr;
460
461	use arrow_array::{ArrayRef, RecordBatch};
462	use arrow_schema::FieldRef;
463	use reifydb_core::{
464		expression::{
465			CastExpression, ColumnExpression, ConstantExpression, ElseIfExpression, Expression,
466			IfExpression, MapExpression, TypeExpression,
467		},
468		interface::identifier::ColumnIdentifier,
469		value::{batch::batch, column::factory},
470	};
471	use reifydb_value::{
472		fragment::Fragment,
473		value::{
474			Value,
475			column_view::ColumnView,
476			constraint::{precision::Precision, scale::Scale},
477			decimal::Decimal,
478			value_type::ValueType,
479		},
480	};
481
482	use super::combine_bool_columns;
483	use crate::expression::{context::EvalContext, eval::evaluate};
484
485	fn column(name: &str) -> Expression {
486		Expression::Column(ColumnExpression(ColumnIdentifier::with_alias(
487			Fragment::internal("t"),
488			Fragment::internal(name),
489		)))
490	}
491
492	fn uncastable() -> Expression {
493		Expression::Cast(CastExpression {
494			fragment: Fragment::testing_empty(),
495			expression: Box::new(Expression::Constant(ConstantExpression::Text {
496				fragment: Fragment::internal("not-a-number"),
497			})),
498			to: TypeExpression {
499				fragment: Fragment::testing_empty(),
500				ty: ValueType::Int4,
501				constraint: None,
502			},
503		})
504	}
505
506	fn conditional(
507		condition: Expression,
508		then_expr: Expression,
509		else_ifs: Vec<(Expression, Expression)>,
510		else_expr: Option<Expression>,
511	) -> Expression {
512		Expression::If(IfExpression {
513			condition: Box::new(condition),
514			then_expr: Box::new(then_expr),
515			else_ifs: else_ifs
516				.into_iter()
517				.map(|(condition, then_expr)| ElseIfExpression {
518					condition: Box::new(condition),
519					then_expr: Box::new(then_expr),
520					fragment: Fragment::testing_empty(),
521				})
522				.collect(),
523			else_expr: else_expr.map(Box::new),
524			fragment: Fragment::testing_empty(),
525		})
526	}
527
528	fn bools(name: &str, data: [bool; 4]) -> (FieldRef, ArrayRef) {
529		factory::bool(name, data)
530	}
531
532	fn ints(name: &str, data: [i32; 4]) -> (FieldRef, ArrayRef) {
533		factory::int4(name, data)
534	}
535
536	#[test]
537	fn every_row_reads_its_own_index_from_the_branch_it_selected() {
538		// A branch is evaluated as a whole column, so row i must take branch[i]. Assembling the
539		// result in append order instead of by row index shifts every value after the first switch.
540		let base = EvalContext::testing();
541		let ctx = base.with_eval(
542			batch(vec![
543				bools("flag", [true, false, false, true]),
544				ints("hi", [100, 200, 300, 400]),
545				ints("lo", [1, 2, 3, 4]),
546			])
547			.unwrap(),
548			4,
549		);
550
551		let result =
552			evaluate(&ctx, &conditional(column("flag"), column("hi"), vec![], Some(column("lo")))).unwrap();
553
554		assert_eq!(result.1.as_ref(), factory::int4("", [100, 2, 3, 400]).1.as_ref());
555	}
556
557	#[test]
558	fn an_else_if_chain_gives_each_row_its_first_matching_branch() {
559		// Later conditions must never override an earlier match: row 1 satisfies both `second` and
560		// nothing else, row 2 satisfies `second` alone, and row 3 falls through to the else.
561		let base = EvalContext::testing();
562		let ctx = base.with_eval(
563			batch(vec![
564				bools("first", [true, false, false, false]),
565				bools("second", [true, true, true, false]),
566				ints("a", [10, 20, 30, 40]),
567				ints("b", [1, 2, 3, 4]),
568				ints("c", [-1, -2, -3, -4]),
569			])
570			.unwrap(),
571			4,
572		);
573
574		let result = evaluate(
575			&ctx,
576			&conditional(
577				column("first"),
578				column("a"),
579				vec![(column("second"), column("b"))],
580				Some(column("c")),
581			),
582		)
583		.unwrap();
584
585		assert_eq!(result.1.as_ref(), factory::int4("", [10, 2, 3, -4]).1.as_ref());
586	}
587
588	#[test]
589	fn a_row_that_matches_no_branch_becomes_none() {
590		// Without an else branch the unmatched rows must still occupy their slot, otherwise the
591		// result column is shorter than the input and every downstream row pairs with the wrong key.
592		let base = EvalContext::testing();
593		let ctx = base.with_eval(
594			batch(vec![bools("flag", [true, false, false, true]), ints("hi", [7, 8, 9, 10])]).unwrap(),
595			4,
596		);
597
598		let result = evaluate(&ctx, &conditional(column("flag"), column("hi"), vec![], None)).unwrap();
599
600		let view = ColumnView::try_from(&result).unwrap();
601		assert_eq!(result.1.len(), 4);
602		assert_eq!(view.get_value(0), Value::Int4(7));
603		assert!(matches!(view.get_value(1), Value::None { .. }));
604		assert!(matches!(view.get_value(2), Value::None { .. }));
605		assert_eq!(view.get_value(3), Value::Int4(10));
606	}
607
608	#[test]
609	fn a_branch_that_no_row_selects_is_never_evaluated() {
610		// Hoisting a branch out of the row loop must not make it eager: a guard exists precisely to
611		// keep a failing expression away from the rows that cannot satisfy it.
612		let base = EvalContext::testing();
613		let all_true = base.with_eval(
614			batch(vec![bools("flag", [true, true, true, true]), ints("hi", [1, 2, 3, 4])]).unwrap(),
615			4,
616		);
617
618		let skipped =
619			evaluate(&all_true, &conditional(column("flag"), column("hi"), vec![], Some(uncastable())));
620
621		assert!(skipped.is_ok(), "an else branch no row selects must not be evaluated");
622
623		let one_false = base.with_eval(
624			batch(vec![bools("flag", [true, true, false, true]), ints("hi", [1, 2, 3, 4])]).unwrap(),
625			4,
626		);
627
628		let taken =
629			evaluate(&one_false, &conditional(column("flag"), column("hi"), vec![], Some(uncastable())));
630
631		assert!(
632			taken.is_err(),
633			"the branch must really fail when a row selects it, or the case above is vacuous"
634		);
635	}
636
637	fn multi(names: [&str; 2]) -> Expression {
638		Expression::Map(MapExpression {
639			expressions: names.iter().map(|name| column(name)).collect(),
640			fragment: Fragment::testing_empty(),
641		})
642	}
643
644	fn none_literal() -> Expression {
645		Expression::Constant(ConstantExpression::None {
646			fragment: Fragment::testing_empty(),
647		})
648	}
649
650	fn four_row_ctx(extra: Vec<(FieldRef, ArrayRef)>) -> RecordBatch {
651		let mut cols = vec![bools("flag", [true, false, false, true])];
652		cols.extend(extra);
653		batch(cols).unwrap()
654	}
655
656	#[test]
657	fn branches_of_different_types_are_rejected_instead_of_aborting() {
658		// The column buffer matches the value variant exactly and panics on anything else, so an
659		// unvalidated mismatch takes the process down rather than failing the query.
660		let base = EvalContext::testing();
661		let ctx = base.with_eval(
662			four_row_ctx(vec![ints("small", [1, 2, 3, 4]), factory::int8("wide", [5i64, 6, 7, 8])]),
663			4,
664		);
665
666		let err = evaluate(&ctx, &conditional(column("flag"), column("small"), vec![], Some(column("wide"))))
667			.expect_err("int4 and int8 branches must not be accepted");
668
669		assert_eq!(err.0.code, "RUNTIME_012");
670	}
671
672	#[test]
673	fn an_optional_branch_still_pairs_with_its_bare_type() {
674		// Option is a wrapper over the same base type, so these branches agree; rejecting them
675		// would break every conditional whose branches differ only in nullability.
676		let base = EvalContext::testing();
677		let ctx = base.with_eval(
678			four_row_ctx(vec![
679				ints("bare", [1, 2, 3, 4]),
680				factory::int4_with_bitvec("opt", [9, 8, 7, 6], vec![true, false, true, true]),
681			]),
682			4,
683		);
684
685		let result = evaluate(&ctx, &conditional(column("flag"), column("bare"), vec![], Some(column("opt"))))
686			.expect("a bare and an optional branch of one base type must agree");
687
688		assert_eq!(result.1.len(), 4);
689	}
690
691	#[test]
692	fn a_none_branch_widens_to_the_other_branch_type() {
693		// A none literal carries Option(Any); treating Any as a concrete type would reject the
694		// guarded-value shape that every conditional projection relies on.
695		let base = EvalContext::testing();
696		let ctx = base.with_eval(four_row_ctx(vec![ints("hi", [7, 8, 9, 10])]), 4);
697
698		let result = evaluate(&ctx, &conditional(column("flag"), column("hi"), vec![], Some(none_literal())))
699			.expect("a none branch must widen to the other branch type");
700
701		let view = ColumnView::try_from(&result).unwrap();
702		assert_eq!(view.get_value(0), Value::Int4(7));
703		assert!(matches!(view.get_value(1), Value::None { .. }));
704	}
705
706	#[test]
707	fn a_none_first_row_does_not_fix_the_result_type() {
708		// Typing the result from the first row's none branch made it Any, so the first int after it aborted the
709		// process.
710		let base = EvalContext::testing();
711		let ctx = base.with_eval(
712			batch(vec![bools("flag", [false, true, false, true]), ints("hi", [7, 8, 9, 10])]).unwrap(),
713			4,
714		);
715
716		let result = evaluate(&ctx, &conditional(column("flag"), column("hi"), vec![], Some(none_literal())))
717			.expect("a none first row must not fix the result type");
718
719		let view = ColumnView::try_from(&result).unwrap();
720		assert!(matches!(view.get_value(0), Value::None { .. }));
721		assert_eq!(view.get_value(1), Value::Int4(8));
722		assert!(matches!(view.get_value(2), Value::None { .. }));
723		assert_eq!(view.get_value(3), Value::Int4(10));
724	}
725
726	#[test]
727	fn branches_of_different_widths_are_rejected() {
728		// A narrower branch used to leave its unfilled columns short, silently misaligning every
729		// value after the first switch; a wider one had its surplus columns dropped.
730		let base = EvalContext::testing();
731		let ctx = base.with_eval(four_row_ctx(vec![ints("a", [1, 2, 3, 4]), ints("b", [10, 20, 30, 40])]), 4);
732
733		let wide_then =
734			evaluate(&ctx, &conditional(column("flag"), multi(["a", "b"]), vec![], Some(column("a"))))
735				.expect_err("a two column branch must not pair with a one column branch");
736		assert_eq!(wide_then.0.code, "RUNTIME_012");
737
738		let wide_else =
739			evaluate(&ctx, &conditional(column("flag"), column("a"), vec![], Some(multi(["a", "b"]))))
740				.expect_err("a one column branch must not pair with a two column branch");
741		assert_eq!(wide_else.0.code, "RUNTIME_012");
742	}
743
744	#[test]
745	fn a_mismatched_branch_no_row_selects_is_rejected() {
746		// A branch type must not depend on which rows a batch holds, so an unselected mismatch is still an
747		// error.
748		let base = EvalContext::testing();
749		let ctx = base.with_eval(
750			batch(vec![
751				bools("flag", [true, true, true, true]),
752				ints("small", [1, 2, 3, 4]),
753				factory::int8("wide", [5i64, 6, 7, 8]),
754			])
755			.unwrap(),
756			4,
757		);
758
759		let err = evaluate(&ctx, &conditional(column("flag"), column("small"), vec![], Some(column("wide"))))
760			.expect_err("an unselected branch of another type must be rejected");
761
762		assert_eq!(err.code, "RUNTIME_012", "expected a conditional branch mismatch, got {err:?}");
763	}
764
765	#[test]
766	fn decimal_and_float8_branches_unify_to_float8_in_either_order() {
767		// A decimal literal next to a float branch must not be a mismatch, and its rows must be cast, never
768		// pushed raw.
769		let base = EvalContext::testing();
770		let decimals = ["10.5", "20.5", "30.5", "40.5"].map(|text| Decimal::from_str(text).unwrap());
771		let ctx = base.with_eval(
772			four_row_ctx(vec![
773				factory::float8("f", [1.5, 2.5, 3.5, 4.5]),
774				factory::decimal("d", Precision::new(3), Scale::new(1), decimals),
775			]),
776			4,
777		);
778
779		let float_then = evaluate(&ctx, &conditional(column("flag"), column("f"), vec![], Some(column("d"))))
780			.expect("a float then branch must unify with a decimal else branch");
781		let view = ColumnView::try_from(&float_then).unwrap();
782		assert_eq!(*view.get_type().inner_type(), ValueType::Float8);
783		assert_eq!(view.get_value(0), Value::float8(1.5));
784		assert_eq!(view.get_value(1), Value::float8(20.5));
785		assert_eq!(view.get_value(2), Value::float8(30.5));
786		assert_eq!(view.get_value(3), Value::float8(4.5));
787
788		let decimal_then = evaluate(&ctx, &conditional(column("flag"), column("d"), vec![], Some(column("f"))))
789			.expect("a decimal then branch must unify with a float else branch");
790		let view = ColumnView::try_from(&decimal_then).unwrap();
791		assert_eq!(*view.get_type().inner_type(), ValueType::Float8);
792		assert_eq!(view.get_value(0), Value::float8(10.5));
793		assert_eq!(view.get_value(1), Value::float8(2.5));
794		assert_eq!(view.get_value(2), Value::float8(3.5));
795		assert_eq!(view.get_value(3), Value::float8(40.5));
796	}
797
798	#[test]
799	fn a_multi_column_value_in_a_single_column_slot_is_rejected() {
800		// Taking the first column and discarding the rest loses data with no signal; map, extend
801		// and patch all reach a value expression through this path.
802		let base = EvalContext::testing();
803		let ctx = base.with_eval(four_row_ctx(vec![ints("a", [1, 2, 3, 4]), ints("b", [10, 20, 30, 40])]), 4);
804
805		let err = evaluate(&ctx, &multi(["a", "b"])).expect_err("a two column value must not be truncated");
806
807		assert_eq!(err.0.code, "RUNTIME_010");
808	}
809
810	#[test]
811	fn combining_boolean_columns_of_different_lengths_is_an_error() {
812		// A length mismatch in one batch must be an error, never padded none rows or an out of range read.
813		let left = factory::bool("l", [true, true, false]);
814		let right = factory::bool("r", [true, false]);
815
816		let result = combine_bool_columns(left, right, Fragment::internal("and"), |l, r| l && r);
817
818		assert!(
819			result.is_err(),
820			"a right side shorter than the left must be an error, got {:?}",
821			result.map(|c| c.1)
822		);
823	}
824}