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::{collections::VecDeque, ops::Bound, sync::Arc};
5
6use arrow_array::{ArrayRef, RecordBatch, UInt64Array};
7use arrow_schema::FieldRef;
8use reifydb_codec::row::{
9	bytes::EncodedBytes,
10	shape::{RowShape, fingerprint::RowShapeFingerprint},
11	table::EncodedTableRow,
12};
13use reifydb_core::{
14	common::TimeSource,
15	error::diagnostic,
16	interface::{
17		catalog::{dictionary::Dictionary, storage::StorageId},
18		resolved::ResolvedTable,
19		store::MultiVersionRow,
20	},
21	key::{
22		any::TaggedKey,
23		row::{PartitionedRowKey, RowKeyRange, StoragePartitionedRowKey, StorageRowKey},
24	},
25	value::{
26		batch::{append_rows_fitted, batch},
27		column::{builder::ColumnBuilder, headers::ColumnHeaders},
28	},
29};
30use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
31use reifydb_value::{
32	error, reifydb_assertions,
33	value::{
34		partition::Partition,
35		row_number::RowNumber,
36		system_columns::{SystemColumn, stamp_system_columns},
37		value_type::ValueType,
38	},
39};
40use tracing::instrument;
41
42use super::{
43	super::decode_dictionary_columns,
44	empty_scan,
45	merge::{MergeLayout, PartitionMerge},
46	partition_array, scan_headers, source_system_columns, storage_partitioned_row, storage_row,
47};
48use crate::{
49	Result,
50	vm::volcano::query::{QueryContext, QueryNode},
51};
52
53pub struct TableScanNode {
54	table: ResolvedTable,
55	context: Option<Arc<QueryContext>>,
56	headers: ColumnHeaders,
57
58	storage_types: Vec<ValueType>,
59
60	dictionaries: Vec<Option<(Dictionary, ValueType)>>,
61
62	shape: Option<RowShape>,
63	resume: Resume,
64	exhausted: bool,
65
66	partition: Option<Partition>,
67
68	pending: Option<VecDeque<Partition>>,
69
70	advanced: bool,
71
72	system_columns: Vec<SystemColumn>,
73
74	oldest_first: bool,
75	keep_dictionary_ids: bool,
76
77	merge: Option<PartitionMerge>,
78
79	storage_batch: Option<RecordBatch>,
80}
81
82impl TableScanNode {
83	pub fn new(
84		table: ResolvedTable,
85		partition: Option<Partition>,
86		context: Arc<QueryContext>,
87		rx: &mut Transaction<'_>,
88	) -> Result<Self> {
89		let mut storage_types = Vec::with_capacity(table.columns().len());
90		let mut dictionaries = Vec::with_capacity(table.columns().len());
91
92		for col in table.columns() {
93			if let Some(dict_id) = col.dictionary_id {
94				if let Some(dict) = context.services.catalog.find_dictionary(rx, dict_id)? {
95					storage_types.push(ValueType::DictionaryId);
96					dictionaries.push(Some((dict, col.constraint.get_type())));
97				} else {
98					storage_types.push(col.constraint.get_type());
99					dictionaries.push(None);
100				}
101			} else {
102				storage_types.push(col.constraint.get_type());
103				dictionaries.push(None);
104			}
105		}
106
107		let system_columns = source_system_columns(
108			!table.def().partition_by.is_empty(),
109			table.def().time != TimeSource::None,
110			true,
111		);
112		let headers = scan_headers(table.columns().iter().map(|col| col.name.as_str()), &system_columns);
113
114		let resume = if table.def().partition_by.is_empty() {
115			Resume::Row(None)
116		} else {
117			Resume::Partitioned(None)
118		};
119
120		Ok(Self {
121			table,
122			context: Some(context),
123			headers,
124			storage_types,
125			dictionaries,
126			shape: None,
127			resume,
128			exhausted: false,
129			partition,
130			pending: None,
131			advanced: false,
132			system_columns,
133			oldest_first: false,
134			keep_dictionary_ids: false,
135			merge: None,
136			storage_batch: None,
137		})
138	}
139
140	pub fn with_partitions(
141		table: ResolvedTable,
142		partitions: Vec<Partition>,
143		context: Arc<QueryContext>,
144		rx: &mut Transaction<'_>,
145	) -> Result<Self> {
146		let mut pending = VecDeque::from(partitions);
147		let first = pending.pop_front();
148		let mut node = Self::new(table, first, context, rx)?;
149		node.pending = Some(pending);
150		Ok(node)
151	}
152
153	fn scan_partition_list<'a>(
154		&mut self,
155		rx: &mut Transaction<'a>,
156		storage: StorageId,
157		scope: RangeScope,
158		batch_size: u64,
159		mut last: Option<StoragePartitionedRowKey>,
160	) -> Result<(ScannedBatch, Option<StoragePartitionedRowKey>)> {
161		let mut batch = ScannedBatch::default();
162		loop {
163			let Some(partition) = self.partition else {
164				batch.exhausted = true;
165				return Ok((batch, None));
166			};
167			let room = batch_size - batch.rows.len() as u64;
168			let (start, end) = partitioned_bounds(Some(partition), last);
169			let (scanned, new_last) = {
170				let mut stream = rx.range_partitioned_row(storage, start, end, scope, room as usize)?;
171				Self::drain_batch_partitioned(&mut stream, room)?
172			};
173			let partition_done = scanned.exhausted;
174			batch.rows.extend(scanned.rows);
175			batch.row_numbers.extend(scanned.row_numbers);
176			batch.partitions.extend(scanned.partitions);
177			batch.commit_versions.extend(scanned.commit_versions);
178			if !partition_done {
179				return Ok((batch, new_last));
180			}
181			self.partition = self.pending.as_mut().and_then(VecDeque::pop_front);
182			self.advanced = true;
183			last = None;
184			if batch.rows.len() as u64 >= batch_size {
185				batch.exhausted = self.partition.is_none();
186				return Ok((batch, None));
187			}
188		}
189	}
190
191	pub(crate) fn oldest_first(mut self) -> Self {
192		self.oldest_first = true;
193		self
194	}
195
196	pub(crate) fn keep_dictionary_ids(mut self) -> Self {
197		self.keep_dictionary_ids = true;
198		self
199	}
200
201	fn get_or_load_shape<'a>(
202		&mut self,
203		rx: &mut Transaction<'a>,
204		fingerprint: RowShapeFingerprint,
205	) -> Result<RowShape> {
206		if let Some(shape) = &self.shape
207			&& shape.fingerprint() == fingerprint
208		{
209			return Ok(shape.clone());
210		}
211
212		let stored_ctx = self.context.as_ref().expect("TableScanNode context not set");
213		let shape = stored_ctx.services.catalog.get_or_load_row_shape(fingerprint, rx)?.ok_or_else(|| {
214			error!(diagnostic::internal::internal(format!(
215				"RowShape with fingerprint {:?} not found for table {}",
216				fingerprint,
217				self.table.def().name
218			)))
219		})?;
220
221		self.shape = Some(shape.clone());
222
223		Ok(shape)
224	}
225
226	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::drain_partitioned")]
227	fn drain_batch_partitioned(
228		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<StoragePartitionedRowKey>>>,
229		batch_size: u64,
230	) -> Result<(ScannedBatch, Option<StoragePartitionedRowKey>)> {
231		let mut batch = ScannedBatch::default();
232		let mut last = None;
233
234		for _ in 0..batch_size {
235			match stream.next() {
236				Some(Ok(multi)) => {
237					batch.rows.push(multi.bytes);
238					batch.row_numbers.push(multi.key.row());
239					batch.partitions.push(multi.key.partition());
240					batch.commit_versions.push(multi.version.0);
241					last = Some(multi.key);
242				}
243				Some(Err(e)) => return Err(e),
244				None => {
245					batch.exhausted = true;
246					break;
247				}
248			}
249		}
250
251		Ok((batch, last))
252	}
253
254	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::drain_row")]
255	fn drain_batch_row(
256		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<StorageRowKey>>>,
257		batch_size: u64,
258	) -> Result<(ScannedBatch, Option<StorageRowKey>)> {
259		let mut batch = ScannedBatch::default();
260		let mut last = None;
261
262		for _ in 0..batch_size {
263			match stream.next() {
264				Some(Ok(multi)) => {
265					batch.rows.push(multi.bytes);
266					batch.row_numbers.push(multi.key.row());
267					batch.commit_versions.push(multi.version.0);
268					last = Some(multi.key);
269				}
270				Some(Err(e)) => return Err(e),
271				None => {
272					batch.exhausted = true;
273					break;
274				}
275			}
276		}
277
278		Ok((batch, last))
279	}
280
281	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::column_alloc")]
282	fn storage_columns(&self) -> Vec<(FieldRef, ArrayRef)> {
283		self.table
284			.columns()
285			.iter()
286			.enumerate()
287			.map(|(idx, col)| {
288				ColumnBuilder::with_capacity(self.storage_types[idx].clone(), 0).finish(&col.name)
289			})
290			.collect()
291	}
292
293	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::empty_columns")]
294	fn empty_columns(&self) -> Vec<(FieldRef, ArrayRef)> {
295		self.table
296			.columns()
297			.iter()
298			.map(|col| ColumnBuilder::with_capacity(col.constraint.get_type(), 0).finish(&col.name))
299			.collect()
300	}
301
302	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::append_rows")]
303	fn append_batch<'a>(
304		&mut self,
305		rx: &mut Transaction<'a>,
306		columns: RecordBatch,
307		bytes_vec: Vec<EncodedBytes>,
308		row_numbers: Vec<RowNumber>,
309	) -> Result<RecordBatch> {
310		let mut columns = columns;
311		let mut rows = bytes_vec.into_iter().peekable();
312		let mut numbers = row_numbers.into_iter();
313		while let Some(first) = rows.next() {
314			let fingerprint = EncodedTableRow::view(&first).fingerprint();
315			let mut run = vec![first];
316			while let Some(next) =
317				rows.next_if(|row| EncodedTableRow::view(row).fingerprint() == fingerprint)
318			{
319				run.push(next);
320			}
321			let run_numbers: Vec<RowNumber> = numbers.by_ref().take(run.len()).collect();
322			let shape = self.get_or_load_shape(rx, fingerprint)?;
323			columns = append_rows_fitted(columns, &shape, run, run_numbers)?;
324		}
325		Ok(columns)
326	}
327}
328
329#[derive(Clone, Copy)]
330enum Resume {
331	Row(Option<StorageRowKey>),
332	Partitioned(Option<StoragePartitionedRowKey>),
333}
334
335#[derive(Default)]
336struct ScannedBatch {
337	rows: Vec<EncodedBytes>,
338	row_numbers: Vec<RowNumber>,
339	partitions: Vec<Partition>,
340	commit_versions: Vec<u64>,
341	exhausted: bool,
342}
343
344fn partitioned_bounds(
345	partition: Option<Partition>,
346	last: Option<StoragePartitionedRowKey>,
347) -> (Bound<StoragePartitionedRowKey>, Bound<StoragePartitionedRowKey>) {
348	let start = match (last, partition) {
349		(Some(key), _) => Bound::Excluded(key),
350		(None, Some(partition)) => {
351			Bound::Included(StoragePartitionedRowKey::new(partition, RowNumber(u64::MAX)))
352		}
353		(None, None) => Bound::Unbounded,
354	};
355	let end = match partition {
356		Some(partition) => Bound::Included(StoragePartitionedRowKey::new(partition, RowNumber(u64::MIN))),
357		None => Bound::Unbounded,
358	};
359	(start, end)
360}
361
362impl QueryNode for TableScanNode {
363	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::initialize")]
364	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
365		Ok(())
366	}
367
368	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::next")]
369	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
370		reifydb_assertions! {
371			assert!(self.context.is_some(), "TableScanNode::next() called before initialize()");
372		}
373		let stored_ctx = self.context.as_ref().unwrap();
374
375		if self.exhausted {
376			return Ok(None);
377		}
378
379		let batch_size = stored_ctx.batch_size;
380
381		let scope = RangeScope::All;
382
383		let storage: StorageId = self.table.def().id.into();
384
385		let (scanned, next_resume, resumed) = match self.resume {
386			Resume::Partitioned(last) if self.pending.is_some() => {
387				let resumed = last.is_some() || self.advanced;
388				let (scanned, new_last) =
389					self.scan_partition_list(rx, storage, scope, batch_size, last)?;
390				(scanned, Resume::Partitioned(new_last), resumed)
391			}
392			Resume::Partitioned(last) if self.oldest_first => {
393				let partition = self.partition;
394				let merge = self.merge.get_or_insert_with(|| {
395					PartitionMerge::new(
396						MergeLayout::Row,
397						storage,
398						partition.map(|partition| {
399							PartitionedRowKey::partition_range(storage, partition)
400						}),
401					)
402				});
403				let rows = merge.next(rx, batch_size)?;
404				let (scanned, new_last) = Self::drain_batch_partitioned(
405					&mut rows.into_iter().map(storage_partitioned_row),
406					batch_size,
407				)?;
408				(scanned, Resume::Partitioned(new_last), last.is_some())
409			}
410			Resume::Partitioned(last) => {
411				let (start, end) = partitioned_bounds(self.partition, last);
412				let (scanned, new_last) = {
413					let mut stream = rx.range_partitioned_row(
414						storage,
415						start,
416						end,
417						scope,
418						batch_size as usize,
419					)?;
420					Self::drain_batch_partitioned(&mut stream, batch_size)?
421				};
422				(scanned, Resume::Partitioned(new_last), last.is_some())
423			}
424			Resume::Row(last) if self.oldest_first => {
425				let last_key = last.map(|key| TaggedKey::Row(key.with_storage(storage)));
426				let range = RowKeyRange::scan_range_rev(storage, last_key.as_ref());
427				let (scanned, new_last) = {
428					let mut stream = rx
429						.range_rev(range, scope, batch_size as usize)?
430						.map(|row| row.and_then(storage_row));
431					Self::drain_batch_row(&mut stream, batch_size)?
432				};
433				(scanned, Resume::Row(new_last), last.is_some())
434			}
435			Resume::Row(last) => {
436				let start = match last {
437					Some(key) => Bound::Excluded(key),
438					None => Bound::Unbounded,
439				};
440				let (scanned, new_last) = {
441					let mut stream = rx.range_row(
442						storage,
443						start,
444						Bound::Unbounded,
445						scope,
446						batch_size as usize,
447					)?;
448					Self::drain_batch_row(&mut stream, batch_size)?
449				};
450				(scanned, Resume::Row(new_last), last.is_some())
451			}
452		};
453
454		if scanned.exhausted {
455			self.exhausted = true;
456		}
457
458		if scanned.rows.is_empty() {
459			self.exhausted = true;
460			if !resumed {
461				return Ok(Some(empty_scan(self.empty_columns(), &self.system_columns)?));
462			}
463			return Ok(None);
464		}
465
466		self.resume = next_resume;
467
468		let columns = match &self.storage_batch {
469			Some(storage) => storage.clone(),
470			None => {
471				let storage = batch(self.storage_columns())?;
472				self.storage_batch = Some(storage.clone());
473				storage
474			}
475		};
476		let columns = self.append_batch(rx, columns, scanned.rows, scanned.row_numbers)?;
477
478		let mut stamps: Vec<(SystemColumn, ArrayRef)> = Vec::new();
479		if !scanned.partitions.is_empty() {
480			stamps.push((SystemColumn::Partitions, partition_array(&scanned.partitions)));
481		}
482		stamps.push((SystemColumn::CommitVersion, Arc::new(UInt64Array::from(scanned.commit_versions))));
483		let columns = stamp_system_columns(columns, stamps)?;
484
485		if self.keep_dictionary_ids {
486			return Ok(Some(columns));
487		}
488		Ok(Some(decode_dictionary_columns(columns, &self.dictionaries, rx)?))
489	}
490
491	fn headers(&self) -> Option<ColumnHeaders> {
492		Some(self.headers.clone())
493	}
494}