Skip to main content

nodedb_sql/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Error types for the nodedb-sql crate.
4
5/// Errors produced during SQL parsing, resolution, or planning.
6#[derive(Debug, PartialEq, thiserror::Error)]
7pub enum SqlError {
8    #[error("parse error: {detail}")]
9    Parse { detail: String },
10
11    #[error("table not found: {name}")]
12    UnknownTable { name: String },
13
14    #[error("unknown column '{column}' in table '{table}'")]
15    UnknownColumn { table: String, column: String },
16
17    #[error("ambiguous column '{column}' — qualify with table name")]
18    AmbiguousColumn { column: String },
19
20    #[error("type mismatch: {detail}")]
21    TypeMismatch { detail: String },
22
23    #[error("unsupported: {detail}")]
24    Unsupported { detail: String },
25
26    #[error("invalid function call: {detail}")]
27    InvalidFunction { detail: String },
28
29    #[error("invalid window frame: {detail}")]
30    InvalidWindowFrame { detail: String },
31
32    #[error("missing required field '{field}' for {context}")]
33    MissingField { field: String, context: String },
34
35    /// A descriptor the planner depends on is being drained by
36    /// an in-flight DDL. Callers (pgwire handlers) should retry
37    /// the whole statement after a short backoff. Propagated
38    /// from `SqlCatalogError::RetryableSchemaChanged`.
39    #[error("retryable schema change on {descriptor}")]
40    RetryableSchemaChanged { descriptor: String },
41
42    /// Identifier is a NodeDB reserved keyword. Use a quoted identifier to bypass.
43    #[error(
44        "identifier '{name}' is reserved by NodeDB ({reason}); \
45         use a quoted identifier (e.g., \"{name}\") to bypass"
46    )]
47    ReservedIdentifier { name: String, reason: &'static str },
48
49    /// An unsupported SQL constraint was used in a DDL statement.
50    ///
51    /// Rendered as SQLSTATE `0A000` (feature_not_supported). The `feature` field
52    /// names the constraint keyword and `hint` points to the NodeDB equivalent.
53    #[error("unsupported constraint: {feature}; {hint}")]
54    UnsupportedConstraint { feature: String, hint: String },
55
56    /// WITH RECURSIVE used a set operator other than UNION or UNION ALL.
57    ///
58    /// Only `UNION` and `UNION ALL` are permitted in the recursive term of a
59    /// `WITH RECURSIVE` CTE. `INTERSECT` and `EXCEPT` are rejected because
60    /// they cannot guarantee termination in standard iterative evaluation.
61    #[error(
62        "WITH RECURSIVE: only UNION / UNION ALL are allowed in the recursive term; \
63         {op} is not permitted"
64    )]
65    InvalidRecursiveSetOp { op: String },
66
67    /// The recursive self-reference is absent, appears more than once, or
68    /// appears inside a subquery, aggregate, or the nullable side of an outer join.
69    #[error("WITH RECURSIVE: invalid self-reference to '{cte_name}' in recursive term: {reason}")]
70    InvalidRecursiveSelfRef { cte_name: String, reason: String },
71
72    /// The anchor SELECT produces a different number of columns than the
73    /// column list declared on the CTE (or the recursive arm).
74    #[error(
75        "WITH RECURSIVE CTE '{cte_name}': anchor produces {anchor_cols} column(s) \
76         but {declared_cols} were declared"
77    )]
78    RecursiveColumnMismatch {
79        cte_name: String,
80        anchor_cols: usize,
81        declared_cols: usize,
82    },
83
84    /// The recursive CTE exceeded the configured `max_recursion_depth`.
85    ///
86    /// This is a runtime error produced by the executor, not the planner.
87    #[error(
88        "WITH RECURSIVE CTE '{cte_name}' exceeded max recursion depth {max_depth}; \
89         add a stricter termination condition or raise max_recursion_depth"
90    )]
91    RecursionDepthExceeded { cte_name: String, max_depth: usize },
92
93    /// Collection is soft-deleted (within retention window).
94    /// Propagated from `SqlCatalogError::CollectionDeactivated`;
95    /// the pgwire layer renders this as sqlstate 42P01 with an
96    /// `UNDROP COLLECTION <name>` hint in the message.
97    #[error(
98        "collection '{name}' was dropped; \
99         restore with `{undrop_hint}` before retention elapses \
100         at {retention_expires_at_ns} ns"
101    )]
102    CollectionDeactivated {
103        name: String,
104        retention_expires_at_ns: u64,
105        undrop_hint: String,
106    },
107}
108
109impl From<crate::catalog::SqlCatalogError> for SqlError {
110    fn from(e: crate::catalog::SqlCatalogError) -> Self {
111        match e {
112            crate::catalog::SqlCatalogError::RetryableSchemaChanged { descriptor } => {
113                Self::RetryableSchemaChanged { descriptor }
114            }
115            crate::catalog::SqlCatalogError::CollectionDeactivated {
116                name,
117                retention_expires_at_ns,
118            } => {
119                let undrop_hint = format!("UNDROP COLLECTION {name}");
120                Self::CollectionDeactivated {
121                    name,
122                    retention_expires_at_ns,
123                    undrop_hint,
124                }
125            }
126        }
127    }
128}
129
130impl From<nodedb_query::expr_parse::ExprParseError> for SqlError {
131    fn from(e: nodedb_query::expr_parse::ExprParseError) -> Self {
132        Self::Parse {
133            detail: e.to_string(),
134        }
135    }
136}
137
138impl From<sqlparser::parser::ParserError> for SqlError {
139    fn from(e: sqlparser::parser::ParserError) -> Self {
140        Self::Parse {
141            detail: e.to_string(),
142        }
143    }
144}
145
146pub type Result<T> = std::result::Result<T, SqlError>;