reddb_server/sqlstate.rs
1//! SQLSTATE error codes — Post-MVP credibility item.
2//!
3//! Mirrors PostgreSQL's `errcodes.h` 5-character SQLSTATE codes
4//! so reddb errors carry the same standardized identifiers
5//! drivers / client libs / monitoring tools already understand.
6//! A SQLSTATE is a fixed 5-character ASCII string where the
7//! first 2 characters identify a class and the next 3 identify
8//! a specific condition within that class.
9//!
10//! Examples:
11//! - `08006` connection_failure
12//! - `22012` division_by_zero
13//! - `23505` unique_violation
14//! - `42601` syntax_error
15//! - `42P01` undefined_table
16//! - `XX000` internal_error
17//!
18//! ## Why this matters
19//!
20//! ODBC / JDBC / psycopg / pgx all switch on SQLSTATE for retry
21//! logic, error categorization, and i18n. A reddb client that
22//! sees `42P01` knows the table doesn't exist regardless of the
23//! human-readable message language. Without SQLSTATE, drivers
24//! have to string-match error text — fragile across versions.
25//!
26//! ## Mapping to RedDBError
27//!
28//! `RedDBError -> SqlState` is a one-line lookup. The reverse
29//! direction (parse a 5-char code into a category) is also
30//! available via `SqlState::class()` for filter UIs.
31//!
32//! ## Wiring
33//!
34//! Phase post-MVP wiring adds a `sqlstate()` method on
35//! `RedDBError` and threads the code through the wire protocol
36//! `ErrorResponse` frame so HTTP / gRPC / stdio clients all
37//! receive it.
38
39/// 5-character SQLSTATE code wrapper. Stored as a fixed
40/// `[u8; 5]` so it's `Copy`, fits in a register, and can be
41/// compared with a single `==`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct SqlState(pub [u8; 5]);
44
45impl SqlState {
46 /// Build from a string literal at compile time. Caller is
47 /// responsible for ensuring the input is exactly 5 ASCII
48 /// characters; non-ASCII / wrong length panics in debug,
49 /// truncates in release.
50 pub const fn new(s: &str) -> Self {
51 let bytes = s.as_bytes();
52 debug_assert!(bytes.len() == 5, "SQLSTATE must be 5 chars");
53 let mut buf = [b'?'; 5];
54 let mut i = 0;
55 while i < 5 && i < bytes.len() {
56 buf[i] = bytes[i];
57 i += 1;
58 }
59 Self(buf)
60 }
61
62 /// Return the 2-character class prefix as a string slice.
63 pub fn class(&self) -> &str {
64 // Safety: SqlState is built from ASCII only.
65 std::str::from_utf8(&self.0[..2]).unwrap_or("??")
66 }
67
68 /// Render as a stack-allocated string for display.
69 pub fn as_str(&self) -> &str {
70 std::str::from_utf8(&self.0).unwrap_or("?????")
71 }
72}
73
74impl std::fmt::Display for SqlState {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80// ────────────────────────────────────────────────────────────────
81// Class 00 — Successful Completion
82// ────────────────────────────────────────────────────────────────
83pub const SUCCESSFUL_COMPLETION: SqlState = SqlState::new("00000");
84
85// ────────────────────────────────────────────────────────────────
86// Class 08 — Connection Exception
87// ────────────────────────────────────────────────────────────────
88pub const CONNECTION_EXCEPTION: SqlState = SqlState::new("08000");
89pub const CONNECTION_FAILURE: SqlState = SqlState::new("08006");
90pub const CONNECTION_DOES_NOT_EXIST: SqlState = SqlState::new("08003");
91pub const SQLSERVER_REJECTED_ESTABLISHMENT: SqlState = SqlState::new("08004");
92
93// ────────────────────────────────────────────────────────────────
94// Class 22 — Data Exception
95// ────────────────────────────────────────────────────────────────
96pub const DATA_EXCEPTION: SqlState = SqlState::new("22000");
97pub const DIVISION_BY_ZERO: SqlState = SqlState::new("22012");
98pub const NUMERIC_VALUE_OUT_OF_RANGE: SqlState = SqlState::new("22003");
99pub const NULL_VALUE_NOT_ALLOWED_DATA: SqlState = SqlState::new("22004");
100pub const STRING_DATA_RIGHT_TRUNCATION: SqlState = SqlState::new("22001");
101pub const INVALID_DATETIME_FORMAT: SqlState = SqlState::new("22007");
102pub const INVALID_TEXT_REPRESENTATION: SqlState = SqlState::new("22P02");
103
104// ────────────────────────────────────────────────────────────────
105// Class 23 — Integrity Constraint Violation
106// ────────────────────────────────────────────────────────────────
107pub const INTEGRITY_CONSTRAINT_VIOLATION: SqlState = SqlState::new("23000");
108pub const NOT_NULL_VIOLATION: SqlState = SqlState::new("23502");
109pub const FOREIGN_KEY_VIOLATION: SqlState = SqlState::new("23503");
110pub const UNIQUE_VIOLATION: SqlState = SqlState::new("23505");
111pub const CHECK_VIOLATION: SqlState = SqlState::new("23514");
112
113// ────────────────────────────────────────────────────────────────
114// Class 25 — Invalid Transaction State
115// ────────────────────────────────────────────────────────────────
116pub const INVALID_TRANSACTION_STATE: SqlState = SqlState::new("25000");
117pub const ACTIVE_SQL_TRANSACTION: SqlState = SqlState::new("25001");
118pub const NO_ACTIVE_SQL_TRANSACTION: SqlState = SqlState::new("25P01");
119pub const READ_ONLY_SQL_TRANSACTION: SqlState = SqlState::new("25006");
120
121// ────────────────────────────────────────────────────────────────
122// Class 28 — Invalid Authorization Specification
123// ────────────────────────────────────────────────────────────────
124pub const INVALID_PASSWORD: SqlState = SqlState::new("28P01");
125
126// ────────────────────────────────────────────────────────────────
127// Class 40 — Transaction Rollback
128// ────────────────────────────────────────────────────────────────
129pub const TRANSACTION_ROLLBACK: SqlState = SqlState::new("40000");
130pub const SERIALIZATION_FAILURE: SqlState = SqlState::new("40001");
131pub const DEADLOCK_DETECTED: SqlState = SqlState::new("40P01");
132
133// ────────────────────────────────────────────────────────────────
134// Class 42 — Syntax Error or Access Rule Violation
135// ────────────────────────────────────────────────────────────────
136pub const SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION: SqlState = SqlState::new("42000");
137pub const SYNTAX_ERROR: SqlState = SqlState::new("42601");
138pub const UNDEFINED_COLUMN: SqlState = SqlState::new("42703");
139pub const UNDEFINED_FUNCTION: SqlState = SqlState::new("42883");
140pub const UNDEFINED_TABLE: SqlState = SqlState::new("42P01");
141pub const UNDEFINED_PARAMETER: SqlState = SqlState::new("42P02");
142pub const DUPLICATE_COLUMN: SqlState = SqlState::new("42701");
143pub const DUPLICATE_TABLE: SqlState = SqlState::new("42P07");
144pub const AMBIGUOUS_COLUMN: SqlState = SqlState::new("42702");
145pub const DATATYPE_MISMATCH: SqlState = SqlState::new("42804");
146pub const WRONG_OBJECT_TYPE: SqlState = SqlState::new("42809");
147
148// ────────────────────────────────────────────────────────────────
149// Class 53 — Insufficient Resources
150// ────────────────────────────────────────────────────────────────
151pub const INSUFFICIENT_RESOURCES: SqlState = SqlState::new("53000");
152pub const DISK_FULL: SqlState = SqlState::new("53100");
153pub const OUT_OF_MEMORY: SqlState = SqlState::new("53200");
154pub const TOO_MANY_CONNECTIONS: SqlState = SqlState::new("53300");
155
156// ────────────────────────────────────────────────────────────────
157// Class 57 — Operator Intervention
158// ────────────────────────────────────────────────────────────────
159pub const OPERATOR_INTERVENTION: SqlState = SqlState::new("57000");
160pub const QUERY_CANCELED: SqlState = SqlState::new("57014");
161
162// ────────────────────────────────────────────────────────────────
163// Class 58 — System Error (errors external to PostgreSQL itself)
164// ────────────────────────────────────────────────────────────────
165pub const SYSTEM_ERROR: SqlState = SqlState::new("58000");
166pub const IO_ERROR: SqlState = SqlState::new("58030");
167
168// ────────────────────────────────────────────────────────────────
169// Class XX — Internal Error (PG extension)
170// ────────────────────────────────────────────────────────────────
171pub const INTERNAL_ERROR: SqlState = SqlState::new("XX000");
172pub const DATA_CORRUPTED: SqlState = SqlState::new("XX001");
173
174/// Map a `RedDBError` variant to its SQLSTATE code. Used by
175/// the wire protocol's error response frame so clients get
176/// the standardized identifier alongside the human message.
177pub fn sqlstate_for_reddb_error(err: &crate::api::RedDBError) -> SqlState {
178 use crate::api::RedDBError as E;
179 match err {
180 E::InvalidConfig(_) => SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION,
181 E::SchemaVersionMismatch { .. } => DATA_CORRUPTED,
182 E::FeatureNotEnabled(_) => SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION,
183 E::NotFound(_) => UNDEFINED_TABLE,
184 E::ReadOnly(_) => READ_ONLY_SQL_TRANSACTION,
185 E::InvalidOperation(_) => WRONG_OBJECT_TYPE,
186 E::Engine(_) => INTERNAL_ERROR,
187 E::Catalog(_) => INTERNAL_ERROR,
188 E::Query(_) => SYNTAX_ERROR,
189 E::Validation { .. } => SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION,
190 E::Io(_) => IO_ERROR,
191 E::VersionUnavailable => INTERNAL_ERROR,
192 // 53400 = configuration_limit_exceeded — closest PG class for
193 // operator-pinned RED_MAX_* enforcement (PLAN.md Phase 4.1).
194 E::QuotaExceeded(_) => SqlState::new("53400"),
195 E::Internal(_) => INTERNAL_ERROR,
196 }
197}