Skip to main content

reifydb_routine/routine/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_catalog::error::CatalogError;
5use reifydb_type::{
6	error::{Diagnostic, Error, IntoDiagnostic, TypeError},
7	fragment::Fragment,
8	value::r#type::Type,
9};
10
11/// Unified error type for all routines (functions and procedures).
12///
13/// Replaces the previously-separate `FunctionError` and `ProcedureError`.
14/// Variant names are prefixed (`Function*` / `Procedure*`) so the user-visible
15/// diagnostic wording can stay distinct, but the field names within each
16/// variant follow the legacy convention (`function:` / `procedure:`) so the
17/// migration from the legacy types only changes the variant name, not the
18/// per-variant struct layout.
19#[derive(Debug, thiserror::Error)]
20pub enum RoutineError {
21	#[error("function {} expects {expected} arguments, got {actual}", function.text())]
22	FunctionArityMismatch {
23		function: Fragment,
24		expected: usize,
25		actual: usize,
26	},
27
28	#[error("function {} argument {} has invalid type: got {actual:?}", function.text(), argument_index + 1)]
29	FunctionInvalidArgumentType {
30		function: Fragment,
31		argument_index: usize,
32		expected: Vec<Type>,
33		actual: Type,
34	},
35
36	#[error("function {} execution failed: {reason}", function.text())]
37	FunctionExecutionFailed {
38		function: Fragment,
39		reason: String,
40	},
41
42	#[error("generator function '{}' not found", function.text())]
43	FunctionNotFound {
44		function: Fragment,
45	},
46
47	#[error("procedure {} expects {expected} arguments, got {actual}", procedure.text())]
48	ProcedureArityMismatch {
49		procedure: Fragment,
50		expected: usize,
51		actual: usize,
52	},
53
54	#[error("procedure {} argument {} has invalid type: got {actual:?}", procedure.text(), argument_index + 1)]
55	ProcedureInvalidArgumentType {
56		procedure: Fragment,
57		argument_index: usize,
58		expected: Vec<Type>,
59		actual: Type,
60	},
61
62	#[error("procedure {} execution failed: {reason}", procedure.text())]
63	ProcedureExecutionFailed {
64		procedure: Fragment,
65		reason: String,
66	},
67
68	#[error(transparent)]
69	Wrapped(Box<Error>),
70}
71
72impl From<Error> for RoutineError {
73	fn from(err: Error) -> Self {
74		RoutineError::Wrapped(Box::new(err))
75	}
76}
77
78impl From<CatalogError> for RoutineError {
79	fn from(err: CatalogError) -> Self {
80		RoutineError::Wrapped(Box::new(Error::from(err)))
81	}
82}
83
84impl From<TypeError> for RoutineError {
85	fn from(err: TypeError) -> Self {
86		RoutineError::Wrapped(Box::new(Error::from(err)))
87	}
88}
89
90impl From<Box<TypeError>> for RoutineError {
91	fn from(err: Box<TypeError>) -> Self {
92		RoutineError::Wrapped(Box::new(Error::from(err)))
93	}
94}
95
96impl From<RoutineError> for Error {
97	fn from(err: RoutineError) -> Self {
98		Error(Box::new(err.into_diagnostic()))
99	}
100}
101
102impl IntoDiagnostic for RoutineError {
103	fn into_diagnostic(self) -> Diagnostic {
104		match self {
105			RoutineError::FunctionArityMismatch {
106				function,
107				expected,
108				actual,
109			} => {
110				let name = function.text().to_string();
111				Diagnostic {
112					code: "FUNCTION_002".to_string(),
113					rql: None,
114					message: format!(
115						"Function {} expects {} arguments, got {}",
116						name, expected, actual
117					),
118					column: None,
119					fragment: function,
120					label: Some("wrong number of arguments".to_string()),
121					help: Some(format!(
122						"Provide exactly {} arguments to function {}",
123						expected, name
124					)),
125					notes: vec![],
126					cause: None,
127					operator_chain: None,
128				}
129			}
130			RoutineError::FunctionInvalidArgumentType {
131				function,
132				argument_index,
133				expected,
134				actual,
135			} => {
136				let name = function.text().to_string();
137				let expected_types =
138					expected.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ");
139				Diagnostic {
140					code: "FUNCTION_004".to_string(),
141					rql: None,
142					message: format!(
143						"Function {} argument {} has invalid type: expected one of [{}], got {:?}",
144						name,
145						argument_index + 1,
146						expected_types,
147						actual
148					),
149					column: None,
150					fragment: function,
151					label: Some("invalid argument type".to_string()),
152					help: Some(format!("Provide an argument of type: {}", expected_types)),
153					notes: vec![],
154					cause: None,
155					operator_chain: None,
156				}
157			}
158			RoutineError::FunctionExecutionFailed {
159				function,
160				reason,
161			} => {
162				let name = function.text().to_string();
163				Diagnostic {
164					code: "FUNCTION_007".to_string(),
165					rql: None,
166					message: format!("Function {} execution failed: {}", name, reason),
167					column: None,
168					fragment: function,
169					label: Some("execution failed".to_string()),
170					help: Some("Check function arguments and data".to_string()),
171					notes: vec![],
172					cause: None,
173					operator_chain: None,
174				}
175			}
176			RoutineError::FunctionNotFound {
177				function,
178			} => {
179				let name = function.text().to_string();
180				Diagnostic {
181					code: "FUNCTION_009".to_string(),
182					rql: None,
183					message: format!("Generator function '{}' not found", name),
184					column: None,
185					fragment: function,
186					label: Some("unknown generator function".to_string()),
187					help: Some("Check the generator function name and ensure it is registered"
188						.to_string()),
189					notes: vec![],
190					cause: None,
191					operator_chain: None,
192				}
193			}
194			RoutineError::ProcedureArityMismatch {
195				procedure,
196				expected,
197				actual,
198			} => {
199				let name = procedure.text().to_string();
200				Diagnostic {
201					code: "PROCEDURE_001".to_string(),
202					rql: None,
203					message: format!(
204						"Procedure {} expects {} arguments, got {}",
205						name, expected, actual
206					),
207					column: None,
208					fragment: procedure,
209					label: Some("wrong number of arguments".to_string()),
210					help: Some(format!(
211						"Provide exactly {} arguments to procedure {}",
212						expected, name
213					)),
214					notes: vec![],
215					cause: None,
216					operator_chain: None,
217				}
218			}
219			RoutineError::ProcedureInvalidArgumentType {
220				procedure,
221				argument_index,
222				expected,
223				actual,
224			} => {
225				let name = procedure.text().to_string();
226				let expected_types =
227					expected.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ");
228				Diagnostic {
229					code: "PROCEDURE_002".to_string(),
230					rql: None,
231					message: format!(
232						"Procedure {} argument {} has invalid type: expected one of [{}], got {:?}",
233						name,
234						argument_index + 1,
235						expected_types,
236						actual
237					),
238					column: None,
239					fragment: procedure,
240					label: Some("invalid argument type".to_string()),
241					help: Some(format!("Provide an argument of type: {}", expected_types)),
242					notes: vec![],
243					cause: None,
244					operator_chain: None,
245				}
246			}
247			RoutineError::ProcedureExecutionFailed {
248				procedure,
249				reason,
250			} => {
251				let name = procedure.text().to_string();
252				Diagnostic {
253					code: "PROCEDURE_003".to_string(),
254					rql: None,
255					message: format!("Procedure {} execution failed: {}", name, reason),
256					column: None,
257					fragment: procedure,
258					label: Some("execution failed".to_string()),
259					help: Some("Check procedure arguments and context".to_string()),
260					notes: vec![],
261					cause: None,
262					operator_chain: None,
263				}
264			}
265			RoutineError::Wrapped(err) => *err.0,
266		}
267	}
268}
269
270impl RoutineError {
271	/// Attach routine name context to Wrapped errors.
272	///
273	/// `is_procedure` selects whether the wrapped diagnostic uses the FUNCTION_007
274	/// or PROCEDURE_003 code so the user-visible wording stays correct.
275	pub fn with_context(self, fragment: Fragment, is_procedure: bool) -> Error {
276		match self {
277			RoutineError::Wrapped(inner) => {
278				let name = fragment.text().to_string();
279				let mut cause = *inner.0;
280				cause.with_fragment(fragment.clone());
281				let (code, message, help) = if is_procedure {
282					(
283						"PROCEDURE_003",
284						format!("Procedure {} execution failed", name),
285						"Check procedure arguments and context",
286					)
287				} else {
288					(
289						"FUNCTION_007",
290						format!("Function {} execution failed", name),
291						"Check function arguments and data",
292					)
293				};
294				Error(Box::new(Diagnostic {
295					code: code.to_string(),
296					rql: None,
297					message,
298					column: None,
299					fragment,
300					label: Some("execution failed".to_string()),
301					help: Some(help.to_string()),
302					notes: vec![],
303					cause: Some(Box::new(cause)),
304					operator_chain: None,
305				}))
306			}
307			other => Error(Box::new(other.into_diagnostic())),
308		}
309	}
310}