Skip to main content

reifydb_engine/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_core::interface::catalog::shape::ShapeId;
5use reifydb_value::{
6	error::{Diagnostic, Error, IntoDiagnostic},
7	fragment::Fragment,
8	value::value_type::ValueType,
9};
10
11#[derive(Debug, thiserror::Error)]
12pub enum EngineError {
13	#[error("column `{column}` not found in `{table_name}`")]
14	BulkInsertColumnNotFound {
15		fragment: Fragment,
16		table_name: String,
17		column: String,
18	},
19
20	#[error("too many values: expected {expected} columns, got {actual}")]
21	BulkInsertTooManyValues {
22		fragment: Fragment,
23		expected: usize,
24		actual: usize,
25	},
26
27	#[error("Frame must have a __ROW__ID__ column for UPDATE operations")]
28	MissingRowNumberColumn,
29
30	#[error("assertion failed: {message}")]
31	AssertionFailed {
32		fragment: Fragment,
33		message: String,
34		expression: Option<String>,
35	},
36
37	#[error("Cannot insert none into non-optional column of type {column_type}")]
38	NoneNotAllowed {
39		fragment: Fragment,
40		column_type: ValueType,
41	},
42
43	#[error("Unknown function: {name}")]
44	UnknownFunction {
45		name: String,
46		fragment: Fragment,
47	},
48
49	#[error("Unknown callable: {name}")]
50	UnknownCallable {
51		name: String,
52		fragment: Fragment,
53	},
54
55	#[error("Generator function '{name}' not found")]
56	GeneratorNotFound {
57		name: String,
58		fragment: Fragment,
59	},
60
61	#[error("Variable '{name}' is not defined")]
62	VariableNotFound {
63		name: String,
64	},
65
66	#[error("Cannot reassign immutable variable '{name}'")]
67	VariableIsImmutable {
68		name: String,
69	},
70
71	#[error(
72		"cannot locate partitioned rows for {operation} on shape {shape}: query shape carries no partition address"
73	)]
74	MissingPartitionAddress {
75		shape: ShapeId,
76		operation: &'static str,
77	},
78
79	#[error("cannot change partition column via UPDATE on shape {shape}: partition columns are immutable")]
80	ImmutablePartitionColumn {
81		shape: ShapeId,
82	},
83
84	#[error(
85		"partition hash collision on shape {shape}: hash {hash:032x} maps to two distinct partition value tuples"
86	)]
87	PartitionHashCollision {
88		shape: ShapeId,
89		hash: u128,
90	},
91}
92
93impl IntoDiagnostic for EngineError {
94	fn into_diagnostic(self) -> Diagnostic {
95		match self {
96			EngineError::BulkInsertColumnNotFound {
97				fragment,
98				table_name,
99				column,
100			} => Diagnostic {
101				code: "BI_001".to_string(),
102				rql: None,
103				message: format!("column `{}` not found in `{}`", column, table_name),
104				column: None,
105				fragment,
106				label: Some("unknown column".to_string()),
107				help: Some("check that the column name matches the shape".to_string()),
108				notes: vec![],
109				cause: None,
110				operator_chain: None,
111			},
112			EngineError::BulkInsertTooManyValues {
113				fragment,
114				expected,
115				actual,
116			} => Diagnostic {
117				code: "BI_003".to_string(),
118				rql: None,
119				message: format!("too many values: expected {} columns, got {}", expected, actual),
120				column: None,
121				fragment,
122				label: Some("value count mismatch".to_string()),
123				help: Some("ensure the number of values matches the column count".to_string()),
124				notes: vec![],
125				cause: None,
126				operator_chain: None,
127			},
128			EngineError::MissingRowNumberColumn => Diagnostic {
129				code: "ENG_003".to_string(),
130				rql: None,
131				message: "Frame must have a __ROW__ID__ column for UPDATE operations".to_string(),
132				column: None,
133				fragment: Fragment::None,
134				label: Some("missing required column".to_string()),
135				help: Some("Ensure the query includes the encoded ID in the result set".to_string()),
136				notes: vec!["UPDATE operations require encoded identifiers to locate existing rows"
137					.to_string()],
138				cause: None,
139				operator_chain: None,
140			},
141			EngineError::AssertionFailed {
142				fragment,
143				message,
144				expression,
145			} => {
146				let base_msg = if !message.is_empty() {
147					message.clone()
148				} else if let Some(ref expr) = expression {
149					format!("assertion failed: {}", expr)
150				} else {
151					"assertion failed".to_string()
152				};
153				let label = expression
154					.as_ref()
155					.map(|expr| format!("this expression is false: {}", expr))
156					.or_else(|| Some("assertion failed".to_string()));
157				Diagnostic {
158					code: "ASSERT".to_string(),
159					rql: None,
160					message: base_msg,
161					fragment,
162					label,
163					help: None,
164					notes: vec![],
165					column: None,
166					cause: None,
167					operator_chain: None,
168				}
169			}
170			EngineError::NoneNotAllowed {
171				fragment,
172				column_type,
173			} => Diagnostic {
174				code: "CONSTRAINT_007".to_string(),
175				rql: None,
176				message: format!(
177					"Cannot insert none into non-optional column of type {}. Declare the column as Option({}) to allow none values.",
178					column_type, column_type
179				),
180				column: None,
181				fragment,
182				label: Some("constraint violation".to_string()),
183				help: Some(format!(
184					"The column type is {} which does not accept none. Use Option({}) if the column should be nullable.",
185					column_type, column_type
186				)),
187				notes: vec![],
188				cause: None,
189				operator_chain: None,
190			},
191			EngineError::UnknownFunction {
192				name,
193				fragment,
194			} => Diagnostic {
195				code: "FUNCTION_001".to_string(),
196				rql: None,
197				message: format!("Unknown function: {}", name),
198				column: None,
199				fragment,
200				label: Some("unknown function".to_string()),
201				help: Some("Check the function name and available functions".to_string()),
202				notes: vec![],
203				cause: None,
204				operator_chain: None,
205			},
206			EngineError::UnknownCallable {
207				name,
208				fragment,
209			} => Diagnostic {
210				code: "CALLABLE_001".to_string(),
211				rql: None,
212				message: format!("Unknown callable: {}", name),
213				column: None,
214				fragment,
215				label: Some("unknown callable".to_string()),
216				help: Some(
217					"Check the name and available functions, procedures, and closures".to_string()
218				),
219				notes: vec![],
220				cause: None,
221				operator_chain: None,
222			},
223			EngineError::GeneratorNotFound {
224				name,
225				fragment,
226			} => Diagnostic {
227				code: "FUNCTION_009".to_string(),
228				rql: None,
229				message: format!("Generator function '{}' not found", name),
230				column: None,
231				fragment,
232				label: Some("unknown generator function".to_string()),
233				help: Some("Check the generator function name and ensure it is registered".to_string()),
234				notes: vec![],
235				cause: None,
236				operator_chain: None,
237			},
238			EngineError::VariableNotFound {
239				name,
240			} => Diagnostic {
241				code: "RUNTIME_001".to_string(),
242				rql: None,
243				message: format!("Variable '{}' is not defined", name),
244				column: None,
245				fragment: Fragment::None,
246				label: None,
247				help: Some(format!(
248					"Define the variable using 'let {} = <value>' before using it",
249					name
250				)),
251				notes: vec![],
252				cause: None,
253				operator_chain: None,
254			},
255			EngineError::VariableIsImmutable {
256				name,
257			} => Diagnostic {
258				code: "RUNTIME_003".to_string(),
259				rql: None,
260				message: format!("Cannot reassign immutable variable '{}'", name),
261				column: None,
262				fragment: Fragment::None,
263				label: None,
264				help: Some("Use 'let mut $name := value' to declare a mutable variable".to_string()),
265				notes: vec!["Only mutable variables can be reassigned".to_string()],
266				cause: None,
267				operator_chain: None,
268			},
269
270			EngineError::MissingPartitionAddress {
271				shape,
272				operation,
273			} => Diagnostic {
274				code: "PART_001".to_string(),
275				rql: None,
276				message: format!(
277					"cannot locate partitioned rows for {} on shape {}: query shape carries no partition address",
278					operation, shape
279				),
280				column: None,
281				fragment: Fragment::None,
282				label: Some("missing partition address".to_string()),
283				help: Some(
284					"the query must carry the row's partition alongside its row number; rewrite the query so the partitioned source's columns flow through unmodified"
285						.to_string(),
286				),
287				notes: vec![],
288				cause: None,
289				operator_chain: None,
290			},
291
292			EngineError::ImmutablePartitionColumn {
293				shape,
294			} => Diagnostic {
295				code: "PART_002".to_string(),
296				rql: None,
297				message: format!(
298					"cannot change partition column via UPDATE on shape {}: partition columns are immutable",
299					shape
300				),
301				column: None,
302				fragment: Fragment::None,
303				label: Some("partition column change rejected".to_string()),
304				help: Some(
305					"partition columns determine a row's physical location and cannot be updated; delete and re-insert the row instead"
306						.to_string(),
307				),
308				notes: vec![],
309				cause: None,
310				operator_chain: None,
311			},
312
313			EngineError::PartitionHashCollision {
314				shape,
315				hash,
316			} => Diagnostic {
317				code: "PART_003".to_string(),
318				rql: None,
319				message: format!(
320					"partition hash collision on shape {}: hash {:032x} maps to two distinct partition value tuples",
321					shape, hash
322				),
323				column: None,
324				fragment: Fragment::None,
325				label: Some("128-bit hash collision".to_string()),
326				help: Some(
327					"two distinct partition value tuples produced the same 128-bit hash; this is astronomically unlikely and points to a hashing bug or data corruption, report it as a bug"
328						.to_string(),
329				),
330				notes: vec![],
331				cause: None,
332				operator_chain: None,
333			},
334		}
335	}
336}
337
338impl From<EngineError> for Error {
339	fn from(err: EngineError) -> Self {
340		Error(Box::new(err.into_diagnostic()))
341	}
342}