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