Skip to main content

graphdblite/
types.rs

1use std::collections::{BTreeMap, HashMap};
2use std::fmt;
3use std::hash::{Hash, Hasher};
4
5use serde::{Deserialize, Serialize};
6
7/// Unique identifier for a node in the graph.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct NodeId(pub u64);
10
11impl fmt::Display for NodeId {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        write!(f, "NodeId({})", self.0)
14    }
15}
16
17impl NodeId {
18    /// Encode as 8-byte big-endian for B-tree key ordering.
19    pub fn to_be_bytes(self) -> [u8; 8] {
20        self.0.to_be_bytes()
21    }
22
23    /// Decode from 8-byte big-endian.
24    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
25        Self(u64::from_be_bytes(bytes))
26    }
27}
28
29/// Dynamic property value stored on nodes and edges.
30///
31/// The `Node`, `Edge`, and `Path` variants are runtime-only — they appear in
32/// query result records but must never be written into stored properties
33/// (openCypher forbids it and storage paths rely on the property-value subset
34/// being scalar-or-collection).
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[allow(missing_docs)]
37pub enum Value {
38    Null,
39    Bool(bool),
40    I64(i64),
41    F64(f64),
42    String(String),
43    List(Vec<Value>),
44    /// A full node value (id + label + properties). Runtime-only.
45    Node(Node),
46    /// A full edge value (src + dst + label + properties). Runtime-only.
47    Edge(Edge),
48    /// A path with full node and edge contents. Runtime-only.
49    Path(PathValue),
50    /// Ordered string-keyed map (BTreeMap gives deterministic iteration and
51    /// hashing regardless of insertion order).
52    Map(BTreeMap<String, Value>),
53    /// Temporal types.
54    Date(crate::temporal::CypherDate),
55    LocalTime(crate::temporal::CypherLocalTime),
56    Time(crate::temporal::CypherTime),
57    LocalDateTime(crate::temporal::CypherLocalDateTime),
58    DateTime(crate::temporal::CypherDateTime),
59    Duration(crate::temporal::CypherDuration),
60}
61
62/// NaN-safe equality: two NaN values are considered equal (bit-equal comparison).
63/// This satisfies the `Eq` reflexivity contract that `derive(PartialEq)` would violate.
64impl PartialEq for Value {
65    fn eq(&self, other: &Self) -> bool {
66        match (self, other) {
67            (Value::Null, Value::Null) => true,
68            (Value::Bool(a), Value::Bool(b)) => a == b,
69            (Value::I64(a), Value::I64(b)) => a == b,
70            (Value::F64(a), Value::F64(b)) => a.to_bits() == b.to_bits(),
71            (Value::String(a), Value::String(b)) => a == b,
72            (Value::List(a), Value::List(b)) => a == b,
73            (Value::Node(a), Value::Node(b)) => a == b,
74            (Value::Edge(a), Value::Edge(b)) => a == b,
75            (Value::Path(a), Value::Path(b)) => a == b,
76            (Value::Map(a), Value::Map(b)) => a == b,
77            (Value::Date(a), Value::Date(b)) => a == b,
78            (Value::LocalTime(a), Value::LocalTime(b)) => a == b,
79            (Value::Time(a), Value::Time(b)) => a == b,
80            (Value::LocalDateTime(a), Value::LocalDateTime(b)) => a == b,
81            (Value::DateTime(a), Value::DateTime(b)) => a == b,
82            (Value::Duration(a), Value::Duration(b)) => a == b,
83            _ => false,
84        }
85    }
86}
87
88impl Eq for Value {}
89
90/// Hash a `Properties` (HashMap) deterministically by sorting keys.
91fn hash_properties<H: Hasher>(props: &Properties, state: &mut H) {
92    let mut entries: Vec<(&String, &Value)> = props.iter().collect();
93    entries.sort_by(|a, b| a.0.cmp(b.0));
94    entries.len().hash(state);
95    for (k, v) in entries {
96        k.hash(state);
97        v.hash(state);
98    }
99}
100
101fn hash_node<H: Hasher>(n: &Node, state: &mut H) {
102    n.id.hash(state);
103    n.labels.hash(state);
104    hash_properties(&n.properties, state);
105}
106
107fn hash_edge<H: Hasher>(e: &Edge, state: &mut H) {
108    e.src.hash(state);
109    e.dst.hash(state);
110    e.label.hash(state);
111    hash_properties(&e.properties, state);
112}
113
114impl Hash for Value {
115    fn hash<H: Hasher>(&self, state: &mut H) {
116        std::mem::discriminant(self).hash(state);
117        match self {
118            Value::Null => {}
119            Value::Bool(b) => b.hash(state),
120            Value::I64(n) => n.hash(state),
121            Value::F64(f) => f.to_bits().hash(state),
122            Value::String(s) => s.hash(state),
123            Value::List(items) => items.hash(state),
124            Value::Node(n) => hash_node(n, state),
125            Value::Edge(e) => hash_edge(e, state),
126            Value::Path(p) => {
127                p.nodes.len().hash(state);
128                for n in &p.nodes {
129                    hash_node(n, state);
130                }
131                p.edges.len().hash(state);
132                for e in &p.edges {
133                    hash_edge(e, state);
134                }
135            }
136            Value::Map(map) => {
137                // BTreeMap already iterates in key order — stable hash.
138                for (k, v) in map {
139                    k.hash(state);
140                    v.hash(state);
141                }
142            }
143            Value::Date(d) => d.hash(state),
144            Value::LocalTime(t) => t.hash(state),
145            Value::Time(t) => t.hash(state),
146            Value::LocalDateTime(dt) => dt.hash(state),
147            Value::DateTime(dt) => dt.hash(state),
148            Value::Duration(d) => d.hash(state),
149        }
150    }
151}
152
153/// Format a `Properties` map deterministically (keys sorted) into a `Display` sink.
154fn fmt_properties(props: &Properties, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155    let mut entries: Vec<(&String, &Value)> = props.iter().collect();
156    entries.sort_by(|a, b| a.0.cmp(b.0));
157    for (i, (k, v)) in entries.into_iter().enumerate() {
158        if i > 0 {
159            write!(f, ", ")?;
160        }
161        write!(f, "{k}: {v}")?;
162    }
163    Ok(())
164}
165
166impl fmt::Display for Value {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Value::Null => write!(f, "null"),
170            Value::Bool(b) => write!(f, "{b}"),
171            Value::I64(n) => write!(f, "{n}"),
172            Value::F64(n) => write!(f, "{n}"),
173            Value::String(s) => write!(f, "\"{s}\""),
174            Value::List(items) => {
175                write!(f, "[")?;
176                for (i, v) in items.iter().enumerate() {
177                    if i > 0 {
178                        write!(f, ", ")?;
179                    }
180                    write!(f, "{v}")?;
181                }
182                write!(f, "]")
183            }
184            Value::Node(n) => {
185                write!(f, "(")?;
186                for lbl in &n.labels {
187                    write!(f, ":{lbl}")?;
188                }
189                if !n.properties.is_empty() {
190                    write!(f, " {{")?;
191                    fmt_properties(&n.properties, f)?;
192                    write!(f, "}}")?;
193                }
194                write!(f, ")")
195            }
196            Value::Edge(e) => {
197                write!(f, "[:{} {{", e.label)?;
198                fmt_properties(&e.properties, f)?;
199                write!(f, "}}]")
200            }
201            Value::Path(p) => {
202                write!(f, "<")?;
203                if let Some(first) = p.nodes.first() {
204                    write!(f, "(")?;
205                    for lbl in &first.labels {
206                        write!(f, ":{lbl}")?;
207                    }
208                    if !first.properties.is_empty() {
209                        if first.labels.is_empty() {
210                            write!(f, "{{")?;
211                        } else {
212                            write!(f, " {{")?;
213                        }
214                        fmt_properties(&first.properties, f)?;
215                        write!(f, "}}")?;
216                    }
217                    write!(f, ")")?;
218                }
219                for (i, (edge, node)) in p.edges.iter().zip(p.nodes.iter().skip(1)).enumerate() {
220                    // Determine if edge goes forward (src=prev node) or backward.
221                    let prev_node = &p.nodes[i];
222                    let forward = prev_node.id == edge.src;
223                    if forward {
224                        write!(f, "-[:{}", edge.label)?;
225                    } else {
226                        write!(f, "<-[:{}", edge.label)?;
227                    }
228                    if !edge.properties.is_empty() {
229                        write!(f, " {{")?;
230                        fmt_properties(&edge.properties, f)?;
231                        write!(f, "}}")?;
232                    }
233                    if forward {
234                        write!(f, "]->(")?;
235                    } else {
236                        write!(f, "]-(")?;
237                    }
238                    for lbl in &node.labels {
239                        write!(f, ":{lbl}")?;
240                    }
241                    if !node.properties.is_empty() {
242                        if node.labels.is_empty() {
243                            write!(f, "{{")?;
244                        } else {
245                            write!(f, " {{")?;
246                        }
247                        fmt_properties(&node.properties, f)?;
248                        write!(f, "}}")?;
249                    }
250                    write!(f, ")")?;
251                }
252                write!(f, ">")
253            }
254            Value::Map(map) => {
255                write!(f, "{{")?;
256                for (i, (k, v)) in map.iter().enumerate() {
257                    if i > 0 {
258                        write!(f, ", ")?;
259                    }
260                    write!(f, "{k}: {v}")?;
261                }
262                write!(f, "}}")
263            }
264            Value::Date(d) => write!(f, "{d}"),
265            Value::LocalTime(t) => write!(f, "{t}"),
266            Value::Time(t) => write!(f, "{t}"),
267            Value::LocalDateTime(dt) => write!(f, "{dt}"),
268            Value::DateTime(dt) => write!(f, "{dt}"),
269            Value::Duration(d) => write!(f, "{d}"),
270        }
271    }
272}
273
274/// Property map for nodes and edges.
275pub type Properties = HashMap<String, Value>;
276
277/// A node in the graph.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[allow(missing_docs)]
280pub struct Node {
281    pub id: NodeId,
282    /// Node labels (sorted). Empty vec for unlabeled nodes.
283    /// Backward-compat: deserializes from either `label: String` or `labels: Vec`.
284    #[serde(default, alias = "label", deserialize_with = "deserialize_labels")]
285    pub labels: Vec<String>,
286    pub properties: Properties,
287}
288
289/// An edge in the graph.
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291#[allow(missing_docs)]
292pub struct Edge {
293    pub src: NodeId,
294    pub dst: NodeId,
295    pub label: String,
296    pub properties: Properties,
297}
298
299/// A path through the graph: a sequence of nodes linked by edges.
300///
301/// Invariant: `nodes.len() == edges.len() + 1`. An edge at index `i` connects
302/// the node at index `i` to the node at index `i + 1`. A path with a single
303/// node (and zero edges) represents a length-zero path.
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305#[allow(missing_docs)]
306pub struct PathValue {
307    pub nodes: Vec<Node>,
308    pub edges: Vec<Edge>,
309}
310
311impl PathValue {
312    /// Construct a zero-length path (single node, no edges).
313    pub fn single(node: Node) -> Self {
314        Self {
315            nodes: vec![node],
316            edges: Vec::new(),
317        }
318    }
319
320    /// Number of edges in the path.
321    pub fn len(&self) -> usize {
322        self.edges.len()
323    }
324
325    /// A zero-length path (single node only).
326    pub fn is_empty(&self) -> bool {
327        self.edges.is_empty()
328    }
329}
330
331/// Direction for edge traversal.
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333#[allow(missing_docs)]
334pub enum Direction {
335    Outgoing,
336    Incoming,
337    Both,
338}
339
340/// Serializable node record stored in the `nodes` table.
341#[derive(Serialize, Deserialize)]
342pub(crate) struct NodeRecord {
343    #[serde(default, alias = "label", deserialize_with = "deserialize_labels")]
344    pub labels: Vec<String>,
345    pub properties: Properties,
346}
347
348/// Deserialize labels from either a single string ("A") or a vec (["A", "B"]).
349/// This handles backward compat with the old `label: String` format.
350fn deserialize_labels<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
351where
352    D: serde::Deserializer<'de>,
353{
354    use serde::de;
355
356    struct LabelsVisitor;
357
358    impl<'de> de::Visitor<'de> for LabelsVisitor {
359        type Value = Vec<String>;
360
361        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
362            f.write_str("a string or list of strings")
363        }
364
365        fn visit_str<E: de::Error>(self, v: &str) -> std::result::Result<Vec<String>, E> {
366            if v.is_empty() {
367                Ok(Vec::new())
368            } else {
369                Ok(vec![v.to_string()])
370            }
371        }
372
373        fn visit_string<E: de::Error>(self, v: String) -> std::result::Result<Vec<String>, E> {
374            if v.is_empty() {
375                Ok(Vec::new())
376            } else {
377                Ok(vec![v])
378            }
379        }
380
381        fn visit_seq<A: de::SeqAccess<'de>>(
382            self,
383            mut seq: A,
384        ) -> std::result::Result<Vec<String>, A::Error> {
385            let mut labels = Vec::new();
386            while let Some(s) = seq.next_element()? {
387                labels.push(s);
388            }
389            Ok(labels)
390        }
391    }
392
393    deserializer.deserialize_any(LabelsVisitor)
394}
395
396/// Phase of query processing at which an error was raised.
397///
398/// Aligns with openCypher's error model so TCK scenarios that assert
399/// "an error should be raised at `<phase>`" can match precisely.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum QueryPhase {
402    /// Lexing / parsing the Cypher source text.
403    Parse,
404    /// After parsing, before execution: variable binding, pattern validation,
405    /// type inference, parameter substitution.
406    SemanticAnalysis,
407    /// During plan execution.
408    Runtime,
409}
410
411impl fmt::Display for QueryPhase {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        match self {
414            QueryPhase::Parse => write!(f, "parse"),
415            QueryPhase::SemanticAnalysis => write!(f, "semantic analysis"),
416            QueryPhase::Runtime => write!(f, "runtime"),
417        }
418    }
419}
420
421/// A source-text span (zero-indexed byte offsets, 1-indexed line/column).
422///
423/// Lightweight and parser-library-agnostic — pest spans are converted to this
424/// shape at the parser/AST boundary so downstream code never sees pest types.
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426#[allow(missing_docs)]
427pub struct Span {
428    pub start: usize,
429    pub end: usize,
430    pub line: u32,
431    pub col: u32,
432}
433
434impl Span {
435    /// A placeholder span for AST nodes synthesized by the planner (no source location).
436    pub const fn synthetic() -> Self {
437        Self {
438            start: 0,
439            end: 0,
440            line: 0,
441            col: 0,
442        }
443    }
444
445    /// True if this span has no source location (was synthesized).
446    pub fn is_synthetic(&self) -> bool {
447        self.line == 0 && self.col == 0 && self.start == 0 && self.end == 0
448    }
449}
450
451impl fmt::Display for Span {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        write!(f, "line {}:{}", self.line, self.col)
454    }
455}
456
457/// Structured openCypher error code.
458///
459/// Encodes the specific category beyond the broader `QueryError` variant
460/// (e.g. a `SemanticError` may have code `UndefinedVariable` or
461/// `VariableTypeConflict`). Keeping this typed prevents drift across call
462/// sites and lets the TCK harness match without parsing strings.
463///
464/// `#[non_exhaustive]` so adding new codes never breaks downstream `match`es.
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466#[non_exhaustive]
467#[allow(missing_docs)]
468pub enum ErrorCode {
469    /// Generic / no specific code.
470    Other,
471    AmbiguousAggregationExpression,
472    CreatingVarLength,
473    DeleteConnectedNode,
474    DeletedEntityAccess,
475    DifferentColumnsInUnion,
476    InvalidAggregation,
477    InvalidArgumentPassingMode,
478    InvalidArgumentType,
479    InvalidArgumentValue,
480    InvalidClauseComposition,
481    InvalidDelete,
482    InvalidNumberOfArguments,
483    InvalidPropertyType,
484    InvalidUnicodeLiteral,
485    MapElementAccessByNonString,
486    MergeReadOwnWrites,
487    MissingParameter,
488    NegativeIntegerArgument,
489    NoExpressionAlias,
490    NoSingleRelationshipType,
491    NonConstantExpression,
492    NumberOutOfRange,
493    ProcedureNotFound,
494    RequiresDirectedRelationship,
495    UndefinedVariable,
496    UnknownFunction,
497    UnexpectedSyntax,
498    VariableAlreadyBound,
499    VariableTypeConflict,
500}
501
502impl ErrorCode {
503    /// The exact openCypher code name (e.g. `"UndefinedVariable"`).
504    pub fn as_str(&self) -> &'static str {
505        match self {
506            ErrorCode::Other => "Other",
507            ErrorCode::AmbiguousAggregationExpression => "AmbiguousAggregationExpression",
508            ErrorCode::CreatingVarLength => "CreatingVarLength",
509            ErrorCode::DeleteConnectedNode => "DeleteConnectedNode",
510            ErrorCode::DeletedEntityAccess => "DeletedEntityAccess",
511            ErrorCode::DifferentColumnsInUnion => "DifferentColumnsInUnion",
512            ErrorCode::InvalidAggregation => "InvalidAggregation",
513            ErrorCode::InvalidArgumentPassingMode => "InvalidArgumentPassingMode",
514            ErrorCode::InvalidArgumentType => "InvalidArgumentType",
515            ErrorCode::InvalidArgumentValue => "InvalidArgumentValue",
516            ErrorCode::InvalidClauseComposition => "InvalidClauseComposition",
517            ErrorCode::InvalidDelete => "InvalidDelete",
518            ErrorCode::InvalidNumberOfArguments => "InvalidNumberOfArguments",
519            ErrorCode::InvalidPropertyType => "InvalidPropertyType",
520            ErrorCode::InvalidUnicodeLiteral => "InvalidUnicodeLiteral",
521            ErrorCode::MapElementAccessByNonString => "MapElementAccessByNonString",
522            ErrorCode::MergeReadOwnWrites => "MergeReadOwnWrites",
523            ErrorCode::MissingParameter => "MissingParameter",
524            ErrorCode::NegativeIntegerArgument => "NegativeIntegerArgument",
525            ErrorCode::NoExpressionAlias => "NoExpressionAlias",
526            ErrorCode::NoSingleRelationshipType => "NoSingleRelationshipType",
527            ErrorCode::NonConstantExpression => "NonConstantExpression",
528            ErrorCode::NumberOutOfRange => "NumberOutOfRange",
529            ErrorCode::ProcedureNotFound => "ProcedureNotFound",
530            ErrorCode::RequiresDirectedRelationship => "RequiresDirectedRelationship",
531            ErrorCode::UndefinedVariable => "UndefinedVariable",
532            ErrorCode::UnknownFunction => "UnknownFunction",
533            ErrorCode::UnexpectedSyntax => "UnexpectedSyntax",
534            ErrorCode::VariableAlreadyBound => "VariableAlreadyBound",
535            ErrorCode::VariableTypeConflict => "VariableTypeConflict",
536        }
537    }
538}
539
540impl fmt::Display for ErrorCode {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        f.write_str(self.as_str())
543    }
544}
545
546/// Errors raised while processing a Cypher query.
547///
548/// Categorized along the openCypher error taxonomy (SyntaxError, TypeError,
549/// SemanticError, etc.) so that TCK conformance scenarios can distinguish
550/// error kinds without resorting to string matching.
551///
552/// All variants carry the same shape: `phase`, `code`, `message`, `hint`,
553/// `span`. The variant tag itself is the broad openCypher kind; `code`
554/// narrows it further. `hint` and `span` are optional — populate when they
555/// add value (e.g. parser errors fill `span`; "did-you-mean" fills `hint`).
556#[derive(Debug)]
557#[non_exhaustive]
558#[allow(missing_docs)]
559pub enum QueryError {
560    SyntaxError {
561        phase: QueryPhase,
562        code: ErrorCode,
563        message: String,
564        hint: Option<String>,
565        span: Option<Span>,
566    },
567    TypeError {
568        phase: QueryPhase,
569        code: ErrorCode,
570        message: String,
571        hint: Option<String>,
572        span: Option<Span>,
573    },
574    SemanticError {
575        phase: QueryPhase,
576        code: ErrorCode,
577        message: String,
578        hint: Option<String>,
579        span: Option<Span>,
580    },
581    EntityNotFound {
582        phase: QueryPhase,
583        code: ErrorCode,
584        message: String,
585        hint: Option<String>,
586        span: Option<Span>,
587    },
588    ArgumentError {
589        phase: QueryPhase,
590        code: ErrorCode,
591        message: String,
592        hint: Option<String>,
593        span: Option<Span>,
594    },
595    ArithmeticError {
596        phase: QueryPhase,
597        code: ErrorCode,
598        message: String,
599        hint: Option<String>,
600        span: Option<Span>,
601    },
602    ConstraintViolation {
603        phase: QueryPhase,
604        code: ErrorCode,
605        message: String,
606        hint: Option<String>,
607        span: Option<Span>,
608    },
609    ProcedureError {
610        phase: QueryPhase,
611        code: ErrorCode,
612        message: String,
613        hint: Option<String>,
614        span: Option<Span>,
615    },
616}
617
618/// Helper to extract the common fields shared by every `QueryError` variant.
619macro_rules! query_error_fields {
620    ($self:expr) => {
621        match $self {
622            QueryError::SyntaxError {
623                phase,
624                code,
625                message,
626                hint,
627                span,
628            }
629            | QueryError::TypeError {
630                phase,
631                code,
632                message,
633                hint,
634                span,
635            }
636            | QueryError::SemanticError {
637                phase,
638                code,
639                message,
640                hint,
641                span,
642            }
643            | QueryError::EntityNotFound {
644                phase,
645                code,
646                message,
647                hint,
648                span,
649            }
650            | QueryError::ArgumentError {
651                phase,
652                code,
653                message,
654                hint,
655                span,
656            }
657            | QueryError::ArithmeticError {
658                phase,
659                code,
660                message,
661                hint,
662                span,
663            }
664            | QueryError::ConstraintViolation {
665                phase,
666                code,
667                message,
668                hint,
669                span,
670            }
671            | QueryError::ProcedureError {
672                phase,
673                code,
674                message,
675                hint,
676                span,
677            } => (phase, code, message, hint, span),
678        }
679    };
680}
681
682impl QueryError {
683    /// The phase at which this error was raised.
684    pub fn phase(&self) -> QueryPhase {
685        let (phase, _, _, _, _) = query_error_fields!(self);
686        *phase
687    }
688
689    /// Short tag identifying the error kind (matches openCypher category names).
690    pub fn kind(&self) -> &'static str {
691        match self {
692            QueryError::SyntaxError { .. } => "SyntaxError",
693            QueryError::TypeError { .. } => "TypeError",
694            QueryError::SemanticError { .. } => "SemanticError",
695            QueryError::EntityNotFound { .. } => "EntityNotFound",
696            QueryError::ArgumentError { .. } => "ArgumentError",
697            QueryError::ArithmeticError { .. } => "ArithmeticError",
698            QueryError::ConstraintViolation { .. } => "ConstraintViolation",
699            QueryError::ProcedureError { .. } => "ProcedureError",
700        }
701    }
702
703    /// Structured error code.
704    pub fn code(&self) -> ErrorCode {
705        let (_, code, _, _, _) = query_error_fields!(self);
706        *code
707    }
708
709    /// Free-form human message.
710    pub fn message(&self) -> &str {
711        let (_, _, message, _, _) = query_error_fields!(self);
712        message.as_str()
713    }
714
715    /// Actionable hint, if any.
716    pub fn hint(&self) -> Option<&str> {
717        let (_, _, _, hint, _) = query_error_fields!(self);
718        hint.as_deref()
719    }
720
721    /// Source-text span, if known.
722    pub fn span(&self) -> Option<Span> {
723        let (_, _, _, _, span) = query_error_fields!(self);
724        *span
725    }
726
727    /// Replace the hint on this error.
728    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
729        let (_, _, _, hint_field, _) = query_error_fields!(&mut self);
730        *hint_field = Some(hint.into());
731        self
732    }
733
734    /// Replace the span on this error. Synthetic spans (planner-rewritten nodes)
735    /// are ignored so error messages don't display "line 0:0".
736    pub fn with_span(mut self, new_span: Span) -> Self {
737        if new_span.is_synthetic() {
738            return self;
739        }
740        let (_, _, _, _, span_field) = query_error_fields!(&mut self);
741        *span_field = Some(new_span);
742        self
743    }
744
745    /// Replace the structured code on this error.
746    pub fn with_code(mut self, new_code: ErrorCode) -> Self {
747        let (_, code_field, _, _, _) = query_error_fields!(&mut self);
748        *code_field = new_code;
749        self
750    }
751}
752
753impl fmt::Display for QueryError {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        let (phase, code, message, hint, span) = query_error_fields!(self);
756        match span {
757            Some(s) => write!(f, "{}({}) at {}: {}", self.kind(), code, s, message)?,
758            None => write!(f, "{}({}) at {}: {}", self.kind(), code, phase, message)?,
759        }
760        if let Some(h) = hint {
761            write!(f, "\n  hint: {h}")?;
762        }
763        Ok(())
764    }
765}
766
767impl std::error::Error for QueryError {}
768
769/// All errors returned by graphdblite.
770///
771/// Every variant (except `Query`, which delegates to `QueryError`'s own hint)
772/// carries a `hint: Option<String>` for actionable suggestions.
773#[derive(Debug)]
774#[non_exhaustive]
775#[allow(missing_docs)]
776pub enum GraphError {
777    Storage {
778        source: rusqlite::Error,
779        hint: Option<String>,
780    },
781    Serialization {
782        context: String,
783        source: String,
784        hint: Option<String>,
785    },
786    /// Cypher query processing error (parse/semantic/runtime).
787    Query(QueryError),
788    NodeNotFound {
789        id: NodeId,
790        hint: Option<String>,
791    },
792    EdgeNotFound {
793        src: NodeId,
794        label: String,
795        dst: NodeId,
796        hint: Option<String>,
797    },
798    HasEdges {
799        id: NodeId,
800        hint: Option<String>,
801    },
802    /// Internal transaction / concurrency error.
803    Transaction {
804        message: String,
805        hint: Option<String>,
806    },
807    IndexAlreadyExists {
808        label: String,
809        property: String,
810        hint: Option<String>,
811    },
812    IndexNotFound {
813        label: String,
814        property: String,
815        hint: Option<String>,
816    },
817    InvalidName {
818        name: String,
819        hint: Option<String>,
820    },
821    SizeLimit {
822        what: String,
823        limit: usize,
824        actual: usize,
825        hint: Option<String>,
826    },
827    SchemaMismatch {
828        found: u64,
829        supported: u64,
830        hint: Option<String>,
831    },
832}
833
834impl fmt::Display for GraphError {
835    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836        match self {
837            GraphError::Storage { source, hint } => {
838                write!(f, "storage error: {source}")?;
839                fmt_hint(f, hint.as_deref())
840            }
841            GraphError::Serialization {
842                context,
843                source,
844                hint,
845            } => {
846                write!(f, "serialization error ({context}): {source}")?;
847                fmt_hint(f, hint.as_deref())
848            }
849            GraphError::Query(q) => q.fmt(f),
850            GraphError::NodeNotFound { id, hint } => {
851                write!(f, "node not found: {id}")?;
852                fmt_hint(f, hint.as_deref())
853            }
854            GraphError::EdgeNotFound {
855                src,
856                label,
857                dst,
858                hint,
859            } => {
860                write!(f, "edge not found: {src} -[:{label}]-> {dst}")?;
861                fmt_hint(f, hint.as_deref())
862            }
863            GraphError::HasEdges { id, hint } => {
864                write!(
865                    f,
866                    "cannot delete node {id} because it still has edges; use DETACH DELETE to remove edges too"
867                )?;
868                fmt_hint(f, hint.as_deref())
869            }
870            GraphError::Transaction { message, hint } => {
871                write!(f, "transaction error: {message}")?;
872                fmt_hint(f, hint.as_deref())
873            }
874            GraphError::IndexAlreadyExists {
875                label,
876                property,
877                hint,
878            } => {
879                write!(f, "index already exists: {label}.{property}")?;
880                fmt_hint(f, hint.as_deref())
881            }
882            GraphError::IndexNotFound {
883                label,
884                property,
885                hint,
886            } => {
887                write!(f, "index not found: {label}.{property}")?;
888                fmt_hint(f, hint.as_deref())
889            }
890            GraphError::InvalidName { name, hint } => {
891                write!(
892                    f,
893                    "invalid name '{name}': must contain only ASCII letters, digits, or underscores"
894                )?;
895                fmt_hint(f, hint.as_deref())
896            }
897            GraphError::SizeLimit {
898                what,
899                limit,
900                actual,
901                hint,
902            } => {
903                write!(
904                    f,
905                    "{what} ({actual} bytes) exceeds maximum of {limit} bytes"
906                )?;
907                fmt_hint(f, hint.as_deref())
908            }
909            GraphError::SchemaMismatch {
910                found,
911                supported,
912                hint,
913            } => {
914                write!(
915                    f,
916                    "schema version mismatch: database is v{found}, this library supports up to v{supported}"
917                )?;
918                fmt_hint(f, hint.as_deref())
919            }
920        }
921    }
922}
923
924fn fmt_hint(f: &mut fmt::Formatter<'_>, hint: Option<&str>) -> fmt::Result {
925    if let Some(h) = hint {
926        write!(f, "\n  hint: {h}")?;
927    }
928    Ok(())
929}
930
931impl std::error::Error for GraphError {
932    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
933        match self {
934            GraphError::Storage { source, .. } => Some(source),
935            GraphError::Query(q) => Some(q),
936            _ => None,
937        }
938    }
939}
940
941impl From<rusqlite::Error> for GraphError {
942    fn from(source: rusqlite::Error) -> Self {
943        GraphError::Storage { source, hint: None }
944    }
945}
946
947impl From<QueryError> for GraphError {
948    fn from(error: QueryError) -> Self {
949        GraphError::Query(error)
950    }
951}
952
953impl GraphError {
954    // -- Simple, code-less constructors (kept for brevity at call sites
955    //    where no specific openCypher code applies).
956
957    /// Convenience constructor for syntax errors at the parse phase.
958    pub fn syntax(message: impl Into<String>) -> Self {
959        GraphError::Query(QueryError::SyntaxError {
960            phase: QueryPhase::Parse,
961            code: ErrorCode::Other,
962            message: message.into(),
963            hint: None,
964            span: None,
965        })
966    }
967
968    /// Convenience constructor for semantic errors at the analysis phase.
969    pub fn semantic(message: impl Into<String>) -> Self {
970        GraphError::Query(QueryError::SemanticError {
971            phase: QueryPhase::SemanticAnalysis,
972            code: ErrorCode::Other,
973            message: message.into(),
974            hint: None,
975            span: None,
976        })
977    }
978
979    /// Convenience constructor for runtime constraint violations.
980    pub fn constraint(message: impl Into<String>) -> Self {
981        GraphError::Query(QueryError::ConstraintViolation {
982            phase: QueryPhase::Runtime,
983            code: ErrorCode::Other,
984            message: message.into(),
985            hint: None,
986            span: None,
987        })
988    }
989
990    /// Convenience constructor for type errors.
991    pub fn type_error(phase: QueryPhase, message: impl Into<String>) -> Self {
992        GraphError::Query(QueryError::TypeError {
993            phase,
994            code: ErrorCode::Other,
995            message: message.into(),
996            hint: None,
997            span: None,
998        })
999    }
1000
1001    /// Convenience constructor for argument errors (e.g. missing parameters).
1002    pub fn argument(phase: QueryPhase, message: impl Into<String>) -> Self {
1003        GraphError::Query(QueryError::ArgumentError {
1004            phase,
1005            code: ErrorCode::Other,
1006            message: message.into(),
1007            hint: None,
1008            span: None,
1009        })
1010    }
1011
1012    // -- Structured helpers (preferred — pin down the openCypher code).
1013
1014    /// Reference to a name not bound in the current scope.
1015    pub fn undefined_variable(name: impl fmt::Display) -> Self {
1016        GraphError::Query(QueryError::SemanticError {
1017            phase: QueryPhase::SemanticAnalysis,
1018            code: ErrorCode::UndefinedVariable,
1019            message: format!("variable `{name}` is not defined"),
1020            hint: None,
1021            span: None,
1022        })
1023    }
1024
1025    /// Procedure name not registered in the procedure registry.
1026    pub fn procedure_not_found(name: impl fmt::Display) -> Self {
1027        GraphError::Query(QueryError::ProcedureError {
1028            phase: QueryPhase::SemanticAnalysis,
1029            code: ErrorCode::ProcedureNotFound,
1030            message: format!("unknown procedure `{name}`"),
1031            hint: None,
1032            span: None,
1033        })
1034    }
1035
1036    /// Function called with a value of an unsupported type.
1037    pub fn invalid_argument_type(
1038        phase: QueryPhase,
1039        function: impl fmt::Display,
1040        got: impl fmt::Display,
1041    ) -> Self {
1042        GraphError::Query(QueryError::TypeError {
1043            phase,
1044            code: ErrorCode::InvalidArgumentType,
1045            message: format!("{got} is not a valid argument type for {function}"),
1046            hint: None,
1047            span: None,
1048        })
1049    }
1050
1051    /// Value passed to a function is out of the supported range.
1052    pub fn invalid_argument_value(
1053        phase: QueryPhase,
1054        function: impl fmt::Display,
1055        message: impl fmt::Display,
1056    ) -> Self {
1057        GraphError::Query(QueryError::ArgumentError {
1058            phase,
1059            code: ErrorCode::InvalidArgumentValue,
1060            message: format!("{function}: {message}"),
1061            hint: None,
1062            span: None,
1063        })
1064    }
1065
1066    /// Numeric overflow / value outside the representable range.
1067    pub fn number_out_of_range(phase: QueryPhase, message: impl Into<String>) -> Self {
1068        GraphError::Query(QueryError::ArithmeticError {
1069            phase,
1070            code: ErrorCode::NumberOutOfRange,
1071            message: message.into(),
1072            hint: None,
1073            span: None,
1074        })
1075    }
1076
1077    /// Lower-level builder for arbitrary query errors with a known code.
1078    pub fn query(phase: QueryPhase, code: ErrorCode, message: impl Into<String>) -> Self {
1079        let message = message.into();
1080        let mk = |kind: fn(QueryPhase, ErrorCode, String) -> QueryError| -> Self {
1081            GraphError::Query(kind(phase, code, message))
1082        };
1083        // Choose the variant from the code's natural openCypher kind.
1084        match code {
1085            ErrorCode::InvalidUnicodeLiteral
1086            | ErrorCode::InvalidClauseComposition
1087            | ErrorCode::UnexpectedSyntax => mk(QueryError::syntax_with),
1088            ErrorCode::InvalidArgumentType
1089            | ErrorCode::InvalidPropertyType
1090            | ErrorCode::MapElementAccessByNonString
1091            | ErrorCode::DeletedEntityAccess => mk(QueryError::type_with),
1092            ErrorCode::UndefinedVariable
1093            | ErrorCode::VariableAlreadyBound
1094            | ErrorCode::VariableTypeConflict
1095            | ErrorCode::AmbiguousAggregationExpression
1096            | ErrorCode::CreatingVarLength
1097            | ErrorCode::DifferentColumnsInUnion
1098            | ErrorCode::InvalidAggregation
1099            | ErrorCode::InvalidArgumentPassingMode
1100            | ErrorCode::InvalidArgumentValue
1101            | ErrorCode::InvalidDelete
1102            | ErrorCode::InvalidNumberOfArguments
1103            | ErrorCode::NegativeIntegerArgument
1104            | ErrorCode::NoExpressionAlias
1105            | ErrorCode::NoSingleRelationshipType
1106            | ErrorCode::NonConstantExpression
1107            | ErrorCode::RequiresDirectedRelationship => mk(QueryError::semantic_with),
1108            ErrorCode::MissingParameter => mk(QueryError::argument_with),
1109            ErrorCode::NumberOutOfRange => mk(QueryError::arithmetic_with),
1110            ErrorCode::DeleteConnectedNode | ErrorCode::MergeReadOwnWrites => {
1111                mk(QueryError::constraint_with)
1112            }
1113            ErrorCode::ProcedureNotFound => mk(QueryError::procedure_with),
1114            ErrorCode::UnknownFunction => mk(QueryError::syntax_with),
1115            ErrorCode::Other => mk(QueryError::semantic_with),
1116        }
1117    }
1118
1119    /// Attach a hint to a query error in place. No-op for non-Query variants
1120    /// that already have a `hint` field — call the variant constructor with
1121    /// the hint instead.
1122    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
1123        match &mut self {
1124            GraphError::Query(q) => {
1125                let (_, _, _, h, _) = query_error_fields!(q);
1126                *h = Some(hint.into());
1127            }
1128            GraphError::Storage { hint: h, .. }
1129            | GraphError::Serialization { hint: h, .. }
1130            | GraphError::NodeNotFound { hint: h, .. }
1131            | GraphError::EdgeNotFound { hint: h, .. }
1132            | GraphError::HasEdges { hint: h, .. }
1133            | GraphError::Transaction { hint: h, .. }
1134            | GraphError::IndexAlreadyExists { hint: h, .. }
1135            | GraphError::IndexNotFound { hint: h, .. }
1136            | GraphError::InvalidName { hint: h, .. }
1137            | GraphError::SizeLimit { hint: h, .. }
1138            | GraphError::SchemaMismatch { hint: h, .. } => *h = Some(hint.into()),
1139        }
1140        self
1141    }
1142
1143    /// Attach a structured code to a query error. No-op for non-Query variants.
1144    pub fn with_code(mut self, code: ErrorCode) -> Self {
1145        if let GraphError::Query(q) = &mut self {
1146            let (_, c, _, _, _) = query_error_fields!(q);
1147            *c = code;
1148        }
1149        self
1150    }
1151
1152    /// Attach a source-text span to a query error. No-op for non-Query variants
1153    /// or for synthetic spans (planner-rewritten nodes).
1154    pub fn with_span(self, span: Span) -> Self {
1155        if span.is_synthetic() {
1156            return self;
1157        }
1158        match self {
1159            GraphError::Query(q) => GraphError::Query(q.with_span(span)),
1160            other => other,
1161        }
1162    }
1163
1164    /// Wrap an internal serialization failure with a context tag.
1165    pub fn serialization(context: impl Into<String>, source: impl fmt::Display) -> Self {
1166        GraphError::Serialization {
1167            context: context.into(),
1168            source: source.to_string(),
1169            hint: None,
1170        }
1171    }
1172
1173    /// Wrap an internal transaction failure.
1174    pub fn transaction(message: impl Into<String>) -> Self {
1175        GraphError::Transaction {
1176            message: message.into(),
1177            hint: None,
1178        }
1179    }
1180}
1181
1182// -- Internal QueryError constructors used by the dispatch table above. --
1183impl QueryError {
1184    fn syntax_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1185        QueryError::SyntaxError {
1186            phase,
1187            code,
1188            message,
1189            hint: None,
1190            span: None,
1191        }
1192    }
1193    fn type_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1194        QueryError::TypeError {
1195            phase,
1196            code,
1197            message,
1198            hint: None,
1199            span: None,
1200        }
1201    }
1202    fn semantic_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1203        QueryError::SemanticError {
1204            phase,
1205            code,
1206            message,
1207            hint: None,
1208            span: None,
1209        }
1210    }
1211    fn argument_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1212        QueryError::ArgumentError {
1213            phase,
1214            code,
1215            message,
1216            hint: None,
1217            span: None,
1218        }
1219    }
1220    fn arithmetic_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1221        QueryError::ArithmeticError {
1222            phase,
1223            code,
1224            message,
1225            hint: None,
1226            span: None,
1227        }
1228    }
1229    fn constraint_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1230        QueryError::ConstraintViolation {
1231            phase,
1232            code,
1233            message,
1234            hint: None,
1235            span: None,
1236        }
1237    }
1238    fn procedure_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
1239        QueryError::ProcedureError {
1240            phase,
1241            code,
1242            message,
1243            hint: None,
1244            span: None,
1245        }
1246    }
1247}
1248
1249/// Convenience alias: `Result<T, GraphError>`.
1250pub type Result<T> = std::result::Result<T, GraphError>;
1251
1252/// Validate that a name (label, property key) contains only `[A-Za-z0-9_]`.
1253pub fn validate_name(name: &str) -> Result<()> {
1254    if name.is_empty() || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
1255        return Err(GraphError::InvalidName {
1256            name: name.to_string(),
1257            hint: None,
1258        });
1259    }
1260    Ok(())
1261}
1262
1263/// Validate name length against a configured maximum.
1264pub fn validate_name_length(name: &str, max_bytes: usize) -> Result<()> {
1265    if name.len() > max_bytes {
1266        return Err(GraphError::SizeLimit {
1267            what: format!("name '{}...'", &name[..max_bytes.min(32)]),
1268            limit: max_bytes,
1269            actual: name.len(),
1270            hint: None,
1271        });
1272    }
1273    Ok(())
1274}
1275
1276/// Estimate the byte size of a property value.
1277///
1278/// Note: `Node`, `Edge`, and `Path` are runtime-only variants and should never
1279/// appear in stored properties, but we compute a sensible size for them
1280/// anyway so this helper stays total.
1281fn value_byte_size(val: &Value) -> usize {
1282    match val {
1283        Value::Null | Value::Bool(_) | Value::I64(_) | Value::F64(_) => 8,
1284        Value::String(s) => s.len(),
1285        Value::List(items) => items.iter().map(value_byte_size).sum(),
1286        Value::Node(n) => node_byte_size(n),
1287        Value::Edge(e) => edge_byte_size(e),
1288        Value::Path(p) => {
1289            p.nodes.iter().map(node_byte_size).sum::<usize>()
1290                + p.edges.iter().map(edge_byte_size).sum::<usize>()
1291        }
1292        Value::Map(map) => map.iter().map(|(k, v)| k.len() + value_byte_size(v)).sum(),
1293        Value::Date(_) => 12,
1294        Value::LocalTime(_) => 16,
1295        Value::Time(_) => 20,
1296        Value::LocalDateTime(_) => 20,
1297        Value::DateTime(_) => 28,
1298        Value::Duration(_) => 32,
1299    }
1300}
1301
1302fn node_byte_size(n: &Node) -> usize {
1303    8 + n.labels.iter().map(|l| l.len()).sum::<usize>()
1304        + n.properties
1305            .iter()
1306            .map(|(k, v)| k.len() + value_byte_size(v))
1307            .sum::<usize>()
1308}
1309
1310fn edge_byte_size(e: &Edge) -> usize {
1311    16 + e.label.len()
1312        + e.properties
1313            .iter()
1314            .map(|(k, v)| k.len() + value_byte_size(v))
1315            .sum::<usize>()
1316}
1317
1318/// Validate that all property values are within size limits.
1319pub fn validate_properties(
1320    label: &str,
1321    properties: &Properties,
1322    max_name_bytes: usize,
1323    max_value_bytes: usize,
1324) -> Result<()> {
1325    validate_name_length(label, max_name_bytes)?;
1326    for (key, val) in properties {
1327        validate_name_length(key, max_name_bytes)?;
1328        let size = value_byte_size(val);
1329        if size > max_value_bytes {
1330            return Err(GraphError::SizeLimit {
1331                what: format!("property '{key}' value"),
1332                limit: max_value_bytes,
1333                actual: size,
1334                hint: None,
1335            });
1336        }
1337    }
1338    Ok(())
1339}