Skip to main content

reifydb_evaluate/expression/
eval.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use arrow_array::ArrayRef;
5use arrow_schema::FieldRef;
6use reifydb_core::{
7	expression::Expression,
8	value::column::{cast::cast_column_data, write::check_digest_write},
9};
10use reifydb_value::{
11	fragment::LazyFragment,
12	value::{
13		Value,
14		column_view::ColumnView,
15		constraint::{TypeConstraint, precision::Precision},
16		value_type::ValueType,
17	},
18};
19
20use crate::{Result, expression::context::EvalContext, lower::LoweredExpr};
21
22pub fn evaluate(ctx: &EvalContext, expr: &Expression) -> Result<(FieldRef, ArrayRef)> {
23	let column = LoweredExpr::new(expr.clone(), "evaluate").evaluate(ctx)?;
24
25	if let Some(ty) = ctx.target.as_ref().map(|c| c.column_type()) {
26		let view = ColumnView::try_from(&column)?;
27		check_digest_write(&view, &ty, expr.lazy_fragment())?;
28		cast_for_write(ctx, &view, ty, &expr.lazy_fragment())
29	} else {
30		Ok(column)
31	}
32}
33
34pub fn cast_for_write(
35	ctx: &EvalContext,
36	view: &ColumnView<'_>,
37	target: ValueType,
38	fragment: impl LazyFragment + Clone,
39) -> Result<(FieldRef, ArrayRef)> {
40	refuse_lost_scale(view, &target, fragment.clone())?;
41	cast_column_data(ctx, view, target, fragment)
42}
43
44pub fn loses_scale(value: &Value, target: &ValueType) -> bool {
45	matches!(
46		(value, target.inner_type()),
47		(Value::Decimal(decimal), ValueType::Decimal { scale, .. })
48			if decimal.scale() > scale.value() && decimal.rescale(scale.value()).is_none()
49	)
50}
51
52fn refuse_lost_scale(data: &ColumnView, target: &ValueType, fragment: impl LazyFragment) -> Result<()> {
53	match (data.get_type().inner_type(), target.inner_type()) {
54		(
55			ValueType::Decimal {
56				scale: from,
57				..
58			},
59			ValueType::Decimal {
60				scale: to,
61				..
62			},
63		) if from > to => data.iter().filter(|value| loses_scale(value, target)).try_for_each(|mut value| {
64			let exact = TypeConstraint::unconstrained(ValueType::decimal(Precision::MAX, *to));
65			exact.coerce(&mut value).map_err(|mut e| {
66				e.0.fragment = fragment.fragment();
67				e
68			})
69		}),
70		_ => Ok(()),
71	}
72}
73
74#[cfg(test)]
75pub mod tests {
76	use reifydb_core::{
77		expression::{
78			CastExpression, ConstantExpression,
79			ConstantExpression::Number,
80			Expression::{Cast, Constant, Prefix},
81			PrefixExpression, PrefixOperator, TypeExpression,
82		},
83		value::column::factory::{bool, float4, float8, int1, int4},
84	};
85	use reifydb_value::{
86		fragment::Fragment,
87		value::{
88			column_view::{ColumnView, ViewData},
89			container::decimal_array::{decimal_at, decimals},
90			value_type::ValueType,
91		},
92	};
93
94	use crate::expression::{context::EvalContext, eval::evaluate};
95
96	#[test]
97	fn test_cast_integer() {
98		let ctx = EvalContext::testing();
99		let result = evaluate(
100			&ctx,
101			&Cast(CastExpression {
102				fragment: Fragment::testing_empty(),
103				expression: Box::new(Constant(Number {
104					fragment: Fragment::internal("42"),
105				})),
106				to: TypeExpression {
107					fragment: Fragment::testing_empty(),
108					ty: ValueType::Int4,
109					constraint: None,
110				},
111			}),
112		)
113		.unwrap();
114
115		assert_eq!(result.1.as_ref(), int4("", [42]).1.as_ref());
116	}
117
118	#[test]
119	fn test_cast_negative_integer() {
120		let ctx = EvalContext::testing();
121		let result = evaluate(
122			&ctx,
123			&Cast(CastExpression {
124				fragment: Fragment::testing_empty(),
125				expression: Box::new(Prefix(PrefixExpression {
126					operator: PrefixOperator::Minus(Fragment::testing_empty()),
127					expression: Box::new(Constant(Number {
128						fragment: Fragment::internal("42"),
129					})),
130					fragment: Fragment::testing_empty(),
131				})),
132				to: TypeExpression {
133					fragment: Fragment::testing_empty(),
134					ty: ValueType::Int4,
135					constraint: None,
136				},
137			}),
138		)
139		.unwrap();
140
141		assert_eq!(result.1.as_ref(), int4("", [-42]).1.as_ref());
142	}
143
144	#[test]
145	fn test_cast_negative_min() {
146		let ctx = EvalContext::testing();
147		let result = evaluate(
148			&ctx,
149			&Cast(CastExpression {
150				fragment: Fragment::testing_empty(),
151				expression: Box::new(Prefix(PrefixExpression {
152					operator: PrefixOperator::Minus(Fragment::testing_empty()),
153					expression: Box::new(Constant(Number {
154						fragment: Fragment::internal("128"),
155					})),
156					fragment: Fragment::testing_empty(),
157				})),
158				to: TypeExpression {
159					fragment: Fragment::testing_empty(),
160					ty: ValueType::Int1,
161					constraint: None,
162				},
163			}),
164		)
165		.unwrap();
166
167		assert_eq!(result.1.as_ref(), int1("", [-128]).1.as_ref());
168	}
169
170	#[test]
171	fn test_cast_float_8() {
172		let ctx = EvalContext::testing();
173		let result = evaluate(
174			&ctx,
175			&Cast(CastExpression {
176				fragment: Fragment::testing_empty(),
177				expression: Box::new(Constant(Number {
178					fragment: Fragment::internal("4.2"),
179				})),
180				to: TypeExpression {
181					fragment: Fragment::testing_empty(),
182					ty: ValueType::Float8,
183					constraint: None,
184				},
185			}),
186		)
187		.unwrap();
188
189		assert_eq!(result.1.as_ref(), float8("", [4.2]).1.as_ref());
190	}
191
192	#[test]
193	fn test_cast_float_4() {
194		let ctx = EvalContext::testing();
195		let result = evaluate(
196			&ctx,
197			&Cast(CastExpression {
198				fragment: Fragment::testing_empty(),
199				expression: Box::new(Constant(Number {
200					fragment: Fragment::internal("4.2"),
201				})),
202				to: TypeExpression {
203					fragment: Fragment::testing_empty(),
204					ty: ValueType::Float4,
205					constraint: None,
206				},
207			}),
208		)
209		.unwrap();
210
211		assert_eq!(result.1.as_ref(), float4("", [4.2]).1.as_ref());
212	}
213
214	#[test]
215	fn test_cast_negative_float_4() {
216		let ctx = EvalContext::testing();
217		let result = evaluate(
218			&ctx,
219			&Cast(CastExpression {
220				fragment: Fragment::testing_empty(),
221				expression: Box::new(Constant(Number {
222					fragment: Fragment::internal("-1.1"),
223				})),
224				to: TypeExpression {
225					fragment: Fragment::testing_empty(),
226					ty: ValueType::Float4,
227					constraint: None,
228				},
229			}),
230		)
231		.unwrap();
232
233		assert_eq!(result.1.as_ref(), float4("", [-1.1]).1.as_ref());
234	}
235
236	#[test]
237	fn test_cast_negative_float_8() {
238		let ctx = EvalContext::testing();
239		let result = evaluate(
240			&ctx,
241			&Cast(CastExpression {
242				fragment: Fragment::testing_empty(),
243				expression: Box::new(Constant(Number {
244					fragment: Fragment::internal("-1.1"),
245				})),
246				to: TypeExpression {
247					fragment: Fragment::testing_empty(),
248					ty: ValueType::Float8,
249					constraint: None,
250				},
251			}),
252		)
253		.unwrap();
254
255		assert_eq!(result.1.as_ref(), float8("", [-1.1]).1.as_ref());
256	}
257
258	#[test]
259	fn test_cast_string_to_bool() {
260		let ctx = EvalContext::testing();
261		let result = evaluate(
262			&ctx,
263			&Cast(CastExpression {
264				fragment: Fragment::testing_empty(),
265				expression: Box::new(Constant(ConstantExpression::Text {
266					fragment: Fragment::internal("0"),
267				})),
268				to: TypeExpression {
269					fragment: Fragment::testing_empty(),
270					ty: ValueType::Boolean,
271					constraint: None,
272				},
273			}),
274		)
275		.unwrap();
276
277		assert_eq!(result.1.as_ref(), bool("", [false]).1.as_ref());
278	}
279
280	#[test]
281	fn test_cast_string_neg_one_to_bool_should_fail() {
282		let ctx = EvalContext::testing();
283		let result = evaluate(
284			&ctx,
285			&Cast(CastExpression {
286				fragment: Fragment::testing_empty(),
287				expression: Box::new(Constant(ConstantExpression::Text {
288					fragment: Fragment::internal("-1"),
289				})),
290				to: TypeExpression {
291					fragment: Fragment::testing_empty(),
292					ty: ValueType::Boolean,
293					constraint: None,
294				},
295			}),
296		);
297
298		assert!(result.is_err());
299
300		// The cast code must carry the underlying reason as its cause, not flatten to one code.
301		let err = result.unwrap_err();
302		let diagnostic = err.0;
303		assert_eq!(diagnostic.code, "CAST_004");
304		assert!(diagnostic.cause.is_some());
305		let cause = diagnostic.cause.unwrap();
306		assert_eq!(cause.code, "BOOLEAN_003"); // invalid_number_boolean
307	}
308
309	#[test]
310	fn test_cast_boolean_to_date_should_fail() {
311		let ctx = EvalContext::testing();
312		let result = evaluate(
313			&ctx,
314			&Cast(CastExpression {
315				fragment: Fragment::testing_empty(),
316				expression: Box::new(Constant(ConstantExpression::Bool {
317					fragment: Fragment::internal("true"),
318				})),
319				to: TypeExpression {
320					fragment: Fragment::testing_empty(),
321					ty: ValueType::Date,
322					constraint: None,
323				},
324			}),
325		);
326
327		assert!(result.is_err());
328
329		// An impossible cast pair is unsupported (CAST_001), not a value-level failure.
330		let err = result.unwrap_err();
331		let diagnostic = err.0;
332		assert_eq!(diagnostic.code, "CAST_001");
333	}
334
335	#[test]
336	fn test_cast_text_to_decimal() {
337		let ctx = EvalContext::testing();
338		let result = evaluate(
339			&ctx,
340			&Cast(CastExpression {
341				fragment: Fragment::testing_empty(),
342				expression: Box::new(Constant(ConstantExpression::Text {
343					fragment: Fragment::internal("123.456789"),
344				})),
345				to: TypeExpression {
346					fragment: Fragment::testing_empty(),
347					ty: ValueType::DECIMAL,
348					constraint: None,
349				},
350			}),
351		)
352		.unwrap();
353
354		if let ViewData::Decimal(container) = &ColumnView::try_from(&result).unwrap().data {
355			assert_eq!(container.len(), 1);
356			assert!(decimal_at(container, 0).is_some());
357			let value = &decimals(container)[0];
358			assert_eq!(value.to_string(), "123.4567890000");
359		} else {
360			panic!("Expected Decimal column data");
361		}
362	}
363}