Skip to main content

reifydb_value/value/container/
decimal_array.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{borrow::Borrow, sync::Arc};
5
6use arrow_array::{Array, ArrayRef, Decimal128Array, Decimal256Array, PrimitiveArray};
7use arrow_buffer::{ScalarBuffer, i256};
8use arrow_schema::DataType;
9
10use crate::value::{
11	Value,
12	constraint::{precision::Precision, scale::Scale},
13	decimal::{Decimal, unscaled},
14};
15
16pub const DECIMAL128_MAX_PRECISION: u8 = 38;
17
18#[derive(Clone, Debug)]
19pub enum DecimalArray {
20	Decimal128(Decimal128Array),
21	Decimal256(Decimal256Array),
22}
23
24impl DecimalArray {
25	pub fn from_unscaled(precision: Precision, scale: Scale, values: impl IntoIterator<Item = i256>) -> Self {
26		let values = values.into_iter().inspect(|&value| {
27			assert!(
28				unscaled::digits(value) <= precision.value(),
29				"unscaled value {value} does not fit precision {precision}"
30			)
31		});
32		if precision.value() <= DECIMAL128_MAX_PRECISION {
33			let natives: Vec<i128> = values
34				.map(|value| value.to_i128().expect("a value within precision 38 fits i128"))
35				.collect();
36			DecimalArray::Decimal128(
37				PrimitiveArray::new(ScalarBuffer::from(natives), None)
38					.with_data_type(data_type(precision, scale)),
39			)
40		} else {
41			let natives: Vec<i256> = values.collect();
42			DecimalArray::Decimal256(
43				PrimitiveArray::new(ScalarBuffer::from(natives), None)
44					.with_data_type(data_type(precision, scale)),
45			)
46		}
47	}
48
49	pub fn view(&self) -> DecimalView<'_> {
50		DecimalView::from(self)
51	}
52
53	pub fn into_array(self) -> ArrayRef {
54		match self {
55			DecimalArray::Decimal128(array) => Arc::new(array),
56			DecimalArray::Decimal256(array) => Arc::new(array),
57		}
58	}
59
60	pub fn len(&self) -> usize {
61		self.view().len()
62	}
63
64	pub fn is_empty(&self) -> bool {
65		self.len() == 0
66	}
67
68	pub fn precision(&self) -> Precision {
69		self.view().precision()
70	}
71
72	pub fn scale(&self) -> Scale {
73		self.view().scale()
74	}
75
76	pub fn data_type(&self) -> &DataType {
77		self.view().data_type()
78	}
79
80	pub fn unscaled_at(&self, index: usize) -> Option<i256> {
81		self.view().unscaled_at(index)
82	}
83
84	pub fn unscaled_values(&self) -> Vec<i256> {
85		self.view().unscaled_values()
86	}
87}
88
89#[derive(Clone, Copy, Debug)]
90pub enum DecimalView<'a> {
91	Decimal128(&'a Decimal128Array),
92	Decimal256(&'a Decimal256Array),
93}
94
95impl<'a> From<&'a DecimalArray> for DecimalView<'a> {
96	fn from(array: &'a DecimalArray) -> Self {
97		match array {
98			DecimalArray::Decimal128(array) => DecimalView::Decimal128(array),
99			DecimalArray::Decimal256(array) => DecimalView::Decimal256(array),
100		}
101	}
102}
103
104impl<'a> From<&DecimalView<'a>> for DecimalView<'a> {
105	fn from(view: &DecimalView<'a>) -> Self {
106		*view
107	}
108}
109
110impl From<DecimalView<'_>> for DecimalArray {
111	fn from(view: DecimalView<'_>) -> Self {
112		match view {
113			DecimalView::Decimal128(array) => DecimalArray::Decimal128(array.clone()),
114			DecimalView::Decimal256(array) => DecimalArray::Decimal256(array.clone()),
115		}
116	}
117}
118
119impl<'a> DecimalView<'a> {
120	pub fn array(&self) -> &'a dyn Array {
121		match *self {
122			DecimalView::Decimal128(array) => array,
123			DecimalView::Decimal256(array) => array,
124		}
125	}
126
127	pub fn len(&self) -> usize {
128		self.array().len()
129	}
130
131	pub fn is_empty(&self) -> bool {
132		self.len() == 0
133	}
134
135	pub fn precision(&self) -> Precision {
136		Precision::new(match *self {
137			DecimalView::Decimal128(array) => array.precision(),
138			DecimalView::Decimal256(array) => array.precision(),
139		})
140	}
141
142	pub fn scale(&self) -> Scale {
143		let scale = match *self {
144			DecimalView::Decimal128(array) => array.scale(),
145			DecimalView::Decimal256(array) => array.scale(),
146		};
147		Scale::new(u8::try_from(scale).expect("decimal arrays are built with a non-negative scale"))
148	}
149
150	pub fn data_type(&self) -> &'a DataType {
151		self.array().data_type()
152	}
153
154	pub fn unscaled_at(&self, index: usize) -> Option<i256> {
155		match *self {
156			DecimalView::Decimal128(array) => {
157				array.values().get(index).map(|&value| i256::from_i128(value))
158			}
159			DecimalView::Decimal256(array) => array.values().get(index).copied(),
160		}
161	}
162
163	pub fn unscaled_values(&self) -> Vec<i256> {
164		match *self {
165			DecimalView::Decimal128(array) => {
166				array.values().iter().map(|&value| i256::from_i128(value)).collect()
167			}
168			DecimalView::Decimal256(array) => array.values().to_vec(),
169		}
170	}
171}
172
173impl PartialEq for DecimalArray {
174	fn eq(&self, other: &Self) -> bool {
175		self.len() == other.len()
176			&& (0..self.len()).all(|index| decimal_at(self, index) == decimal_at(other, index))
177	}
178}
179
180pub fn data_type(precision: Precision, scale: Scale) -> DataType {
181	if precision.value() <= DECIMAL128_MAX_PRECISION {
182		DataType::Decimal128(precision.value(), scale.value() as i8)
183	} else {
184		DataType::Decimal256(precision.value(), scale.value() as i8)
185	}
186}
187
188pub fn decimal_array<B: Borrow<Decimal>>(
189	precision: Precision,
190	scale: Scale,
191	values: impl IntoIterator<Item = B>,
192) -> DecimalArray {
193	let unscaled = values.into_iter().map(|value| {
194		let value = value.borrow();
195		value.rescale(scale.value())
196			.unwrap_or_else(|| panic!("decimal {value} does not rescale exactly to scale {scale}"))
197			.unscaled()
198	});
199	DecimalArray::from_unscaled(precision, scale, unscaled)
200}
201
202pub fn decimal_at<'a>(array: impl Into<DecimalView<'a>>, index: usize) -> Option<Decimal> {
203	checked_decimal_at(array, index).expect("a decimal array holds only valid decimals")
204}
205
206pub fn checked_decimal_at<'a>(array: impl Into<DecimalView<'a>>, index: usize) -> Option<Option<Decimal>> {
207	let array = array.into();
208	let scale = array.scale().value();
209	match array.unscaled_at(index) {
210		Some(value) => Decimal::from_parts(value, scale).map(Some),
211		None => Some(None),
212	}
213}
214
215pub fn decimals<'a>(array: impl Into<DecimalView<'a>>) -> Vec<Decimal> {
216	let array = array.into();
217	(0..array.len()).filter_map(|index| decimal_at(array, index)).collect()
218}
219
220pub fn decimal_get_value<'a>(array: impl Into<DecimalView<'a>>, index: usize) -> Value {
221	decimal_at(array, index).map(Value::Decimal).unwrap_or_else(Value::none)
222}
223
224pub fn decimal_as_string<'a>(array: impl Into<DecimalView<'a>>, index: usize) -> String {
225	decimal_at(array, index).map(|value| value.to_string()).unwrap_or_else(|| "none".to_string())
226}
227
228#[cfg(test)]
229mod family {
230	use std::str::FromStr;
231
232	use super::*;
233
234	fn decimal(text: &str) -> Decimal {
235		Decimal::from_str(text).unwrap()
236	}
237
238	#[test]
239	fn width_switches_to_decimal256_past_precision_thirty_eight() {
240		// Arrow rejects 39 digits in a Decimal128, so the width must follow the precision, never the values.
241		let widest = decimal_array(Precision::MAX, Scale::new(10), [decimal("1.5")]);
242		assert_eq!(widest.data_type(), &DataType::Decimal256(76, 10));
243		assert_eq!(data_type(Precision::new(38), Scale::new(38)), DataType::Decimal128(38, 38));
244		assert_eq!(data_type(Precision::MAX, Scale::MAX), DataType::Decimal256(76, 76));
245	}
246
247	#[test]
248	fn precision_and_scale_are_read_back_from_the_data_type() {
249		// A reader that assumes the arrow default (38, 10) would misread every value by a power of ten.
250		let array = decimal_array(Precision::new(12), Scale::new(4), [decimal("-3.25")]);
251		assert_eq!(array.precision(), Precision::new(12));
252		assert_eq!(array.scale(), Scale::new(4));
253		assert_eq!(array.unscaled_at(0), Some(i256::from_i128(-32500)));
254		assert_eq!(decimal_as_string(&array, 0), "-3.2500");
255	}
256
257	#[test]
258	fn decimals_are_rescaled_exactly_to_the_array_scale() {
259		// A write that rounds would store a value the caller never wrote.
260		let array = decimal_array(
261			Precision::new(10),
262			Scale::new(2),
263			[decimal("1.5"), decimal("-0.25"), decimal("7")],
264		);
265		assert_eq!(array.unscaled_values(), [i256::from_i128(150), i256::from_i128(-25), i256::from_i128(700)]);
266		assert_eq!(decimals(&array), [decimal("1.50"), decimal("-0.25"), decimal("7.00")]);
267		assert_eq!(decimal_get_value(&array, 0).to_string(), "1.50");
268	}
269
270	#[test]
271	#[should_panic(expected = "does not rescale exactly")]
272	fn a_decimal_with_more_fraction_digits_than_the_scale_panics() {
273		// Silently rounding 1.555 to 1.56 would hide a caller bug.
274		decimal_array(Precision::new(10), Scale::new(2), [decimal("1.555")]);
275	}
276
277	#[test]
278	#[should_panic(expected = "does not fit precision")]
279	fn a_value_wider_than_the_precision_panics() {
280		// Storing 100.0 under decimal(2, 1) would break every reader that trusts the precision.
281		decimal_array(Precision::new(2), Scale::new(1), [decimal("100.0")]);
282	}
283
284	#[test]
285	fn reads_past_the_end_are_none() {
286		// An out of range row must read as none, never as a zero or a panic.
287		let dec = decimal_array(Precision::new(5), Scale::new(1), [decimal("0.5")]);
288		assert_eq!(decimal_at(&dec, 1), None);
289		assert_eq!(decimal_get_value(&dec, 1), Value::none());
290		assert_eq!(decimal_as_string(&dec, 1), "none");
291	}
292
293	#[test]
294	fn equality_compares_values_not_widths() {
295		// Two arrays holding the same numbers must compare equal whatever width or scale stores them.
296		let narrow = decimal_array(Precision::new(10), Scale::new(1), [decimal("1.5"), decimal("-2")]);
297		let wide = decimal_array(Precision::new(60), Scale::new(3), [decimal("1.5"), decimal("-2")]);
298		assert!(narrow == wide);
299		let other = decimal_array(Precision::new(10), Scale::new(1), [decimal("1.5"), decimal("-2.1")]);
300		assert!(narrow != other);
301		let shorter = decimal_array(Precision::new(10), Scale::new(1), [decimal("1.5")]);
302		assert!(narrow != shorter);
303	}
304}