Skip to main content

reifydb_engine/vm/volcano/scan/
ringbuffer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{ops::Bound, sync::Arc};
5
6use arrow_array::{ArrayRef, RecordBatch};
7use arrow_schema::FieldRef;
8use reifydb_codec::row::{bytes::EncodedBytes, ringbuffer::EncodedRingBufferRow, shape::RowShape};
9use reifydb_core::{
10	common::TimeSource,
11	interface::{
12		catalog::{dictionary::Dictionary, ringbuffer::PartitionedMetadata},
13		resolved::ResolvedRingBuffer,
14		store::MultiVersionRow,
15	},
16	internal_error,
17	key::{
18		any::TaggedKey,
19		row::{PartitionedRowKey, StorageRowKey},
20	},
21	partition::partition_col_indices,
22	value::{
23		batch::{append_rows, batch},
24		column::{builder::ColumnBuilder, headers::ColumnHeaders},
25	},
26};
27use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
28use reifydb_value::value::{
29	partition::Partition,
30	row_number::RowNumber,
31	system_columns::{SystemColumn, with_system_column},
32	value_type::ValueType,
33};
34use tracing::instrument;
35
36use super::{
37	super::decode_dictionary_columns,
38	empty_scan,
39	merge::{MergeLayout, PartitionMerge},
40	partition_array, scan_headers, source_system_columns, storage_partitioned_row,
41};
42use crate::{
43	Result,
44	vm::volcano::query::{QueryContext, QueryNode},
45};
46
47pub struct RingBufferScan {
48	ringbuffer: ResolvedRingBuffer,
49
50	partitions: Vec<PartitionedMetadata>,
51	current_partition_index: usize,
52	headers: ColumnHeaders,
53	shape: Option<RowShape>,
54
55	storage_types: Vec<ValueType>,
56
57	dictionaries: Vec<Option<(Dictionary, ValueType)>>,
58
59	partition_col_indices: Vec<usize>,
60	current_partition_rows: Vec<(RowNumber, EncodedBytes)>,
61	current_partition_cursor: usize,
62	current_partition_loaded: bool,
63	finished: bool,
64	context: Option<Arc<QueryContext>>,
65	initialized: bool,
66	system_columns: Vec<SystemColumn>,
67	merge: Option<PartitionMerge>,
68	keep_dictionary_ids: bool,
69}
70
71impl RingBufferScan {
72	pub fn new(
73		ringbuffer: ResolvedRingBuffer,
74		context: Arc<QueryContext>,
75		rx: &mut Transaction<'_>,
76	) -> Result<Self> {
77		let mut storage_types = Vec::with_capacity(ringbuffer.columns().len());
78		let mut dictionaries = Vec::with_capacity(ringbuffer.columns().len());
79
80		for col in ringbuffer.columns() {
81			if let Some(dict_id) = col.dictionary_id {
82				if let Some(dict) = context.services.catalog.find_dictionary(rx, dict_id)? {
83					storage_types.push(ValueType::DictionaryId);
84					dictionaries.push(Some((dict, col.constraint.get_type())));
85				} else {
86					storage_types.push(col.constraint.get_type());
87					dictionaries.push(None);
88				}
89			} else {
90				storage_types.push(col.constraint.get_type());
91				dictionaries.push(None);
92			}
93		}
94
95		let partition_col_indices = partition_col_indices(ringbuffer.columns(), &ringbuffer.def().partition_by);
96
97		let system_columns = source_system_columns(
98			!partition_col_indices.is_empty(),
99			ringbuffer.def().time != TimeSource::None,
100			false,
101		);
102		let headers = scan_headers(ringbuffer.columns().iter().map(|col| col.name.as_str()), &system_columns);
103
104		Ok(Self {
105			ringbuffer,
106			partitions: Vec::new(),
107			current_partition_index: 0,
108			headers,
109			shape: None,
110			storage_types,
111			dictionaries,
112			partition_col_indices,
113			current_partition_rows: Vec::new(),
114			current_partition_cursor: 0,
115			current_partition_loaded: false,
116			finished: false,
117			context: Some(context),
118			initialized: false,
119			system_columns,
120			merge: None,
121			keep_dictionary_ids: false,
122		})
123	}
124
125	pub(crate) fn oldest_first(mut self) -> Self {
126		if !self.partition_col_indices.is_empty() {
127			self.merge = Some(PartitionMerge::new(MergeLayout::Row, self.ringbuffer.def().id.into(), None));
128		}
129		self
130	}
131
132	pub(crate) fn keep_dictionary_ids(mut self) -> Self {
133		self.keep_dictionary_ids = true;
134		self
135	}
136
137	fn get_or_load_shape(&mut self, rx: &mut Transaction, first: &EncodedBytes) -> Result<RowShape> {
138		if let Some(shape) = &self.shape {
139			return Ok(shape.clone());
140		}
141
142		let fingerprint = EncodedRingBufferRow::view(first).fingerprint();
143
144		let stored_ctx = self.context.as_ref().expect("RingBufferScan context not set");
145		let shape = stored_ctx.services.catalog.get_or_load_row_shape(fingerprint, rx)?.ok_or_else(|| {
146			internal_error!(
147				"RowShape with fingerprint {:?} not found for ringbuffer {}",
148				fingerprint,
149				self.ringbuffer.def().name
150			)
151		})?;
152
153		self.shape = Some(shape.clone());
154
155		Ok(shape)
156	}
157
158	#[instrument(level = "trace", skip_all, name = "volcano::scan::ringbuffer::drain")]
159	fn drain_batch(
160		&mut self,
161		txn: &mut Transaction<'_>,
162		batch_size: usize,
163		partitioned: bool,
164	) -> Result<(Vec<EncodedBytes>, Vec<RowNumber>, Vec<Partition>)> {
165		let mut batch: Vec<EncodedBytes> = Vec::new();
166		let mut row_numbers: Vec<RowNumber> = Vec::new();
167		let mut partitions_sidecar: Vec<Partition> = Vec::new();
168
169		while batch.len() < batch_size && self.current_partition_index < self.partitions.len() {
170			if !self.current_partition_loaded {
171				self.current_partition_rows =
172					self.load_partition_rows(txn, self.current_partition_index)?;
173				self.current_partition_cursor = 0;
174				self.current_partition_loaded = true;
175			}
176
177			#[allow(clippy::disallowed_methods)]
178			let hash = if partitioned {
179				Some(Partition::of(&self.partitions[self.current_partition_index].partition_values))
180			} else {
181				None
182			};
183
184			while batch.len() < batch_size
185				&& self.current_partition_cursor < self.current_partition_rows.len()
186			{
187				let (rn, row) = self.current_partition_rows[self.current_partition_cursor].clone();
188				batch.push(row);
189				row_numbers.push(rn);
190				if let Some(h) = hash {
191					partitions_sidecar.push(h);
192				}
193				self.current_partition_cursor += 1;
194			}
195
196			if self.current_partition_cursor >= self.current_partition_rows.len() {
197				self.current_partition_index += 1;
198				self.current_partition_loaded = false;
199			}
200		}
201
202		Ok((batch, row_numbers, partitions_sidecar))
203	}
204
205	#[instrument(level = "trace", skip_all, name = "volcano::scan::ringbuffer::column_alloc")]
206	fn storage_columns(&self) -> Vec<(FieldRef, ArrayRef)> {
207		self.ringbuffer
208			.columns()
209			.iter()
210			.enumerate()
211			.map(|(idx, col)| {
212				ColumnBuilder::with_capacity(self.storage_types[idx].clone(), 0).finish(&col.name)
213			})
214			.collect()
215	}
216
217	#[instrument(level = "trace", skip_all, name = "volcano::scan::ringbuffer::empty_columns")]
218	fn empty_columns(&self) -> Vec<(FieldRef, ArrayRef)> {
219		self.ringbuffer
220			.columns()
221			.iter()
222			.map(|col| ColumnBuilder::with_capacity(col.constraint.get_type(), 0).finish(&col.name))
223			.collect()
224	}
225
226	#[instrument(level = "trace", skip_all, name = "volcano::scan::ringbuffer::append_rows")]
227	fn append_batch(
228		&mut self,
229		txn: &mut Transaction<'_>,
230		columns: RecordBatch,
231		bytes_vec: Vec<EncodedBytes>,
232		row_numbers: Vec<RowNumber>,
233	) -> Result<RecordBatch> {
234		let shape = self.get_or_load_shape(txn, &bytes_vec[0])?;
235		append_rows(columns, &shape, bytes_vec, row_numbers)
236	}
237
238	#[instrument(level = "trace", skip_all, name = "volcano::scan::ringbuffer::load_partition")]
239	fn load_partition_rows(
240		&self,
241		txn: &mut Transaction<'_>,
242		partition_index: usize,
243	) -> Result<Vec<(RowNumber, EncodedBytes)>> {
244		let pm = &self.partitions[partition_index];
245		let rb_id = self.ringbuffer.def().id;
246
247		if self.partition_col_indices.is_empty() {
248			if pm.metadata.head >= pm.metadata.tail {
249				return Ok(Vec::new());
250			}
251			let mut out = txn
252				.range_row(
253					rb_id.into(),
254					Bound::Included(StorageRowKey::new(RowNumber(pm.metadata.tail - 1))),
255					Bound::Included(StorageRowKey::new(RowNumber(pm.metadata.head))),
256					RangeScope::All,
257					1024,
258				)?
259				.map(|entry| entry.map(|entry| (entry.key.row(), entry.bytes)))
260				.collect::<Result<Vec<_>>>()?;
261			out.sort_by_key(|(rn, _)| rn.0);
262			return Ok(out);
263		}
264
265		#[allow(clippy::disallowed_methods)]
266		let hash = Partition::of(&pm.partition_values);
267		let mut out = Vec::new();
268		let mut last_key = None;
269		loop {
270			let batch: Vec<_> = txn
271				.range(
272					PartitionedRowKey::partition_scan_range(rb_id, hash, last_key.as_ref()),
273					RangeScope::All,
274					1024,
275				)?
276				.collect::<Result<Vec<_>>>()?;
277			if batch.is_empty() {
278				break;
279			}
280			let n = batch.len();
281			for entry in batch {
282				if let TaggedKey::PartitionedRow(pk) = &entry.key {
283					out.push((pk.row, entry.bytes));
284				}
285				last_key = Some(entry.key.clone());
286			}
287			if n < 1024 {
288				break;
289			}
290		}
291		out.sort_by_key(|(rn, _)| rn.0);
292		Ok(out)
293	}
294}
295
296impl QueryNode for RingBufferScan {
297	#[instrument(name = "volcano::scan::ringbuffer::initialize", level = "trace", skip_all)]
298	fn initialize<'a>(&mut self, txn: &mut Transaction<'a>, ctx: &QueryContext) -> Result<()> {
299		if !self.initialized {
300			self.partitions =
301				ctx.services.catalog.list_ringbuffer_partitions(txn, self.ringbuffer.def())?;
302			self.initialized = true;
303		}
304		Ok(())
305	}
306
307	#[instrument(name = "volcano::scan::ringbuffer::next", level = "trace", skip_all)]
308	fn next<'a>(&mut self, txn: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
309		if self.finished {
310			return Ok(None);
311		}
312
313		let batch_size = self.context.as_ref().expect("RingBufferScan context not set").batch_size as usize;
314		let partitioned = !self.partition_col_indices.is_empty();
315
316		let (batch_rows, row_numbers, partitions_sidecar) = match self.merge.as_mut() {
317			Some(merge) => merged_batch(merge.next(txn, batch_size as u64)?)?,
318			None => self.drain_batch(txn, batch_size, partitioned)?,
319		};
320
321		if !batch_rows.is_empty() {
322			let columns = batch(self.storage_columns())?;
323			let mut columns = self.append_batch(txn, columns, batch_rows, row_numbers)?;
324			if partitioned {
325				columns = with_system_column(
326					columns,
327					SystemColumn::Partitions,
328					partition_array(&partitions_sidecar),
329				)?;
330			}
331
332			if self.keep_dictionary_ids {
333				return Ok(Some(columns));
334			}
335			return Ok(Some(decode_dictionary_columns(columns, &self.dictionaries, txn)?));
336		}
337
338		self.finished = true;
339		if self.partitions.is_empty() || self.partitions.iter().all(|p| p.metadata.is_empty()) {
340			return Ok(Some(empty_scan(self.empty_columns(), &self.system_columns)?));
341		}
342		Ok(None)
343	}
344
345	fn headers(&self) -> Option<ColumnHeaders> {
346		Some(self.headers.clone())
347	}
348}
349
350fn merged_batch(rows: Vec<MultiVersionRow<TaggedKey>>) -> Result<(Vec<EncodedBytes>, Vec<RowNumber>, Vec<Partition>)> {
351	let mut batch = Vec::with_capacity(rows.len());
352	let mut row_numbers = Vec::with_capacity(rows.len());
353	let mut partitions = Vec::with_capacity(rows.len());
354	for row in rows {
355		let row = storage_partitioned_row(row)?;
356		batch.push(row.bytes);
357		row_numbers.push(row.key.row());
358		partitions.push(row.key.partition());
359	}
360	Ok((batch, row_numbers, partitions))
361}