Skip to main content

reifydb_codec/frame/encoding/
dict.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, str};
5
6use arrow_array::{Array, LargeBinaryArray, LargeStringArray};
7use reifydb_value::value::blob::Blob;
8
9use crate::{error::DecodeError, frame::format::dict_index_width_to_flags, tag::ValueKind};
10
11pub struct DictEncoded {
12	pub data: Vec<u8>,
13
14	pub extra: Vec<u8>,
15
16	pub type_code: u8,
17
18	pub flags_bits: u8,
19}
20
21pub fn try_dict_encode_utf8(container: &LargeStringArray, min_ratio: f64) -> Option<DictEncoded> {
22	let row_count = container.len();
23	if row_count == 0 {
24		return None;
25	}
26
27	let mut dict_map: HashMap<&str, u32> = HashMap::new();
28	let mut dict_entries: Vec<&str> = Vec::new();
29
30	for s in (0..row_count).map(|i| container.value(i)) {
31		if !dict_map.contains_key(s) {
32			let idx = dict_entries.len() as u32;
33			dict_map.insert(s, idx);
34			dict_entries.push(s);
35		}
36	}
37
38	let dict_count = dict_entries.len();
39
40	if dict_count as f64 >= row_count as f64 * min_ratio {
41		return None;
42	}
43
44	let (index_width, flags_bits) = if dict_count <= 255 {
45		(1usize, dict_index_width_to_flags(1))
46	} else if dict_count <= 65535 {
47		(2, dict_index_width_to_flags(2))
48	} else {
49		(4, dict_index_width_to_flags(4))
50	};
51
52	let mut data = Vec::with_capacity(row_count * index_width);
53	for s in (0..row_count).map(|i| container.value(i)) {
54		let idx = dict_map[s];
55		match index_width {
56			1 => data.push(idx as u8),
57			2 => data.extend_from_slice(&(idx as u16).to_le_bytes()),
58			4 => data.extend_from_slice(&idx.to_le_bytes()),
59			_ => unreachable!(),
60		}
61	}
62
63	let mut extra = Vec::new();
64	extra.extend_from_slice(&(dict_count as u32).to_le_bytes());
65
66	let mut offset: u32 = 0;
67	extra.extend_from_slice(&offset.to_le_bytes());
68	for entry in &dict_entries {
69		offset += entry.len() as u32;
70		extra.extend_from_slice(&offset.to_le_bytes());
71	}
72
73	for entry in &dict_entries {
74		extra.extend_from_slice(entry.as_bytes());
75	}
76
77	Some(DictEncoded {
78		data,
79		extra,
80		type_code: ValueKind::Utf8.byte(),
81		flags_bits,
82	})
83}
84
85pub fn try_dict_encode_blob(container: &LargeBinaryArray, min_ratio: f64) -> Option<DictEncoded> {
86	let row_count = container.len();
87	if row_count == 0 {
88		return None;
89	}
90
91	let mut dict_map: HashMap<&[u8], u32> = HashMap::new();
92	let mut dict_entries: Vec<&[u8]> = Vec::new();
93
94	for bytes in (0..row_count).map(|i| container.value(i)) {
95		if !dict_map.contains_key(bytes) {
96			let idx = dict_entries.len() as u32;
97			dict_map.insert(bytes, idx);
98			dict_entries.push(bytes);
99		}
100	}
101
102	let dict_count = dict_entries.len();
103
104	if dict_count as f64 >= row_count as f64 * min_ratio {
105		return None;
106	}
107
108	let (index_width, flags_bits) = if dict_count <= 255 {
109		(1usize, dict_index_width_to_flags(1))
110	} else if dict_count <= 65535 {
111		(2, dict_index_width_to_flags(2))
112	} else {
113		(4, dict_index_width_to_flags(4))
114	};
115
116	let mut data = Vec::with_capacity(row_count * index_width);
117	for bytes in (0..row_count).map(|i| container.value(i)) {
118		let idx = dict_map[bytes];
119		match index_width {
120			1 => data.push(idx as u8),
121			2 => data.extend_from_slice(&(idx as u16).to_le_bytes()),
122			4 => data.extend_from_slice(&idx.to_le_bytes()),
123			_ => unreachable!(),
124		}
125	}
126
127	let mut extra = Vec::new();
128	extra.extend_from_slice(&(dict_count as u32).to_le_bytes());
129
130	let mut offset: u32 = 0;
131	extra.extend_from_slice(&offset.to_le_bytes());
132	for entry in &dict_entries {
133		offset += entry.len() as u32;
134		extra.extend_from_slice(&offset.to_le_bytes());
135	}
136	for entry in &dict_entries {
137		extra.extend_from_slice(entry);
138	}
139
140	Some(DictEncoded {
141		data,
142		extra,
143		type_code: ValueKind::Blob.byte(),
144		flags_bits,
145	})
146}
147
148pub fn decode_dict_utf8(
149	data: &[u8],
150	extra: &[u8],
151	row_count: usize,
152	index_width: usize,
153) -> Result<Vec<String>, DecodeError> {
154	let dict_entries = decode_dict_table(extra)?;
155
156	let mut values = Vec::with_capacity(row_count);
157	for i in 0..row_count {
158		let idx = read_index(data, i, index_width) as usize;
159		if idx >= dict_entries.len() {
160			return Err(DecodeError::InvalidData(format!(
161				"dict index {} out of range (dict has {} entries)",
162				idx,
163				dict_entries.len()
164			)));
165		}
166		values.push(dict_entries[idx].clone());
167	}
168
169	Ok(values)
170}
171
172pub fn decode_dict_blob(
173	data: &[u8],
174	extra: &[u8],
175	row_count: usize,
176	index_width: usize,
177) -> Result<Vec<Blob>, DecodeError> {
178	let dict_entries = decode_dict_table_bytes(extra)?;
179
180	let mut values = Vec::with_capacity(row_count);
181	for i in 0..row_count {
182		let idx = read_index(data, i, index_width) as usize;
183		if idx >= dict_entries.len() {
184			return Err(DecodeError::InvalidData(format!(
185				"dict index {} out of range (dict has {} entries)",
186				idx,
187				dict_entries.len()
188			)));
189		}
190		values.push(Blob::new(dict_entries[idx].clone()));
191	}
192
193	Ok(values)
194}
195
196fn decode_dict_table(extra: &[u8]) -> Result<Vec<String>, DecodeError> {
197	if extra.len() < 4 {
198		return Err(DecodeError::InvalidData("dict table too short".to_string()));
199	}
200
201	let dict_count = u32::from_le_bytes([extra[0], extra[1], extra[2], extra[3]]) as usize;
202	let offsets_start = 4;
203	let offsets_end = offsets_start + (dict_count + 1) * 4;
204
205	if extra.len() < offsets_end {
206		return Err(DecodeError::InvalidData("dict offsets truncated".to_string()));
207	}
208
209	let mut offsets = Vec::with_capacity(dict_count + 1);
210	for i in 0..=dict_count {
211		let pos = offsets_start + i * 4;
212		offsets.push(u32::from_le_bytes([extra[pos], extra[pos + 1], extra[pos + 2], extra[pos + 3]]) as usize);
213	}
214
215	let data_start = offsets_end;
216	let mut entries = Vec::with_capacity(dict_count);
217	for i in 0..dict_count {
218		let start = data_start + offsets[i];
219		let end = data_start + offsets[i + 1];
220		let s = str::from_utf8(&extra[start..end])
221			.map_err(|e| DecodeError::InvalidData(format!("invalid dict UTF-8: {}", e)))?;
222		entries.push(s.to_string());
223	}
224
225	Ok(entries)
226}
227
228pub fn decode_dict_table_bytes(extra: &[u8]) -> Result<Vec<Vec<u8>>, DecodeError> {
229	if extra.len() < 4 {
230		return Err(DecodeError::InvalidData("dict table too short".to_string()));
231	}
232
233	let dict_count = u32::from_le_bytes([extra[0], extra[1], extra[2], extra[3]]) as usize;
234	let offsets_start = 4;
235	let offsets_end = offsets_start + (dict_count + 1) * 4;
236
237	if extra.len() < offsets_end {
238		return Err(DecodeError::InvalidData("dict offsets truncated".to_string()));
239	}
240
241	let mut offsets = Vec::with_capacity(dict_count + 1);
242	for i in 0..=dict_count {
243		let pos = offsets_start + i * 4;
244		offsets.push(u32::from_le_bytes([extra[pos], extra[pos + 1], extra[pos + 2], extra[pos + 3]]) as usize);
245	}
246
247	let data_start = offsets_end;
248	let mut entries = Vec::with_capacity(dict_count);
249	for i in 0..dict_count {
250		let start = data_start + offsets[i];
251		let end = data_start + offsets[i + 1];
252		entries.push(extra[start..end].to_vec());
253	}
254
255	Ok(entries)
256}
257
258pub fn read_index(data: &[u8], i: usize, width: usize) -> u32 {
259	let off = i * width;
260	match width {
261		1 => data[off] as u32,
262		2 => u16::from_le_bytes([data[off], data[off + 1]]) as u32,
263		4 => u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]),
264		_ => unreachable!(),
265	}
266}