Skip to main content

reifydb_value/value/container/
dictionary_array.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use arrow_array::{Array, FixedSizeBinaryArray};
5use arrow_buffer::MutableBuffer;
6
7use crate::value::{
8	Value,
9	container::fixed_array::{assert_whole_rows, from_buffer},
10	dictionary::DictionaryEntryId,
11	value_type::ValueType,
12};
13
14pub const DICTIONARY_ENTRY_WIDTH: usize = 17;
15
16pub fn encode(entry: DictionaryEntryId) -> [u8; DICTIONARY_ENTRY_WIDTH] {
17	let (tag, value) = match entry {
18		DictionaryEntryId::U1(v) => (0u8, v as u128),
19		DictionaryEntryId::U2(v) => (1u8, v as u128),
20		DictionaryEntryId::U4(v) => (2u8, v as u128),
21		DictionaryEntryId::U8(v) => (3u8, v as u128),
22		DictionaryEntryId::U16(v) => (4u8, v),
23	};
24	let mut row = [0u8; DICTIONARY_ENTRY_WIDTH];
25	row[0] = tag;
26	row[1..].copy_from_slice(&value.to_le_bytes());
27	row
28}
29
30pub fn decode(row: &[u8; DICTIONARY_ENTRY_WIDTH]) -> DictionaryEntryId {
31	let [tag, value @ ..] = *row;
32	let value = u128::from_le_bytes(value);
33	match tag {
34		0 => DictionaryEntryId::U1(value as u8),
35		1 => DictionaryEntryId::U2(value as u16),
36		2 => DictionaryEntryId::U4(value as u32),
37		3 => DictionaryEntryId::U8(value as u64),
38		4 => DictionaryEntryId::U16(value),
39		other => panic!("dictionary entry row has width byte {other}, expected 0 to 4"),
40	}
41}
42
43fn rows(array: &FixedSizeBinaryArray) -> &[[u8; DICTIONARY_ENTRY_WIDTH]] {
44	assert_eq!(
45		array.value_length() as usize,
46		DICTIONARY_ENTRY_WIDTH,
47		"dictionary id column must hold {DICTIONARY_ENTRY_WIDTH} byte values, found {}",
48		array.value_length()
49	);
50	assert_whole_rows(DICTIONARY_ENTRY_WIDTH, array.value_data().len());
51	array.value_data()[..array.len() * DICTIONARY_ENTRY_WIDTH].as_chunks::<DICTIONARY_ENTRY_WIDTH>().0
52}
53
54pub fn push_entry(buffer: &mut MutableBuffer, entry: DictionaryEntryId) {
55	buffer.extend_from_slice(&encode(entry));
56}
57
58pub fn dictionary_array(values: impl IntoIterator<Item = DictionaryEntryId>) -> FixedSizeBinaryArray {
59	let values = values.into_iter();
60	let mut buffer = MutableBuffer::with_capacity(values.size_hint().0 * DICTIONARY_ENTRY_WIDTH);
61	for entry in values {
62		push_entry(&mut buffer, entry);
63	}
64	from_buffer(DICTIONARY_ENTRY_WIDTH, buffer)
65}
66
67pub fn iter(array: &FixedSizeBinaryArray) -> impl ExactSizeIterator<Item = DictionaryEntryId> + '_ {
68	rows(array).iter().map(decode)
69}
70
71pub fn get(array: &FixedSizeBinaryArray, index: usize) -> Option<DictionaryEntryId> {
72	rows(array).get(index).map(decode)
73}
74
75pub fn get_value(array: &FixedSizeBinaryArray, index: usize) -> Value {
76	get(array, index).map(Value::DictionaryId).unwrap_or(Value::none_of(ValueType::DictionaryId))
77}
78
79pub fn as_string(array: &FixedSizeBinaryArray, index: usize) -> String {
80	get(array, index).map(|id| id.to_string()).unwrap_or_else(|| "none".to_string())
81}
82
83#[cfg(test)]
84mod tests {
85
86	use super::*;
87
88	fn boundaries() -> Vec<DictionaryEntryId> {
89		vec![
90			DictionaryEntryId::U1(0),
91			DictionaryEntryId::U1(u8::MAX),
92			DictionaryEntryId::U2(0),
93			DictionaryEntryId::U2(u16::MAX),
94			DictionaryEntryId::U4(0),
95			DictionaryEntryId::U4(u32::MAX),
96			DictionaryEntryId::U8(0),
97			DictionaryEntryId::U8(u64::MAX),
98			DictionaryEntryId::U16(0),
99			DictionaryEntryId::U16(u128::MAX),
100		]
101	}
102
103	#[test]
104	fn every_width_round_trips_at_zero_and_its_max() {
105		// A lost width tag turns U2(0) into U1(0), which compares unequal and hashes differently.
106		let entries = boundaries();
107		for entry in &entries {
108			assert_eq!(decode(&encode(*entry)), *entry);
109		}
110		let array = dictionary_array(entries.clone());
111		assert_eq!(array.value_length() as usize, DICTIONARY_ENTRY_WIDTH);
112		assert_eq!(array.len(), entries.len());
113		assert_eq!(iter(&array).collect::<Vec<_>>(), entries);
114		for (i, entry) in entries.iter().enumerate() {
115			assert_eq!(get(&array, i), Some(*entry));
116			assert_eq!(get_value(&array, i), Value::DictionaryId(*entry));
117		}
118		assert_eq!(get(&array, entries.len()), None);
119		assert_eq!(get_value(&array, entries.len()), Value::none_of(ValueType::DictionaryId));
120	}
121
122	#[test]
123	fn row_layout_is_width_byte_then_little_endian_u128() {
124		// The row bytes are the storage contract; a reordered layout breaks every stored buffer.
125		let row = encode(DictionaryEntryId::U4(0x0102_0304));
126		assert_eq!(row, [2, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
127		let row = encode(DictionaryEntryId::U16(u128::MAX));
128		assert_eq!(row[0], 4);
129		assert!(row[1..].iter().all(|&b| b == 0xff));
130	}
131
132	#[test]
133	fn all_zero_row_reads_the_default_entry() {
134		// Default and out-of-range rows are zero filled, so the zero row must read U1(0).
135		assert_eq!(decode(&[0; DICTIONARY_ENTRY_WIDTH]), DictionaryEntryId::U1(0));
136		assert_eq!(decode(&[0; DICTIONARY_ENTRY_WIDTH]), DictionaryEntryId::default());
137		let mut buffer = MutableBuffer::new(0);
138		buffer.extend_zeros(DICTIONARY_ENTRY_WIDTH * 2);
139		let array = from_buffer(DICTIONARY_ENTRY_WIDTH, buffer);
140		assert_eq!(iter(&array).collect::<Vec<_>>(), [DictionaryEntryId::U1(0); 2]);
141	}
142
143	#[test]
144	#[should_panic(expected = "width byte 5")]
145	fn bad_width_byte_panics_naming_the_byte() {
146		// Reading an unknown width as some default would silently corrupt dictionary lookups.
147		let mut row = [0; DICTIONARY_ENTRY_WIDTH];
148		row[0] = 5;
149		decode(&row);
150	}
151
152	#[test]
153	#[should_panic(expected = "not a whole number of 17 byte rows")]
154	fn partial_row_buffer_is_refused() {
155		// Arrow alone would drop the trailing partial row and hide the corruption.
156		let mut buffer = MutableBuffer::new(0);
157		push_entry(&mut buffer, DictionaryEntryId::U2(7));
158		buffer.push(0u8);
159		from_buffer(DICTIONARY_ENTRY_WIDTH, buffer);
160	}
161
162	#[test]
163	#[should_panic(expected = "must hold 17 byte values")]
164	fn wrong_width_array_is_refused() {
165		// A 16 byte uuid array read as dictionary rows would shift every entry.
166		let array = FixedSizeBinaryArray::new(16, vec![0u8; 32].into(), None);
167		get(&array, 0);
168	}
169}