Skip to main content

selene_gql/runtime/
error.rs

1//! Executor diagnostics and GQLSTATUS mapping.
2
3use std::{
4    borrow::Cow,
5    time::{Duration, Instant},
6};
7
8use selene_core::DbString;
9
10use crate::{AnalysisError, GqlStatus, ParserError, PlannerError, ProcedureError, SourceSpan};
11
12/// Table 8 data-exception subclasses used by runtime evaluation.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[non_exhaustive]
15pub enum DataExceptionSubclass {
16    /// Generic data exception fallback for genuinely unclassified runtime data failures.
17    DataException,
18    /// String data, right truncation (`22001`).
19    StringDataRightTruncation,
20    /// Numeric value out of range (`22003`).
21    NumericValueOutOfRange,
22    /// Null value not allowed (`22004`).
23    NullValueNotAllowed,
24    /// Invalid datetime format (`22007`).
25    InvalidDatetimeFormat,
26    /// Substring error (`22011`).
27    SubstringError,
28    /// Division by zero (`22012`).
29    DivisionByZero,
30    /// Invalid argument for natural logarithm (`2201E`).
31    InvalidArgumentForNaturalLogarithm,
32    /// Invalid argument for power function (`2201F`).
33    InvalidArgumentForPowerFunction,
34    /// Trim error (`22027`).
35    TrimError,
36    /// Invalid character value for cast (`22018`).
37    InvalidCharacterValueForCast,
38    /// Invalid time zone displacement value (`22009`).
39    InvalidTimeZone,
40    /// Negative limit value (`22G02`).
41    NegativeLimitValue,
42    /// Invalid value type (`22G03`).
43    InvalidValueType,
44    /// Values not comparable (`22G04`).
45    ValuesNotComparable,
46    /// Invalid date, time, or datetime function field name (`22G05`).
47    InvalidDatetimeFunctionFieldName,
48    /// Invalid date, time, or datetime function value (`22G06`).
49    InvalidDatetimeFunctionValue,
50    /// Invalid duration function field name (`22G07`).
51    InvalidDurationFunctionFieldName,
52    /// List data, right truncation (`22G0B`).
53    ListDataRightTruncation,
54    /// List element error (`22G0C`).
55    ListElementError,
56    /// Invalid duration format (`22G0H`).
57    InvalidDurationFormat,
58    /// Path data, right truncation (`22G10`).
59    PathDataRightTruncation,
60    /// Incompatible temporal instant unit groups (`22G14`).
61    IncompatibleTemporalInstantUnitGroups,
62    /// Multiple assignments to a graph element property (`22G0M`).
63    MultipleAssignmentsToGraphElementProperty,
64    /// Number of node labels below supported minimum (`22G0N`).
65    NodeLabelsBelowSupportedMinimum,
66    /// Number of node labels exceeds supported maximum (`22G0P`).
67    NodeLabelsExceedSupportedMaximum,
68    /// Number of edge labels below supported minimum (`22G0Q`).
69    EdgeLabelsBelowSupportedMinimum,
70    /// Number of edge labels exceeds supported maximum (`22G0R`).
71    EdgeLabelsExceedSupportedMaximum,
72    /// Number of node properties exceeds supported maximum (`22G0S`).
73    NodePropertiesExceedSupportedMaximum,
74    /// Number of edge properties exceeds supported maximum (`22G0T`).
75    EdgePropertiesExceedSupportedMaximum,
76    /// Record fields do not match the target record type on CAST (`22G0U`).
77    RecordFieldsDoNotMatch,
78    /// Record data field unassignable (`22G0X`).
79    RecordDataFieldUnassignable,
80    /// Malformed path (`22G0Z`).
81    MalformedPath,
82}
83
84impl DataExceptionSubclass {
85    /// Return this subclass's GQLSTATUS code.
86    #[must_use]
87    pub const fn gqlstatus(self) -> GqlStatus {
88        match self {
89            Self::DataException => GqlStatus::DATA_EXCEPTION,
90            Self::StringDataRightTruncation => GqlStatus::STRING_DATA_RIGHT_TRUNCATION,
91            Self::NumericValueOutOfRange => GqlStatus::NUMERIC_VALUE_OUT_OF_RANGE,
92            Self::NullValueNotAllowed => GqlStatus::NULL_VALUE_NOT_ALLOWED,
93            Self::InvalidDatetimeFormat => GqlStatus::INVALID_DATETIME_FORMAT,
94            Self::SubstringError => GqlStatus::SUBSTRING_ERROR,
95            Self::DivisionByZero => GqlStatus::DIVISION_BY_ZERO,
96            Self::InvalidArgumentForNaturalLogarithm => {
97                GqlStatus::INVALID_ARGUMENT_FOR_NATURAL_LOGARITHM
98            }
99            Self::InvalidArgumentForPowerFunction => GqlStatus::INVALID_ARGUMENT_FOR_POWER_FUNCTION,
100            Self::TrimError => GqlStatus::TRIM_ERROR,
101            Self::InvalidCharacterValueForCast => GqlStatus::INVALID_CHARACTER_VALUE_FOR_CAST,
102            Self::InvalidTimeZone => GqlStatus::INVALID_TIME_ZONE,
103            Self::NegativeLimitValue => GqlStatus::NEGATIVE_LIMIT_VALUE,
104            Self::InvalidValueType => GqlStatus::DATATYPE_MISMATCH,
105            Self::ValuesNotComparable => GqlStatus::VALUES_NOT_COMPARABLE,
106            Self::InvalidDatetimeFunctionFieldName => {
107                GqlStatus::INVALID_DATETIME_FUNCTION_FIELD_NAME
108            }
109            Self::InvalidDatetimeFunctionValue => GqlStatus::INVALID_DATETIME_FUNCTION_VALUE,
110            Self::InvalidDurationFunctionFieldName => {
111                GqlStatus::INVALID_DURATION_FUNCTION_FIELD_NAME
112            }
113            Self::ListDataRightTruncation => GqlStatus::LIST_DATA_RIGHT_TRUNCATION,
114            Self::ListElementError => GqlStatus::LIST_ELEMENT_ERROR,
115            Self::InvalidDurationFormat => GqlStatus::INVALID_DURATION_FORMAT,
116            Self::PathDataRightTruncation => GqlStatus::PATH_DATA_RIGHT_TRUNCATION,
117            Self::IncompatibleTemporalInstantUnitGroups => {
118                GqlStatus::INCOMPATIBLE_TEMPORAL_INSTANT_UNIT_GROUPS
119            }
120            Self::MultipleAssignmentsToGraphElementProperty => {
121                GqlStatus::MULTIPLE_ASSIGNMENTS_TO_GRAPH_ELEMENT_PROPERTY
122            }
123            Self::NodeLabelsBelowSupportedMinimum => GqlStatus::NODE_LABELS_BELOW_SUPPORTED_MINIMUM,
124            Self::NodeLabelsExceedSupportedMaximum => {
125                GqlStatus::NODE_LABELS_EXCEED_SUPPORTED_MAXIMUM
126            }
127            Self::EdgeLabelsBelowSupportedMinimum => GqlStatus::EDGE_LABELS_BELOW_SUPPORTED_MINIMUM,
128            Self::EdgeLabelsExceedSupportedMaximum => {
129                GqlStatus::EDGE_LABELS_EXCEED_SUPPORTED_MAXIMUM
130            }
131            Self::NodePropertiesExceedSupportedMaximum => {
132                GqlStatus::NODE_PROPERTIES_EXCEED_SUPPORTED_MAXIMUM
133            }
134            Self::EdgePropertiesExceedSupportedMaximum => {
135                GqlStatus::EDGE_PROPERTIES_EXCEED_SUPPORTED_MAXIMUM
136            }
137            Self::RecordFieldsDoNotMatch => GqlStatus::RECORD_FIELDS_DO_NOT_MATCH,
138            Self::RecordDataFieldUnassignable => GqlStatus::RECORD_DATA_FIELD_UNASSIGNABLE,
139            Self::MalformedPath => GqlStatus::MALFORMED_PATH,
140        }
141    }
142}
143
144/// Non-fatal executor diagnostic emitted through an opt-in warning sink.
145#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct ExecutorWarning {
147    /// ISO GQLSTATUS warning code.
148    pub code: GqlStatus,
149    /// Human-readable diagnostic message.
150    pub message: String,
151    /// Source span associated with the warning.
152    pub span: SourceSpan,
153}
154
155/// Receiver for runtime warnings.
156///
157/// Sessions without an explicit sink silently discard warnings, preserving the
158/// default executor behavior. Embedders that need ISO warning visibility can
159/// pass a sink with [`crate::Session::with_warning_sink`].
160pub trait WarningSink: Send {
161    /// Receive one runtime warning.
162    fn emit(&mut self, warning: ExecutorWarning);
163}
164
165/// Query execution failure.
166#[derive(Debug, thiserror::Error, miette::Diagnostic)]
167#[non_exhaustive]
168pub enum ExecutorError {
169    /// Source text failed to parse before execution.
170    #[error("parse failed: {source}")]
171    #[diagnostic(code(SLENE_X_PARSE))]
172    Parse {
173        /// Underlying parser error.
174        #[source]
175        source: ParserError,
176    },
177
178    /// Semantic analysis failed before execution.
179    #[error("analysis failed: {source}")]
180    #[diagnostic(code(SLENE_X_ANALYZE))]
181    Analysis {
182        /// Underlying analyzer error.
183        #[source]
184        source: AnalysisError,
185    },
186
187    /// Planning failed before execution.
188    #[error("planning failed: {source}")]
189    #[diagnostic(code(SLENE_X_PLAN))]
190    Plan {
191        /// Underlying planner error.
192        #[source]
193        source: PlannerError,
194    },
195
196    /// Runtime data exception such as arithmetic overflow or division by zero.
197    #[error("execution data exception: {message}")]
198    #[diagnostic(code(SLENE_X_22000))]
199    DataException {
200        /// ISO Table 8 subclass selected by the emitting runtime site.
201        subclass: DataExceptionSubclass,
202        /// Human-readable diagnostic message.
203        message: String,
204        /// Source span for the failing expression.
205        #[label("data exception")]
206        span: SourceSpan,
207    },
208
209    /// A graph mutation would leave a dependent object behind.
210    #[error("dependent object still exists: {message}")]
211    #[diagnostic(code(SLENE_X_G1001))]
212    DependentObjectStillExists {
213        /// Human-readable diagnostic message.
214        message: String,
215        /// Source span for the failing mutation.
216        #[label("dependent object still exists")]
217        span: SourceSpan,
218    },
219
220    /// A closed-graph operation violated the bound graph type.
221    #[error("graph type violation: {message}")]
222    #[diagnostic(code(SLENE_X_G2000))]
223    GraphTypeViolation {
224        /// Human-readable diagnostic message.
225        message: String,
226        /// Source span for the failing graph-type operation.
227        #[label("graph type violation")]
228        span: SourceSpan,
229    },
230
231    /// Runtime reference lookup failed for a malformed row or dynamic access.
232    #[error("invalid runtime reference: {name}")]
233    #[diagnostic(code(SLENE_X_42002))]
234    InvalidReference {
235        /// Missing or malformed reference name.
236        name: String,
237        /// Source span requiring the reference.
238        #[label("invalid reference")]
239        span: SourceSpan,
240    },
241
242    /// A `$name` parameter was referenced but not bound on the session.
243    ///
244    /// Maps to GQLSTATUS 22G03 per ISO/IEC 39075:2024 section 23.1 Table 8.
245    #[error("unbound parameter: ${name}")]
246    #[diagnostic(code(SLENE_X_22G03))]
247    UnboundParameter {
248        /// Parameter name without the leading `$`.
249        name: DbString,
250        /// Source span requiring the parameter.
251        #[label("unbound parameter")]
252        span: SourceSpan,
253    },
254
255    /// A bound `$name` parameter had the wrong type for its runtime position.
256    ///
257    /// Maps to GQLSTATUS 22G03 per ISO/IEC 39075:2024 section 23.1 Table 8.
258    #[error("invalid parameter type for ${name}: expected {expected}, got {actual}")]
259    #[diagnostic(code(SLENE_X_22G03))]
260    InvalidParameterType {
261        /// Parameter name without the leading `$`.
262        name: DbString,
263        /// Human-readable expected type.
264        expected: Cow<'static, str>,
265        /// Human-readable actual type.
266        actual: &'static str,
267        /// Source span requiring the parameter.
268        #[label("invalid parameter type")]
269        span: SourceSpan,
270    },
271
272    /// Scalar function name was not registered in the v1.1 closed set.
273    ///
274    /// Maps to GQLSTATUS 22G03 per ISO/IEC 39075:2024 section 23.1 Table 8.
275    #[error("unknown function: {name}")]
276    #[diagnostic(code(SLENE_X_22G03))]
277    UnknownFunction {
278        /// Function name as written by the caller.
279        name: String,
280        /// Source span for the function call.
281        #[label("unknown function")]
282        span: SourceSpan,
283    },
284
285    /// Scalar function received the wrong number of arguments.
286    ///
287    /// Maps to GQLSTATUS 22G03 per ISO/IEC 39075:2024 section 23.1 Table 8.
288    #[error("function {name} expected {expected} argument(s), got {actual}")]
289    #[diagnostic(code(SLENE_X_22G03))]
290    FunctionArityMismatch {
291        /// Function name as written by the caller.
292        name: String,
293        /// Human-readable arity contract.
294        expected: &'static str,
295        /// Actual argument count.
296        actual: usize,
297        /// Source span for the function call.
298        #[label("wrong arity")]
299        span: SourceSpan,
300    },
301
302    /// Scalar function call used an aggregate-only modifier.
303    ///
304    /// Maps to GQLSTATUS 22G03 per ISO/IEC 39075:2024 section 23.1 Table 8.
305    #[error("function {name} does not allow {modifier}")]
306    #[diagnostic(code(SLENE_X_22G03))]
307    InvalidFunctionModifier {
308        /// Function name as written by the caller.
309        name: String,
310        /// Rejected modifier.
311        modifier: &'static str,
312        /// Source span for the function call.
313        #[label("invalid function modifier")]
314        span: SourceSpan,
315    },
316
317    /// Construct is ISO-legal but not yet implemented in the evaluator surface.
318    ///
319    /// Maps to GQLSTATUS 42N01, a selene-db implementation-defined subclass
320    /// under standard class 42 per ISO/IEC 39075:2024 section 23.1. The message
321    /// is deliberately version-agnostic so it does not go stale across releases.
322    #[error("feature not yet supported: {feature}")]
323    #[diagnostic(code(SLENE_X_42N01))]
324    FeatureNotSupportedYet {
325        /// Stable feature tag.
326        feature: &'static str,
327        /// Source span requiring the feature.
328        #[label("feature not yet supported")]
329        span: SourceSpan,
330    },
331
332    /// Catalog object name collides with an existing declaration.
333    #[error("duplicate {kind} name: {name}")]
334    #[diagnostic(code(SLENE_X_42N10))]
335    DuplicateObject {
336        /// Object class.
337        kind: &'static str,
338        /// Colliding object name.
339        name: DbString,
340        /// Source span of the duplicate name.
341        #[label("duplicate object name")]
342        span: SourceSpan,
343    },
344
345    /// Transaction state does not permit the requested executor operation.
346    #[error("invalid transaction state: {detail}")]
347    #[diagnostic(code(SLENE_X_25000))]
348    InvalidTransactionState {
349        /// Stable detail tag asserted by tests.
350        detail: &'static str,
351        /// Source span requiring a write transaction.
352        #[label("invalid transaction state")]
353        span: SourceSpan,
354    },
355
356    /// `START TRANSACTION` was requested while an explicit transaction exists.
357    #[error("transaction already active")]
358    #[diagnostic(code(SLENE_X_25000))]
359    TransactionAlreadyActive {
360        /// Source span for the transaction-control statement.
361        #[label("transaction already active")]
362        span: SourceSpan,
363    },
364
365    /// `COMMIT` or `ROLLBACK` was requested without an explicit transaction.
366    #[error("no active transaction")]
367    #[diagnostic(code(SLENE_X_25000))]
368    NoActiveTransaction {
369        /// Source span for the transaction-control statement.
370        #[label("no active transaction")]
371        span: SourceSpan,
372    },
373
374    /// Statement was issued while the explicit transaction is aborted.
375    #[error("statement issued against aborted explicit transaction")]
376    #[diagnostic(code(SLENE_X_25N02))]
377    InFailedTransaction {
378        /// Source span for the rejected statement.
379        #[label("aborted transaction; issue ROLLBACK to recover")]
380        span: SourceSpan,
381    },
382
383    /// A GQL-request was issued against a session closed by `SESSION CLOSE`.
384    ///
385    /// Maps to GQLSTATUS 2DN01, a selene-db implementation-defined subclass
386    /// under standard class 2D per ISO/IEC 39075:2024 section 23.1 (section 7.3).
387    #[error("session is closed")]
388    #[diagnostic(code(SLENE_X_2DN01))]
389    SessionClosed {
390        /// Source span for the rejected request.
391        #[label("session closed; open a new session")]
392        span: SourceSpan,
393    },
394
395    /// Caller-requested cooperative cancellation interrupted the statement.
396    #[error("statement cancelled")]
397    #[diagnostic(code(SLENE_X_5GQL2))]
398    Cancelled {
399        /// Source span for the cancellation checkpoint.
400        #[label("cancelled here")]
401        span: SourceSpan,
402    },
403
404    /// The statement deadline elapsed before completion.
405    #[error("statement deadline exceeded")]
406    #[diagnostic(code(SLENE_X_5GQL3))]
407    Timeout {
408        /// Deadline configured on the session.
409        deadline: Instant,
410        /// Duration since the deadline elapsed.
411        elapsed: Duration,
412        /// Source span for the timeout checkpoint.
413        #[label("deadline exceeded here")]
414        span: SourceSpan,
415    },
416
417    /// The statement produced more outermost result rows than allowed.
418    #[error("statement row cap exceeded ({cap})")]
419    #[diagnostic(code(SLENE_X_5GQL1))]
420    RowCapExceeded {
421        /// Maximum allowed outermost result rows.
422        cap: usize,
423        /// Source span for the result boundary.
424        #[label("row cap exceeded here")]
425        span: SourceSpan,
426    },
427
428    /// An implementation-defined program limit was exceeded.
429    #[error("program limit exceeded: {detail}")]
430    #[diagnostic(code(SLENE_X_5GQL1))]
431    ProgramLimitExceeded {
432        /// Stable limit detail asserted by tests.
433        detail: &'static str,
434        /// Source span for the limit boundary.
435        #[label("program limit exceeded here")]
436        span: SourceSpan,
437    },
438
439    /// The graph mutation funnel rejected a write.
440    #[error("graph mutation failed: {source}")]
441    #[diagnostic(code(SLENE_X_5GQL0_GRAPH_MUTATION))]
442    GraphMutation {
443        /// Underlying graph-layer error.
444        #[source]
445        source: selene_graph::GraphError,
446        /// Source span for the write site.
447        #[label("graph mutation failed")]
448        span: SourceSpan,
449    },
450
451    /// Commit-critical durability provider flush failed.
452    #[error("durability flush failed for provider {provider_tag}: {reason}")]
453    #[diagnostic(code(SLENE_X_5GQL0_FLUSH))]
454    Flush {
455        /// Durable provider tag.
456        provider_tag: selene_graph::ProviderTag,
457        /// Human-readable provider failure reason.
458        reason: String,
459    },
460
461    /// Procedure registry execution failed.
462    #[error("procedure execution failed: {source}")]
463    #[diagnostic(code(SLENE_X_PROC))]
464    Procedure {
465        /// Underlying procedure error.
466        #[source]
467        source: ProcedureError,
468        /// Source span for the CALL site.
469        #[label("procedure failed")]
470        span: SourceSpan,
471    },
472
473    /// Implementation-defined executor surface not supported by this brief.
474    #[error("implementation-defined executor failure: {detail}")]
475    #[diagnostic(code(SLENE_X_5GQL0_IMPLEMENTATION_DEFINED))]
476    ImplementationDefined {
477        /// Stable detail tag asserted by tests.
478        detail: &'static str,
479    },
480}
481
482impl ExecutorError {
483    /// Map this executor failure to its GQLSTATUS code.
484    #[must_use]
485    pub fn gqlstatus(&self) -> GqlStatus {
486        match self {
487            Self::Parse { source } => source.gqlstatus(),
488            Self::Analysis { source } => source.gqlstatus(),
489            Self::Plan { source } => source.gqlstatus(),
490            Self::DataException { subclass, .. } => subclass.gqlstatus(),
491            Self::DependentObjectStillExists { .. } => GqlStatus::DEPENDENT_OBJECT_STILL_EXISTS,
492            Self::GraphTypeViolation { .. } => GqlStatus::GRAPH_TYPE_VIOLATION,
493            Self::InvalidReference { .. } => GqlStatus::INVALID_REFERENCE,
494            Self::UnboundParameter { .. } | Self::InvalidParameterType { .. } => {
495                GqlStatus::INVALID_PROCEDURE_ARGUMENT
496            }
497            Self::UnknownFunction { .. }
498            | Self::FunctionArityMismatch { .. }
499            | Self::InvalidFunctionModifier { .. } => GqlStatus::DATATYPE_MISMATCH,
500            Self::FeatureNotSupportedYet { .. } => GqlStatus::FEATURE_NOT_SUPPORTED,
501            Self::DuplicateObject { .. } => GqlStatus::DUPLICATE_OBJECT,
502            Self::InvalidTransactionState { .. } => GqlStatus::READ_ONLY_TRANSACTION_VIOLATION,
503            Self::TransactionAlreadyActive { .. } => GqlStatus::ACTIVE_TRANSACTION,
504            Self::NoActiveTransaction { .. } => GqlStatus::INVALID_TRANSACTION_TERMINATION,
505            Self::InFailedTransaction { .. } => GqlStatus::IN_FAILED_TRANSACTION,
506            Self::SessionClosed { .. } => GqlStatus::SESSION_CLOSED,
507            Self::Cancelled { .. } => GqlStatus::OPERATION_CANCELLED,
508            Self::Timeout { .. } => GqlStatus::DEADLINE_EXCEEDED,
509            Self::RowCapExceeded { .. } | Self::ProgramLimitExceeded { .. } => {
510                GqlStatus::PROGRAM_LIMIT_EXCEEDED
511            }
512            Self::GraphMutation { source, .. } => GqlStatus::from_code(source.gqlstatus())
513                .unwrap_or(GqlStatus::IMPLEMENTATION_DEFINED_ERROR),
514            Self::Flush { .. } => GqlStatus::IMPLEMENTATION_DEFINED_ERROR,
515            Self::Procedure { source, .. } => source.gqlstatus(),
516            Self::ImplementationDefined { .. } => GqlStatus::IMPLEMENTATION_DEFINED_ERROR,
517        }
518    }
519
520    pub(crate) fn data_exception(
521        subclass: DataExceptionSubclass,
522        message: impl Into<String>,
523        span: SourceSpan,
524    ) -> Self {
525        Self::DataException {
526            subclass,
527            message: message.into(),
528            span,
529        }
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::DataExceptionSubclass;
536
537    #[test]
538    fn substring_error_subclass_maps_to_22011() {
539        assert_eq!(
540            DataExceptionSubclass::SubstringError.gqlstatus().as_str(),
541            "22011"
542        );
543    }
544
545    #[test]
546    fn trim_error_subclass_maps_to_22027() {
547        assert_eq!(
548            DataExceptionSubclass::TrimError.gqlstatus().as_str(),
549            "22027"
550        );
551    }
552}