Skip to main content

reifydb_engine/vm/instruction/dml/
table_delete.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::Arc;
5
6use reifydb_catalog::error::{CatalogError, CatalogObjectKind};
7use reifydb_codec::row::bytes::{EncodedBytes, read_fingerprint};
8use reifydb_core::{
9	interface::{
10		catalog::{
11			config::{ConfigKey, GetConfig},
12			id::IndexId,
13			key::PrimaryKey,
14			namespace::Namespace,
15			object::ObjectId,
16			policy::{DataOp, PolicyTargetType},
17			table::Table,
18		},
19		resolved::{ResolvedNamespace, ResolvedObject, ResolvedTable},
20	},
21	internal_error,
22	key::{
23		any::TaggedKey,
24		catalog::IndexEntryKey,
25		row::{PartitionedRowKey, RowKeyRange},
26	},
27	value::column::columns::Columns,
28};
29use reifydb_evaluate::stack::SymbolTable;
30use reifydb_rql::{nodes::DeleteTableNode, query::QueryPlan};
31use reifydb_transaction::{multi::RangeScope, transaction::Transaction};
32use reifydb_value::{
33	fragment::Fragment,
34	params::Params,
35	value::{Value, partition::Partition, row_number::RowNumber},
36};
37
38use super::{
39	context::{TableTarget, WriteExecCtx},
40	primary_key,
41	returning::{decode_returning_dictionaries, decode_rows_to_columns, evaluate_returning, with_pre_image},
42	shape::get_or_create_table_shape,
43};
44use crate::{
45	Result,
46	error::EngineError,
47	partition::row_key_from_partition,
48	policy::PolicyEvaluator,
49	transaction::operation::table::TableOperations,
50	vm::{
51		services::Services,
52		volcano::{
53			compile::compile,
54			query::{QueryContext, QueryNode, query_budget},
55		},
56	},
57};
58
59pub(crate) fn delete(
60	services: &Arc<Services>,
61	txn: &mut Transaction<'_>,
62	plan: DeleteTableNode,
63	params: Params,
64	symbols: &SymbolTable,
65) -> Result<Columns> {
66	let DeleteTableNode {
67		input,
68		target,
69		returning,
70	} = plan;
71	let target = target.expect("DELETE without input requires explicit target table");
72	let (namespace, table) = resolve_delete_table_target(services, txn, &target)?;
73	let resolved_source = build_delete_table_resolved_source(&namespace, &table);
74	let target_data = TableTarget {
75		namespace: &namespace,
76		table: &table,
77		fragment: target.identifier(),
78	};
79
80	let exec = WriteExecCtx {
81		services,
82		symbols,
83	};
84	let (deleted_count, returned_rows) = if let Some(input_plan) = input {
85		run_table_delete_with_input(
86			&exec,
87			txn,
88			*input_plan,
89			&target_data,
90			&resolved_source,
91			&params,
92			returning.is_some(),
93		)?
94	} else {
95		run_table_delete_all(services, txn, &table, returning.is_some())?
96	};
97
98	if let Some(returning_exprs) = &returning {
99		let shape = get_or_create_table_shape(&services.catalog, &table, txn)?;
100		let mut columns = decode_rows_to_columns(&shape, &returned_rows);
101		decode_returning_dictionaries(services, txn, &table.columns, &mut columns)?;
102		let columns = with_pre_image(columns.clone(), &columns);
103		return evaluate_returning(services, symbols, returning_exprs, columns, txn.identity());
104	}
105	Ok(delete_table_result(namespace.name(), &table.name, deleted_count))
106}
107
108#[inline]
109fn resolve_delete_table_target(
110	services: &Arc<Services>,
111	txn: &mut Transaction<'_>,
112	target: &ResolvedTable,
113) -> Result<(Namespace, Table)> {
114	let namespace_name = target.namespace().name();
115	let Some(namespace) = services.catalog.find_namespace_by_name(txn, namespace_name)? else {
116		return Err(CatalogError::NotFound {
117			kind: CatalogObjectKind::Namespace,
118			namespace: namespace_name.to_string(),
119			name: String::new(),
120			fragment: Fragment::internal(namespace_name),
121		}
122		.into());
123	};
124	let Some(table) = services.catalog.find_table_by_name(txn, namespace.id(), target.name())? else {
125		return Err(CatalogError::NotFound {
126			kind: CatalogObjectKind::Table,
127			namespace: namespace_name.to_string(),
128			name: target.name().to_string(),
129			fragment: target.identifier().clone(),
130		}
131		.into());
132	};
133	Ok((namespace, table))
134}
135
136#[inline]
137fn build_delete_table_resolved_source(namespace: &Namespace, table: &Table) -> Option<ResolvedObject> {
138	let namespace_ident = Fragment::internal(namespace.name());
139	let resolved_namespace = ResolvedNamespace::new(namespace_ident, namespace.clone());
140	let table_ident = Fragment::internal(table.name.clone());
141	let resolved_table = ResolvedTable::new(table_ident, resolved_namespace, table.clone());
142	Some(ResolvedObject::Table(resolved_table))
143}
144
145fn run_table_delete_with_input(
146	exec: &WriteExecCtx<'_>,
147	txn: &mut Transaction<'_>,
148	input_plan: QueryPlan,
149	target: &TableTarget<'_>,
150	resolved_source: &Option<ResolvedObject>,
151	params: &Params,
152	has_returning: bool,
153) -> Result<(u64, Vec<(RowNumber, EncodedBytes)>)> {
154	let context = QueryContext {
155		services: exec.services.clone(),
156		source: resolved_source.clone(),
157		batch_size: exec.services.catalog.get_config_uint2(ConfigKey::QueryRowBatchSize) as u64,
158		params: params.clone(),
159		symbols: exec.symbols.clone(),
160		identity: txn.identity(),
161		memory: query_budget(exec.services),
162	};
163	let mut input_node = compile(input_plan, txn, Arc::new(context.clone()));
164	input_node.initialize(txn, &context)?;
165
166	let (row_numbers_to_delete, partitions_to_delete) =
167		collect_rows_to_delete(exec, txn, &mut input_node, &context, target)?;
168
169	if !target.table.partition_by.is_empty() && partitions_to_delete.len() != row_numbers_to_delete.len() {
170		return Err(EngineError::MissingPartitionAddress {
171			object: ObjectId::Table(target.table.id),
172			operation: "DELETE",
173		}
174		.into());
175	}
176
177	let pk_def = primary_key::get_primary_key(&exec.services.catalog, txn, target.table)?;
178
179	let mut filtered_ids: Vec<RowNumber> = Vec::with_capacity(row_numbers_to_delete.len());
180	let mut filtered_partitions: Vec<Partition> = Vec::with_capacity(partitions_to_delete.len());
181	for (idx, row_number) in row_numbers_to_delete.into_iter().enumerate() {
182		let partition = partitions_to_delete.get(idx).copied();
183		let row_key = row_key_from_partition(target.table.id, partition, row_number);
184		let bytes = match txn.get(&row_key)? {
185			Some(v) => v.bytes,
186			None => continue,
187		};
188		if let Some(ref pk_def) = pk_def {
189			remove_table_pk_index_for(exec.services, txn, target.table, pk_def, &bytes)?;
190		}
191		filtered_ids.push(row_number);
192		if let Some(p) = partition {
193			filtered_partitions.push(p);
194		}
195	}
196
197	let removed = txn.remove_from_table(target.table, &filtered_ids, &filtered_partitions)?;
198	let deleted_count = removed.len() as u64;
199	let returned_rows: Vec<(RowNumber, EncodedBytes)> = if has_returning {
200		removed
201	} else {
202		Vec::new()
203	};
204	Ok((deleted_count, returned_rows))
205}
206
207fn collect_rows_to_delete(
208	exec: &WriteExecCtx<'_>,
209	txn: &mut Transaction<'_>,
210	input_node: &mut Box<dyn QueryNode>,
211	context: &QueryContext,
212	target: &TableTarget<'_>,
213) -> Result<(Vec<RowNumber>, Vec<Partition>)> {
214	let mut row_numbers_to_delete = Vec::new();
215	let mut partitions_to_delete = Vec::new();
216	let mut mutable_context = context.clone();
217	while let Some(columns) = input_node.next(txn, &mut mutable_context)? {
218		PolicyEvaluator::new(exec.services, exec.symbols).enforce_write_policies(
219			txn,
220			target.namespace.name(),
221			&target.table.name,
222			DataOp::Delete,
223			&columns,
224			PolicyTargetType::Table,
225		)?;
226		if columns.row_numbers().is_empty() {
227			return Err(EngineError::MissingRowNumberColumn.into());
228		}
229		let row_numbers = &columns.row_numbers();
230		for row_idx in 0..columns.row_count() {
231			row_numbers_to_delete.push(row_numbers[row_idx]);
232			if !columns.partitions().is_empty() {
233				partitions_to_delete.push(columns.partitions()[row_idx]);
234			}
235		}
236	}
237	Ok((row_numbers_to_delete, partitions_to_delete))
238}
239
240fn run_table_delete_all(
241	services: &Arc<Services>,
242	txn: &mut Transaction<'_>,
243	table: &Table,
244	has_returning: bool,
245) -> Result<(u64, Vec<(RowNumber, EncodedBytes)>)> {
246	let partitioned = !table.partition_by.is_empty();
247	let range = if partitioned {
248		PartitionedRowKey::full_scan(table.id)
249	} else {
250		RowKeyRange::storage_scan(table.id.into())
251	};
252	let pk_def = primary_key::get_primary_key(&services.catalog, txn, table)?;
253	let rows: Vec<_> = txn.range(range, RangeScope::All, 32)?.collect::<Result<Vec<_>>>()?;
254
255	let mut filtered_ids: Vec<RowNumber> = Vec::with_capacity(rows.len());
256	let mut filtered_partitions: Vec<Partition> = Vec::with_capacity(rows.len());
257	for multi in rows {
258		if let Some(ref pk_def) = pk_def {
259			remove_table_pk_index_for(services, txn, table, pk_def, &multi.bytes)?;
260		}
261		if partitioned {
262			let TaggedKey::PartitionedRow(key) = multi.key else {
263				panic!("valid PartitionedRowKey encoding");
264			};
265			filtered_ids.push(key.row);
266			filtered_partitions.push(key.partition);
267		} else {
268			let TaggedKey::Row(row_key) = multi.key else {
269				panic!("valid RowKey encoding");
270			};
271			filtered_ids.push(row_key.row);
272		}
273	}
274
275	let removed = txn.remove_from_table(table, &filtered_ids, &filtered_partitions)?;
276	let deleted_count = removed.len() as u64;
277	let returned_rows: Vec<(RowNumber, EncodedBytes)> = if has_returning {
278		removed
279	} else {
280		Vec::new()
281	};
282	Ok((deleted_count, returned_rows))
283}
284
285#[inline]
286fn remove_table_pk_index_for(
287	services: &Arc<Services>,
288	txn: &mut Transaction<'_>,
289	table: &Table,
290	pk_def: &PrimaryKey,
291	values: &EncodedBytes,
292) -> Result<()> {
293	let fingerprint = read_fingerprint(values);
294	let shape = services.catalog.get_or_load_row_shape(fingerprint, txn)?.ok_or_else(|| {
295		internal_error!("Row shape with fingerprint {:?} not found for table {}", fingerprint, table.name)
296	})?;
297	let index_key = primary_key::encode_primary_key(pk_def, values, table, &shape)?;
298	txn.remove(&IndexEntryKey::new(table.id, IndexId::primary(pk_def.id), index_key))?;
299	Ok(())
300}
301
302#[inline]
303fn delete_table_result(namespace: &str, table: &str, deleted: u64) -> Columns {
304	Columns::single_row([
305		("namespace", Value::Utf8(namespace.to_string())),
306		("table", Value::Utf8(table.to_string())),
307		("deleted", Value::Uint8(deleted)),
308	])
309}