Skip to main content

reifydb_engine/vm/instruction/dml/
queue_insert.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, sync::Arc};
5
6use postcard::to_stdvec;
7use reifydb_codec::row::{
8	bytes::{EncodedBytes, RowBuilder},
9	queue::EncodedQueueRow,
10	queue_deduplication::EncodedQueueDeduplicationRow,
11	shape::RowShape,
12};
13use reifydb_core::{
14	error::diagnostic::catalog::{
15		namespace_not_found, queue_deduplication_key_not_utf8, queue_not_before_not_datetime, queue_not_found,
16	},
17	interface::{
18		catalog::{
19			config::{ConfigKey, GetConfig},
20			namespace::Namespace,
21			policy::{DataOp, PolicyTargetType},
22			queue::{Queue, decode_queue_deduplication, encode_queue_deduplication},
23		},
24		resolved::{ResolvedColumn, ResolvedNamespace, ResolvedObject, ResolvedQueue},
25	},
26	internal_error,
27	key::{queue::QueueDeduplicationKey, row::RowKey},
28	return_internal_error,
29	value::column::{buffer::ColumnBuffer, columns::Columns},
30};
31use reifydb_evaluate::stack::SymbolTable;
32use reifydb_rql::{
33	expression::Expression,
34	nodes::{InsertQueueNode, QUEUE_CREATED_COLUMN, QUEUE_DEDUPLICATION_KEY_FIELD, QUEUE_NOT_BEFORE_FIELD},
35};
36use reifydb_transaction::transaction::Transaction;
37use reifydb_value::{
38	fragment::Fragment,
39	params::Params,
40	return_error,
41	value::{Value, datetime::DateTime, duration::Duration, identity::IdentityId, row_number::RowNumber},
42};
43use tracing::instrument;
44
45use super::{
46	returning::{decode_returning_dictionaries, decode_rows_to_columns, evaluate_returning},
47	shape::get_or_create_queue_shape,
48};
49use crate::{
50	Result,
51	policy::PolicyEvaluator,
52	queue::partition::{ordered_by_index, placement_of},
53	transaction::operation::{
54		dictionary::DictionaryOperations,
55		queue::{QueueInsertRow, QueueOperations},
56	},
57	vm::{
58		instruction::dml::{coerce::coerce_value_to_column_type, time::resolve_time},
59		services::Services,
60		volcano::{
61			compile::compile,
62			query::{QueryContext, QueryNode, query_budget},
63		},
64	},
65};
66
67struct QueueTarget<'a> {
68	namespace: &'a Namespace,
69	queue: &'a Queue,
70}
71
72#[instrument(name = "mutate::queue::insert", level = "trace", skip_all)]
73pub(crate) fn insert_queue(
74	services: &Arc<Services>,
75	txn: &mut Transaction<'_>,
76	plan: InsertQueueNode,
77	symbols: &mut SymbolTable,
78) -> Result<Columns> {
79	let InsertQueueNode {
80		input,
81		target,
82		has_deduplication,
83		has_not_before,
84		returning,
85	} = plan;
86
87	let (namespace, queue) = resolve_insert_queue_target(services, txn, &target)?;
88	let shape = get_or_create_queue_shape(&services.catalog, &queue, txn)?;
89	let target_data = QueueTarget {
90		namespace: &namespace,
91		queue: &queue,
92	};
93
94	let context = build_insert_queue_query_context(services, &target_data, symbols, txn.identity());
95	let mut input_node = compile(*input, txn, context.clone());
96	input_node.initialize(txn, &context)?;
97
98	let pending = validate_and_encode_input_rows(
99		services,
100		txn,
101		&target_data,
102		&shape,
103		&context,
104		symbols,
105		&mut input_node,
106		has_deduplication,
107		has_not_before,
108	)?;
109
110	if pending.is_empty() {
111		return Ok(insert_queue_result(namespace.name(), &queue.name, 0, 0));
112	}
113
114	let now = services.runtime_context.clock.now();
115	let outcomes = resolve_duplicates(txn, &queue, &shape, &pending, now)?;
116
117	let fresh_count = outcomes.iter().filter(|outcome| matches!(outcome, Outcome::Fresh)).count();
118	let duplicates = outcomes.len() - fresh_count;
119
120	let row_numbers = if fresh_count == 0 {
121		Vec::new()
122	} else {
123		services.catalog.next_row_number_batch_for_queue(txn, queue.id, fresh_count as u64)?
124	};
125
126	let ordered_by_index = ordered_by_index(&queue)?;
127	let mut assigned = row_numbers.into_iter();
128	let mut rows: Vec<QueueInsertRow> = Vec::with_capacity(fresh_count);
129	let mut returned: Vec<ReturnedRow> = Vec::with_capacity(outcomes.len());
130
131	for (item, outcome) in pending.iter().zip(outcomes.into_iter()) {
132		match outcome {
133			Outcome::Fresh => {
134				let row_number = assigned.next().expect("a row number per fresh item");
135				if let Some(key) = &item.deduplication_key {
136					write_deduplication_record(txn, &queue, key, row_number, now)?;
137				}
138				let placement = placement_of(
139					&queue,
140					&shape,
141					EncodedQueueRow::view(&item.encoded),
142					ordered_by_index,
143					row_number,
144				);
145				rows.push(QueueInsertRow {
146					row_number,
147					partition: placement.partition,
148					key_hash: placement.key_hash,
149					not_before: item.not_before,
150					encoded: item.encoded.clone(),
151				});
152				returned.push(ReturnedRow {
153					created: true,
154					row_number,
155					encoded: item.encoded.clone(),
156				});
157			}
158			Outcome::Duplicate {
159				row_number,
160				encoded,
161			} => returned.push(ReturnedRow {
162				created: false,
163				row_number,
164				encoded: encoded.unwrap_or_else(|| shape.allocate_queue().freeze_bytes()),
165			}),
166			Outcome::DuplicateInBatch {
167				origin,
168			} => {
169				let row_number = returned[origin].row_number;
170				let encoded = returned[origin].encoded.clone();
171				returned.push(ReturnedRow {
172					created: false,
173					row_number,
174					encoded,
175				});
176			}
177		}
178	}
179
180	txn.insert_queue(&queue, &rows)?;
181
182	if let Some(returning_exprs) = &returning {
183		return project_returning(services, txn, symbols, &queue, &shape, returning_exprs, &returned);
184	}
185
186	Ok(insert_queue_result(namespace.name(), &queue.name, fresh_count as u64, duplicates as u64))
187}
188
189struct PendingItem {
190	encoded: EncodedBytes,
191	deduplication_key: Option<Vec<u8>>,
192	not_before: Option<DateTime>,
193}
194
195enum Outcome {
196	Fresh,
197	Duplicate {
198		row_number: RowNumber,
199		encoded: Option<EncodedBytes>,
200	},
201	DuplicateInBatch {
202		origin: usize,
203	},
204}
205
206struct ReturnedRow {
207	created: bool,
208	row_number: RowNumber,
209	encoded: EncodedBytes,
210}
211
212fn write_deduplication_record(
213	txn: &mut Transaction<'_>,
214	queue: &Queue,
215	key: &[u8],
216	row_number: RowNumber,
217	now: DateTime,
218) -> Result<()> {
219	let ttl = queue.deduplicate.as_ref().map(|d| d.ttl).unwrap_or(Duration::MAX);
220	let record = encode_queue_deduplication(row_number, now.saturating_add(ttl));
221	txn.set(&QueueDeduplicationKey::new(queue.id, key), record.into_bytes())?;
222	Ok(())
223}
224
225fn resolve_duplicates(
226	txn: &mut Transaction<'_>,
227	queue: &Queue,
228	shape: &RowShape,
229	pending: &[PendingItem],
230	now: DateTime,
231) -> Result<Vec<Outcome>> {
232	let mut outcomes = Vec::with_capacity(pending.len());
233	let mut seen: HashMap<Vec<u8>, usize> = HashMap::new();
234
235	for (index, item) in pending.iter().enumerate() {
236		let Some(key) = &item.deduplication_key else {
237			outcomes.push(Outcome::Fresh);
238			continue;
239		};
240
241		if let Some(&origin) = seen.get(key) {
242			outcomes.push(Outcome::DuplicateInBatch {
243				origin,
244			});
245			continue;
246		}
247
248		let stored = txn.get(&QueueDeduplicationKey::new(queue.id, key.clone()))?;
249		if let Some(stored) = stored {
250			let Some((row_number, expires_at)) =
251				decode_queue_deduplication(EncodedQueueDeduplicationRow::view(&stored.bytes))
252			else {
253				return_internal_error!(
254					"Queue {} deduplication record is {} bytes wide, too short for its header. This indicates a corrupt record.",
255					queue.name,
256					stored.bytes.len()
257				)
258			};
259			if expires_at > now {
260				let encoded = txn.get(&RowKey::new(queue.id, row_number))?.map(|item| item.bytes);
261				outcomes.push(Outcome::Duplicate {
262					row_number,
263					encoded,
264				});
265				continue;
266			}
267		}
268
269		seen.insert(key.clone(), index);
270		outcomes.push(Outcome::Fresh);
271	}
272
273	let _ = shape;
274	Ok(outcomes)
275}
276
277fn project_returning(
278	services: &Arc<Services>,
279	txn: &mut Transaction<'_>,
280	symbols: &mut SymbolTable,
281	queue: &Queue,
282	shape: &RowShape,
283	returning_exprs: &[Expression],
284	returned: &[ReturnedRow],
285) -> Result<Columns> {
286	let rows: Vec<(RowNumber, EncodedBytes)> =
287		returned.iter().map(|row| (row.row_number, row.encoded.clone())).collect();
288	let mut columns = decode_rows_to_columns(shape, &rows);
289	truncate_to_declared(&mut columns, queue.columns.len());
290	decode_returning_dictionaries(services, txn, &queue.columns, &mut columns)?;
291
292	let mut created = ColumnBuffer::bool_with_capacity(returned.len());
293	for row in returned {
294		created.push_value(Value::Boolean(row.created));
295	}
296	columns.columns.push(created);
297	columns.names.push(Fragment::internal(QUEUE_CREATED_COLUMN));
298
299	evaluate_returning(services, symbols, returning_exprs, columns, txn.identity())
300}
301
302fn declared_key_indices(queue: &Queue) -> Result<Option<Vec<usize>>> {
303	let Some(deduplicate) = &queue.deduplicate else {
304		return Ok(None);
305	};
306	let mut indices = Vec::with_capacity(deduplicate.by.len());
307	for column in &deduplicate.by {
308		let index = queue.columns.iter().position(|c| c.name == *column).ok_or_else(|| {
309			internal_error!("queue {} deduplicates by {} which is not a column", queue.name, column)
310		})?;
311		indices.push(index);
312	}
313	Ok(Some(indices))
314}
315
316fn declared_key_bytes(shape: &RowShape, bytes: &EncodedBytes, indices: &[usize]) -> Vec<u8> {
317	let values: Vec<Value> = indices.iter().map(|&index| shape.get_value(bytes, index)).collect();
318	to_stdvec(&values).expect("postcard serialization of a Value list is total")
319}
320
321#[inline]
322fn truncate_to_declared(columns: &mut Columns, declared: usize) {
323	columns.columns.truncate(declared);
324	columns.names.truncate(declared);
325}
326
327#[inline]
328fn resolve_insert_queue_target(
329	services: &Arc<Services>,
330	txn: &mut Transaction<'_>,
331	target: &ResolvedQueue,
332) -> Result<(Namespace, Queue)> {
333	let namespace_name = target.namespace().name();
334	let Some(namespace) = services.catalog.find_namespace_by_name(txn, namespace_name)? else {
335		return_error!(namespace_not_found(Fragment::internal(namespace_name), namespace_name));
336	};
337	let queue_name = target.name();
338	let Some(queue) = services.catalog.find_queue_by_name(txn, namespace.id(), queue_name)? else {
339		return_error!(queue_not_found(target.identifier().clone(), namespace_name, queue_name));
340	};
341	Ok((namespace, queue))
342}
343
344#[inline]
345fn build_insert_queue_query_context(
346	services: &Arc<Services>,
347	target: &QueueTarget<'_>,
348	symbols: &SymbolTable,
349	identity: IdentityId,
350) -> Arc<QueryContext> {
351	let namespace_ident = Fragment::internal(target.namespace.name());
352	let resolved_namespace = ResolvedNamespace::new(namespace_ident, target.namespace.clone());
353	let queue_ident = Fragment::internal(target.queue.name.clone());
354	let resolved_queue = ResolvedQueue::new(queue_ident, resolved_namespace, target.queue.clone());
355	Arc::new(QueryContext {
356		services: services.clone(),
357		source: Some(ResolvedObject::Queue(resolved_queue)),
358		batch_size: services.catalog.get_config_uint2(ConfigKey::QueryRowBatchSize) as u64,
359		params: Params::None,
360		symbols: symbols.clone(),
361		identity,
362		memory: query_budget(services),
363	})
364}
365
366#[allow(clippy::too_many_arguments)]
367fn validate_and_encode_input_rows(
368	services: &Arc<Services>,
369	txn: &mut Transaction<'_>,
370	target: &QueueTarget<'_>,
371	shape: &RowShape,
372	context: &Arc<QueryContext>,
373	symbols: &SymbolTable,
374	input_node: &mut Box<dyn QueryNode>,
375	has_deduplication: bool,
376	has_not_before: bool,
377) -> Result<Vec<PendingItem>> {
378	let mut pending: Vec<PendingItem> = Vec::new();
379	let mut mutable_context = (**context).clone();
380	let declared_key_indices = declared_key_indices(target.queue)?;
381
382	while let Some(columns) = input_node.next(txn, &mut mutable_context)? {
383		PolicyEvaluator::new(services, symbols).enforce_write_policies(
384			txn,
385			target.namespace.name(),
386			&target.queue.name,
387			DataOp::Insert,
388			&columns,
389			PolicyTargetType::Queue,
390		)?;
391
392		let mut column_map: HashMap<&str, usize> = HashMap::new();
393		for (idx, col) in columns.iter().enumerate() {
394			column_map.insert(col.name().text(), idx);
395		}
396
397		for row_idx in 0..columns.row_count() {
398			let declared_key_indices = declared_key_indices.as_deref();
399			let not_before = if has_not_before {
400				read_not_before(target, &columns, &column_map, row_idx)?
401			} else {
402				None
403			};
404
405			let encoded = build_insert_queue_row(
406				services,
407				txn,
408				target,
409				shape,
410				&columns,
411				&column_map,
412				context,
413				row_idx,
414				not_before,
415			)?;
416
417			let deduplication_key = match declared_key_indices {
418				Some(indices) => Some(declared_key_bytes(shape, &encoded, indices)),
419				None if has_deduplication => {
420					read_deduplication_key(target, &columns, &column_map, row_idx)?
421				}
422				None => None,
423			};
424
425			pending.push(PendingItem {
426				encoded,
427				deduplication_key,
428				not_before,
429			});
430		}
431	}
432
433	Ok(pending)
434}
435
436#[inline]
437fn read_deduplication_key(
438	target: &QueueTarget<'_>,
439	columns: &Columns,
440	column_map: &HashMap<&str, usize>,
441	row_idx: usize,
442) -> Result<Option<Vec<u8>>> {
443	let Some(&idx) = column_map.get(QUEUE_DEDUPLICATION_KEY_FIELD) else {
444		return Ok(None);
445	};
446	let value = columns[idx].get_value(row_idx);
447	match value {
448		Value::None {
449			..
450		}
451		| Value::Utf8(_) => Ok(Some(statement_key_bytes(&value))),
452		other => return_error!(queue_deduplication_key_not_utf8(
453			Fragment::internal(target.queue.name.clone()),
454			other.get_type().to_string().as_str()
455		)),
456	}
457}
458
459fn statement_key_bytes(value: &Value) -> Vec<u8> {
460	to_stdvec(value).expect("postcard serialization of a Value is total")
461}
462
463#[inline]
464fn read_not_before(
465	target: &QueueTarget<'_>,
466	columns: &Columns,
467	column_map: &HashMap<&str, usize>,
468	row_idx: usize,
469) -> Result<Option<DateTime>> {
470	let Some(&idx) = column_map.get(QUEUE_NOT_BEFORE_FIELD) else {
471		return Ok(None);
472	};
473	match columns[idx].get_value(row_idx) {
474		Value::None {
475			..
476		} => Ok(None),
477		Value::DateTime(instant) => Ok(Some(instant)),
478		other => return_error!(queue_not_before_not_datetime(
479			Fragment::internal(target.queue.name.clone()),
480			other.get_type().to_string().as_str()
481		)),
482	}
483}
484
485#[allow(clippy::too_many_arguments)]
486#[inline]
487fn build_insert_queue_row(
488	services: &Arc<Services>,
489	txn: &mut Transaction<'_>,
490	target: &QueueTarget<'_>,
491	shape: &RowShape,
492	columns: &Columns,
493	column_map: &HashMap<&str, usize>,
494	context: &Arc<QueryContext>,
495	row_idx: usize,
496	not_before: Option<DateTime>,
497) -> Result<EncodedBytes> {
498	let mut row = shape.allocate_queue();
499
500	for (queue_idx, queue_column) in target.queue.columns.iter().enumerate() {
501		let mut value = if let Some(&input_idx) = column_map.get(queue_column.name.as_str()) {
502			columns[input_idx].get_value(row_idx)
503		} else {
504			Value::none()
505		};
506
507		if queue_column.auto_increment && matches!(value, Value::None { .. }) {
508			value = services.catalog.column_sequence_next_value(txn, target.queue.id, queue_column.id)?;
509		}
510
511		let column_ident = column_map
512			.get(queue_column.name.as_str())
513			.map(|&idx| columns.name_at(idx).clone())
514			.unwrap_or_else(|| Fragment::internal(queue_column.name.clone()));
515
516		let resolved_column = ResolvedColumn::new(
517			column_ident.clone(),
518			context.source.clone().unwrap(),
519			queue_column.clone(),
520		);
521
522		value = coerce_value_to_column_type(
523			value,
524			queue_column.constraint.get_type(),
525			resolved_column,
526			context,
527		)?;
528
529		if let Err(mut e) = queue_column.constraint.validate(&value) {
530			e.0.fragment = column_ident.clone();
531			return Err(e);
532		}
533
534		let value = if let Some(dict_id) = queue_column.dictionary_id {
535			let dictionary = services.catalog.find_dictionary(txn, dict_id)?.ok_or_else(|| {
536				internal_error!("Dictionary {:?} not found for column {}", dict_id, queue_column.name)
537			})?;
538			let entry_id = if matches!(value, Value::None { .. }) {
539				dictionary.id_type.none()
540			} else {
541				txn.insert_into_dictionary(&dictionary, &value)?
542			};
543			entry_id.to_value()
544		} else {
545			value
546		};
547
548		shape.set_value(&mut row, queue_idx, &value);
549	}
550
551	if let Some(instant) = not_before {
552		row.set_not_before(instant);
553	}
554
555	let now = services.runtime_context.clock.now();
556	row.set_timestamps(now, now);
557	if let Some(time) =
558		resolve_time(&target.queue.name, &target.queue.columns, &target.queue.time, shape, &row, now)?
559	{
560		row.set_time(time);
561	}
562
563	Ok(row.freeze_bytes())
564}
565
566#[inline]
567fn insert_queue_result(namespace: &str, queue: &str, inserted: u64, duplicates: u64) -> Columns {
568	Columns::single_row([
569		("namespace", Value::Utf8(namespace.to_string())),
570		("queue", Value::Utf8(queue.to_string())),
571		("inserted", Value::Uint8(inserted)),
572		("duplicates", Value::Uint8(duplicates)),
573	])
574}