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("Generator function '{name}' not found")]
50	GeneratorNotFound {
51		name: String,
52		fragment: Fragment,
53	},
54
55	#[error(
56		"cannot locate partitioned rows for {operation} on object {object}: query object carries no partition address"
57	)]
58	MissingPartitionAddress {
59		object: ObjectId,
60		operation: &'static str,
61	},
62
63	#[error("`{object}` declares `{column}` as its #time populator but has no such column")]
64	TimePopulatorMissing {
65		object: String,
66		column: String,
67	},
68
69	#[error("`{object}.{column}` is the declared #time populator but holds {found} on this row")]
70	TimePopulatorNotDateTime {
71		object: String,
72		column: String,
73		found: String,
74	},
75}
76
77impl IntoDiagnostic for EngineError {
78	fn into_diagnostic(self) -> Diagnostic {
79		match self {
80			EngineError::BulkInsertColumnNotFound {
81				fragment,
82				table_name,
83				column,
84			} => Diagnostic {
85				code: "BI_001".to_string(),
86				rql: None,
87				message: format!("column `{}` not found in `{}`", column, table_name),
88				column: None,
89				fragment,
90				label: Some("unknown column".to_string()),
91				help: Some("check that the column name matches the shape".to_string()),
92				notes: vec![],
93				cause: None,
94				operator_chain: None,
95			},
96			EngineError::BulkInsertTooManyValues {
97				fragment,
98				expected,
99				actual,
100			} => Diagnostic {
101				code: "BI_003".to_string(),
102				rql: None,
103				message: format!("too many values: expected {} columns, got {}", expected, actual),
104				column: None,
105				fragment,
106				label: Some("value count mismatch".to_string()),
107				help: Some("ensure the number of values matches the column count".to_string()),
108				notes: vec![],
109				cause: None,
110				operator_chain: None,
111			},
112			EngineError::MissingRowNumberColumn => Diagnostic {
113				code: "ENG_003".to_string(),
114				rql: None,
115				message: "Frame must have a __ROW__ID__ column for UPDATE operations".to_string(),
116				column: None,
117				fragment: Fragment::None,
118				label: Some("missing required column".to_string()),
119				help: Some("Ensure the query includes the encoded ID in the result set".to_string()),
120				notes: vec!["UPDATE operations require encoded identifiers to locate existing rows"
121					.to_string()],
122				cause: None,
123				operator_chain: None,
124			},
125			EngineError::AssertionFailed {
126				fragment,
127				message,
128				expression,
129			} => {
130				let base_msg = if !message.is_empty() {
131					message.clone()
132				} else if let Some(ref expr) = expression {
133					format!("assertion failed: {}", expr)
134				} else {
135					"assertion failed".to_string()
136				};
137				let label = expression
138					.as_ref()
139					.map(|expr| format!("this expression is false: {}", expr))
140					.or_else(|| Some("assertion failed".to_string()));
141				Diagnostic {
142					code: "ASSERT".to_string(),
143					rql: None,
144					message: base_msg,
145					fragment,
146					label,
147					help: None,
148					notes: vec![],
149					column: None,
150					cause: None,
151					operator_chain: None,
152				}
153			}
154			EngineError::NoneNotAllowed {
155				fragment,
156				column_type,
157			} => Diagnostic {
158				code: "CONSTRAINT_007".to_string(),
159				rql: None,
160				message: format!(
161					"Cannot insert none into non-optional column of type {}. Declare the column as Option({}) to allow none values.",
162					column_type, column_type
163				),
164				column: None,
165				fragment,
166				label: Some("constraint violation".to_string()),
167				help: Some(format!(
168					"The column type is {} which does not accept none. Use Option({}) if the column should be optional.",
169					column_type, column_type
170				)),
171				notes: vec![],
172				cause: None,
173				operator_chain: None,
174			},
175			EngineError::UnknownCallable {
176				name,
177				fragment,
178			} => Diagnostic {
179				code: "CALLABLE_001".to_string(),
180				rql: None,
181				message: format!("Unknown callable: {}", name),
182				column: None,
183				fragment,
184				label: Some("unknown callable".to_string()),
185				help: Some(
186					"Check the name and available functions, procedures, and closures".to_string()
187				),
188				notes: vec![],
189				cause: None,
190				operator_chain: None,
191			},
192			EngineError::GeneratorNotFound {
193				name,
194				fragment,
195			} => Diagnostic {
196				code: "FUNCTION_009".to_string(),
197				rql: None,
198				message: format!("Generator function '{}' not found", name),
199				column: None,
200				fragment,
201				label: Some("unknown generator function".to_string()),
202				help: Some("Check the generator function name and ensure it is registered".to_string()),
203				notes: vec![],
204				cause: None,
205				operator_chain: None,
206			},
207			EngineError::MissingPartitionAddress {
208				object,
209				operation,
210			} => Diagnostic {
211				code: "PART_001".to_string(),
212				rql: None,
213				message: format!(
214					"cannot locate partitioned rows for {} on object {}: query object carries no partition address",
215					operation, object
216				),
217				column: None,
218				fragment: Fragment::None,
219				label: Some("missing partition address".to_string()),
220				help: Some(
221					"the query must carry the row's partition alongside its row number; rewrite the query so the partitioned source's columns flow through unmodified"
222						.to_string(),
223				),
224				notes: vec![],
225				cause: None,
226				operator_chain: None,
227			},
228
229			EngineError::TimePopulatorMissing {
230				object,
231				column,
232			} => Diagnostic {
233				code: "TIME_001".to_string(),
234				rql: None,
235				message: format!(
236					"`{}` declares `{}` as its #time populator but has no such column",
237					object, column
238				),
239				column: None,
240				fragment: Fragment::None,
241				label: Some("declared populator does not exist".to_string()),
242				help: Some(
243					"the populator must name one of the object's own columns; correct the `ts` key in the object's WITH clause"
244						.to_string(),
245				),
246				notes: vec![
247					"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"
248						.to_string(),
249				],
250				cause: None,
251				operator_chain: None,
252			},
253
254			EngineError::TimePopulatorNotDateTime {
255				object,
256				column,
257				found,
258			} => Diagnostic {
259				code: "TIME_002".to_string(),
260				rql: None,
261				message: format!(
262					"`{}.{}` is the declared #time populator but holds {} on this row",
263					object, column, found
264				),
265				column: None,
266				fragment: Fragment::None,
267				label: Some("populator is not a non-none DateTime".to_string()),
268				help: Some(
269					"every row of an event-time object must carry a non-none DateTime in its populator column"
270						.to_string(),
271				),
272				notes: vec![
273					"#time is substrate-owned and cannot fall back to the write clock; silently substituting arrival time would date a replayed row to now"
274						.to_string(),
275				],
276				cause: None,
277				operator_chain: None,
278			},
279		}
280	}
281}
282
283impl From<EngineError> for Error {
284	fn from(err: EngineError) -> Self {
285		Error(Box::new(err.into_diagnostic()))
286	}
287}