Skip to main content

reifydb_engine/vm/volcano/scan/
series.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use arrow_array::{Array, ArrayRef, RecordBatch, UInt64Array};
7use arrow_schema::{FieldRef, SchemaRef};
8use reifydb_codec::row::{bytes::EncodedBytes, series::EncodedSeriesRow, shape::RowShape};
9use reifydb_core::{
10	common::TimeSource,
11	interface::{
12		catalog::{series::Series, storage::StorageId},
13		resolved::ResolvedSeries,
14		store::MultiVersionRow,
15	},
16	key::{
17		any::TaggedKey,
18		bound::TaggedKeyBoundRange,
19		series::{PartitionedSeriesRowKeyRange, SeriesRowKeyRange},
20	},
21	value::{
22		batch::{batch, batch_with, decode_cells},
23		column::{
24			builder::ColumnBuilder,
25			factory::{datetime, none_typed, uint1},
26			headers::ColumnHeaders,
27		},
28	},
29};
30use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
31use reifydb_value::{
32	reifydb_assertions,
33	value::{
34		datetime::DateTime,
35		partition::Partition,
36		system_columns::{SystemColumn, system_field},
37		value_type::ValueType,
38	},
39};
40use tracing::instrument;
41
42use super::{
43	empty_scan,
44	merge::{MergeLayout, PartitionMerge},
45	partition_array, scan_headers, source_system_columns,
46};
47use crate::{
48	Result,
49	vm::{
50		instruction::dml::shape::get_or_create_series_shape,
51		volcano::{
52			decode_dictionary_column,
53			query::{QueryContext, QueryNode},
54		},
55	},
56};
57
58pub struct SeriesScanNode {
59	series: ResolvedSeries,
60	key_range_start: Option<u64>,
61	key_range_end: Option<u64>,
62	variant_tag: Option<u8>,
63	partition: Option<Partition>,
64	context: Option<Arc<QueryContext>>,
65	headers: ColumnHeaders,
66	last_key: Option<TaggedKey>,
67	exhausted: bool,
68	system_columns: Vec<SystemColumn>,
69	oldest_first: bool,
70	merge: Option<PartitionMerge>,
71	schema: Option<SchemaRef>,
72	keep_dictionary_ids: bool,
73}
74
75impl SeriesScanNode {
76	pub fn new(
77		series: ResolvedSeries,
78		key_range_start: Option<u64>,
79		key_range_end: Option<u64>,
80		variant_tag: Option<u8>,
81		partition: Option<Partition>,
82		context: Arc<QueryContext>,
83	) -> Result<Self> {
84		let mut columns = vec![series.def().key.column()];
85		if series.def().tag.is_some() {
86			columns.push("tag");
87		}
88		for col in series.columns() {
89			columns.push(col.name.as_str());
90		}
91		let system_columns = source_system_columns(
92			!series.def().partition_by.is_empty(),
93			series.def().time != TimeSource::None,
94			false,
95		);
96		let headers = scan_headers(columns.into_iter(), &system_columns);
97
98		Ok(Self {
99			series,
100			key_range_start,
101			key_range_end,
102			variant_tag,
103			partition,
104			context: Some(context),
105			headers,
106			last_key: None,
107			exhausted: false,
108			system_columns,
109			oldest_first: false,
110			keep_dictionary_ids: false,
111			merge: None,
112			schema: None,
113		})
114	}
115
116	pub(crate) fn oldest_first(mut self) -> Self {
117		self.oldest_first = true;
118		self
119	}
120
121	pub(crate) fn keep_dictionary_ids(mut self) -> Self {
122		self.keep_dictionary_ids = true;
123		self
124	}
125
126	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::range_open")]
127	fn open_range<'rx, 'tx>(
128		rx: &'rx mut Transaction<'tx>,
129		range: TaggedKeyBoundRange,
130		scope: RangeScope,
131		batch_size: u64,
132		oldest_first: bool,
133	) -> Result<Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + 'rx>> {
134		if oldest_first {
135			return rx.range_rev(range, scope, batch_size as usize);
136		}
137		rx.range(range, scope, batch_size as usize)
138	}
139
140	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::drain")]
141	fn drain_batch(
142		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>>,
143		batch_size: u64,
144		partitioned: bool,
145		has_tag: bool,
146	) -> Result<SeriesBatch> {
147		let mut batch = SeriesBatch::default();
148		let mut count = 0;
149
150		for entry in stream {
151			let entry = entry?;
152
153			let decoded: Option<(u64, u64, Option<u8>, Option<Partition>)> = if partitioned {
154				match &entry.key {
155					TaggedKey::PartitionedSeriesRow(pk) => {
156						Some((pk.key, pk.sequence, pk.variant_tag, Some(pk.partition)))
157					}
158					_ => None,
159				}
160			} else {
161				match &entry.key {
162					TaggedKey::SeriesRow(k) => Some((k.key, k.sequence, k.variant_tag, None)),
163					_ => None,
164				}
165			};
166
167			if let Some((key_val, sequence, variant_tag, partition)) = decoded {
168				batch.key_values.push(key_val);
169				batch.sequences.push(sequence);
170				if let Some(p) = partition {
171					batch.partitions.push(p);
172				}
173				let row = EncodedSeriesRow::view(&entry.bytes);
174				batch.created_at_values.push(row.created_at());
175				if let Some(time) = row.time() {
176					batch.time_values.push(time);
177				}
178				batch.updated_at_values.push(row.updated_at());
179				if has_tag {
180					batch.tags.push(variant_tag.unwrap_or(0));
181				}
182
183				batch.rows.push(entry.bytes);
184
185				batch.last_key = Some(entry.key.clone());
186				count += 1;
187				if count >= batch_size as usize {
188					break;
189				}
190			}
191		}
192
193		Ok(batch)
194	}
195
196	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::assemble")]
197	fn assemble<'a>(
198		&mut self,
199		rx: &mut Transaction<'a>,
200		stored_ctx: &QueryContext,
201		read_shape: &RowShape,
202		scanned: SeriesBatch,
203		has_tag: bool,
204		partitioned: bool,
205	) -> Result<Option<RecordBatch>> {
206		let series = self.series.def();
207		let mut result_columns = Vec::new();
208
209		result_columns.push(series.key_column_data(scanned.key_values));
210
211		if has_tag {
212			result_columns.push(uint1("tag", scanned.tags));
213		}
214
215		for (col_idx, col_def) in series.data_columns().enumerate() {
216			let col_type = col_def.constraint.get_type();
217			let dictionary = match col_def.dictionary_id {
218				Some(dict_id) => stored_ctx.services.catalog.find_dictionary(rx, dict_id)?,
219				None => None,
220			};
221			match dictionary {
222				Some(dictionary) => {
223					let mut ids = ColumnBuilder::with_capacity(
224						ValueType::DictionaryId,
225						scanned.rows.len(),
226					);
227					decode_cells(&mut ids, &col_def.name, read_shape, col_idx + 1, &scanned.rows)?;
228					if self.keep_dictionary_ids {
229						ids.set_dictionary_id(dictionary.id);
230						result_columns.push(ids.finish(&col_def.name));
231						continue;
232					}
233					let (field, array) = ids.finish(&col_def.name);
234					result_columns.push(decode_dictionary_column(
235						&field,
236						&array,
237						&dictionary,
238						col_type,
239						rx,
240					)?);
241				}
242				None => {
243					let mut data = ColumnBuilder::with_capacity(col_type, scanned.rows.len());
244					decode_cells(&mut data, &col_def.name, read_shape, col_idx + 1, &scanned.rows)?;
245					result_columns.push(data.finish(&col_def.name));
246				}
247			}
248		}
249
250		let mut stamps: Vec<(SystemColumn, ArrayRef)> =
251			vec![(SystemColumn::RowNumbers, Arc::new(UInt64Array::from(scanned.sequences)))];
252		if partitioned {
253			stamps.push((SystemColumn::Partitions, partition_array(&scanned.partitions)));
254		}
255		stamps.push((
256			SystemColumn::CreatedAt,
257			datetime(SystemColumn::CreatedAt.name(), scanned.created_at_values).1,
258		));
259		stamps.push((
260			SystemColumn::UpdatedAt,
261			datetime(SystemColumn::UpdatedAt.name(), scanned.updated_at_values).1,
262		));
263		if !scanned.time_values.is_empty() {
264			stamps.push((SystemColumn::Time, datetime(SystemColumn::Time.name(), scanned.time_values).1));
265		}
266		let row_count = result_columns[0].1.len();
267		result_columns.extend(stamps
268			.into_iter()
269			.map(|(column, array)| (system_field(column, array.logical_null_count() > 0), array)));
270		let out = match &self.schema {
271			Some(schema) => batch_with(schema, result_columns, row_count)?,
272			None => batch(result_columns)?,
273		};
274		self.schema = Some(out.schema());
275		Ok(Some(out))
276	}
277}
278
279#[derive(Default)]
280struct SeriesBatch {
281	key_values: Vec<u64>,
282	tags: Vec<u8>,
283	sequences: Vec<u64>,
284	partitions: Vec<Partition>,
285	created_at_values: Vec<DateTime>,
286	time_values: Vec<DateTime>,
287	updated_at_values: Vec<DateTime>,
288	rows: Vec<EncodedBytes>,
289	last_key: Option<TaggedKey>,
290}
291
292pub(crate) fn empty_series_columns(series: &Series) -> Vec<(FieldRef, ArrayRef)> {
293	let key_type = series
294		.columns
295		.iter()
296		.find(|c| c.name == series.key.column())
297		.map(|c| c.constraint.get_type())
298		.unwrap_or(ValueType::Int8);
299
300	let mut result_columns = Vec::new();
301	result_columns.push(none_typed(series.key.column(), key_type, 0));
302	if series.tag.is_some() {
303		result_columns.push(none_typed("tag", ValueType::Uint1, 0));
304	}
305	for col_def in series.data_columns() {
306		result_columns.push(none_typed(&col_def.name, col_def.constraint.get_type(), 0));
307	}
308	result_columns
309}
310
311impl QueryNode for SeriesScanNode {
312	#[instrument(name = "volcano::scan::series::initialize", level = "trace", skip_all)]
313	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
314		Ok(())
315	}
316
317	#[instrument(name = "volcano::scan::series::next", level = "trace", skip_all)]
318	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
319		reifydb_assertions! {
320			assert!(self.context.is_some(), "SeriesScanNode::next() called before initialize()");
321		}
322		let stored_ctx = self.context.as_ref().unwrap();
323
324		if self.exhausted {
325			return Ok(None);
326		}
327
328		let batch_size = stored_ctx.batch_size;
329		let series = self.series.def();
330		let has_tag = series.tag.is_some();
331
332		if let (Some(start), Some(end)) = (self.key_range_start, self.key_range_end)
333			&& start >= end
334		{
335			self.exhausted = true;
336			return Ok(Some(empty_scan(empty_series_columns(series), &self.system_columns)?));
337		}
338
339		let partitioned = !series.partition_by.is_empty();
340		let storage = StorageId::series(series.id);
341		let after = if self.oldest_first {
342			None
343		} else {
344			self.last_key.as_ref()
345		};
346		let range = if partitioned {
347			match self.partition {
348				Some(partition) => PartitionedSeriesRowKeyRange::scan_range(
349					storage,
350					partition,
351					has_tag,
352					self.variant_tag,
353					self.key_range_start,
354					self.key_range_end,
355					after,
356				),
357				None => PartitionedSeriesRowKeyRange::full_scan_range(storage, after),
358			}
359		} else {
360			SeriesRowKeyRange::scan_range(
361				storage,
362				has_tag,
363				self.variant_tag,
364				self.key_range_start,
365				self.key_range_end,
366				after,
367			)
368		};
369		let range = if self.oldest_first {
370			range.resume_before(self.last_key.as_ref())
371		} else {
372			range
373		};
374
375		let read_shape = get_or_create_series_shape(&stored_ctx.services.catalog, self.series.def(), rx)?;
376		let stored_ctx = stored_ctx.clone();
377
378		let scope = RangeScope::All;
379
380		let batch = if self.oldest_first && partitioned {
381			let fixed = self.partition.map(|_| range);
382			let merge = self
383				.merge
384				.get_or_insert_with(|| PartitionMerge::new(MergeLayout::Series, storage, fixed));
385			let rows = merge.next(rx, batch_size)?;
386			Self::drain_batch(&mut rows.into_iter().map(Ok), batch_size, partitioned, has_tag)?
387		} else {
388			let mut stream = Self::open_range(rx, range, scope, batch_size, self.oldest_first)?;
389			Self::drain_batch(&mut stream, batch_size, partitioned, has_tag)?
390		};
391
392		if batch.key_values.is_empty() {
393			self.exhausted = true;
394			if self.last_key.is_none() {
395				return Ok(Some(empty_scan(
396					empty_series_columns(self.series.def()),
397					&self.system_columns,
398				)?));
399			}
400			return Ok(None);
401		}
402
403		self.last_key = batch.last_key.clone();
404
405		self.assemble(rx, &stored_ctx, &read_shape, batch, has_tag, partitioned)
406	}
407
408	fn headers(&self) -> Option<ColumnHeaders> {
409		Some(self.headers.clone())
410	}
411}