Skip to main content

reifydb_engine/expression/cast/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4pub mod any;
5pub mod blob;
6pub mod boolean;
7pub mod number;
8pub mod temporal;
9pub mod text;
10pub mod uuid;
11
12use reifydb_core::value::column::data::ColumnData;
13use reifydb_type::{
14	err, error::diagnostic::cast, fragment::LazyFragment, storage::DataBitVec, util::bitvec::BitVec,
15	value::r#type::Type,
16};
17
18use crate::expression::context::EvalContext;
19
20pub fn cast_column_data(
21	ctx: &EvalContext,
22	data: &ColumnData,
23	target: Type,
24	lazy_fragment: impl LazyFragment + Clone,
25) -> crate::Result<ColumnData> {
26	// Handle Option-wrapped data: cast the inner data, then re-wrap with the bitvec
27	if let ColumnData::Option {
28		inner,
29		bitvec,
30	} = data
31	{
32		let inner_target = match &target {
33			Type::Option(t) => t.as_ref().clone(),
34			other => other.clone(),
35		};
36		let total_len = inner.len();
37		let defined_count = DataBitVec::count_ones(bitvec);
38
39		if defined_count == 0 {
40			return Ok(ColumnData::none_typed(inner_target, total_len));
41		}
42
43		if defined_count < total_len {
44			// Compact: keep only defined positions (avoids parsing placeholders like "" for text)
45			let mut compacted = inner.as_ref().clone();
46			compacted.filter(bitvec)?;
47
48			// Cast only real values
49			let mut cast_compacted = cast_column_data(ctx, &compacted, inner_target, lazy_fragment)?;
50
51			// Expand back to full length: defined positions → compacted index, None positions → sentinel
52			// (gets type default)
53			let sentinel = defined_count;
54			let mut expand_indices = Vec::with_capacity(total_len);
55			let mut src_idx = 0usize;
56			for i in 0..total_len {
57				if DataBitVec::get(bitvec, i) {
58					expand_indices.push(src_idx);
59					src_idx += 1;
60				} else {
61					expand_indices.push(sentinel);
62				}
63			}
64			cast_compacted.reorder(&expand_indices);
65
66			return Ok(match cast_compacted {
67				already @ ColumnData::Option {
68					..
69				} => already,
70				other => ColumnData::Option {
71					inner: Box::new(other),
72					bitvec: bitvec.clone(),
73				},
74			});
75		}
76
77		// All positions defined — cast directly (fast path)
78		let cast_inner = cast_column_data(ctx, inner, inner_target, lazy_fragment)?;
79		return Ok(match cast_inner {
80			already @ ColumnData::Option {
81				..
82			} => already,
83			other => ColumnData::Option {
84				inner: Box::new(other),
85				bitvec: bitvec.clone(),
86			},
87		});
88	}
89	// Handle bare data -> Option(T) target: cast to inner type, wrap with all-defined bitvec
90	if let Type::Option(inner_target) = &target {
91		let cast_inner = cast_column_data(ctx, data, *inner_target.clone(), lazy_fragment)?;
92		return Ok(match cast_inner {
93			already @ ColumnData::Option {
94				..
95			} => already,
96			other => {
97				let bitvec = BitVec::repeat(other.len(), true);
98				ColumnData::Option {
99					inner: Box::new(other),
100					bitvec,
101				}
102			}
103		});
104	}
105
106	let source_type = data.get_type();
107	if target == source_type {
108		return Ok(data.clone());
109	}
110	match (&source_type, &target) {
111		(Type::Any, _) => any::from_any(ctx, data, target, lazy_fragment),
112		(_, t) if t.is_number() => number::to_number(ctx, data, target, lazy_fragment),
113		(_, t) if t.is_blob() => blob::to_blob(data, lazy_fragment),
114		(_, t) if t.is_bool() => boolean::to_boolean(data, lazy_fragment),
115		(_, t) if t.is_utf8() => text::to_text(data, lazy_fragment),
116		(_, t) if t.is_temporal() => temporal::to_temporal(data, target, lazy_fragment),
117		(_, t) if t.is_uuid() => uuid::to_uuid(data, target, lazy_fragment),
118		(source, t) if source.is_uuid() || t.is_uuid() => uuid::to_uuid(data, target, lazy_fragment),
119		_ => {
120			err!(cast::unsupported_cast(lazy_fragment.fragment(), source_type, target))
121		}
122	}
123}
124
125#[cfg(test)]
126pub mod tests {
127	use reifydb_core::value::column::data::ColumnData;
128	use reifydb_function::registry::Functions;
129	use reifydb_rql::expression::{
130		CastExpression, ConstantExpression,
131		ConstantExpression::Number,
132		Expression::{Cast, Constant, Prefix},
133		PrefixExpression, PrefixOperator, TypeExpression,
134	};
135	use reifydb_runtime::clock::Clock;
136	use reifydb_type::{fragment::Fragment, value::r#type::Type};
137
138	use crate::expression::{context::EvalContext, eval::evaluate};
139
140	#[test]
141	fn test_cast_integer() {
142		let mut ctx = EvalContext::testing();
143		let result = evaluate(
144			&mut ctx,
145			&Cast(CastExpression {
146				fragment: Fragment::testing_empty(),
147				expression: Box::new(Constant(Number {
148					fragment: Fragment::internal("42"),
149				})),
150				to: TypeExpression {
151					fragment: Fragment::testing_empty(),
152					ty: Type::Int4,
153				},
154			}),
155			&Functions::empty(),
156			&Clock::default(),
157		)
158		.unwrap();
159
160		assert_eq!(*result.data(), ColumnData::int4([42]));
161	}
162
163	#[test]
164	fn test_cast_negative_integer() {
165		let mut ctx = EvalContext::testing();
166		let result = evaluate(
167			&mut ctx,
168			&Cast(CastExpression {
169				fragment: Fragment::testing_empty(),
170				expression: Box::new(Prefix(PrefixExpression {
171					operator: PrefixOperator::Minus(Fragment::testing_empty()),
172					expression: Box::new(Constant(Number {
173						fragment: Fragment::internal("42"),
174					})),
175					fragment: Fragment::testing_empty(),
176				})),
177				to: TypeExpression {
178					fragment: Fragment::testing_empty(),
179					ty: Type::Int4,
180				},
181			}),
182			&Functions::empty(),
183			&Clock::default(),
184		)
185		.unwrap();
186
187		assert_eq!(*result.data(), ColumnData::int4([-42]));
188	}
189
190	#[test]
191	fn test_cast_negative_min() {
192		let mut ctx = EvalContext::testing();
193		let result = evaluate(
194			&mut ctx,
195			&Cast(CastExpression {
196				fragment: Fragment::testing_empty(),
197				expression: Box::new(Prefix(PrefixExpression {
198					operator: PrefixOperator::Minus(Fragment::testing_empty()),
199					expression: Box::new(Constant(Number {
200						fragment: Fragment::internal("128"),
201					})),
202					fragment: Fragment::testing_empty(),
203				})),
204				to: TypeExpression {
205					fragment: Fragment::testing_empty(),
206					ty: Type::Int1,
207				},
208			}),
209			&Functions::empty(),
210			&Clock::default(),
211		)
212		.unwrap();
213
214		assert_eq!(*result.data(), ColumnData::int1([-128]));
215	}
216
217	#[test]
218	fn test_cast_float_8() {
219		let mut ctx = EvalContext::testing();
220		let result = evaluate(
221			&mut ctx,
222			&Cast(CastExpression {
223				fragment: Fragment::testing_empty(),
224				expression: Box::new(Constant(Number {
225					fragment: Fragment::internal("4.2"),
226				})),
227				to: TypeExpression {
228					fragment: Fragment::testing_empty(),
229					ty: Type::Float8,
230				},
231			}),
232			&Functions::empty(),
233			&Clock::default(),
234		)
235		.unwrap();
236
237		assert_eq!(*result.data(), ColumnData::float8([4.2]));
238	}
239
240	#[test]
241	fn test_cast_float_4() {
242		let mut ctx = EvalContext::testing();
243		let result = evaluate(
244			&mut ctx,
245			&Cast(CastExpression {
246				fragment: Fragment::testing_empty(),
247				expression: Box::new(Constant(Number {
248					fragment: Fragment::internal("4.2"),
249				})),
250				to: TypeExpression {
251					fragment: Fragment::testing_empty(),
252					ty: Type::Float4,
253				},
254			}),
255			&Functions::empty(),
256			&Clock::default(),
257		)
258		.unwrap();
259
260		assert_eq!(*result.data(), ColumnData::float4([4.2]));
261	}
262
263	#[test]
264	fn test_cast_negative_float_4() {
265		let mut ctx = EvalContext::testing();
266		let result = evaluate(
267			&mut ctx,
268			&Cast(CastExpression {
269				fragment: Fragment::testing_empty(),
270				expression: Box::new(Constant(Number {
271					fragment: Fragment::internal("-1.1"),
272				})),
273				to: TypeExpression {
274					fragment: Fragment::testing_empty(),
275					ty: Type::Float4,
276				},
277			}),
278			&Functions::empty(),
279			&Clock::default(),
280		)
281		.unwrap();
282
283		assert_eq!(*result.data(), ColumnData::float4([-1.1]));
284	}
285
286	#[test]
287	fn test_cast_negative_float_8() {
288		let mut ctx = EvalContext::testing();
289		let result = evaluate(
290			&mut ctx,
291			&Cast(CastExpression {
292				fragment: Fragment::testing_empty(),
293				expression: Box::new(Constant(Number {
294					fragment: Fragment::internal("-1.1"),
295				})),
296				to: TypeExpression {
297					fragment: Fragment::testing_empty(),
298					ty: Type::Float8,
299				},
300			}),
301			&Functions::empty(),
302			&Clock::default(),
303		)
304		.unwrap();
305
306		assert_eq!(*result.data(), ColumnData::float8([-1.1]));
307	}
308
309	#[test]
310	fn test_cast_string_to_bool() {
311		let mut ctx = EvalContext::testing();
312		let result = evaluate(
313			&mut ctx,
314			&Cast(CastExpression {
315				fragment: Fragment::testing_empty(),
316				expression: Box::new(Constant(ConstantExpression::Text {
317					fragment: Fragment::internal("0"),
318				})),
319				to: TypeExpression {
320					fragment: Fragment::testing_empty(),
321					ty: Type::Boolean,
322				},
323			}),
324			&Functions::empty(),
325			&Clock::default(),
326		)
327		.unwrap();
328
329		assert_eq!(*result.data(), ColumnData::bool([false]));
330	}
331
332	#[test]
333	fn test_cast_string_neg_one_to_bool_should_fail() {
334		let mut ctx = EvalContext::testing();
335		let result = evaluate(
336			&mut ctx,
337			&Cast(CastExpression {
338				fragment: Fragment::testing_empty(),
339				expression: Box::new(Constant(ConstantExpression::Text {
340					fragment: Fragment::internal("-1"),
341				})),
342				to: TypeExpression {
343					fragment: Fragment::testing_empty(),
344					ty: Type::Boolean,
345				},
346			}),
347			&Functions::empty(),
348			&Clock::default(),
349		);
350
351		assert!(result.is_err());
352
353		// Check that the error is the expected CAST_004
354		// (invalid_boolean) error
355		let err = result.unwrap_err();
356		let diagnostic = err.0;
357		assert_eq!(diagnostic.code, "CAST_004");
358		assert!(diagnostic.cause.is_some());
359		let cause = diagnostic.cause.unwrap();
360		assert_eq!(cause.code, "BOOLEAN_003"); // invalid_number_boolean
361	}
362
363	#[test]
364	fn test_cast_boolean_to_date_should_fail() {
365		let mut ctx = EvalContext::testing();
366		let result = evaluate(
367			&mut ctx,
368			&Cast(CastExpression {
369				fragment: Fragment::testing_empty(),
370				expression: Box::new(Constant(ConstantExpression::Bool {
371					fragment: Fragment::internal("true"),
372				})),
373				to: TypeExpression {
374					fragment: Fragment::testing_empty(),
375					ty: Type::Date,
376				},
377			}),
378			&Functions::empty(),
379			&Clock::default(),
380		);
381
382		assert!(result.is_err());
383
384		// Check that the error is the expected CAST_001
385		// (unsupported_cast) error
386		let err = result.unwrap_err();
387		let diagnostic = err.0;
388		assert_eq!(diagnostic.code, "CAST_001");
389	}
390
391	#[test]
392	fn test_cast_text_to_decimal() {
393		let mut ctx = EvalContext::testing();
394		let result = evaluate(
395			&mut ctx,
396			&Cast(CastExpression {
397				fragment: Fragment::testing_empty(),
398				expression: Box::new(Constant(ConstantExpression::Text {
399					fragment: Fragment::internal("123.456789"),
400				})),
401				to: TypeExpression {
402					fragment: Fragment::testing_empty(),
403					ty: Type::Decimal,
404				},
405			}),
406			&Functions::empty(),
407			&Clock::default(),
408		)
409		.unwrap();
410
411		if let ColumnData::Decimal {
412			container,
413			..
414		} = result.data()
415		{
416			assert_eq!(container.len(), 1);
417			assert!(container.is_defined(0));
418			let value = &container[0];
419			assert_eq!(value.to_string(), "123.456789");
420		} else {
421			panic!("Expected Decimal column data");
422		}
423	}
424}