Skip to content

Commit 3c398eb

Browse files
committed
feat(types): add SQLSTATE codes for undefined-function/div0 (#216)
Add UNDEFINED_FUNCTION (SQLSTATE 42883) and DIVISION_BY_ZERO (SQLSTATE 22012) as first-class ErrorCode/ErrorDetails variants, with msgpack wire tags, encode/decode support, and NodeDbError constructors. Purely additive plumbing: both codes are unused until the callers that raise them land in a follow-up commit. Today a function call naming no registered scalar/aggregate/window function, or an expression that divides or takes a modulus by zero, silently folds to NULL instead of failing the statement — this gives those two cases a distinct, matchable error identity instead of forcing clients to parse a generic message.
1 parent 39598b8 commit 3c398eb

8 files changed

Lines changed: 69 additions & 0 deletions

File tree

nodedb-types/src/error/code.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ impl ErrorCode {
4343
pub const PLAN_ERROR: Self = Self(1200);
4444
pub const FAN_OUT_EXCEEDED: Self = Self(1201);
4545
pub const SQL_NOT_ENABLED: Self = Self(1202);
46+
/// A function call names no registered scalar/aggregate/window function.
47+
pub const UNDEFINED_FUNCTION: Self = Self(1203);
48+
/// Expression evaluation divided or took a modulus by zero.
49+
pub const DIVISION_BY_ZERO: Self = Self(1204);
4650

4751
// Engine ops (1300–1399)
4852
pub const ARRAY: Self = Self(1300);

nodedb-types/src/error/ctors/read_query_auth.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,33 @@ impl NodeDbError {
120120
}
121121
}
122122

123+
/// A function call in a query names no registered scalar, aggregate, or
124+
/// window function. Distinct from `plan_error` so clients can match on
125+
/// the specific code (SQLSTATE `42883`, `undefined_function`) rather
126+
/// than parsing the message.
127+
pub fn undefined_function(name: impl Into<String>) -> Self {
128+
let name = name.into();
129+
Self {
130+
code: ErrorCode::UNDEFINED_FUNCTION,
131+
message: format!("function {name}(...) does not exist"),
132+
details: ErrorDetails::UndefinedFunction { name },
133+
cause: None,
134+
}
135+
}
136+
137+
/// Expression evaluation divided or took a modulus by zero. Distinct
138+
/// from `plan_error` so clients can match on the specific code
139+
/// (SQLSTATE `22012`, `division_by_zero`) rather than parsing the
140+
/// message (nodedb issue #216).
141+
pub fn division_by_zero() -> Self {
142+
Self {
143+
code: ErrorCode::DIVISION_BY_ZERO,
144+
message: "division by zero".to_string(),
145+
details: ErrorDetails::DivisionByZero,
146+
cause: None,
147+
}
148+
}
149+
123150
pub fn authorization_denied(resource: impl Into<String>) -> Self {
124151
let resource = resource.into();
125152
Self {

nodedb-types/src/error/details.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ pub enum ErrorDetails {
8080
FanOutExceeded { shards_touched: u16, limit: u16 },
8181
#[serde(rename = "sql_not_enabled")]
8282
SqlNotEnabled,
83+
/// A function call names no registered scalar/aggregate/window function.
84+
#[serde(rename = "undefined_function")]
85+
UndefinedFunction { name: String },
86+
/// Expression evaluation divided or took a modulus by zero.
87+
#[serde(rename = "division_by_zero")]
88+
DivisionByZero,
8389

8490
// Auth
8591
#[serde(rename = "authorization_denied")]

nodedb-types/src/error/msgpack/constants.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
// | 61 | MoveTenantSnapshotFailed |
7070
// | 62 | MoveTenantCutoverFailed |
7171
// | 63 | MoveTenantAlreadyAtTarget |
72+
// | 64 | MirrorReadOnly |
73+
// | 65 | StaleReadNotLeader |
74+
// | 66 | MirrorNotPromoted |
75+
// | 67 | UndefinedFunction |
76+
// | 68 | DivisionByZero |
7277

7378
pub(super) const TAG_CONSTRAINT_VIOLATION: u16 = 1;
7479
pub(super) const TAG_WRITE_CONFLICT: u16 = 2;
@@ -136,3 +141,5 @@ pub(super) const TAG_MOVE_TENANT_ALREADY_AT_TARGET: u16 = 63;
136141
pub(super) const TAG_MIRROR_READ_ONLY: u16 = 64;
137142
pub(super) const TAG_STALE_READ_NOT_LEADER: u16 = 65;
138143
pub(super) const TAG_MIRROR_NOT_PROMOTED: u16 = 66;
144+
pub(super) const TAG_UNDEFINED_FUNCTION: u16 = 67;
145+
pub(super) const TAG_DIVISION_BY_ZERO: u16 = 68;

nodedb-types/src/error/msgpack/decode/from_messagepack.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ impl<'a> FromMessagePack<'a> for ErrorDetails {
123123
skip_fields(reader, field_count)?;
124124
Ok(ErrorDetails::SqlNotEnabled)
125125
}
126+
TAG_UNDEFINED_FUNCTION => {
127+
let (name,) = read1_str(reader, field_count)?;
128+
Ok(ErrorDetails::UndefinedFunction { name })
129+
}
130+
TAG_DIVISION_BY_ZERO => {
131+
skip_fields(reader, field_count)?;
132+
Ok(ErrorDetails::DivisionByZero)
133+
}
126134
TAG_AUTHORIZATION_DENIED => {
127135
let (resource,) = read1_str(reader, field_count)?;
128136
Ok(ErrorDetails::AuthorizationDenied { resource })
@@ -346,6 +354,7 @@ mod tests {
346354
for v in [
347355
ErrorDetails::DeadlineExceeded,
348356
ErrorDetails::SqlNotEnabled,
357+
ErrorDetails::DivisionByZero,
349358
ErrorDetails::AuthExpired,
350359
ErrorDetails::SyncConnectionFailed,
351360
ErrorDetails::Config,

nodedb-types/src/error/msgpack/encode.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ impl ToMessagePack for ErrorDetails {
153153
limit,
154154
} => write2(writer, TAG_FAN_OUT_EXCEEDED, shards_touched, limit),
155155
ErrorDetails::SqlNotEnabled => write_unit(writer, TAG_SQL_NOT_ENABLED),
156+
ErrorDetails::UndefinedFunction { name } => {
157+
write1(writer, TAG_UNDEFINED_FUNCTION, name)
158+
}
159+
ErrorDetails::DivisionByZero => write_unit(writer, TAG_DIVISION_BY_ZERO),
156160
ErrorDetails::AuthorizationDenied { resource } => {
157161
write1(writer, TAG_AUTHORIZATION_DENIED, resource)
158162
}

nodedb-types/src/error/sqlstate.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ pub const CANNOT_DROP_DEFAULT_DATABASE: &str = "0A000";
4040
/// `22003` — `numeric_value_out_of_range`
4141
pub const NUMERIC_VALUE_OUT_OF_RANGE: &str = "22003";
4242

43+
/// `22012` — `division_by_zero` (`/` or `%` with a zero divisor; nodedb
44+
/// issue #216 — raised at runtime instead of evaluating to `NULL`)
45+
pub const DIVISION_BY_ZERO: &str = "22012";
46+
4347
// ── Class 23 — Integrity Constraint Violation ────────────────────────────────
4448

4549
/// `23000` — `integrity_constraint_violation` (generic)
@@ -105,6 +109,10 @@ pub const CANNOT_COERCE: &str = "42846";
105109
/// `42P01` — `undefined_table` (collection not found)
106110
pub const UNDEFINED_TABLE: &str = "42P01";
107111

112+
/// `42883` — `undefined_function` (no scalar/aggregate/window function
113+
/// registered under the called name)
114+
pub const UNDEFINED_FUNCTION: &str = "42883";
115+
108116
// ── Class 53 — Insufficient Resources ────────────────────────────────────────
109117

110118
/// `53200` — `out_of_memory`
@@ -237,6 +245,7 @@ mod tests {
237245
NO_DATA,
238246
FEATURE_NOT_SUPPORTED,
239247
NUMERIC_VALUE_OUT_OF_RANGE,
248+
DIVISION_BY_ZERO,
240249
INTEGRITY_CONSTRAINT_VIOLATION,
241250
NOT_NULL_VIOLATION,
242251
FOREIGN_KEY_VIOLATION,
@@ -256,6 +265,7 @@ mod tests {
256265
SYNTAX_ERROR,
257266
CANNOT_COERCE,
258267
UNDEFINED_TABLE,
268+
UNDEFINED_FUNCTION,
259269
OUT_OF_MEMORY,
260270
TOO_MANY_CONNECTIONS,
261271
CONFIGURATION_LIMIT_EXCEEDED,

nodedb-types/src/error/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ impl NodeDbError {
9898
| ErrorDetails::AuthExpired
9999
| ErrorDetails::Config
100100
| ErrorDetails::SqlNotEnabled
101+
| ErrorDetails::UndefinedFunction { .. }
102+
| ErrorDetails::DivisionByZero
101103
)
102104
}
103105
}

0 commit comments

Comments
 (0)