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::object::ObjectId;
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 callable: {name}")]
44	UnknownCallable {
45		name: String,
46		fragment: Fragment,
47	},
48
49	#[error("function `{name}` returned a value that cannot be cast to its declared return type {declared}")]
50	ReturnTypeMismatch {
51		name: String,
52		declared: ValueType,
53		fragment: Fragment,
54		cause: Box<Diagnostic>,
55	},
56
57	#[error("Generator function '{name}' not found")]
58	GeneratorNotFound {
59		name: String,
60		fragment: Fragment,
61	},
62
63	#[error(
64		"cannot locate partitioned rows for {operation} on object {object}: query object carries no partition address"
65	)]
66	MissingPartitionAddress {
67		object: ObjectId,
68		operation: &'static str,
69	},
70
71	#[error("`{object}` declares `{column}` as its #time populator but has no such column")]
72	TimePopulatorMissing {
73		object: String,
74		column: String,
75	},
76
77	#[error("`{object}.{column}` is the declared #time populator but holds {found} on this row")]
78	TimePopulatorNotDateTime {
79		object: String,
80		column: String,
81		found: String,
82	},
83
84	#[error("cannot update the tag of series `{series}`: the tag is part of the row key")]
85	SeriesTagImmutable {
86		series: String,
87		fragment: Fragment,
88	},
89
90	#[error("cannot update the key column `{column}` of series `{series}`: the key is part of the row key")]
91	SeriesKeyImmutable {
92		series: String,
93		column: String,
94		fragment: Fragment,
95	},
96
97	#[error("series key column `{column}` cannot store {value}")]
98	SeriesKeyOutOfRange {
99		column: String,
100		value: String,
101		fragment: Fragment,
102	},
103
104	#[error("series key column `{column}` of series `{series}` changed by a pre-insert hook")]
105	SeriesKeyChangedByHook {
106		series: String,
107		column: String,
108	},
109
110	#[error("series `{series}` is partitioned by its key `{column}`, so an insert must give the key")]
111	SeriesKeyPartitionNeedsKey {
112		series: String,
113		column: String,
114		fragment: Fragment,
115	},
116}
117
118impl IntoDiagnostic for EngineError {
119	fn into_diagnostic(self) -> Diagnostic {
120		match self {
121			EngineError::BulkInsertColumnNotFound {
122				fragment,
123				table_name,
124				column,
125			} => Diagnostic {
126				code: "BI_001".to_string(),
127				rql: None,
128				message: format!("column `{}` not found in `{}`", column, table_name),
129				column: None,
130				fragment,
131				label: Some("unknown column".to_string()),
132				help: Some("check that the column name matches the shape".to_string()),
133				notes: vec![],
134				cause: None,
135				operator_chain: None,
136			},
137			EngineError::BulkInsertTooManyValues {
138				fragment,
139				expected,
140				actual,
141			} => Diagnostic {
142				code: "BI_003".to_string(),
143				rql: None,
144				message: format!("too many values: expected {} columns, got {}", expected, actual),
145				column: None,
146				fragment,
147				label: Some("value count mismatch".to_string()),
148				help: Some("ensure the number of values matches the column count".to_string()),
149				notes: vec![],
150				cause: None,
151				operator_chain: None,
152			},
153			EngineError::MissingRowNumberColumn => Diagnostic {
154				code: "ENG_003".to_string(),
155				rql: None,
156				message: "Frame must have a __ROW__ID__ column for UPDATE operations".to_string(),
157				column: None,
158				fragment: Fragment::None,
159				label: Some("missing required column".to_string()),
160				help: Some("Ensure the query includes the encoded ID in the result set".to_string()),
161				notes: vec!["UPDATE operations require encoded identifiers to locate existing rows"
162					.to_string()],
163				cause: None,
164				operator_chain: None,
165			},
166			EngineError::AssertionFailed {
167				fragment,
168				message,
169				expression,
170			} => {
171				let base_msg = if !message.is_empty() {
172					message.clone()
173				} else if let Some(ref expr) = expression {
174					format!("assertion failed: {}", expr)
175				} else {
176					"assertion failed".to_string()
177				};
178				let label = expression
179					.as_ref()
180					.map(|expr| format!("this expression is false: {}", expr))
181					.or_else(|| Some("assertion failed".to_string()));
182				Diagnostic {
183					code: "ASSERT".to_string(),
184					rql: None,
185					message: base_msg,
186					fragment,
187					label,
188					help: None,
189					notes: vec![],
190					column: None,
191					cause: None,
192					operator_chain: None,
193				}
194			}
195			EngineError::NoneNotAllowed {
196				fragment,
197				column_type,
198			} => Diagnostic {
199				code: "CONSTRAINT_007".to_string(),
200				rql: None,
201				message: format!(
202					"Cannot insert none into non-optional column of type {}. Declare the column as Option({}) to allow none values.",
203					column_type, column_type
204				),
205				column: None,
206				fragment,
207				label: Some("constraint violation".to_string()),
208				help: Some(format!(
209					"The column type is {} which does not accept none. Use Option({}) if the column should be optional.",
210					column_type, column_type
211				)),
212				notes: vec![],
213				cause: None,
214				operator_chain: None,
215			},
216			EngineError::UnknownCallable {
217				name,
218				fragment,
219			} => Diagnostic {
220				code: "CALLABLE_001".to_string(),
221				rql: None,
222				message: format!("Unknown callable: {}", name),
223				column: None,
224				fragment,
225				label: Some("unknown callable".to_string()),
226				help: Some(
227					"Check the name and available functions, procedures, and closures".to_string()
228				),
229				notes: vec![],
230				cause: None,
231				operator_chain: None,
232			},
233			EngineError::ReturnTypeMismatch {
234				name,
235				declared,
236				fragment,
237				cause,
238			} => Diagnostic {
239				code: "CALLABLE_002".to_string(),
240				rql: None,
241				message: format!(
242					"function `{}` returned a value that cannot be cast to its declared return type {}",
243					name, declared
244				),
245				column: None,
246				fragment,
247				label: Some(format!("`{}` must return {}", name, declared)),
248				help: Some(format!(
249					"return a value that fits {} from `{}`, or change its declared return type",
250					declared, name
251				)),
252				notes: vec![],
253				cause: Some(cause),
254				operator_chain: None,
255			},
256			EngineError::GeneratorNotFound {
257				name,
258				fragment,
259			} => Diagnostic {
260				code: "FUNCTION_010".to_string(),
261				rql: None,
262				message: format!("Generator function '{}' not found", name),
263				column: None,
264				fragment,
265				label: Some("unknown generator function".to_string()),
266				help: Some("Check the generator function name and ensure it is registered".to_string()),
267				notes: vec![],
268				cause: None,
269				operator_chain: None,
270			},
271			EngineError::MissingPartitionAddress {
272				object,
273				operation,
274			} => Diagnostic {
275				code: "PART_001".to_string(),
276				rql: None,
277				message: format!(
278					"cannot locate partitioned rows for {} on object {}: query object carries no partition address",
279					operation, object
280				),
281				column: None,
282				fragment: Fragment::None,
283				label: Some("missing partition address".to_string()),
284				help: Some(
285					"the query must carry the row's partition alongside its row number; rewrite the query so the partitioned source's columns flow through unmodified"
286						.to_string(),
287				),
288				notes: vec![],
289				cause: None,
290				operator_chain: None,
291			},
292
293			EngineError::TimePopulatorMissing {
294				object,
295				column,
296			} => Diagnostic {
297				code: "TIME_001".to_string(),
298				rql: None,
299				message: format!(
300					"`{}` declares `{}` as its #time populator but has no such column",
301					object, column
302				),
303				column: None,
304				fragment: Fragment::None,
305				label: Some("declared populator does not exist".to_string()),
306				help: Some(
307					"the populator must name one of the object's own columns; correct the `ts` key in the object's WITH clause"
308						.to_string(),
309				),
310				notes: vec![
311					"definition-time validation rejects a populator naming an absent column, so reaching this means the object was stored with a populator it does not have"
312						.to_string(),
313				],
314				cause: None,
315				operator_chain: None,
316			},
317
318			EngineError::TimePopulatorNotDateTime {
319				object,
320				column,
321				found,
322			} => Diagnostic {
323				code: "TIME_002".to_string(),
324				rql: None,
325				message: format!(
326					"`{}.{}` is the declared #time populator but holds {} on this row",
327					object, column, found
328				),
329				column: None,
330				fragment: Fragment::None,
331				label: Some("populator is not a non-none DateTime".to_string()),
332				help: Some(
333					"every row of an event-time object must carry a non-none DateTime in its populator column"
334						.to_string(),
335				),
336				notes: vec![
337					"#time is substrate-owned and cannot fall back to the write clock; silently substituting arrival time would date a replayed row to now"
338						.to_string(),
339				],
340				cause: None,
341				operator_chain: None,
342			},
343
344			EngineError::SeriesTagImmutable {
345				series,
346				fragment,
347			} => Diagnostic {
348				code: "UPDATE_004".to_string(),
349				rql: None,
350				message: format!(
351					"cannot update the tag of series `{}`: the tag is part of the row key",
352					series
353				),
354				column: None,
355				fragment,
356				label: Some("`tag` cannot be updated".to_string()),
357				help: Some("delete the row and insert it again with the new tag".to_string()),
358				notes: vec![],
359				cause: None,
360				operator_chain: None,
361			},
362
363			EngineError::SeriesKeyImmutable {
364				series,
365				column,
366				fragment,
367			} => Diagnostic {
368				code: "UPDATE_005".to_string(),
369				rql: None,
370				message: format!(
371					"cannot update the key column `{}` of series `{}`: the key is part of the row key",
372					column, series
373				),
374				column: None,
375				fragment,
376				label: Some(format!("`{}` cannot be updated", column)),
377				help: Some("delete the row and insert it again with the new key".to_string()),
378				notes: vec![],
379				cause: None,
380				operator_chain: None,
381			},
382
383			EngineError::SeriesKeyOutOfRange {
384				column,
385				value,
386				fragment,
387			} => Diagnostic {
388				code: "SERIES_001".to_string(),
389				rql: None,
390				message: format!("series key column `{}` cannot store {}", column, value),
391				column: None,
392				fragment,
393				label: Some("key out of range".to_string()),
394				help: Some(format!("a series key must be between 0 and {}", u64::MAX)),
395				notes: vec![],
396				cause: None,
397				operator_chain: None,
398			},
399
400			EngineError::SeriesKeyChangedByHook {
401				series,
402				column,
403			} => Diagnostic {
404				code: "SERIES_002".to_string(),
405				rql: None,
406				message: format!(
407					"series key column `{}` of series `{}` changed by a pre-insert hook",
408					column, series
409				),
410				column: None,
411				fragment: Fragment::None,
412				label: Some("series key changed by hook".to_string()),
413				help: Some(format!(
414					"the key `{}` of series `{}` is part of the row key, which is built before pre-insert hooks run, so a hook must not change it",
415					column, series
416				)),
417				notes: vec![],
418				cause: None,
419				operator_chain: None,
420			},
421
422			EngineError::SeriesKeyPartitionNeedsKey {
423				series,
424				column,
425				fragment,
426			} => Diagnostic {
427				code: "SERIES_003".to_string(),
428				rql: None,
429				message: format!(
430					"series `{}` is partitioned by its key `{}`, so an insert must give the key",
431					series, column
432				),
433				column: None,
434				fragment,
435				label: Some("key not given".to_string()),
436				help: Some(format!(
437					"an integer key is generated as the next key of its partition, and the partition of `{}` is made of the key itself; give `{}` on every row",
438					series, column
439				)),
440				notes: vec![],
441				cause: None,
442				operator_chain: None,
443			},
444		}
445	}
446}
447
448impl From<EngineError> for Error {
449	fn from(err: EngineError) -> Self {
450		Error(Box::new(err.into_diagnostic()))
451	}
452}