Skip to main content

reifydb_evaluate/expression/
compare.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{fmt::Display, result::Result as StdResult, sync::Arc};
5
6use arrow_array::{
7	Array, ArrayRef, ArrowPrimitiveType, BooleanArray, Datum, FixedSizeBinaryArray, PrimitiveArray, Scalar,
8	types::{
9		Decimal128Type, Decimal256Type, Float64Type, Int16Type, Int32Type, Int64Type, UInt16Type, UInt32Type,
10		UInt64Type,
11	},
12};
13use arrow_buffer::i256;
14use arrow_ord::cmp;
15use arrow_schema::{Field, FieldRef};
16use reifydb_core::{
17	error::CoreError,
18	value::column::{
19		arith::{family_digits, is_family, length_mismatch, option::is_all_none},
20		factory::none_typed,
21		nulls::split_nulls,
22	},
23};
24use reifydb_value::{
25	error::Diagnostic,
26	fragment::Fragment,
27	return_error,
28	value::{
29		column_view::{ColumnView, ViewData},
30		constraint::{precision::Precision, scale::Scale},
31		container::{
32			decimal_array::{self, DECIMAL128_MAX_PRECISION, DecimalView},
33			fixed_array,
34			wide_int_array::{WideInt, wide_array, wides},
35		},
36		decimal::unscaled,
37		ordered_f64::OrderedF64,
38		value_type::{ValueType, field::from_field},
39	},
40};
41
42use super::logic::bool_column;
43use crate::Result;
44
45pub trait CompareOp {
46	const EQUALITY: bool = false;
47
48	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray>;
49}
50
51pub struct Equal;
52pub struct NotEqual;
53pub struct GreaterThan;
54pub struct GreaterThanEqual;
55pub struct LessThan;
56pub struct LessThanEqual;
57
58impl CompareOp for Equal {
59	const EQUALITY: bool = true;
60
61	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
62		arrow_result(cmp::eq(left, right))
63	}
64}
65
66impl CompareOp for NotEqual {
67	const EQUALITY: bool = true;
68
69	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
70		arrow_result(cmp::neq(left, right))
71	}
72}
73
74impl CompareOp for GreaterThan {
75	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
76		arrow_result(cmp::gt(left, right))
77	}
78}
79
80impl CompareOp for GreaterThanEqual {
81	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
82		arrow_result(cmp::gt_eq(left, right))
83	}
84}
85
86impl CompareOp for LessThan {
87	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
88		arrow_result(cmp::lt(left, right))
89	}
90}
91
92impl CompareOp for LessThanEqual {
93	fn kernel(left: &dyn Datum, right: &dyn Datum) -> Result<BooleanArray> {
94		arrow_result(cmp::lt_eq(left, right))
95	}
96}
97
98fn arrow_result<E: Display>(result: StdResult<BooleanArray, E>) -> Result<BooleanArray> {
99	Ok(result.map_err(|err| CoreError::FrameError {
100		message: err.to_string(),
101	})?)
102}
103
104fn signed_rank(ty: &ValueType) -> Option<u8> {
105	match ty {
106		ValueType::Int1 => Some(1),
107		ValueType::Int2 => Some(2),
108		ValueType::Int4 => Some(3),
109		ValueType::Int8 => Some(4),
110		ValueType::Int16 => Some(5),
111		_ => None,
112	}
113}
114
115fn unsigned_rank(ty: &ValueType) -> Option<u8> {
116	match ty {
117		ValueType::Uint1 => Some(1),
118		ValueType::Uint2 => Some(2),
119		ValueType::Uint4 => Some(3),
120		ValueType::Uint8 => Some(4),
121		ValueType::Uint16 => Some(5),
122		_ => None,
123	}
124}
125
126fn signed_of_rank(rank: u8) -> ValueType {
127	match rank {
128		1 => ValueType::Int1,
129		2 => ValueType::Int2,
130		3 => ValueType::Int4,
131		4 => ValueType::Int8,
132		5 => ValueType::Int16,
133		_ => ValueType::decimal(Precision::new(39), Scale::new(0)),
134	}
135}
136
137fn unsigned_of_rank(rank: u8) -> ValueType {
138	match rank {
139		1 => ValueType::Uint1,
140		2 => ValueType::Uint2,
141		3 => ValueType::Uint4,
142		4 => ValueType::Uint8,
143		_ => ValueType::Uint16,
144	}
145}
146
147pub(crate) fn compare_target(left: &ValueType, right: &ValueType) -> Option<ValueType> {
148	if is_family(left) || is_family(right) {
149		if left.is_floating_point() || right.is_floating_point() {
150			return (left.is_number() && right.is_number()).then_some(ValueType::Float8);
151		}
152		let ((left_digits, left_scale), (right_digits, right_scale)) =
153			(family_digits(left)?, family_digits(right)?);
154		let scale = left_scale.max(right_scale);
155		let precision = (left_digits.max(right_digits) + scale).min(unscaled::MAX_DIGITS);
156		return Some(ValueType::decimal(Precision::new(precision), Scale::new(scale)));
157	}
158	if left == right {
159		return matches!(
160			left,
161			ValueType::Boolean
162				| ValueType::Float4 | ValueType::Float8
163				| ValueType::Int1 | ValueType::Int2
164				| ValueType::Int4 | ValueType::Int8
165				| ValueType::Int16 | ValueType::Uint1
166				| ValueType::Uint2 | ValueType::Uint4
167				| ValueType::Uint8 | ValueType::Uint16
168				| ValueType::Utf8 | ValueType::Blob
169				| ValueType::Date | ValueType::DateTime
170				| ValueType::Time | ValueType::Duration
171				| ValueType::Uuid4 | ValueType::Uuid7
172				| ValueType::IdentityId | ValueType::DictionaryId
173		)
174		.then(|| left.clone());
175	}
176	if left.is_floating_point() || right.is_floating_point() {
177		return (left.is_number() && right.is_number()).then_some(ValueType::Float8);
178	}
179	match (signed_rank(left), unsigned_rank(left), signed_rank(right), unsigned_rank(right)) {
180		(Some(l), _, Some(r), _) => Some(signed_of_rank(l.max(r))),
181		(_, Some(l), _, Some(r)) => Some(unsigned_of_rank(l.max(r))),
182		(Some(s), _, _, Some(u)) | (_, Some(u), Some(s), _) => Some(signed_of_rank(s.max(u + 1))),
183		_ => None,
184	}
185}
186
187pub(crate) fn field_compare_target(
188	(left, left_type): (&Field, &ValueType),
189	(right, right_type): (&Field, &ValueType),
190	equality: bool,
191) -> Result<Option<ValueType>> {
192	let target = compare_target(left_type, right_type);
193	if target != Some(ValueType::DictionaryId) {
194		return Ok(target);
195	}
196	let dictionary = from_field(left)?.dictionary_id;
197	Ok((equality && dictionary.is_some() && dictionary == from_field(right)?.dictionary_id)
198		.then_some(ValueType::DictionaryId))
199}
200
201fn wide_unary<W: WideInt, T: ArrowPrimitiveType>(
202	array: &FixedSizeBinaryArray,
203	f: impl Fn(W) -> T::Native,
204) -> PrimitiveArray<T> {
205	PrimitiveArray::new(wides::<W>(array).into_iter().map(f).collect(), array.logical_nulls())
206}
207
208fn to_wide<T: ArrowPrimitiveType, W: WideInt>(
209	array: &PrimitiveArray<T>,
210	f: impl Fn(T::Native) -> W,
211) -> FixedSizeBinaryArray {
212	fixed_array::attach_nulls(wide_array(array.values().iter().map(|&v| f(v))), array.logical_nulls())
213}
214
215macro_rules! widen {
216	($array:expr, $to:ty, |$v:ident| $conv:expr) => {
217		Arc::new($array.unary::<_, $to>(|$v| $conv)) as ArrayRef
218	};
219}
220
221pub(crate) fn cast_to(column: &(FieldRef, ArrayRef), target: &ValueType) -> Result<ArrayRef> {
222	let view = ColumnView::try_from(column)?;
223	if view.get_type().inner_type() == target {
224		return Ok(column.1.clone());
225	}
226	Ok(match (target, &view.data) {
227		(ValueType::Float8, ViewData::Float4(a)) => widen!(a, Float64Type, |v| OrderedF64::canonical(v as f64)),
228		(ValueType::Float8, ViewData::Int1(a)) => widen!(a, Float64Type, |v| v as f64),
229		(ValueType::Float8, ViewData::Int2(a)) => widen!(a, Float64Type, |v| v as f64),
230		(ValueType::Float8, ViewData::Int4(a)) => widen!(a, Float64Type, |v| v as f64),
231		(ValueType::Float8, ViewData::Int8(a)) => widen!(a, Float64Type, |v| v as f64),
232		(ValueType::Float8, ViewData::Int16(a)) => Arc::new(wide_unary::<i128, Float64Type>(a, |v| v as f64)),
233		(ValueType::Float8, ViewData::Uint1(a)) => widen!(a, Float64Type, |v| v as f64),
234		(ValueType::Float8, ViewData::Uint2(a)) => widen!(a, Float64Type, |v| v as f64),
235		(ValueType::Float8, ViewData::Uint4(a)) => widen!(a, Float64Type, |v| v as f64),
236		(ValueType::Float8, ViewData::Uint8(a)) => widen!(a, Float64Type, |v| v as f64),
237		(ValueType::Float8, ViewData::Uint16(a)) => Arc::new(wide_unary::<u128, Float64Type>(a, |v| v as f64)),
238		(ValueType::Int2, ViewData::Int1(a)) => widen!(a, Int16Type, |v| v as i16),
239		(ValueType::Int2, ViewData::Uint1(a)) => widen!(a, Int16Type, |v| v as i16),
240		(ValueType::Int4, ViewData::Int1(a)) => widen!(a, Int32Type, |v| v as i32),
241		(ValueType::Int4, ViewData::Int2(a)) => widen!(a, Int32Type, |v| v as i32),
242		(ValueType::Int4, ViewData::Uint1(a)) => widen!(a, Int32Type, |v| v as i32),
243		(ValueType::Int4, ViewData::Uint2(a)) => widen!(a, Int32Type, |v| v as i32),
244		(ValueType::Int8, ViewData::Int1(a)) => widen!(a, Int64Type, |v| v as i64),
245		(ValueType::Int8, ViewData::Int2(a)) => widen!(a, Int64Type, |v| v as i64),
246		(ValueType::Int8, ViewData::Int4(a)) => widen!(a, Int64Type, |v| v as i64),
247		(ValueType::Int8, ViewData::Uint1(a)) => widen!(a, Int64Type, |v| v as i64),
248		(ValueType::Int8, ViewData::Uint2(a)) => widen!(a, Int64Type, |v| v as i64),
249		(ValueType::Int8, ViewData::Uint4(a)) => widen!(a, Int64Type, |v| v as i64),
250		(ValueType::Uint2, ViewData::Uint1(a)) => widen!(a, UInt16Type, |v| v as u16),
251		(ValueType::Uint4, ViewData::Uint1(a)) => widen!(a, UInt32Type, |v| v as u32),
252		(ValueType::Uint4, ViewData::Uint2(a)) => widen!(a, UInt32Type, |v| v as u32),
253		(ValueType::Uint8, ViewData::Uint1(a)) => widen!(a, UInt64Type, |v| v as u64),
254		(ValueType::Uint8, ViewData::Uint2(a)) => widen!(a, UInt64Type, |v| v as u64),
255		(ValueType::Uint8, ViewData::Uint4(a)) => widen!(a, UInt64Type, |v| v as u64),
256		(ValueType::Int16, ViewData::Int1(a)) => Arc::new(to_wide(a, |v| v as i128)),
257		(ValueType::Int16, ViewData::Int2(a)) => Arc::new(to_wide(a, |v| v as i128)),
258		(ValueType::Int16, ViewData::Int4(a)) => Arc::new(to_wide(a, |v| v as i128)),
259		(ValueType::Int16, ViewData::Int8(a)) => Arc::new(to_wide(a, |v| v as i128)),
260		(ValueType::Int16, ViewData::Uint1(a)) => Arc::new(to_wide(a, |v| v as i128)),
261		(ValueType::Int16, ViewData::Uint2(a)) => Arc::new(to_wide(a, |v| v as i128)),
262		(ValueType::Int16, ViewData::Uint4(a)) => Arc::new(to_wide(a, |v| v as i128)),
263		(ValueType::Int16, ViewData::Uint8(a)) => Arc::new(to_wide(a, |v| v as i128)),
264		(ValueType::Uint16, ViewData::Uint1(a)) => Arc::new(to_wide(a, |v| v as u128)),
265		(ValueType::Uint16, ViewData::Uint2(a)) => Arc::new(to_wide(a, |v| v as u128)),
266		(ValueType::Uint16, ViewData::Uint4(a)) => Arc::new(to_wide(a, |v| v as u128)),
267		(ValueType::Uint16, ViewData::Uint8(a)) => Arc::new(to_wide(a, |v| v as u128)),
268		(ValueType::Float8, ViewData::Decimal(a)) => family_to_float(a),
269		(
270			ValueType::Decimal {
271				precision,
272				scale,
273			},
274			_,
275		) => family_array(&column.1, &view, *precision, *scale),
276		_ => unreachable!(),
277	})
278}
279
280fn family_to_float(array: &DecimalView) -> ArrayRef {
281	let divisor = 10f64.powi(i32::from(array.scale().value()));
282	match array {
283		DecimalView::Decimal128(a) => widen!(a, Float64Type, |v| v as f64 / divisor),
284		DecimalView::Decimal256(a) => widen!(a, Float64Type, |v| i256_to_f64(v) / divisor),
285	}
286}
287
288fn i256_to_f64(value: i256) -> f64 {
289	match value.to_i128() {
290		Some(narrow) => narrow as f64,
291		None => {
292			let (low, high) = value.to_parts();
293			high as f64 * 2f64.powi(128) + low as f64
294		}
295	}
296}
297
298const BEYOND_FAMILY: i256 = unscaled::MAX.wrapping_add(i256::ONE);
299
300fn upscale_or_beyond(value: i256, by: u8) -> i256 {
301	unscaled::upscale(value, by).unwrap_or(if value.is_negative() {
302		BEYOND_FAMILY.wrapping_neg()
303	} else {
304		BEYOND_FAMILY
305	})
306}
307
308fn family_scale(view: &ColumnView) -> u8 {
309	match &view.data {
310		ViewData::Decimal(a) => a.scale().value(),
311		_ => 0,
312	}
313}
314
315macro_rules! rescale128 {
316	($array:expr, $factor:expr) => {
317		$array.unary::<_, Decimal128Type>(|v| i128::from(v).wrapping_mul($factor))
318	};
319}
320
321macro_rules! rescale256 {
322	($array:expr, $by:expr, |$v:ident| $wide:expr) => {
323		$array.unary::<_, Decimal256Type>(|$v| upscale_or_beyond($wide, $by))
324	};
325}
326
327fn family_array(column: &ArrayRef, view: &ColumnView, precision: Precision, scale: Scale) -> ArrayRef {
328	let data_type = decimal_array::data_type(precision, scale);
329	if let ViewData::Decimal(a) = &view.data
330		&& a.data_type() == &data_type
331	{
332		return column.clone();
333	}
334	let by = scale.value() - family_scale(view);
335	if precision.value() <= DECIMAL128_MAX_PRECISION {
336		let factor = 10i128.pow(u32::from(by));
337		let array = match &view.data {
338			ViewData::Int1(a) => rescale128!(a, factor),
339			ViewData::Int2(a) => rescale128!(a, factor),
340			ViewData::Int4(a) => rescale128!(a, factor),
341			ViewData::Int8(a) => rescale128!(a, factor),
342			ViewData::Uint1(a) => rescale128!(a, factor),
343			ViewData::Uint2(a) => rescale128!(a, factor),
344			ViewData::Uint4(a) => rescale128!(a, factor),
345			ViewData::Uint8(a) => rescale128!(a, factor),
346			ViewData::Decimal(DecimalView::Decimal128(a)) => rescale128!(a, factor),
347			_ => unreachable!(),
348		};
349		Arc::new(array.with_data_type(data_type))
350	} else {
351		let array = match &view.data {
352			ViewData::Int1(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
353			ViewData::Int2(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
354			ViewData::Int4(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
355			ViewData::Int8(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
356			ViewData::Int16(a) => {
357				wide_unary::<i128, Decimal256Type>(a, |v| upscale_or_beyond(i256::from_i128(v), by))
358			}
359			ViewData::Uint1(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
360			ViewData::Uint2(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
361			ViewData::Uint4(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
362			ViewData::Uint8(a) => rescale256!(a, by, |v| i256::from_i128(i128::from(v))),
363			ViewData::Uint16(a) => {
364				wide_unary::<u128, Decimal256Type>(a, |v| upscale_or_beyond(i256::from_parts(v, 0), by))
365			}
366			ViewData::Decimal(DecimalView::Decimal128(a)) => {
367				rescale256!(a, by, |v| i256::from_i128(v))
368			}
369			ViewData::Decimal(DecimalView::Decimal256(a)) => rescale256!(a, by, |v| v),
370			_ => unreachable!(),
371		};
372		Arc::new(array.with_data_type(data_type))
373	}
374}
375
376pub fn compare_columns<Op: CompareOp>(
377	left: &(FieldRef, ArrayRef),
378	right: &(FieldRef, ArrayRef),
379	fragment: Fragment,
380	error_fn: impl FnOnce(Fragment, ValueType, ValueType) -> Diagnostic,
381) -> Result<(FieldRef, ArrayRef)> {
382	let len = match (left.1.len(), right.1.len()) {
383		(l, r) if l == r => l,
384		(1, r) => r,
385		(l, 1) => l,
386		(l, r) => return Err(length_mismatch(l, r, &fragment)),
387	};
388	let (left_data, left_nulls) = split_nulls(left.clone())?;
389	let (right_data, right_nulls) = split_nulls(right.clone())?;
390	if ColumnView::try_from(left)?.is_untyped_none() || ColumnView::try_from(right)?.is_untyped_none() {
391		return Ok(none_typed(fragment.text(), ValueType::Boolean, len));
392	}
393	let (left_type, right_type) =
394		(ColumnView::try_from(&left_data)?.get_type(), ColumnView::try_from(&right_data)?.get_type());
395	let Some(target) = field_compare_target((&left.0, &left_type), (&right.0, &right_type), Op::EQUALITY)? else {
396		return_error!(error_fn(fragment, left_type, right_type))
397	};
398	if is_all_none(left_nulls.as_ref()) || is_all_none(right_nulls.as_ref()) {
399		return Ok(none_typed(fragment.text(), ValueType::Boolean, len));
400	}
401	let left_array = cast_to(left, &target)?;
402	let right_array = cast_to(right, &target)?;
403	let result = kernel_compare::<Op>(left_array, right_array)?;
404	Ok(bool_column(fragment.text(), result, left.0.is_nullable() || right.0.is_nullable()))
405}
406
407fn kernel_compare<Op: CompareOp>(left_array: ArrayRef, right_array: ArrayRef) -> Result<BooleanArray> {
408	Ok(match (left_array.len(), right_array.len()) {
409		(1, r) if r != 1 => Op::kernel(&Scalar::new(left_array), &right_array)?,
410		(l, 1) if l != 1 => Op::kernel(&left_array, &Scalar::new(right_array))?,
411		_ => Op::kernel(&left_array, &right_array)?,
412	})
413}
414
415#[cfg(test)]
416mod tests {
417	use arrow_schema::FieldRef;
418	use reifydb_core::value::column::builder::ColumnBuilder;
419	use reifydb_value::value::{dictionary::DictionaryId, value_type::ValueType};
420
421	use super::field_compare_target;
422
423	fn id_field(dictionary: Option<DictionaryId>) -> FieldRef {
424		let mut builder = ColumnBuilder::with_capacity(ValueType::DictionaryId, 0);
425		if let Some(dictionary) = dictionary {
426			builder.set_dictionary_id(dictionary);
427		}
428		builder.finish("c").0
429	}
430
431	fn target(left: Option<DictionaryId>, right: Option<DictionaryId>, equality: bool) -> Option<ValueType> {
432		let (left, right) = (id_field(left), id_field(right));
433		field_compare_target((&left, &ValueType::DictionaryId), (&right, &ValueType::DictionaryId), equality)
434			.unwrap()
435	}
436
437	#[test]
438	fn ids_compare_as_ids_only_for_equality_within_one_dictionary() {
439		// entry id 1 names a different text in every dictionary, and ids carry no text order, so any other pair
440		// must not compare ids.
441		let (syms, other) = (DictionaryId(1), DictionaryId(2));
442		assert_eq!(target(Some(syms), Some(syms), true), Some(ValueType::DictionaryId));
443		assert_eq!(target(Some(syms), Some(other), true), None);
444		assert_eq!(target(Some(syms), Some(syms), false), None);
445		assert_eq!(target(Some(syms), None, true), None);
446		assert_eq!(target(None, None, true), None);
447	}
448}