Skip to main content

reifydb_type/error/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{
5	fmt::{Display, Formatter},
6	mem,
7	ops::{Deref, DerefMut},
8};
9
10use serde::{Deserialize, Serialize, de, ser};
11
12mod diagnostic;
13pub mod r#macro;
14pub mod render;
15pub mod util;
16
17use std::{array::TryFromSliceError, error, fmt, num, string};
18
19use render::DefaultRenderer;
20
21use crate::{fragment::Fragment, value::r#type::Type};
22
23/// Entry in the operator call chain for flow operator errors
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct OperatorChainEntry {
26	pub node_id: u64,
27	pub operator_name: String,
28	pub operator_version: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Diagnostic {
33	pub code: String,
34	pub rql: Option<String>,
35	pub message: String,
36	pub column: Option<DiagnosticColumn>,
37	pub fragment: Fragment,
38	pub label: Option<String>,
39	pub help: Option<String>,
40	pub notes: Vec<String>,
41	pub cause: Option<Box<Diagnostic>>,
42	/// Operator call chain when error occurred (for flow operator errors)
43	pub operator_chain: Option<Vec<OperatorChainEntry>>,
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct DiagnosticColumn {
48	pub name: String,
49	pub r#type: Type,
50}
51
52impl Default for Diagnostic {
53	fn default() -> Self {
54		Self {
55			code: String::new(),
56			rql: None,
57			message: String::new(),
58			column: None,
59			fragment: Fragment::None,
60			label: None,
61			help: None,
62			notes: Vec::new(),
63			cause: None,
64			operator_chain: None,
65		}
66	}
67}
68
69impl Display for Diagnostic {
70	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
71		f.write_fmt(format_args!("{}", self.code))
72	}
73}
74
75impl Diagnostic {
76	/// Set the RQL for this diagnostic and all nested diagnostics
77	/// recursively
78	pub fn with_rql(&mut self, rql: String) {
79		self.rql = Some(rql.clone());
80
81		if let Some(ref mut cause) = self.cause {
82			let mut updated_cause = mem::take(cause.as_mut());
83			updated_cause.with_rql(rql);
84			**cause = updated_cause;
85		}
86	}
87
88	/// Set or update the fragment for this diagnostic and all nested
89	/// diagnostics recursively
90	pub fn with_fragment(&mut self, new_fragment: Fragment) {
91		// Always update the fragment, not just when it's None
92		// This is needed for cast errors that need to update the
93		// fragment
94		self.fragment = new_fragment;
95
96		if let Some(ref mut cause) = self.cause {
97			cause.with_fragment(self.fragment.clone());
98		}
99	}
100
101	/// Get the fragment if this is a Statement fragment (for backward
102	/// compatibility)
103	pub fn fragment(&self) -> Option<Fragment> {
104		match &self.fragment {
105			Fragment::Statement {
106				..
107			} => Some(self.fragment.clone()),
108			_ => None,
109		}
110	}
111}
112
113/// Trait for converting error types into Diagnostic.
114///
115/// Implement this trait to provide rich diagnostic information for custom error types.
116/// The trait consumes the error (takes `self` by value) to allow moving owned data
117/// into the diagnostic.
118pub trait IntoDiagnostic {
119	/// Convert self into a Diagnostic with error code, message, fragment, and other metadata.
120	fn into_diagnostic(self) -> Diagnostic;
121}
122
123#[derive(Debug, Clone, PartialEq)]
124pub enum UnaryOp {
125	Not,
126}
127
128impl Display for UnaryOp {
129	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
130		match self {
131			UnaryOp::Not => f.write_str("NOT"),
132		}
133	}
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub enum BinaryOp {
138	Add,
139	Sub,
140	Mul,
141	Div,
142	Rem,
143	Equal,
144	NotEqual,
145	LessThan,
146	LessThanEqual,
147	GreaterThan,
148	GreaterThanEqual,
149	Between,
150}
151
152impl BinaryOp {
153	pub fn symbol(&self) -> &'static str {
154		match self {
155			BinaryOp::Add => "+",
156			BinaryOp::Sub => "-",
157			BinaryOp::Mul => "*",
158			BinaryOp::Div => "/",
159			BinaryOp::Rem => "%",
160			BinaryOp::Equal => "==",
161			BinaryOp::NotEqual => "!=",
162			BinaryOp::LessThan => "<",
163			BinaryOp::LessThanEqual => "<=",
164			BinaryOp::GreaterThan => ">",
165			BinaryOp::GreaterThanEqual => ">=",
166			BinaryOp::Between => "BETWEEN",
167		}
168	}
169}
170
171impl Display for BinaryOp {
172	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
173		f.write_str(self.symbol())
174	}
175}
176
177#[derive(Debug, Clone, PartialEq)]
178pub enum LogicalOp {
179	Not,
180	And,
181	Or,
182	Xor,
183}
184
185impl Display for LogicalOp {
186	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187		match self {
188			LogicalOp::Not => f.write_str("NOT"),
189			LogicalOp::And => f.write_str("AND"),
190			LogicalOp::Or => f.write_str("OR"),
191			LogicalOp::Xor => f.write_str("XOR"),
192		}
193	}
194}
195
196#[derive(Debug, Clone, PartialEq)]
197pub enum OperandCategory {
198	Number,
199	Text,
200	Temporal,
201	Uuid,
202}
203
204impl Display for OperandCategory {
205	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
206		match self {
207			OperandCategory::Number => f.write_str("number"),
208			OperandCategory::Text => f.write_str("text"),
209			OperandCategory::Temporal => f.write_str("temporal value"),
210			OperandCategory::Uuid => f.write_str("UUID"),
211		}
212	}
213}
214
215#[derive(Debug, Clone, PartialEq)]
216pub enum ConstraintKind {
217	Utf8MaxBytes {
218		actual: usize,
219		max: usize,
220	},
221	BlobMaxBytes {
222		actual: usize,
223		max: usize,
224	},
225	IntMaxBytes {
226		actual: usize,
227		max: usize,
228	},
229	UintMaxBytes {
230		actual: usize,
231		max: usize,
232	},
233	DecimalPrecision {
234		actual: u8,
235		max: u8,
236	},
237	DecimalScale {
238		actual: u8,
239		max: u8,
240	},
241	NoneNotAllowed {
242		column_type: Type,
243	},
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub enum TemporalKind {
248	InvalidDateFormat,
249	InvalidDateTimeFormat,
250	InvalidTimeFormat,
251	InvalidDurationFormat,
252	InvalidYear,
253	InvalidTimeComponentFormat {
254		component: String,
255	},
256	InvalidMonth,
257	InvalidDay,
258	InvalidHour,
259	InvalidMinute,
260	InvalidSecond,
261	InvalidFractionalSeconds,
262	InvalidDateValues,
263	InvalidTimeValues,
264	InvalidDurationCharacter,
265	IncompleteDurationSpecification,
266	InvalidUnitInContext {
267		unit: char,
268		in_time_part: bool,
269	},
270	InvalidDurationComponentValue {
271		unit: char,
272	},
273	UnrecognizedTemporalPattern,
274	EmptyDateComponent,
275	EmptyTimeComponent,
276	DuplicateDurationComponent {
277		component: char,
278	},
279	OutOfOrderDurationComponent {
280		component: char,
281	},
282	DateTimeOutOfRange,
283	DateTimeOverflow {
284		message: String,
285	},
286	DurationOverflow {
287		message: String,
288	},
289	DurationMixedSign {
290		days: i32,
291		nanos: i64,
292	},
293	TimeOverflow {
294		message: String,
295	},
296	DateOverflow {
297		message: String,
298	},
299}
300
301#[derive(Debug, Clone, PartialEq)]
302pub enum BlobEncodingKind {
303	InvalidHex,
304	InvalidBase64,
305	InvalidBase64Url,
306	InvalidBase58,
307	InvalidUtf8Sequence {
308		error: String,
309	},
310}
311
312#[derive(Debug, Clone, PartialEq)]
313pub enum AstErrorKind {
314	TokenizeError {
315		message: String,
316	},
317	UnexpectedEof,
318	ExpectedIdentifier,
319	InvalidColumnProperty,
320	InvalidPolicy,
321	UnexpectedToken {
322		expected: String,
323	},
324	UnsupportedToken,
325	MultipleExpressionsWithoutBraces,
326	UnrecognizedType,
327	UnsupportedAstNode {
328		node_type: String,
329	},
330	EmptyPipeline,
331}
332
333#[derive(Debug, Clone, PartialEq)]
334pub enum ProcedureErrorKind {
335	UndefinedProcedure {
336		name: String,
337	},
338	/// A catalog entry for a Native/FFI/WASM procedure references a `native_name` that
339	/// is not registered in the runtime `Procedures` registry. This is a user-visible
340	/// condition — can happen after a binary upgrade that removed a built-in, or after
341	/// an FFI/WASM plugin was removed.
342	NoRegisteredImplementation {
343		name: String,
344	},
345}
346
347#[derive(Debug, Clone, PartialEq)]
348pub enum RuntimeErrorKind {
349	VariableNotFound {
350		name: String,
351	},
352	VariableIsDataframe {
353		name: String,
354	},
355	VariableIsImmutable {
356		name: String,
357	},
358	BreakOutsideLoop,
359	ContinueOutsideLoop,
360	MaxIterationsExceeded {
361		limit: usize,
362	},
363	UndefinedFunction {
364		name: String,
365	},
366	FieldNotFound {
367		variable: String,
368		field: String,
369		available: Vec<String>,
370	},
371	AppendTargetNotFrame {
372		name: String,
373	},
374	AppendColumnMismatch {
375		name: String,
376		existing: Vec<String>,
377		incoming: Vec<String>,
378		fragment: Fragment,
379	},
380	ExpectedSingleColumn {
381		actual: usize,
382	},
383}
384
385#[derive(Debug, Clone, PartialEq)]
386pub enum NetworkErrorKind {
387	Connection {
388		message: String,
389	},
390	Engine {
391		message: String,
392	},
393	Transport {
394		message: String,
395	},
396	Status {
397		message: String,
398	},
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub enum AuthErrorKind {
403	AuthenticationFailed {
404		reason: String,
405	},
406	AuthorizationDenied {
407		resource: String,
408	},
409	TokenExpired,
410	InvalidToken,
411}
412
413#[derive(Debug, Clone, PartialEq)]
414pub enum FunctionErrorKind {
415	UnknownFunction,
416	ArityMismatch {
417		expected: usize,
418		actual: usize,
419	},
420	TooManyArguments {
421		max_args: usize,
422		actual: usize,
423	},
424	InvalidArgumentType {
425		index: usize,
426		expected: Vec<Type>,
427		actual: Type,
428	},
429	UndefinedArgument {
430		index: usize,
431	},
432	MissingInput,
433	ExecutionFailed {
434		reason: String,
435	},
436	InternalError {
437		details: String,
438	},
439	GeneratorNotFound,
440}
441
442#[derive(Debug, thiserror::Error)]
443pub enum TypeError {
444	#[error("Cannot apply {operator} operator to {operand_category}")]
445	LogicalOperatorNotApplicable {
446		operator: LogicalOp,
447		operand_category: OperandCategory,
448		fragment: Fragment,
449	},
450
451	#[error("Cannot apply '{operator}' operator to {left} and {right}")]
452	BinaryOperatorNotApplicable {
453		operator: BinaryOp,
454		left: Type,
455		right: Type,
456		fragment: Fragment,
457	},
458
459	#[error("unsupported cast from {from} to {to}")]
460	UnsupportedCast {
461		from: Type,
462		to: Type,
463		fragment: Fragment,
464	},
465
466	#[error("failed to cast to {target}")]
467	CastToNumberFailed {
468		target: Type,
469		fragment: Fragment,
470		cause: Box<TypeError>,
471	},
472
473	#[error("failed to cast to {target}")]
474	CastToTemporalFailed {
475		target: Type,
476		fragment: Fragment,
477		cause: Box<TypeError>,
478	},
479
480	#[error("failed to cast to bool")]
481	CastToBooleanFailed {
482		fragment: Fragment,
483		cause: Box<TypeError>,
484	},
485
486	#[error("failed to cast to {target}")]
487	CastToUuidFailed {
488		target: Type,
489		fragment: Fragment,
490		cause: Box<TypeError>,
491	},
492
493	#[error("failed to cast BLOB to UTF8")]
494	CastBlobToUtf8Failed {
495		fragment: Fragment,
496		cause: Box<TypeError>,
497	},
498
499	#[error("{message}")]
500	ConstraintViolation {
501		kind: ConstraintKind,
502		message: String,
503		fragment: Fragment,
504	},
505
506	#[error("invalid number format")]
507	InvalidNumberFormat {
508		target: Type,
509		fragment: Fragment,
510	},
511
512	#[error("number out of range")]
513	NumberOutOfRange {
514		target: Type,
515		fragment: Fragment,
516		descriptor: Option<NumberOutOfRangeDescriptor>,
517	},
518
519	#[error("NaN not allowed")]
520	NanNotAllowed,
521
522	#[error("too large for precise float conversion")]
523	IntegerPrecisionLoss {
524		shape_type: Type,
525		target: Type,
526		fragment: Fragment,
527	},
528
529	#[error("decimal scale exceeds precision")]
530	DecimalScaleExceedsPrecision {
531		scale: u8,
532		precision: u8,
533		fragment: Fragment,
534	},
535
536	#[error("invalid decimal precision")]
537	DecimalPrecisionInvalid {
538		precision: u8,
539	},
540
541	#[error("invalid boolean format")]
542	InvalidBooleanFormat {
543		fragment: Fragment,
544	},
545
546	#[error("empty boolean value")]
547	EmptyBooleanValue {
548		fragment: Fragment,
549	},
550
551	#[error("invalid boolean")]
552	InvalidNumberBoolean {
553		fragment: Fragment,
554	},
555
556	#[error("{message}")]
557	Temporal {
558		kind: TemporalKind,
559		message: String,
560		fragment: Fragment,
561	},
562
563	#[error("invalid UUID v4 format")]
564	InvalidUuid4Format {
565		fragment: Fragment,
566	},
567
568	#[error("invalid UUID v7 format")]
569	InvalidUuid7Format {
570		fragment: Fragment,
571	},
572
573	#[error("{message}")]
574	BlobEncoding {
575		kind: BlobEncodingKind,
576		message: String,
577		fragment: Fragment,
578	},
579
580	#[error("Serde deserialization error: {message}")]
581	SerdeDeserialize {
582		message: String,
583	},
584
585	#[error("Serde serialization error: {message}")]
586	SerdeSerialize {
587		message: String,
588	},
589
590	#[error("Keycode serialization error: {message}")]
591	SerdeKeycode {
592		message: String,
593	},
594
595	#[error("Array conversion error: {message}")]
596	ArrayConversion {
597		message: String,
598	},
599
600	#[error("UTF-8 conversion error: {message}")]
601	Utf8Conversion {
602		message: String,
603	},
604
605	#[error("Integer conversion error: {message}")]
606	IntegerConversion {
607		message: String,
608	},
609
610	#[error("{message}")]
611	Network {
612		kind: NetworkErrorKind,
613		message: String,
614	},
615
616	#[error("{message}")]
617	Auth {
618		kind: AuthErrorKind,
619		message: String,
620	},
621
622	#[error("dictionary entry ID {value} exceeds maximum {max_value} for type {id_type}")]
623	DictionaryCapacityExceeded {
624		id_type: Type,
625		value: u128,
626		max_value: u128,
627	},
628
629	#[error("{message}")]
630	AssertionFailed {
631		fragment: Fragment,
632		message: String,
633		expression: Option<String>,
634	},
635
636	#[error("{message}")]
637	Function {
638		kind: FunctionErrorKind,
639		message: String,
640		fragment: Fragment,
641	},
642
643	#[error("{message}")]
644	Ast {
645		kind: AstErrorKind,
646		message: String,
647		fragment: Fragment,
648	},
649
650	#[error("{message}")]
651	Runtime {
652		kind: RuntimeErrorKind,
653		message: String,
654	},
655
656	#[error("{message}")]
657	Procedure {
658		kind: ProcedureErrorKind,
659		message: String,
660		fragment: Fragment,
661	},
662}
663
664#[derive(Debug, Clone, PartialEq)]
665pub struct NumberOutOfRangeDescriptor {
666	pub namespace: Option<String>,
667	pub table: Option<String>,
668	pub column: Option<String>,
669	pub column_type: Option<Type>,
670}
671
672impl NumberOutOfRangeDescriptor {
673	pub fn location_string(&self) -> String {
674		match (self.namespace.as_deref(), self.table.as_deref(), self.column.as_deref()) {
675			(Some(s), Some(t), Some(c)) => format!("{}::{}.{}", s, t, c),
676			(Some(s), Some(t), None) => format!("{}::{}", s, t),
677			(None, Some(t), Some(c)) => format!("{}.{}", t, c),
678			(Some(s), None, Some(c)) => format!("{}::{}", s, c),
679			(Some(s), None, None) => s.to_string(),
680			(None, Some(t), None) => t.to_string(),
681			(None, None, Some(c)) => c.to_string(),
682			(None, None, None) => "unknown location".to_string(),
683		}
684	}
685}
686
687#[derive(Debug, PartialEq)]
688pub struct Error(pub Box<Diagnostic>);
689
690impl Deref for Error {
691	type Target = Diagnostic;
692
693	fn deref(&self) -> &Self::Target {
694		&self.0
695	}
696}
697
698impl DerefMut for Error {
699	fn deref_mut(&mut self) -> &mut Self::Target {
700		&mut self.0
701	}
702}
703
704impl Display for Error {
705	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
706		let out = DefaultRenderer::render_string(&self.0);
707		f.write_str(out.as_str())
708	}
709}
710
711impl Error {
712	pub fn diagnostic(self) -> Diagnostic {
713		*self.0
714	}
715}
716
717impl error::Error for Error {}
718
719impl de::Error for Error {
720	fn custom<T: Display>(msg: T) -> Self {
721		TypeError::SerdeDeserialize {
722			message: msg.to_string(),
723		}
724		.into()
725	}
726}
727
728impl ser::Error for Error {
729	fn custom<T: Display>(msg: T) -> Self {
730		TypeError::SerdeSerialize {
731			message: msg.to_string(),
732		}
733		.into()
734	}
735}
736
737impl From<num::TryFromIntError> for Error {
738	fn from(err: num::TryFromIntError) -> Self {
739		TypeError::IntegerConversion {
740			message: err.to_string(),
741		}
742		.into()
743	}
744}
745
746impl From<TryFromSliceError> for Error {
747	fn from(err: TryFromSliceError) -> Self {
748		TypeError::ArrayConversion {
749			message: err.to_string(),
750		}
751		.into()
752	}
753}
754
755impl From<string::FromUtf8Error> for Error {
756	fn from(err: string::FromUtf8Error) -> Self {
757		TypeError::Utf8Conversion {
758			message: err.to_string(),
759		}
760		.into()
761	}
762}
763
764impl From<TypeError> for Error {
765	fn from(err: TypeError) -> Self {
766		Error(Box::new(err.into_diagnostic()))
767	}
768}
769
770impl From<Box<TypeError>> for Error {
771	fn from(err: Box<TypeError>) -> Self {
772		Error(Box::new(err.into_diagnostic()))
773	}
774}
775
776impl From<num::TryFromIntError> for TypeError {
777	fn from(err: num::TryFromIntError) -> Self {
778		TypeError::IntegerConversion {
779			message: err.to_string(),
780		}
781	}
782}
783
784impl From<TryFromSliceError> for TypeError {
785	fn from(err: TryFromSliceError) -> Self {
786		TypeError::ArrayConversion {
787			message: err.to_string(),
788		}
789	}
790}
791
792impl From<string::FromUtf8Error> for TypeError {
793	fn from(err: string::FromUtf8Error) -> Self {
794		TypeError::Utf8Conversion {
795			message: err.to_string(),
796		}
797	}
798}
799
800impl de::Error for TypeError {
801	fn custom<T: Display>(msg: T) -> Self {
802		TypeError::SerdeDeserialize {
803			message: msg.to_string(),
804		}
805	}
806}
807
808impl ser::Error for TypeError {
809	fn custom<T: Display>(msg: T) -> Self {
810		TypeError::SerdeSerialize {
811			message: msg.to_string(),
812		}
813	}
814}