Skip to main content

reifydb_engine/expression/cast/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
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		(_, Type::IdentityId) => to_uuid(data, target, lazy_fragment),
120		(Type::IdentityId, _) => to_uuid(data, target, lazy_fragment),
121		(_, t) if t.is_uuid() => to_uuid(data, target, lazy_fragment),
122		(source, t) if source.is_uuid() || t.is_uuid() => to_uuid(data, target, lazy_fragment),
123		_ => Err(TypeError::UnsupportedCast {
124			from: source_type,
125			to: target,
126			fragment: lazy_fragment.fragment(),
127		}
128		.into()),
129	}
130}
131
132#[cfg(test)]
133pub mod tests {
134	use reifydb_core::value::column::data::ColumnData;
135	use reifydb_rql::expression::{
136		CastExpression, ConstantExpression,
137		ConstantExpression::Number,
138		Expression::{Cast, Constant, Prefix},
139		PrefixExpression, PrefixOperator, TypeExpression,
140	};
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		)
161		.unwrap();
162
163		assert_eq!(*result.data(), ColumnData::int4([42]));
164	}
165
166	#[test]
167	fn test_cast_negative_integer() {
168		let mut ctx = EvalContext::testing();
169		let result = evaluate(
170			&mut ctx,
171			&Cast(CastExpression {
172				fragment: Fragment::testing_empty(),
173				expression: Box::new(Prefix(PrefixExpression {
174					operator: PrefixOperator::Minus(Fragment::testing_empty()),
175					expression: Box::new(Constant(Number {
176						fragment: Fragment::internal("42"),
177					})),
178					fragment: Fragment::testing_empty(),
179				})),
180				to: TypeExpression {
181					fragment: Fragment::testing_empty(),
182					ty: Type::Int4,
183				},
184			}),
185		)
186		.unwrap();
187
188		assert_eq!(*result.data(), ColumnData::int4([-42]));
189	}
190
191	#[test]
192	fn test_cast_negative_min() {
193		let mut ctx = EvalContext::testing();
194		let result = evaluate(
195			&mut ctx,
196			&Cast(CastExpression {
197				fragment: Fragment::testing_empty(),
198				expression: Box::new(Prefix(PrefixExpression {
199					operator: PrefixOperator::Minus(Fragment::testing_empty()),
200					expression: Box::new(Constant(Number {
201						fragment: Fragment::internal("128"),
202					})),
203					fragment: Fragment::testing_empty(),
204				})),
205				to: TypeExpression {
206					fragment: Fragment::testing_empty(),
207					ty: Type::Int1,
208				},
209			}),
210		)
211		.unwrap();
212
213		assert_eq!(*result.data(), ColumnData::int1([-128]));
214	}
215
216	#[test]
217	fn test_cast_float_8() {
218		let mut ctx = EvalContext::testing();
219		let result = evaluate(
220			&mut ctx,
221			&Cast(CastExpression {
222				fragment: Fragment::testing_empty(),
223				expression: Box::new(Constant(Number {
224					fragment: Fragment::internal("4.2"),
225				})),
226				to: TypeExpression {
227					fragment: Fragment::testing_empty(),
228					ty: Type::Float8,
229				},
230			}),
231		)
232		.unwrap();
233
234		assert_eq!(*result.data(), ColumnData::float8([4.2]));
235	}
236
237	#[test]
238	fn test_cast_float_4() {
239		let mut ctx = EvalContext::testing();
240		let result = evaluate(
241			&mut ctx,
242			&Cast(CastExpression {
243				fragment: Fragment::testing_empty(),
244				expression: Box::new(Constant(Number {
245					fragment: Fragment::internal("4.2"),
246				})),
247				to: TypeExpression {
248					fragment: Fragment::testing_empty(),
249					ty: Type::Float4,
250				},
251			}),
252		)
253		.unwrap();
254
255		assert_eq!(*result.data(), ColumnData::float4([4.2]));
256	}
257
258	#[test]
259	fn test_cast_negative_float_4() {
260		let mut ctx = EvalContext::testing();
261		let result = evaluate(
262			&mut ctx,
263			&Cast(CastExpression {
264				fragment: Fragment::testing_empty(),
265				expression: Box::new(Constant(Number {
266					fragment: Fragment::internal("-1.1"),
267				})),
268				to: TypeExpression {
269					fragment: Fragment::testing_empty(),
270					ty: Type::Float4,
271				},
272			}),
273		)
274		.unwrap();
275
276		assert_eq!(*result.data(), ColumnData::float4([-1.1]));
277	}
278
279	#[test]
280	fn test_cast_negative_float_8() {
281		let mut ctx = EvalContext::testing();
282		let result = evaluate(
283			&mut ctx,
284			&Cast(CastExpression {
285				fragment: Fragment::testing_empty(),
286				expression: Box::new(Constant(Number {
287					fragment: Fragment::internal("-1.1"),
288				})),
289				to: TypeExpression {
290					fragment: Fragment::testing_empty(),
291					ty: Type::Float8,
292				},
293			}),
294		)
295		.unwrap();
296
297		assert_eq!(*result.data(), ColumnData::float8([-1.1]));
298	}
299
300	#[test]
301	fn test_cast_string_to_bool() {
302		let mut ctx = EvalContext::testing();
303		let result = evaluate(
304			&mut ctx,
305			&Cast(CastExpression {
306				fragment: Fragment::testing_empty(),
307				expression: Box::new(Constant(ConstantExpression::Text {
308					fragment: Fragment::internal("0"),
309				})),
310				to: TypeExpression {
311					fragment: Fragment::testing_empty(),
312					ty: Type::Boolean,
313				},
314			}),
315		)
316		.unwrap();
317
318		assert_eq!(*result.data(), ColumnData::bool([false]));
319	}
320
321	#[test]
322	fn test_cast_string_neg_one_to_bool_should_fail() {
323		let mut ctx = EvalContext::testing();
324		let result = evaluate(
325			&mut ctx,
326			&Cast(CastExpression {
327				fragment: Fragment::testing_empty(),
328				expression: Box::new(Constant(ConstantExpression::Text {
329					fragment: Fragment::internal("-1"),
330				})),
331				to: TypeExpression {
332					fragment: Fragment::testing_empty(),
333					ty: Type::Boolean,
334				},
335			}),
336		);
337
338		assert!(result.is_err());
339
340		// Check that the error is the expected CAST_004
341		// (invalid_boolean) error
342		let err = result.unwrap_err();
343		let diagnostic = err.0;
344		assert_eq!(diagnostic.code, "CAST_004");
345		assert!(diagnostic.cause.is_some());
346		let cause = diagnostic.cause.unwrap();
347		assert_eq!(cause.code, "BOOLEAN_003"); // invalid_number_boolean
348	}
349
350	#[test]
351	fn test_cast_boolean_to_date_should_fail() {
352		let mut ctx = EvalContext::testing();
353		let result = evaluate(
354			&mut ctx,
355			&Cast(CastExpression {
356				fragment: Fragment::testing_empty(),
357				expression: Box::new(Constant(ConstantExpression::Bool {
358					fragment: Fragment::internal("true"),
359				})),
360				to: TypeExpression {
361					fragment: Fragment::testing_empty(),
362					ty: Type::Date,
363				},
364			}),
365		);
366
367		assert!(result.is_err());
368
369		// Check that the error is the expected CAST_001
370		// (unsupported_cast) error
371		let err = result.unwrap_err();
372		let diagnostic = err.0;
373		assert_eq!(diagnostic.code, "CAST_001");
374	}
375
376	#[test]
377	fn test_cast_text_to_decimal() {
378		let mut ctx = EvalContext::testing();
379		let result = evaluate(
380			&mut ctx,
381			&Cast(CastExpression {
382				fragment: Fragment::testing_empty(),
383				expression: Box::new(Constant(ConstantExpression::Text {
384					fragment: Fragment::internal("123.456789"),
385				})),
386				to: TypeExpression {
387					fragment: Fragment::testing_empty(),
388					ty: Type::Decimal,
389				},
390			}),
391		)
392		.unwrap();
393
394		if let ColumnData::Decimal {
395			container,
396			..
397		} = result.data()
398		{
399			assert_eq!(container.len(), 1);
400			assert!(container.is_defined(0));
401			let value = &container[0];
402			assert_eq!(value.to_string(), "123.456789");
403		} else {
404			panic!("Expected Decimal column data");
405		}
406	}
407}