Skip to main content

selene_gql/analyze/
error.rs

1//! Analyzer diagnostics.
2//!
3//! # `#[diagnostic(code(..))]` prefix taxonomy
4//!
5//! The `code(..)` attribute is the miette/`thiserror` *display* code only; the
6//! authoritative GQLSTATUS for every variant is [`AnalysisError::gqlstatus`]
7//! (ISO/IEC 39075:2024 ยง23.1 Table 8), which downstream surfaces use. Two
8//! prefixes coexist intentionally:
9//!
10//! - `SLENE_GQL_<status>` โ€” embeds the GQLSTATUS code directly (e.g.
11//!   `SLENE_GQL_42N03` for `UNDEFINED_REFERENCE`). Used by every variant whose
12//!   GQLSTATUS is a single, stable, public code.
13//! - `SLENE_A_0NN` โ€” opaque analyzer-local ordinals (`010`..`018`). Used by the
14//!   nine closed-graph (GG02) static-schema variants
15//!   ([`AnalysisError::SchemaUnknownNodeType`] through
16//!   [`AnalysisError::SchemaRequiredEdgeLabelRemoved`]). These all map to the
17//!   same `GqlStatus::GRAPH_TYPE_VIOLATION` (G2000) class, so embedding the
18//!   status in the display code would make all nine collide on one string; the
19//!   `SLENE_A_*` ordinals keep them distinguishable in diagnostics while
20//!   `gqlstatus()` reports the correct shared G2000 to callers. The ordinals
21//!   are display-only and are *not* a contract โ€” do not parse them.
22
23use selene_core::{DbString, LabelSet, PropertyValueType};
24
25mod context;
26
27pub use context::{ConditionClause, ExpectedType, PatternElementKind, Side, TypeMismatchContext};
28
29use crate::{
30    GqlStatus, GqlType, PathMode, PathSelector, ProcedureMutability, SourceSpan,
31    analyze::binding::BindingDeclKind,
32};
33
34/// Semantic-analysis failure.
35#[derive(Debug, thiserror::Error, miette::Diagnostic)]
36#[non_exhaustive]
37pub enum AnalysisError {
38    /// A reference does not resolve to any binding in the enclosing scopes.
39    #[error("undefined reference: {name}")]
40    #[diagnostic(code(SLENE_GQL_42N03))]
41    UndefinedReference {
42        /// Unresolved binding name.
43        name: DbString,
44        /// Source span of the unresolved reference.
45        #[label("not bound in scope")]
46        span: SourceSpan,
47        /// Optional repair hint.
48        #[help]
49        hint: Option<String>,
50    },
51
52    /// A strict declaration site redeclared a binding already present in its scope.
53    #[error("binding {name} is already declared in this scope")]
54    #[diagnostic(code(SLENE_GQL_42N10))]
55    Shadow {
56        /// Redeclared binding name.
57        name: DbString,
58        /// Source span of the redeclaration.
59        #[label("conflicts with an earlier binding")]
60        span: SourceSpan,
61        /// Source span of the prior declaration.
62        #[label("first declared here")]
63        prior_span: SourceSpan,
64    },
65
66    /// A pattern variable is reused with an element kind incompatible with
67    /// its prior declaration (e.g. a node variable later used as an edge,
68    /// or a path binding aliased over an existing node variable).
69    #[error(
70        "pattern variable {name} is already bound as a {prior} and cannot be reused as a {current}"
71    )]
72    #[diagnostic(code(SLENE_GQL_42N10))]
73    PatternKindMismatch {
74        /// Reused binding name.
75        name: DbString,
76        /// Element kind of the prior declaration.
77        prior: PatternElementKind,
78        /// Element kind of the new occurrence.
79        current: PatternElementKind,
80        /// Source span of the new occurrence.
81        #[label("incompatible reuse")]
82        span: SourceSpan,
83        /// Source span of the prior declaration.
84        #[label("first declared here")]
85        prior_span: SourceSpan,
86    },
87
88    /// A value alias was reused where GQL requires a graph pattern binding.
89    #[error(
90        "binding {name} is already bound as {prior_kind:?} and cannot be reused as a {new_kind}"
91    )]
92    #[diagnostic(code(SLENE_GQL_42N10))]
93    AliasReusedAsPatternBinding {
94        /// Reused binding name.
95        name: DbString,
96        /// Prior non-pattern declaration kind.
97        prior_kind: BindingDeclKind,
98        /// New graph-pattern occurrence kind.
99        new_kind: PatternElementKind,
100        /// Source span of the new occurrence.
101        #[label("alias cannot be reused as a pattern binding")]
102        span: SourceSpan,
103    },
104
105    /// The analyzer encountered an AST surface it does not route yet.
106    #[error("not implemented: {message}")]
107    #[diagnostic(code(SLENE_GQL_42N01))]
108    NotImplemented {
109        /// Human-readable missing capability.
110        message: String,
111        /// Source span requiring the missing analyzer capability.
112        #[label("not implemented yet")]
113        span: SourceSpan,
114        /// Optional implementation hint.
115        #[help]
116        hint: Option<String>,
117    },
118
119    /// ISO 16.4 forbids unbounded quantifiers without a restrictive or selective gate.
120    #[error(
121        "unbounded variable-length edge pattern requires a restrictive path mode, selective path selector, or DIFFERENT EDGES match mode"
122    )]
123    #[diagnostic(code(SLENE_GQL_42001))]
124    UnboundedRequiresGate {
125        /// Path mode in scope for the offending pattern.
126        mode: PathMode,
127        /// Path selector in scope for the offending pattern.
128        selector: Option<PathSelector>,
129        /// Source span of the unbounded quantifier.
130        #[label("unbounded quantifier requires an ISO 16.4 gate")]
131        span: SourceSpan,
132    },
133
134    /// ISO 20.6 scalar value query expression shape violation.
135    #[error("invalid VALUE subquery shape: {message}")]
136    #[diagnostic(code(SLENE_GQL_42001))]
137    ValueSubqueryShapeViolation {
138        /// Human-readable ISO 20.6 rule failure.
139        message: String,
140        /// Source span of the invalid VALUE subquery shape.
141        #[label("violates ISO 20.6 scalar value query expression shape")]
142        span: SourceSpan,
143    },
144
145    /// ISO 20.9 forbids aggregate functions directly containing aggregate
146    /// functions.
147    #[error("invalid aggregate expression: {message}")]
148    #[diagnostic(code(SLENE_GQL_42001))]
149    AggregateNestingViolation {
150        /// Human-readable ISO 20.9 rule failure.
151        message: String,
152        /// Source span of the nested aggregate expression.
153        #[label("aggregate cannot contain another aggregate")]
154        span: SourceSpan,
155    },
156
157    /// ISO 14.11 requires every grouped non-aggregate projection item to be
158    /// one of the grouping keys.
159    #[error("grouped projection item must be a grouping key or aggregate expression")]
160    #[diagnostic(code(SLENE_GQL_42001))]
161    GroupedProjectionItemNotGrouped {
162        /// Source span of the invalid projection item.
163        #[label("not a grouping key or aggregate expression")]
164        span: SourceSpan,
165    },
166
167    /// ISO 14.11 forbids `RETURN *` over a unit incoming binding table.
168    #[error("RETURN * requires a non-unit incoming binding table")]
169    #[diagnostic(code(SLENE_GQL_42001))]
170    ReturnStarRequiresInput {
171        /// Source span of the invalid `RETURN *`.
172        #[label("no incoming bindings to expand")]
173        span: SourceSpan,
174    },
175
176    /// ISO 14.10 forbids nested query specifications inside sort keys.
177    #[error("ORDER BY sort key cannot contain a nested query specification")]
178    #[diagnostic(code(SLENE_GQL_42001))]
179    SortKeyContainsNestedQuery {
180        /// Source span of the invalid sort key.
181        #[label("nested query specification is not allowed in a sort key")]
182        span: SourceSpan,
183    },
184
185    /// ISO 14.10 restricts aggregate functions in post-`RETURN` sort keys.
186    #[error("ORDER BY sort key cannot contain an aggregate function in this RETURN context")]
187    #[diagnostic(code(SLENE_GQL_42001))]
188    SortKeyContainsAggregate {
189        /// Source span of the invalid sort key.
190        #[label("aggregate function is not allowed in this sort key")]
191        span: SourceSpan,
192    },
193
194    /// A reference is syntactically resolved but not valid in this expression context.
195    #[error("invalid reference: {message}")]
196    #[diagnostic(code(SLENE_GQL_42002))]
197    InvalidReference {
198        /// Human-readable rule failure.
199        message: String,
200        /// Source span of the invalid reference.
201        #[label("invalid reference here")]
202        span: SourceSpan,
203    },
204
205    /// Analyzer expression recursion exceeded the implementation-defined cap.
206    #[error("expression nesting depth {depth} exceeds analyzer limit")]
207    #[diagnostic(code(SLENE_GQL_5GQL1))]
208    RecursionLimitExceeded {
209        /// Depth observed when the limit was exceeded.
210        depth: u32,
211    },
212
213    /// A statically-decidable type mismatch.
214    #[error("{context}: expected {expected}, found {found:?}")]
215    #[diagnostic(code(SLENE_GQL_22G03))]
216    TypeMismatch {
217        /// Operation or clause that required a different type.
218        context: TypeMismatchContext,
219        /// Expected type category.
220        expected: ExpectedType,
221        /// Resolved type that violated the expectation.
222        found: GqlType,
223        /// Source span of the incompatible expression.
224        #[label("incompatible type")]
225        span: SourceSpan,
226    },
227    /// Conflicting inline types were declared for one parameter.
228    #[error("conflicting declared types for parameter ${name}")]
229    #[diagnostic(code(SLENE_GQL_22G03))]
230    ConflictingParameterTypes {
231        /// Name without the leading `$`.
232        name: DbString,
233        /// Conflicts in encounter order.
234        declarations: Vec<(GqlType, SourceSpan)>,
235    },
236    /// Procedure name was not registered.
237    #[error("unknown procedure: {}", display_qualified_name(name))]
238    #[diagnostic(code(SLENE_GQL_42N04))]
239    UnknownProcedure {
240        /// Qualified procedure name.
241        name: Box<[DbString]>,
242        /// Source span of the procedure call.
243        #[label("procedure is not registered")]
244        span: SourceSpan,
245    },
246
247    /// Procedure argument arity mismatch.
248    #[error(
249        "wrong argument count for {}: expected {}, found {actual}",
250        display_qualified_name(procedure),
251        display_argument_range(*minimum, *expected)
252    )]
253    #[diagnostic(code(SLENE_GQL_22G03))]
254    WrongArgumentCount {
255        /// Qualified procedure name.
256        procedure: Box<[DbString]>,
257        /// Maximum expected argument count.
258        expected: usize,
259        /// Minimum expected argument count.
260        minimum: usize,
261        /// Actual argument count.
262        actual: usize,
263        /// Source span of the procedure call.
264        #[label("wrong number of arguments")]
265        span: SourceSpan,
266    },
267
268    /// `YIELD col` referenced a column not in the procedure output schema.
269    #[error(
270        "unknown YIELD column {column} for procedure {}",
271        display_qualified_name(procedure)
272    )]
273    #[diagnostic(code(SLENE_GQL_42N03))]
274    UnknownYieldColumn {
275        /// Qualified procedure name.
276        procedure: Box<[DbString]>,
277        /// Requested output column.
278        column: DbString,
279        /// Source span of the YIELD item.
280        #[label("column is not produced by this procedure")]
281        span: SourceSpan,
282    },
283
284    /// A read-only pipeline invoked a procedure declared as graph-writing,
285    /// schema-writing, or administrative.
286    #[error(
287        "mutating procedure {} cannot be invoked in a read pipeline",
288        display_qualified_name(procedure)
289    )]
290    #[diagnostic(code(SLENE_GQL_25G02))]
291    MutatingProcedureInReadPipeline {
292        /// Qualified procedure name.
293        procedure: Box<[DbString]>,
294        /// Declared procedure mutability.
295        mutability: ProcedureMutability,
296        /// Source span of the procedure call.
297        #[label("read pipelines cannot invoke mutating procedures")]
298        span: SourceSpan,
299    },
300
301    /// Static closed-graph validation found no matching node type.
302    #[error("{labels:?} does not match any node type in graph type {graph_type}")]
303    #[diagnostic(code(SLENE_A_010))]
304    SchemaUnknownNodeType {
305        /// Observed static label set.
306        labels: LabelSet,
307        /// Bound graph type name.
308        graph_type: DbString,
309        /// Source span of the offending label expression or pattern.
310        #[label("unknown node type")]
311        span: SourceSpan,
312    },
313
314    /// Static closed-graph validation found no matching edge type.
315    #[error("edge label {label} does not match any edge type in graph type {graph_type}")]
316    #[diagnostic(code(SLENE_A_011))]
317    SchemaUnknownEdgeType {
318        /// Edge label.
319        label: DbString,
320        /// Bound graph type name.
321        graph_type: DbString,
322        /// Source span of the offending edge label.
323        #[label("unknown edge type")]
324        span: SourceSpan,
325    },
326
327    /// Static closed-graph validation found an edge endpoint mismatch.
328    #[error(
329        "edge label {label}: declared as {expected_source} -> {expected_target} but used as {observed_source:?} -> {observed_target:?}"
330    )]
331    #[diagnostic(code(SLENE_A_012))]
332    SchemaEdgeEndpointMismatch {
333        /// Edge label.
334        label: DbString,
335        /// Expected source node type name.
336        expected_source: String,
337        /// Expected target node type name.
338        expected_target: String,
339        /// Observed source label set.
340        ///
341        /// Boxed (together with `observed_target`) so this variant does not
342        /// inflate `AnalysisError` past clippy's `result_large_err` threshold:
343        /// `LabelSet` wraps `SmallVec<[DbString; 3]>`, so two inline copies
344        /// dominated the variant. The `Box` moves both onto the cold
345        /// error-construction path.
346        observed_source: Box<LabelSet>,
347        /// Observed target label set. Boxed for the same reason as
348        /// `observed_source`.
349        observed_target: Box<LabelSet>,
350        /// Source span of the offending edge pattern.
351        #[label("endpoint types do not match edge declaration")]
352        span: SourceSpan,
353    },
354
355    /// Static closed-graph validation found an undeclared property.
356    #[error("property {property} is not declared by {declared_in} in graph type {graph_type}")]
357    #[diagnostic(code(SLENE_A_013))]
358    SchemaUndeclaredProperty {
359        /// Undeclared property key.
360        property: DbString,
361        /// Node or edge type name that was checked.
362        declared_in: DbString,
363        /// Bound graph type name.
364        graph_type: DbString,
365        /// Source span of the property write.
366        #[label("property is not declared")]
367        span: SourceSpan,
368    },
369
370    /// Static closed-graph validation found a property value type mismatch.
371    #[error("property {property} of {declared_in} declared {expected} but value is {found:?}")]
372    #[diagnostic(code(SLENE_A_014))]
373    SchemaPropertyTypeMismatch {
374        /// Property key.
375        property: DbString,
376        /// Node or edge type name that declared the property.
377        declared_in: DbString,
378        /// Expected runtime storage type.
379        expected: PropertyValueType,
380        /// Statically inferred GQL type.
381        found: GqlType,
382        /// Source span of the offending value expression.
383        #[label("value type is incompatible with property declaration")]
384        span: SourceSpan,
385    },
386
387    /// Static closed-graph validation found a missing required property.
388    #[error("required property {property} of {declared_in} missing at INSERT site")]
389    #[diagnostic(code(SLENE_A_015))]
390    SchemaRequiredPropertyMissing {
391        /// Required property key.
392        property: DbString,
393        /// Node or edge type name that declared the property.
394        declared_in: DbString,
395        /// Source span of the insert pattern.
396        #[label("required property is not supplied")]
397        span: SourceSpan,
398    },
399
400    /// Static closed-graph validation found a required property removal.
401    #[error("required property {property} of {declared_in} cannot be REMOVE'd")]
402    #[diagnostic(code(SLENE_A_016))]
403    SchemaRequiredPropertyRemoved {
404        /// Required property key.
405        property: DbString,
406        /// Node or edge type name that declared the property.
407        declared_in: DbString,
408        /// Source span of the remove item.
409        #[label("required property cannot be removed")]
410        span: SourceSpan,
411    },
412
413    /// Static closed-graph validation found an invalid INSERT label expression.
414    #[error("INSERT requires a single label or label conjunction; {form} is not allowed")]
415    #[diagnostic(code(SLENE_A_017))]
416    SchemaInvalidInsertLabelExpr {
417        /// Invalid label-expression form.
418        form: InvalidLabelForm,
419        /// Source span of the invalid pattern.
420        #[label("invalid INSERT label expression")]
421        span: SourceSpan,
422    },
423
424    /// Static closed-graph validation found removal of an edge's required label.
425    #[error("required edge label {label} of {declared_in} cannot be REMOVE'd")]
426    #[diagnostic(code(SLENE_A_018))]
427    SchemaRequiredEdgeLabelRemoved {
428        /// Required edge label.
429        label: DbString,
430        /// Edge type name that declared the label.
431        declared_in: DbString,
432        /// Source span of the remove item.
433        #[label("edge label cannot be removed")]
434        span: SourceSpan,
435    },
436}
437
438/// Label-expression forms that cannot identify a fresh closed-graph INSERT type.
439#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
440pub enum InvalidLabelForm {
441    /// Label disjunction, such as `:Person|Company`.
442    Disjunction,
443    /// Label negation, such as `:!Person`.
444    Negation,
445    /// Label wildcard, such as `:%`.
446    Wildcard,
447    /// No label expression was present.
448    Missing,
449}
450
451impl std::fmt::Display for InvalidLabelForm {
452    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453        formatter.write_str(match self {
454            Self::Disjunction => "label disjunction",
455            Self::Negation => "label negation",
456            Self::Wildcard => "label wildcard",
457            Self::Missing => "missing label",
458        })
459    }
460}
461
462fn display_qualified_name(name: &[DbString]) -> QualifiedNameDisplay<'_> {
463    QualifiedNameDisplay(name)
464}
465
466struct QualifiedNameDisplay<'a>(&'a [DbString]);
467
468impl std::fmt::Display for QualifiedNameDisplay<'_> {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        fmt_qualified_name(f, self.0)
471    }
472}
473
474fn fmt_qualified_name(f: &mut std::fmt::Formatter<'_>, name: &[DbString]) -> std::fmt::Result {
475    let mut first = true;
476    for segment in name {
477        if !first {
478            f.write_str(".")?;
479        }
480        let text = segment.as_str();
481        if text.contains('.') || text.contains('"') {
482            write!(f, "\"{}\"", text.replace('"', "\"\""))?;
483        } else {
484            f.write_str(text)?;
485        }
486        first = false;
487    }
488    Ok(())
489}
490
491fn display_argument_range(minimum: usize, maximum: usize) -> String {
492    if minimum == maximum {
493        maximum.to_string()
494    } else {
495        format!("{minimum}..={maximum}")
496    }
497}
498
499impl AnalysisError {
500    /// Return this error's ISO GQLSTATUS code.
501    #[must_use]
502    pub const fn gqlstatus(&self) -> GqlStatus {
503        match self {
504            Self::UndefinedReference { .. } => GqlStatus::UNDEFINED_REFERENCE,
505            Self::Shadow { .. }
506            | Self::PatternKindMismatch { .. }
507            | Self::AliasReusedAsPatternBinding { .. } => GqlStatus::DUPLICATE_OBJECT,
508            Self::NotImplemented { .. } => GqlStatus::FEATURE_NOT_SUPPORTED,
509            Self::UnboundedRequiresGate { .. } => GqlStatus::SYNTAX_ERROR,
510            Self::ValueSubqueryShapeViolation { .. } => GqlStatus::SYNTAX_ERROR,
511            Self::AggregateNestingViolation { .. } => GqlStatus::SYNTAX_ERROR,
512            Self::GroupedProjectionItemNotGrouped { .. } => GqlStatus::SYNTAX_ERROR,
513            Self::ReturnStarRequiresInput { .. } => GqlStatus::SYNTAX_ERROR,
514            Self::SortKeyContainsNestedQuery { .. } => GqlStatus::SYNTAX_ERROR,
515            Self::SortKeyContainsAggregate { .. } => GqlStatus::SYNTAX_ERROR,
516            Self::InvalidReference { .. } => GqlStatus::INVALID_REFERENCE,
517            Self::RecursionLimitExceeded { .. } => GqlStatus::PROGRAM_LIMIT_EXCEEDED,
518            Self::TypeMismatch { .. } | Self::ConflictingParameterTypes { .. } => {
519                GqlStatus::DATATYPE_MISMATCH
520            }
521            Self::UnknownProcedure { .. } => GqlStatus::UNKNOWN_PROCEDURE,
522            Self::WrongArgumentCount { .. } => GqlStatus::DATATYPE_MISMATCH,
523            Self::UnknownYieldColumn { .. } => GqlStatus::UNDEFINED_REFERENCE,
524            Self::MutatingProcedureInReadPipeline { .. } => {
525                GqlStatus::INVALID_TRANSACTION_STATE_MIXING
526            }
527            Self::SchemaUnknownNodeType { .. }
528            | Self::SchemaUnknownEdgeType { .. }
529            | Self::SchemaEdgeEndpointMismatch { .. }
530            | Self::SchemaUndeclaredProperty { .. }
531            | Self::SchemaPropertyTypeMismatch { .. }
532            | Self::SchemaRequiredPropertyMissing { .. }
533            | Self::SchemaRequiredPropertyRemoved { .. }
534            | Self::SchemaInvalidInsertLabelExpr { .. }
535            | Self::SchemaRequiredEdgeLabelRemoved { .. } => GqlStatus::GRAPH_TYPE_VIOLATION,
536        }
537    }
538
539    pub(crate) fn undefined_reference(name: DbString, span: SourceSpan) -> Self {
540        Self::UndefinedReference {
541            name,
542            span,
543            hint: Some("declare the variable before this reference".into()),
544        }
545    }
546}