Skip to main content

reifydb_value/error/
mod.rs

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