Skip to main content

reifydb_engine/vm/volcano/scan/
queue.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use arrow_array::{ArrayRef, RecordBatch};
7use arrow_schema::FieldRef;
8use reifydb_codec::row::{bytes::EncodedBytes, queue::EncodedQueueRow, shape::RowShape};
9use reifydb_core::{
10	common::TimeSource,
11	interface::{catalog::dictionary::Dictionary, resolved::ResolvedQueue, store::MultiVersionRow},
12	internal_error,
13	key::{any::TaggedKey, bound::TaggedKeyBoundRange, row::RowKeyRange},
14	value::{
15		batch::{append_rows, batch},
16		column::{builder::ColumnBuilder, headers::ColumnHeaders},
17	},
18};
19use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
20use reifydb_value::value::{row_number::RowNumber, system_columns::SystemColumn, value_type::ValueType};
21use tracing::instrument;
22
23use super::{
24	super::{decode_dictionary_columns, user_pairs, with_user_columns},
25	empty_scan, scan_headers, source_system_columns,
26};
27use crate::{
28	Result,
29	vm::volcano::query::{QueryContext, QueryNode},
30};
31
32type DrainedBatch = (Vec<EncodedBytes>, Vec<RowNumber>, Option<TaggedKey>, bool);
33
34pub struct QueueScan {
35	queue: ResolvedQueue,
36	headers: ColumnHeaders,
37	shape: Option<RowShape>,
38	storage_types: Vec<ValueType>,
39	dictionaries: Vec<Option<(Dictionary, ValueType)>>,
40	last_key: Option<TaggedKey>,
41	exhausted: bool,
42	context: Option<Arc<QueryContext>>,
43	system_columns: Vec<SystemColumn>,
44}
45
46impl QueueScan {
47	pub fn new(queue: ResolvedQueue, context: Arc<QueryContext>, rx: &mut Transaction<'_>) -> Result<Self> {
48		let mut storage_types = Vec::with_capacity(queue.columns().len());
49		let mut dictionaries = Vec::with_capacity(queue.columns().len());
50
51		for col in queue.columns() {
52			if let Some(dict_id) = col.dictionary_id
53				&& let Some(dict) = context.services.catalog.find_dictionary(rx, dict_id)?
54			{
55				storage_types.push(ValueType::DictionaryId);
56				dictionaries.push(Some((dict, col.constraint.get_type())));
57				continue;
58			}
59			storage_types.push(col.constraint.get_type());
60			dictionaries.push(None);
61		}
62
63		let system_columns = source_system_columns(false, queue.def().time != TimeSource::None, false);
64		let headers = scan_headers(queue.columns().iter().map(|col| col.name.as_str()), &system_columns);
65
66		Ok(Self {
67			queue,
68			headers,
69			shape: None,
70			storage_types,
71			dictionaries,
72			last_key: None,
73			exhausted: false,
74			context: Some(context),
75			system_columns,
76		})
77	}
78
79	fn get_or_load_shape(&mut self, rx: &mut Transaction, first: &EncodedBytes) -> Result<RowShape> {
80		if let Some(shape) = &self.shape {
81			return Ok(shape.clone());
82		}
83
84		let fingerprint = EncodedQueueRow::view(first).fingerprint();
85		let stored_ctx = self.context.as_ref().expect("QueueScan context not set");
86		let shape = stored_ctx.services.catalog.get_or_load_row_shape(fingerprint, rx)?.ok_or_else(|| {
87			internal_error!(
88				"RowShape with fingerprint {:?} not found for queue {}",
89				fingerprint,
90				self.queue.def().name
91			)
92		})?;
93
94		self.shape = Some(shape.clone());
95
96		Ok(shape)
97	}
98
99	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::range_open")]
100	fn open_range<'rx, 'tx>(
101		rx: &'rx mut Transaction<'tx>,
102		range: TaggedKeyBoundRange,
103		batch_size: u64,
104	) -> Result<Box<dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>> + Send + 'rx>> {
105		rx.range_rev(range, RangeScope::All, batch_size as usize)
106	}
107
108	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::drain")]
109	fn drain_batch(
110		stream: &mut dyn Iterator<Item = Result<MultiVersionRow<TaggedKey>>>,
111		batch_size: u64,
112	) -> Result<DrainedBatch> {
113		let mut batch: Vec<EncodedBytes> = Vec::new();
114		let mut row_numbers: Vec<RowNumber> = Vec::new();
115		let mut new_last_key = None;
116		let mut drained = false;
117
118		for _ in 0..batch_size {
119			match stream.next() {
120				Some(Ok(multi)) => {
121					if let TaggedKey::Row(key) = &multi.key {
122						row_numbers.push(key.row);
123						batch.push(multi.bytes);
124						new_last_key = Some(multi.key);
125					}
126				}
127				Some(Err(e)) => return Err(e),
128				None => {
129					drained = true;
130					break;
131				}
132			}
133		}
134
135		Ok((batch, row_numbers, new_last_key, drained))
136	}
137
138	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::column_alloc")]
139	fn storage_columns(&self, shape: &RowShape, declared: usize) -> Result<Vec<(FieldRef, ArrayRef)>> {
140		let mut storage_columns: Vec<(FieldRef, ArrayRef)> = self
141			.queue
142			.columns()
143			.iter()
144			.enumerate()
145			.map(|(idx, col)| {
146				ColumnBuilder::with_capacity(self.storage_types[idx].clone(), 0).finish(&col.name)
147			})
148			.collect();
149
150		for index in declared..shape.field_count() {
151			let field = shape.get_field(index).ok_or_else(|| {
152				internal_error!("queue {} shape lost field {}", self.queue.def().name, index)
153			})?;
154			storage_columns
155				.push(ColumnBuilder::with_capacity(field.constraint.get_type(), 0).finish(&field.name));
156		}
157
158		Ok(storage_columns)
159	}
160
161	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::append_rows")]
162	fn append_batch(
163		shape: &RowShape,
164		columns: RecordBatch,
165		bytes_vec: Vec<EncodedBytes>,
166		row_numbers: Vec<RowNumber>,
167	) -> Result<RecordBatch> {
168		append_rows(columns, shape, bytes_vec, row_numbers)
169	}
170
171	fn enqueue_order_range(&self) -> TaggedKeyBoundRange {
172		RowKeyRange::scan_range(self.queue.def().id.into(), None).resume_before(self.last_key.as_ref())
173	}
174
175	fn empty_declared_columns(&self) -> Result<RecordBatch> {
176		empty_scan(
177			self.queue
178				.columns()
179				.iter()
180				.map(|col| ColumnBuilder::with_capacity(col.constraint.get_type(), 0).finish(&col.name))
181				.collect(),
182			&self.system_columns,
183		)
184	}
185}
186
187impl QueryNode for QueueScan {
188	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::initialize")]
189	fn initialize<'a>(&mut self, _rx: &mut Transaction<'a>, _ctx: &QueryContext) -> Result<()> {
190		Ok(())
191	}
192
193	#[instrument(level = "trace", skip_all, name = "volcano::scan::queue::next")]
194	fn next<'a>(&mut self, rx: &mut Transaction<'a>, _ctx: &mut QueryContext) -> Result<Option<RecordBatch>> {
195		if self.exhausted {
196			return Ok(None);
197		}
198
199		let batch_size = self.context.as_ref().expect("QueueScan context not set").batch_size;
200		let range = self.enqueue_order_range();
201
202		let (batch_rows, row_numbers, new_last_key, drained) = {
203			let mut stream = Self::open_range(rx, range, batch_size)?;
204			Self::drain_batch(&mut stream, batch_size)?
205		};
206
207		if drained {
208			self.exhausted = true;
209		}
210
211		if batch_rows.is_empty() {
212			self.exhausted = true;
213			if self.last_key.is_none() {
214				return Ok(Some(self.empty_declared_columns()?));
215			}
216			return Ok(None);
217		}
218
219		self.last_key = new_last_key;
220
221		let shape = self.get_or_load_shape(rx, &batch_rows[0])?;
222		let declared = self.queue.columns().len();
223
224		let storage_columns = self.storage_columns(&shape, declared)?;
225
226		let columns = batch(storage_columns)?;
227		let columns = Self::append_batch(&shape, columns, batch_rows, row_numbers)?;
228
229		let columns = decode_dictionary_columns(columns, &self.dictionaries, rx)?;
230
231		let mut user = user_pairs(&columns);
232		user.truncate(declared);
233
234		Ok(Some(with_user_columns(user, &columns)?))
235	}
236
237	fn headers(&self) -> Option<ColumnHeaders> {
238		Some(self.headers.clone())
239	}
240}