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::sync::Arc;
5
6use reifydb_codec::{
7	encoded::{row::EncodedRow, shape::RowShape},
8	key::encoded::EncodedKey,
9};
10use reifydb_core::{
11	common::CommitVersion,
12	error::diagnostic,
13	interface::{catalog::dictionary::Dictionary, resolved::ResolvedTable},
14	key::{
15		EncodableKey,
16		partitioned_row::{PartitionedRowKey, RowLocator},
17		row::{RowKey, RowKeyRange},
18	},
19	value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns, headers::ColumnHeaders},
20};
21use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
22use reifydb_value::{
23	error,
24	fragment::Fragment,
25	reifydb_assertions,
26	util::cowvec::CowVec,
27	value::{partition::Partition, value_type::ValueType},
28};
29use tracing::instrument;
30
31use super::super::decode_dictionary_columns;
32use crate::{
33	Result,
34	vm::volcano::query::{QueryContext, QueryNode},
35};
36
37pub struct TableScanNode {
38	table: ResolvedTable,
39	context: Option<Arc<QueryContext>>,
40	headers: ColumnHeaders,
41
42	storage_types: Vec<ValueType>,
43
44	dictionaries: Vec<Option<Dictionary>>,
45
46	shape: Option<RowShape>,
47	last_key: Option<EncodedKey>,
48	exhausted: bool,
49
50	partition: Option<Partition>,
51
52	min_commit_version: Option<CommitVersion>,
53}
54
55impl TableScanNode {
56	pub fn with_min_commit_version(mut self, min_commit_version: Option<CommitVersion>) -> Self {
57		self.min_commit_version = min_commit_version;
58		self
59	}
60
61	pub fn new(
62		table: ResolvedTable,
63		partition: Option<Partition>,
64		context: Arc<QueryContext>,
65		rx: &mut Transaction<'_>,
66	) -> Result<Self> {
67		let mut storage_types = Vec::with_capacity(table.columns().len());
68		let mut dictionaries = Vec::with_capacity(table.columns().len());
69
70		for col in table.columns() {
71			if let Some(dict_id) = col.dictionary_id {
72				if let Some(dict) = context.services.catalog.find_dictionary(rx, dict_id)? {
73					storage_types.push(ValueType::DictionaryId);
74					dictionaries.push(Some(dict));
75				} else {
76					storage_types.push(col.constraint.get_type());
77					dictionaries.push(None);
78				}
79			} else {
80				storage_types.push(col.constraint.get_type());
81				dictionaries.push(None);
82			}
83		}
84
85		let headers = ColumnHeaders {
86			columns: table.columns().iter().map(|col| Fragment::internal(&col.name)).collect(),
87		};
88
89		Ok(Self {
90			table,
91			context: Some(context),
92			headers,
93			storage_types,
94			dictionaries,
95			shape: None,
96			last_key: None,
97			exhausted: false,
98			partition,
99			min_commit_version: None,
100		})
101	}
102
103	fn get_or_load_shape<'a>(&mut self, rx: &mut Transaction<'a>, first_row: &EncodedRow) -> Result<RowShape> {
104		if let Some(shape) = &self.shape {
105			return Ok(shape.clone());
106		}
107
108		let fingerprint = first_row.fingerprint();
109
110		let stored_ctx = self.context.as_ref().expect("TableScanNode context not set");
111		let shape = stored_ctx.services.catalog.get_or_load_row_shape(fingerprint, rx)?.ok_or_else(|| {
112			error!(diagnostic::internal::internal(format!(
113				"RowShape with fingerprint {:?} not found for table {}",
114				fingerprint,
115				self.table.def().name
116			)))
117		})?;
118
119		self.shape = Some(shape.clone());
120
121		Ok(shape)
122	}
123}
124
125impl QueryNode for TableScanNode {
126	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::initialize")]
127	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
128		Ok(())
129	}
130
131	#[instrument(level = "trace", skip_all, name = "volcano::scan::table::next")]
132	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
133		reifydb_assertions! {
134			assert!(self.context.is_some(), "TableScanNode::next() called before initialize()");
135		}
136		let stored_ctx = self.context.as_ref().unwrap();
137
138		if self.exhausted {
139			return Ok(None);
140		}
141
142		let batch_size = stored_ctx.batch_size;
143
144		let partitioned = !self.table.def().partition_by.is_empty();
145		let range = if partitioned {
146			match self.partition {
147				Some(partition) => PartitionedRowKey::partition_scan_range(
148					self.table.def().id,
149					partition,
150					self.last_key.as_ref(),
151				),
152				None => PartitionedRowKey::scan_range(self.table.def().id, self.last_key.as_ref()),
153			}
154		} else {
155			RowKeyRange::scan_range(self.table.def().id.into(), self.last_key.as_ref())
156		};
157
158		let mut batch_rows = Vec::new();
159		let mut row_numbers = Vec::new();
160		let mut partitions: Vec<Partition> = Vec::new();
161		let mut new_last_key = None;
162
163		let scope = match self.min_commit_version {
164			Some(v) => RangeScope::After(v),
165			None => RangeScope::All,
166		};
167
168		let mut stream = rx.range(range, scope, batch_size as usize)?;
169
170		for _ in 0..batch_size {
171			match stream.next() {
172				Some(Ok(multi)) => {
173					let decoded = if partitioned {
174						PartitionedRowKey::decode(&multi.key).and_then(|k| match k.locator {
175							RowLocator::Row(rn) => Some((rn, Some(k.partition))),
176							_ => None,
177						})
178					} else {
179						RowKey::decode(&multi.key).map(|k| (k.row, None))
180					};
181					if let Some((rn, partition)) = decoded {
182						batch_rows.push(multi.row);
183						row_numbers.push(rn);
184						if let Some(p) = partition {
185							partitions.push(p);
186						}
187						new_last_key = Some(multi.key);
188					}
189				}
190				Some(Err(e)) => return Err(e),
191				None => {
192					self.exhausted = true;
193					break;
194				}
195			}
196		}
197
198		drop(stream);
199
200		if batch_rows.is_empty() {
201			self.exhausted = true;
202			if self.last_key.is_none() {
203				let columns: Vec<ColumnWithName> = self
204					.table
205					.columns()
206					.iter()
207					.map(|col| ColumnWithName {
208						name: Fragment::internal(&col.name),
209						data: ColumnBuffer::none_typed(col.constraint.get_type(), 0),
210					})
211					.collect();
212				return Ok(Some(Columns::new(columns)));
213			}
214			return Ok(None);
215		}
216
217		self.last_key = new_last_key;
218
219		let storage_columns: Vec<ColumnWithName> = {
220			self.table
221				.columns()
222				.iter()
223				.enumerate()
224				.map(|(idx, col)| ColumnWithName {
225					name: Fragment::internal(&col.name),
226					data: ColumnBuffer::with_capacity(self.storage_types[idx].clone(), 0),
227				})
228				.collect()
229		};
230
231		let mut columns = Columns::with_system_columns(storage_columns, Vec::new(), Vec::new(), Vec::new());
232		{
233			let shape = self.get_or_load_shape(rx, &batch_rows[0])?;
234			columns.append_rows(&shape, batch_rows.into_iter(), row_numbers.clone())?;
235		}
236		if partitioned {
237			columns.partitions = CowVec::new(partitions);
238		}
239
240		columns.row_numbers = CowVec::new(row_numbers);
241
242		decode_dictionary_columns(&mut columns, &self.dictionaries, rx)?;
243
244		Ok(Some(columns))
245	}
246
247	fn headers(&self) -> Option<ColumnHeaders> {
248		Some(self.headers.clone())
249	}
250}