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