Skip to main content

reifydb_engine/vm/volcano/scan/
table.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{ops::Bound, sync::Arc};
5
6use reifydb_codec::row::{bytes::EncodedBytes, shape::RowShape, table::EncodedTableRow};
7use reifydb_core::{
8	common::CommitVersion,
9	error::diagnostic,
10	interface::{
11		catalog::{dictionary::Dictionary, storage::StorageId},
12		resolved::ResolvedTable,
13		store::MultiVersionRow,
14	},
15	key::row::{StoragePartitionedRowKey, StorageRowKey},
16	value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns, headers::ColumnHeaders},
17};
18use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
19use reifydb_value::{
20	error,
21	fragment::Fragment,
22	reifydb_assertions,
23	value::{partition::Partition, row_number::RowNumber, system_columns::SystemColumns, value_type::ValueType},
24};
25use tracing::instrument;
26
27use super::super::decode_dictionary_columns;
28use crate::{
29	Result,
30	vm::volcano::query::{QueryContext, QueryNode},
31};
32
33pub struct TableScanNode {
34	table: ResolvedTable,
35	context: Option<Arc<QueryContext>>,
36	headers: ColumnHeaders,
37
38	storage_types: Vec<ValueType>,
39
40	dictionaries: Vec<Option<Dictionary>>,
41
42	shape: Option<RowShape>,
43	resume: Resume,
44	exhausted: bool,
45
46	partition: Option<Partition>,
47
48	min_commit_version: Option<CommitVersion>,
49}
50
51impl TableScanNode {
52	pub fn with_min_commit_version(mut self, min_commit_version: Option<CommitVersion>) -> Self {
53		self.min_commit_version = min_commit_version;
54		self
55	}
56
57	pub fn new(
58		table: ResolvedTable,
59		partition: Option<Partition>,
60		context: Arc<QueryContext>,
61		rx: &mut Transaction<'_>,
62	) -> Result<Self> {
63		let mut storage_types = Vec::with_capacity(table.columns().len());
64		let mut dictionaries = Vec::with_capacity(table.columns().len());
65
66		for col in table.columns() {
67			if let Some(dict_id) = col.dictionary_id {
68				if let Some(dict) = context.services.catalog.find_dictionary(rx, dict_id)? {
69					storage_types.push(ValueType::DictionaryId);
70					dictionaries.push(Some(dict));
71				} else {
72					storage_types.push(col.constraint.get_type());
73					dictionaries.push(None);
74				}
75			} else {
76				storage_types.push(col.constraint.get_type());
77				dictionaries.push(None);
78			}
79		}
80
81		let headers = ColumnHeaders {
82			columns: table.columns().iter().map(|col| Fragment::internal(&col.name)).collect(),
83		};
84
85		let resume = if table.def().partition_by.is_empty() {
86			Resume::Row(None)
87		} else {
88			Resume::Partitioned(None)
89		};
90
91		Ok(Self {
92			table,
93			context: Some(context),
94			headers,
95			storage_types,
96			dictionaries,
97			shape: None,
98			resume,
99			exhausted: false,
100			partition,
101			min_commit_version: None,
102		})
103	}
104
105	fn get_or_load_shape<'a>(&mut self, rx: &mut Transaction<'a>, first: &EncodedBytes) -> Result<RowShape> {
106		if let Some(shape) = &self.shape {
107			return Ok(shape.clone());
108		}
109
110		let fingerprint = EncodedTableRow::view(first).fingerprint();
111
112		let stored_ctx = self.context.as_ref().expect("TableScanNode context not set");
113		let shape = stored_ctx.services.catalog.get_or_load_row_shape(fingerprint, rx)?.ok_or_else(|| {
114			error!(diagnostic::internal::internal(format!(
115				"RowShape with fingerprint {:?} not found for table {}",
116				fingerprint,
117				self.table.def().name
118			)))
119		})?;
120
121		self.shape = Some(shape.clone());
122
123		Ok(shape)
124	}
125
126	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::drain_partitioned")]
127	fn drain_batch_partitioned(
128		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<StoragePartitionedRowKey>>>,
129		batch_size: u64,
130	) -> Result<(ScannedBatch, Option<StoragePartitionedRowKey>)> {
131		let mut batch = ScannedBatch::default();
132		let mut last = None;
133
134		for _ in 0..batch_size {
135			match stream.next() {
136				Some(Ok(multi)) => {
137					batch.rows.push(multi.bytes);
138					batch.row_numbers.push(multi.key.row());
139					batch.partitions.push(multi.key.partition());
140					last = Some(multi.key);
141				}
142				Some(Err(e)) => return Err(e),
143				None => {
144					batch.exhausted = true;
145					break;
146				}
147			}
148		}
149
150		Ok((batch, last))
151	}
152
153	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::drain_row")]
154	fn drain_batch_row(
155		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<StorageRowKey>>>,
156		batch_size: u64,
157	) -> Result<(ScannedBatch, Option<StorageRowKey>)> {
158		let mut batch = ScannedBatch::default();
159		let mut last = None;
160
161		for _ in 0..batch_size {
162			match stream.next() {
163				Some(Ok(multi)) => {
164					batch.rows.push(multi.bytes);
165					batch.row_numbers.push(multi.key.row());
166					last = Some(multi.key);
167				}
168				Some(Err(e)) => return Err(e),
169				None => {
170					batch.exhausted = true;
171					break;
172				}
173			}
174		}
175
176		Ok((batch, last))
177	}
178
179	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::column_alloc")]
180	fn storage_columns(&self) -> Vec<ColumnWithName> {
181		self.table
182			.columns()
183			.iter()
184			.enumerate()
185			.map(|(idx, col)| ColumnWithName {
186				name: Fragment::internal(&col.name),
187				data: ColumnBuffer::with_capacity(self.storage_types[idx].clone(), 0),
188			})
189			.collect()
190	}
191
192	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::empty_columns")]
193	fn empty_columns(&self) -> Vec<ColumnWithName> {
194		self.table
195			.columns()
196			.iter()
197			.map(|col| ColumnWithName {
198				name: Fragment::internal(&col.name),
199				data: ColumnBuffer::none_typed(col.constraint.get_type(), 0),
200			})
201			.collect()
202	}
203
204	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::append_rows")]
205	fn append_batch<'a>(
206		&mut self,
207		rx: &mut Transaction<'a>,
208		columns: &mut Columns,
209		bytes_vec: Vec<EncodedBytes>,
210		row_numbers: Vec<RowNumber>,
211	) -> Result<()> {
212		let shape = self.get_or_load_shape(rx, &bytes_vec[0])?;
213		columns.append_rows(&shape, bytes_vec.into_iter(), row_numbers)?;
214		Ok(())
215	}
216}
217
218#[derive(Clone, Copy)]
219enum Resume {
220	Row(Option<StorageRowKey>),
221	Partitioned(Option<StoragePartitionedRowKey>),
222}
223
224#[derive(Default)]
225struct ScannedBatch {
226	rows: Vec<EncodedBytes>,
227	row_numbers: Vec<RowNumber>,
228	partitions: Vec<Partition>,
229	exhausted: bool,
230}
231
232fn partitioned_bounds(
233	partition: Option<Partition>,
234	last: Option<StoragePartitionedRowKey>,
235) -> (Bound<StoragePartitionedRowKey>, Bound<StoragePartitionedRowKey>) {
236	let start = match (last, partition) {
237		(Some(key), _) => Bound::Excluded(key),
238		(None, Some(partition)) => {
239			Bound::Included(StoragePartitionedRowKey::new(partition, RowNumber(u64::MAX)))
240		}
241		(None, None) => Bound::Unbounded,
242	};
243	let end = match partition {
244		Some(partition) => Bound::Included(StoragePartitionedRowKey::new(partition, RowNumber(u64::MIN))),
245		None => Bound::Unbounded,
246	};
247	(start, end)
248}
249
250impl QueryNode for TableScanNode {
251	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::initialize")]
252	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
253		Ok(())
254	}
255
256	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::next")]
257	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
258		reifydb_assertions! {
259			assert!(self.context.is_some(), "TableScanNode::next() called before initialize()");
260		}
261		let stored_ctx = self.context.as_ref().unwrap();
262
263		if self.exhausted {
264			return Ok(None);
265		}
266
267		let batch_size = stored_ctx.batch_size;
268
269		let scope = match self.min_commit_version {
270			Some(v) => RangeScope::After(v),
271			None => RangeScope::All,
272		};
273
274		let storage: StorageId = self.table.def().id.into();
275
276		let (scanned, next_resume, resumed) = match self.resume {
277			Resume::Partitioned(last) => {
278				let (start, end) = partitioned_bounds(self.partition, last);
279				let (scanned, new_last) = {
280					let mut stream = rx.range_partitioned_row(
281						storage,
282						start,
283						end,
284						scope,
285						batch_size as usize,
286					)?;
287					Self::drain_batch_partitioned(&mut stream, batch_size)?
288				};
289				(scanned, Resume::Partitioned(new_last), last.is_some())
290			}
291			Resume::Row(last) => {
292				let start = match last {
293					Some(key) => Bound::Excluded(key),
294					None => Bound::Unbounded,
295				};
296				let (scanned, new_last) = {
297					let mut stream = rx.range_row(
298						storage,
299						start,
300						Bound::Unbounded,
301						scope,
302						batch_size as usize,
303					)?;
304					Self::drain_batch_row(&mut stream, batch_size)?
305				};
306				(scanned, Resume::Row(new_last), last.is_some())
307			}
308		};
309
310		if scanned.exhausted {
311			self.exhausted = true;
312		}
313
314		if scanned.rows.is_empty() {
315			self.exhausted = true;
316			if !resumed {
317				return Ok(Some(Columns::new(self.empty_columns())));
318			}
319			return Ok(None);
320		}
321
322		self.resume = next_resume;
323
324		let mut columns = Columns::with_system(self.storage_columns(), SystemColumns::default());
325		self.append_batch(rx, &mut columns, scanned.rows, scanned.row_numbers)?;
326
327		if !scanned.partitions.is_empty() {
328			columns.system.set_partitions(scanned.partitions);
329		}
330
331		decode_dictionary_columns(&mut columns, &self.dictionaries, rx)?;
332
333		Ok(Some(columns))
334	}
335
336	fn headers(&self) -> Option<ColumnHeaders> {
337		Some(self.headers.clone())
338	}
339}