Skip to main content

reifydb_core/value/column/arith/
scalar.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use arrow_buffer::i256;
5use reifydb_value::{
6	Result,
7	error::{Error, TypeError},
8	fragment::LazyFragment,
9	value::{
10		constraint::precision::Precision,
11		decimal::{Decimal, unscaled},
12		is::IsNumber,
13		number::{
14			promote::Promote,
15			safe::{add::SafeAdd, div::SafeDiv, mul::SafeMul, remainder::SafeRemainder, sub::SafeSub},
16		},
17		value_type::{ValueType, get::GetType},
18	},
19};
20
21use crate::{
22	interface::catalog::property::ColumnSaturationStrategy,
23	value::column::arith::context::{ArithContext, Overflow},
24};
25
26enum Mode {
27	Error,
28	None,
29	Saturate,
30	Wrap,
31}
32
33impl ArithContext {
34	fn mode(&self) -> Mode {
35		match self.overflow {
36			Overflow::Target => match self.saturation_policy() {
37				ColumnSaturationStrategy::Error => Mode::Error,
38				ColumnSaturationStrategy::None => Mode::None,
39			},
40			Overflow::Error => Mode::Error,
41			Overflow::None => Mode::None,
42			Overflow::Saturate => Mode::Saturate,
43			Overflow::Wrap => Mode::Wrap,
44		}
45	}
46
47	fn out_of_range(&self, target: ValueType, fragment: impl LazyFragment) -> Error {
48		TypeError::NumberOutOfRange {
49			target,
50			fragment: fragment.fragment(),
51			descriptor: self.target.as_ref().and_then(|c| c.to_number_descriptor()),
52		}
53		.into()
54	}
55}
56
57macro_rules! impl_scalar_op {
58	($method:ident, $safe_trait:ident, $checked:ident, $saturating:ident, $wrapping:ident $(, $zero:ident)?) => {
59		impl ArithContext {
60			pub fn $method<L, R>(
61				&self,
62				l: &L,
63				r: &R,
64				fragment: impl LazyFragment + Copy,
65			) -> Result<Option<<L as Promote<R>>::Output>>
66			where
67				L: Promote<R>,
68				R: IsNumber,
69				<L as Promote<R>>::Output: IsNumber,
70				<L as Promote<R>>::Output: $safe_trait,
71			{
72				let target = || <L as Promote<R>>::Output::get_type();
73				let mode = self.mode();
74				let Some((lp, rp)) = l.checked_promote(r) else {
75					return match mode {
76						Mode::None => Ok(None),
77						_ => Err(self.out_of_range(target(), fragment)),
78					};
79				};
80				match mode {
81					Mode::Error => {
82						$(
83							if <<L as Promote<R>>::Output as $safe_trait>::$zero(&rp) {
84								return Err(TypeError::DivisionByZero {
85									target: target(),
86									fragment: fragment.fragment(),
87								}
88								.into());
89							}
90						)?
91						lp.$checked(&rp).ok_or_else(|| self.out_of_range(target(), fragment)).map(Some)
92					}
93					Mode::None => Ok(lp.$checked(&rp)),
94					Mode::Saturate => Ok(Some(lp.$saturating(&rp))),
95					Mode::Wrap => Ok(Some(lp.$wrapping(&rp))),
96				}
97			}
98		}
99	};
100}
101
102pub trait FitFamily: Sized {
103	fn fit_family(&self, target: &ValueType) -> Option<Self>;
104
105	fn clamp_family(&self, target: &ValueType) -> Self;
106}
107
108impl FitFamily for Decimal {
109	fn fit_family(&self, target: &ValueType) -> Option<Self> {
110		let (Some(precision), Some(scale)) = (target.precision(), target.scale()) else {
111			return Some(self.clone());
112		};
113		let rounded = self.round_to_scale(scale.value())?;
114		(rounded.digits() <= precision.value()).then_some(rounded)
115	}
116
117	fn clamp_family(&self, target: &ValueType) -> Self {
118		let (Some(precision), Some(scale)) = (target.precision(), target.scale()) else {
119			return self.clone();
120		};
121		Decimal::from_parts(family_bound(precision, self.is_negative()), scale.value())
122			.expect("a precision bound is in range")
123	}
124}
125
126fn family_bound(precision: Precision, negative: bool) -> i256 {
127	let max = unscaled::pow10(precision.value()).expect("a precision is at most 76 digits").wrapping_sub(i256::ONE);
128	if negative {
129		max.wrapping_neg()
130	} else {
131		max
132	}
133}
134
135impl ArithContext {
136	pub fn fit_family<T: FitFamily>(
137		&self,
138		value: T,
139		target: &ValueType,
140		fragment: impl LazyFragment + Copy,
141	) -> Result<Option<T>> {
142		match (value.fit_family(target), self.mode()) {
143			(Some(fitted), _) => Ok(Some(fitted)),
144			(None, Mode::None) => Ok(None),
145			(None, Mode::Error) => Err(self.out_of_range(target.clone(), fragment)),
146			(None, Mode::Saturate | Mode::Wrap) => Ok(Some(value.clamp_family(target))),
147		}
148	}
149}
150
151impl_scalar_op!(add, SafeAdd, checked_add, saturating_add, wrapping_add);
152impl_scalar_op!(sub, SafeSub, checked_sub, saturating_sub, wrapping_sub);
153impl_scalar_op!(mul, SafeMul, checked_mul, saturating_mul, wrapping_mul);
154impl_scalar_op!(div, SafeDiv, checked_div, saturating_div, wrapping_div, is_zero);
155impl_scalar_op!(remainder, SafeRemainder, checked_rem, saturating_rem, wrapping_rem, is_zero);
156
157#[cfg(test)]
158pub mod tests {
159	use reifydb_value::fragment::Fragment;
160
161	use crate::value::column::arith::context::ArithContext;
162
163	#[test]
164	fn test_add() {
165		let test_instance = ArithContext::default();
166		let result = test_instance.add(&1i8, &255i16, Fragment::testing_empty);
167		assert_eq!(result, Ok(Some(256i128)));
168	}
169
170	#[test]
171	fn test_sub() {
172		let test_instance = ArithContext::default();
173		let result = test_instance.sub(&1i8, &255i16, Fragment::testing_empty);
174		assert_eq!(result, Ok(Some(-254i128)));
175	}
176
177	#[test]
178	fn test_mul() {
179		let test_instance = ArithContext::default();
180		let result = test_instance.mul(&23i8, &255i16, Fragment::testing_empty);
181		assert_eq!(result, Ok(Some(5865i128)));
182	}
183
184	#[test]
185	fn test_div() {
186		let test_instance = ArithContext::default();
187		let result = test_instance.div(&120i8, &20i16, Fragment::testing_empty);
188		assert_eq!(result, Ok(Some(6i128)));
189	}
190
191	#[test]
192	fn test_remainder() {
193		let test_instance = ArithContext::default();
194		let result = test_instance.remainder(&120i8, &21i16, Fragment::testing_empty);
195		assert_eq!(result, Ok(Some(15i128)));
196	}
197}