Skip to main content

reifydb_engine/vm/volcano/scan/
dictionary.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use postcard::from_bytes;
7use reifydb_core::{
8	interface::{catalog::dictionary::Dictionary, resolved::ResolvedDictionary, store::SingleVersionRange},
9	internal_error,
10	key::{any::TaggedKey, bound::TaggedKeyBoundRange, catalog::DictionaryEntryIndexKey},
11	value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns, headers::ColumnHeaders},
12};
13use reifydb_transaction::transaction::Transaction;
14use reifydb_value::{
15	fragment::Fragment,
16	reifydb_assertions,
17	value::{Value, dictionary::DictionaryEntryId, value_type::ValueType},
18};
19use tracing::instrument;
20
21use crate::{
22	Result,
23	vm::volcano::query::{QueryContext, QueryNode},
24};
25
26pub struct DictionaryScanNode {
27	dictionary: ResolvedDictionary,
28	context: Option<Arc<QueryContext>>,
29	headers: ColumnHeaders,
30	last_key: Option<TaggedKey>,
31	exhausted: bool,
32}
33
34impl DictionaryScanNode {
35	pub fn new(dictionary: ResolvedDictionary, context: Arc<QueryContext>) -> Result<Self> {
36		let headers = ColumnHeaders {
37			columns: vec![Fragment::internal("id"), Fragment::internal("value")],
38		};
39
40		Ok(Self {
41			dictionary,
42			context: Some(context),
43			headers,
44			last_key: None,
45			exhausted: false,
46		})
47	}
48
49	#[instrument(level = "trace", skip_all, name = "volcano::scan::dictionary::drain")]
50	fn drain_batch<'a>(
51		rx: &mut Transaction<'a>,
52		range: TaggedKeyBoundRange,
53		batch_size: u64,
54		dict_def: &Dictionary,
55	) -> Result<(Vec<DictionaryEntryId>, Vec<Value>, Option<TaggedKey>)> {
56		let mut ids: Vec<DictionaryEntryId> = Vec::new();
57		let mut values: Vec<Value> = Vec::new();
58		let mut new_last_key = None;
59
60		let single = rx
61			.single()
62			.ok_or_else(|| internal_error!("single-version store is not available for dictionary scans"))?;
63		let store = single.read_store();
64		let batch = SingleVersionRange::range_batch(&store, range.encode(), batch_size)?;
65
66		for entry in batch.items {
67			let Some(key) = DictionaryEntryIndexKey::decode(&entry.key) else {
68				panic!(
69					"dictionary {} holds an entry index key that does not decode: {:?}",
70					dict_def.id, entry.key
71				);
72			};
73
74			let entry_id = DictionaryEntryId::from_u128(key.id, dict_def.id_type.clone())?;
75			new_last_key = Some(TaggedKey::from(key));
76
77			let value: Value = from_bytes(&entry.bytes)
78				.map_err(|e| internal_error!("Failed to deserialize dictionary value: {}", e))?;
79
80			ids.push(entry_id);
81			values.push(value);
82		}
83
84		Ok((ids, values, new_last_key))
85	}
86
87	#[instrument(level = "trace", skip_all, name = "volcano::scan::dictionary::empty_columns")]
88	fn empty_columns(dict_def: &Dictionary) -> Vec<ColumnWithName> {
89		vec![
90			ColumnWithName {
91				name: Fragment::internal("id"),
92				data: ColumnBuffer::none_typed(dict_def.id_type.clone(), 0),
93			},
94			ColumnWithName {
95				name: Fragment::internal("value"),
96				data: ColumnBuffer::none_typed(dict_def.value_type.clone(), 0),
97			},
98		]
99	}
100
101	#[instrument(level = "trace", skip_all, name = "volcano::scan::dictionary::assemble")]
102	fn assemble(ids: &[DictionaryEntryId], values: &[Value], dict_def: &Dictionary) -> Result<Option<Columns>> {
103		let id_column = build_id_column(ids, dict_def.id_type.clone())?;
104		let value_column = build_value_column(values, dict_def.value_type.clone())?;
105
106		Ok(Some(Columns::new(vec![id_column, value_column])))
107	}
108}
109
110impl QueryNode for DictionaryScanNode {
111	#[instrument(name = "volcano::scan::dictionary::initialize", level = "trace", skip_all)]
112	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
113		Ok(())
114	}
115
116	#[instrument(name = "volcano::scan::dictionary::next", level = "trace", skip_all)]
117	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
118		reifydb_assertions! {
119			assert!(self.context.is_some(), "DictionaryScan::next() called before initialize()");
120		}
121		let stored_ctx = self.context.as_ref().unwrap();
122
123		if self.exhausted {
124			return Ok(None);
125		}
126
127		let batch_size = stored_ctx.batch_size;
128		let dict_def = self.dictionary.def();
129
130		let range = DictionaryEntryIndexKey::full_scan(dict_def.id).resume_after(self.last_key.as_ref());
131
132		let (ids, values, new_last_key) = Self::drain_batch(rx, range, batch_size, dict_def)?;
133
134		if ids.is_empty() {
135			self.exhausted = true;
136			if self.last_key.is_none() {
137				return Ok(Some(Columns::new(Self::empty_columns(dict_def))));
138			}
139			return Ok(None);
140		}
141
142		self.last_key = new_last_key;
143
144		Self::assemble(&ids, &values, dict_def)
145	}
146
147	fn headers(&self) -> Option<ColumnHeaders> {
148		Some(self.headers.clone())
149	}
150}
151
152fn build_id_column(ids: &[DictionaryEntryId], id_type: ValueType) -> Result<ColumnWithName> {
153	let data = match id_type {
154		ValueType::Uint1 => {
155			let vals: Vec<u8> = ids.iter().map(|id| id.to_u128() as u8).collect();
156			ColumnBuffer::uint1(vals)
157		}
158		ValueType::Uint2 => {
159			let vals: Vec<u16> = ids.iter().map(|id| id.to_u128() as u16).collect();
160			ColumnBuffer::uint2(vals)
161		}
162		ValueType::Uint4 => {
163			let vals: Vec<u32> = ids.iter().map(|id| id.to_u128() as u32).collect();
164			ColumnBuffer::uint4(vals)
165		}
166		ValueType::Uint8 => {
167			let vals: Vec<u64> = ids.iter().map(|id| id.to_u128() as u64).collect();
168			ColumnBuffer::uint8(vals)
169		}
170		ValueType::Uint16 => {
171			let vals: Vec<u128> = ids.iter().map(|id| id.to_u128()).collect();
172			ColumnBuffer::uint16(vals)
173		}
174		_ => return Err(internal_error!("Invalid dictionary id_type: {:?}", id_type)),
175	};
176
177	Ok(ColumnWithName {
178		name: Fragment::internal("id"),
179		data,
180	})
181}
182
183fn build_value_column(values: &[Value], value_type: ValueType) -> Result<ColumnWithName> {
184	let data = match value_type {
185		ValueType::Utf8 => {
186			let vals: Vec<String> = values
187				.iter()
188				.map(|v| match v {
189					Value::Utf8(s) => s.clone(),
190					_ => format!("{:?}", v),
191				})
192				.collect();
193			ColumnBuffer::utf8(vals)
194		}
195		ValueType::Int1 => {
196			let vals: Vec<i8> = values
197				.iter()
198				.map(|v| match v {
199					Value::Int1(n) => *n,
200					_ => 0,
201				})
202				.collect();
203			ColumnBuffer::int1(vals)
204		}
205		ValueType::Int2 => {
206			let vals: Vec<i16> = values
207				.iter()
208				.map(|v| match v {
209					Value::Int2(n) => *n,
210					_ => 0,
211				})
212				.collect();
213			ColumnBuffer::int2(vals)
214		}
215		ValueType::Int4 => {
216			let vals: Vec<i32> = values
217				.iter()
218				.map(|v| match v {
219					Value::Int4(n) => *n,
220					_ => 0,
221				})
222				.collect();
223			ColumnBuffer::int4(vals)
224		}
225		ValueType::Int8 => {
226			let vals: Vec<i64> = values
227				.iter()
228				.map(|v| match v {
229					Value::Int8(n) => *n,
230					_ => 0,
231				})
232				.collect();
233			ColumnBuffer::int8(vals)
234		}
235		ValueType::Uint1 => {
236			let vals: Vec<u8> = values
237				.iter()
238				.map(|v| match v {
239					Value::Uint1(n) => *n,
240					_ => 0,
241				})
242				.collect();
243			ColumnBuffer::uint1(vals)
244		}
245		ValueType::Uint2 => {
246			let vals: Vec<u16> = values
247				.iter()
248				.map(|v| match v {
249					Value::Uint2(n) => *n,
250					_ => 0,
251				})
252				.collect();
253			ColumnBuffer::uint2(vals)
254		}
255		ValueType::Uint4 => {
256			let vals: Vec<u32> = values
257				.iter()
258				.map(|v| match v {
259					Value::Uint4(n) => *n,
260					_ => 0,
261				})
262				.collect();
263			ColumnBuffer::uint4(vals)
264		}
265		ValueType::Uint8 => {
266			let vals: Vec<u64> = values
267				.iter()
268				.map(|v| match v {
269					Value::Uint8(n) => *n,
270					_ => 0,
271				})
272				.collect();
273			ColumnBuffer::uint8(vals)
274		}
275		_ => {
276			let vals: Vec<String> = values.iter().map(|v| format!("{:?}", v)).collect();
277			ColumnBuffer::utf8(vals)
278		}
279	};
280
281	Ok(ColumnWithName {
282		name: Fragment::internal("value"),
283		data,
284	})
285}