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