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 reifydb_codec::row::{series::EncodedSeriesRow, shape::RowShape};
7use reifydb_core::{
8	common::CommitVersion,
9	interface::{catalog::storage::StorageId, resolved::ResolvedSeries, store::MultiVersionRow},
10	key::{
11		any::TaggedKey,
12		bound::TaggedKeyBoundRange,
13		series::{PartitionedSeriesRowKeyRange, SeriesRowKeyRange},
14	},
15	value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns, headers::ColumnHeaders},
16};
17use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
18use reifydb_value::{
19	fragment::Fragment,
20	reifydb_assertions,
21	value::{
22		Value, datetime::DateTime, dictionary::DictionaryEntryId, partition::Partition, row_number::RowNumber,
23		system_columns::SystemColumns, value_type::ValueType,
24	},
25};
26use tracing::instrument;
27
28use crate::{
29	Result,
30	transaction::operation::dictionary::DictionaryOperations,
31	vm::{
32		instruction::dml::shape::get_or_create_series_shape,
33		volcano::query::{QueryContext, QueryNode},
34	},
35};
36
37pub struct SeriesScanNode {
38	series: ResolvedSeries,
39	key_range_start: Option<u64>,
40	key_range_end: Option<u64>,
41	variant_tag: Option<u8>,
42	partition: Option<Partition>,
43	context: Option<Arc<QueryContext>>,
44	headers: ColumnHeaders,
45	last_key: Option<TaggedKey>,
46	exhausted: bool,
47
48	min_commit_version: Option<CommitVersion>,
49}
50
51impl SeriesScanNode {
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		series: ResolvedSeries,
59		key_range_start: Option<u64>,
60		key_range_end: Option<u64>,
61		variant_tag: Option<u8>,
62		partition: Option<Partition>,
63		context: Arc<QueryContext>,
64	) -> Result<Self> {
65		let mut columns = vec![Fragment::internal(series.def().key.column())];
66		if series.def().tag.is_some() {
67			columns.push(Fragment::internal("tag"));
68		}
69		for col in series.columns() {
70			columns.push(Fragment::internal(&col.name));
71		}
72		let headers = ColumnHeaders {
73			columns,
74		};
75
76		Ok(Self {
77			series,
78			key_range_start,
79			key_range_end,
80			variant_tag,
81			partition,
82			context: Some(context),
83			headers,
84			last_key: None,
85			exhausted: false,
86			min_commit_version: None,
87		})
88	}
89
90	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::range_open")]
91	fn open_range<'rx, 'tx>(
92		rx: &'rx mut Transaction<'tx>,
93		range: TaggedKeyBoundRange,
94		scope: RangeScope,
95		batch_size: u64,
96	) -> Result<Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + 'rx>> {
97		rx.range(range, scope, batch_size as usize)
98	}
99
100	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::drain")]
101	fn drain_batch(
102		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>>,
103		batch_size: u64,
104		partitioned: bool,
105		has_tag: bool,
106		data_column_count: usize,
107		read_shape: &RowShape,
108	) -> Result<SeriesBatch> {
109		let mut batch = SeriesBatch::default();
110		let mut count = 0;
111
112		for entry in stream {
113			let entry = entry?;
114
115			let decoded: Option<(u64, u64, Option<u8>, Option<Partition>)> = if partitioned {
116				match &entry.key {
117					TaggedKey::PartitionedSeriesRow(pk) => {
118						Some((pk.key, pk.sequence, pk.variant_tag, Some(pk.partition)))
119					}
120					_ => None,
121				}
122			} else {
123				match &entry.key {
124					TaggedKey::SeriesRow(k) => Some((k.key, k.sequence, k.variant_tag, None)),
125					_ => None,
126				}
127			};
128
129			if let Some((key_val, sequence, variant_tag, partition)) = decoded {
130				batch.key_values.push(key_val);
131				batch.sequences.push(sequence);
132				if let Some(p) = partition {
133					batch.partitions.push(p);
134				}
135				let row = EncodedSeriesRow::view(&entry.bytes);
136				batch.created_at_values.push(row.created_at());
137				if let Some(time) = row.time() {
138					batch.time_values.push(time);
139				}
140				batch.updated_at_values.push(row.updated_at());
141				if has_tag {
142					batch.tags.push(variant_tag.unwrap_or(0));
143				}
144
145				let mut values = Vec::with_capacity(data_column_count);
146				for i in 0..data_column_count {
147					values.push(read_shape.get_value(&entry.bytes, i + 1));
148				}
149				batch.data_rows.push(values);
150
151				batch.last_key = Some(entry.key.clone());
152				count += 1;
153				if count >= batch_size as usize {
154					break;
155				}
156			}
157		}
158
159		Ok(batch)
160	}
161
162	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::empty_columns")]
163	fn empty_columns(&self, has_tag: bool) -> Vec<ColumnWithName> {
164		let series = self.series.def();
165		let key_type = series
166			.columns
167			.iter()
168			.find(|c| c.name == series.key.column())
169			.map(|c| c.constraint.get_type())
170			.unwrap_or(ValueType::Int8);
171
172		let mut result_columns = Vec::new();
173		result_columns.push(ColumnWithName {
174			name: Fragment::internal(series.key.column()),
175			data: ColumnBuffer::none_typed(key_type, 0),
176		});
177		if has_tag {
178			result_columns.push(ColumnWithName {
179				name: Fragment::internal("tag"),
180				data: ColumnBuffer::none_typed(ValueType::Uint1, 0),
181			});
182		}
183		for col_def in series.data_columns() {
184			result_columns.push(ColumnWithName {
185				name: Fragment::internal(&col_def.name),
186				data: ColumnBuffer::none_typed(col_def.constraint.get_type(), 0),
187			});
188		}
189		result_columns
190	}
191
192	#[instrument(level = "trace", skip_all, name = "volcano::scan::series::assemble")]
193	fn assemble<'a>(
194		&self,
195		rx: &mut Transaction<'a>,
196		stored_ctx: &QueryContext,
197		batch: SeriesBatch,
198		has_tag: bool,
199		partitioned: bool,
200	) -> Result<Option<Columns>> {
201		let series = self.series.def();
202		let mut result_columns = Vec::new();
203
204		result_columns.push(ColumnWithName::new(
205			Fragment::internal(series.key.column()),
206			series.key_column_data(batch.key_values),
207		));
208
209		if has_tag {
210			result_columns
211				.push(ColumnWithName::new(Fragment::internal("tag"), ColumnBuffer::uint1(batch.tags)));
212		}
213
214		for (col_idx, col_def) in series.data_columns().enumerate() {
215			let col_type = col_def.constraint.get_type();
216			let mut col_values: Vec<Value> = batch
217				.data_rows
218				.iter()
219				.map(|row| row.get(col_idx).cloned().unwrap_or(Value::none()))
220				.collect();
221
222			if let Some(dict_id) = col_def.dictionary_id
223				&& let Some(dictionary) = stored_ctx.services.catalog.find_dictionary(rx, dict_id)?
224			{
225				for value in col_values.iter_mut() {
226					if let Some(entry_id) = DictionaryEntryId::from_value(value) {
227						*value = rx
228							.get_from_dictionary(&dictionary, entry_id)?
229							.unwrap_or_else(Value::none);
230					}
231				}
232			}
233
234			result_columns.push(build_data_column(&col_def.name, &col_values, col_type)?);
235		}
236
237		let row_numbers: Vec<RowNumber> = batch.sequences.into_iter().map(RowNumber::from).collect();
238		let mut result = Columns::with_system(
239			result_columns,
240			SystemColumns::new(
241				row_numbers,
242				Vec::new(),
243				batch.created_at_values,
244				batch.updated_at_values,
245				batch.time_values,
246			),
247		);
248		if partitioned {
249			result.system.set_partitions(batch.partitions);
250		}
251		Ok(Some(result))
252	}
253}
254
255#[derive(Default)]
256struct SeriesBatch {
257	key_values: Vec<u64>,
258	tags: Vec<u8>,
259	sequences: Vec<u64>,
260	partitions: Vec<Partition>,
261	created_at_values: Vec<DateTime>,
262	time_values: Vec<DateTime>,
263	updated_at_values: Vec<DateTime>,
264	data_rows: Vec<Vec<Value>>,
265	last_key: Option<TaggedKey>,
266}
267
268impl QueryNode for SeriesScanNode {
269	#[instrument(name = "volcano::scan::series::initialize", level = "trace", skip_all)]
270	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
271		Ok(())
272	}
273
274	#[instrument(name = "volcano::scan::series::next", level = "trace", skip_all)]
275	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<Columns>> {
276		reifydb_assertions! {
277			assert!(self.context.is_some(), "SeriesScanNode::next() called before initialize()");
278		}
279		let stored_ctx = self.context.as_ref().unwrap();
280
281		if self.exhausted {
282			return Ok(None);
283		}
284
285		let batch_size = stored_ctx.batch_size;
286		let series = self.series.def();
287		let has_tag = series.tag.is_some();
288
289		let partitioned = !series.partition_by.is_empty();
290		let storage = StorageId::series(series.id);
291		let range = if partitioned {
292			match self.partition {
293				Some(partition) => PartitionedSeriesRowKeyRange::scan_range(
294					storage,
295					partition,
296					self.variant_tag,
297					self.key_range_start,
298					self.key_range_end,
299					self.last_key.as_ref(),
300				),
301				None => PartitionedSeriesRowKeyRange::full_scan_range(storage, self.last_key.as_ref()),
302			}
303		} else {
304			SeriesRowKeyRange::scan_range(
305				storage,
306				self.variant_tag,
307				self.key_range_start,
308				self.key_range_end,
309				self.last_key.as_ref(),
310			)
311		};
312
313		let read_shape = get_or_create_series_shape(&stored_ctx.services.catalog, self.series.def(), rx)?;
314		let stored_ctx = stored_ctx.clone();
315
316		let scope = match self.min_commit_version {
317			Some(v) => RangeScope::After(v),
318			None => RangeScope::All,
319		};
320
321		let data_column_count = series.data_columns().count();
322		let batch = {
323			let mut stream = Self::open_range(rx, range, scope, batch_size)?;
324			Self::drain_batch(
325				&mut stream,
326				batch_size,
327				partitioned,
328				has_tag,
329				data_column_count,
330				&read_shape,
331			)?
332		};
333
334		if batch.key_values.is_empty() {
335			self.exhausted = true;
336			if self.last_key.is_none() {
337				return Ok(Some(Columns::new(self.empty_columns(has_tag))));
338			}
339			return Ok(None);
340		}
341
342		self.last_key = batch.last_key.clone();
343
344		self.assemble(rx, &stored_ctx, batch, has_tag, partitioned)
345	}
346
347	fn headers(&self) -> Option<ColumnHeaders> {
348		Some(self.headers.clone())
349	}
350}
351
352pub(crate) fn build_data_column(name: &str, values: &[Value], col_type: ValueType) -> Result<ColumnWithName> {
353	let data = match col_type {
354		ValueType::Boolean => {
355			let vals: Vec<bool> = values
356				.iter()
357				.map(|v| match v {
358					Value::Boolean(b) => *b,
359					_ => false,
360				})
361				.collect();
362			ColumnBuffer::bool(vals)
363		}
364		ValueType::Int1 => {
365			let vals: Vec<i8> = values
366				.iter()
367				.map(|v| match v {
368					Value::Int1(n) => *n,
369					_ => 0,
370				})
371				.collect();
372			ColumnBuffer::int1(vals)
373		}
374		ValueType::Int2 => {
375			let vals: Vec<i16> = values
376				.iter()
377				.map(|v| match v {
378					Value::Int2(n) => *n,
379					_ => 0,
380				})
381				.collect();
382			ColumnBuffer::int2(vals)
383		}
384		ValueType::Int4 => {
385			let vals: Vec<i32> = values
386				.iter()
387				.map(|v| match v {
388					Value::Int4(n) => *n,
389					_ => 0,
390				})
391				.collect();
392			ColumnBuffer::int4(vals)
393		}
394		ValueType::Int8 => {
395			let vals: Vec<i64> = values
396				.iter()
397				.map(|v| match v {
398					Value::Int8(n) => *n,
399					_ => 0,
400				})
401				.collect();
402			ColumnBuffer::int8(vals)
403		}
404		ValueType::Uint1 => {
405			let vals: Vec<u8> = values
406				.iter()
407				.map(|v| match v {
408					Value::Uint1(n) => *n,
409					_ => 0,
410				})
411				.collect();
412			ColumnBuffer::uint1(vals)
413		}
414		ValueType::Uint2 => {
415			let vals: Vec<u16> = values
416				.iter()
417				.map(|v| match v {
418					Value::Uint2(n) => *n,
419					_ => 0,
420				})
421				.collect();
422			ColumnBuffer::uint2(vals)
423		}
424		ValueType::Uint4 => {
425			let vals: Vec<u32> = values
426				.iter()
427				.map(|v| match v {
428					Value::Uint4(n) => *n,
429					_ => 0,
430				})
431				.collect();
432			ColumnBuffer::uint4(vals)
433		}
434		ValueType::Uint8 => {
435			let vals: Vec<u64> = values
436				.iter()
437				.map(|v| match v {
438					Value::Uint8(n) => *n,
439					_ => 0,
440				})
441				.collect();
442			ColumnBuffer::uint8(vals)
443		}
444		ValueType::Float4 => {
445			let vals: Vec<f32> = values
446				.iter()
447				.map(|v| match v {
448					Value::Float4(n) => n.value(),
449					_ => 0.0,
450				})
451				.collect();
452			ColumnBuffer::float4(vals)
453		}
454		ValueType::Float8 => {
455			let vals: Vec<f64> = values
456				.iter()
457				.map(|v| match v {
458					Value::Float8(n) => n.value(),
459					_ => 0.0,
460				})
461				.collect();
462			ColumnBuffer::float8(vals)
463		}
464		ValueType::Utf8 => {
465			let vals: Vec<String> = values
466				.iter()
467				.map(|v| match v {
468					Value::Utf8(s) => s.clone(),
469					_ => String::new(),
470				})
471				.collect();
472			ColumnBuffer::utf8(vals)
473		}
474		_ => {
475			let vals: Vec<String> = values.iter().map(|v| format!("{:?}", v)).collect();
476			ColumnBuffer::utf8(vals)
477		}
478	};
479
480	Ok(ColumnWithName {
481		name: Fragment::internal(name),
482		data,
483	})
484}