nodedb_types/error/code.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Stable numeric error codes for programmatic error handling.
4
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9/// Stable numeric error codes for programmatic error handling.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ErrorCode(pub u16);
12
13/// Defines each named code once, and [`ErrorCode::ALL`] from the same list,
14/// so the list of every code cannot miss a constant.
15macro_rules! error_codes {
16 ( $( $(#[$meta:meta])* $name:ident = $value:literal; )* ) => {
17 impl ErrorCode {
18 $(
19 $(#[$meta])*
20 pub const $name: Self = Self($value);
21 )*
22
23 /// Every named code, in declaration order.
24 pub const ALL: &'static [ErrorCode] = &[ $( Self::$name ),* ];
25 }
26 };
27}
28
29error_codes! {
30 // Write path (1000–1099)
31 CONSTRAINT_VIOLATION = 1000;
32 WRITE_CONFLICT = 1001;
33 DEADLINE_EXCEEDED = 1002;
34 PREVALIDATION_REJECTED = 1003;
35 APPEND_ONLY_VIOLATION = 1010;
36 BALANCE_VIOLATION = 1011;
37 PERIOD_LOCKED = 1012;
38 STATE_TRANSITION_VIOLATION = 1013;
39 TRANSITION_CHECK_VIOLATION = 1014;
40 RETENTION_VIOLATION = 1015;
41 LEGAL_HOLD_ACTIVE = 1016;
42 /// A period-lock reference row exists but does not carry the
43 /// configured `status_column` — a misconfigured column name, not a
44 /// locked period.
45 PERIOD_LOCK_MISCONFIGURED = 1017;
46 TYPE_MISMATCH = 1020;
47 OVERFLOW = 1021;
48 INSUFFICIENT_BALANCE = 1022;
49 RATE_EXCEEDED = 1023;
50 TYPE_GUARD_VIOLATION = 1024;
51 /// A transaction rolled back for a reason other than a serialization
52 /// conflict, such as a participant error. The client retries it.
53 TRANSACTION_ROLLBACK = 1030;
54 /// The statement cannot run inside an explicit transaction block.
55 ACTIVE_SQL_TRANSACTION = 1031;
56
57 // Read path (1100–1199)
58 COLLECTION_NOT_FOUND = 1100;
59 DOCUMENT_NOT_FOUND = 1101;
60 COLLECTION_DRAINING = 1102;
61 COLLECTION_DEACTIVATED = 1103;
62 /// The named database does not exist.
63 DATABASE_NOT_FOUND = 1110;
64 /// A named catalog object (type, role, index, alert, …) other than a
65 /// collection or database does not exist. Generic: use the object name
66 /// in the message for specifics.
67 UNDEFINED_OBJECT = 1111;
68 /// A named catalog object already exists under that name.
69 ALREADY_EXISTS = 1112;
70 /// The target object exists but is not in a state that accepts this
71 /// operation (locked, busy, mid-transition).
72 OBJECT_NOT_READY = 1113;
73 /// A requested value/record does not exist. Generic: use for lookups
74 /// that don't fit `DOCUMENT_NOT_FOUND`'s collection/id shape.
75 NOT_FOUND = 1114;
76 /// A drop or revoke refused because other objects still depend on the
77 /// named object.
78 DEPENDENT_OBJECTS_EXIST = 1115;
79
80 // Query (1200–1299)
81 PLAN_ERROR = 1200;
82 SQL_NOT_ENABLED = 1202;
83 /// A function call names no registered scalar/aggregate/window function.
84 UNDEFINED_FUNCTION = 1203;
85 /// Expression evaluation divided or took a modulus by zero.
86 DIVISION_BY_ZERO = 1204;
87 /// A LIMIT/OFFSET/FETCH bound resolved outside `[0, usize::MAX]`.
88 INVALID_LIMIT_VALUE = 1205;
89 /// A column reference names no column of any relation in scope.
90 UNDEFINED_COLUMN = 1206;
91 /// A bare column name resolves against more than one relation in scope.
92 AMBIGUOUS_COLUMN = 1207;
93 /// A function received a value it cannot compute on: a vector of the
94 /// wrong dimension, an argument of the wrong shape, a malformed path.
95 DATA_EXCEPTION = 1208;
96 /// A statement exceeded a server limit on its own size or depth: a
97 /// recursion depth, a per-transaction staging budget.
98 PROGRAM_LIMIT_EXCEEDED = 1209;
99
100 // Engine ops (1300–1399)
101 ARRAY = 1300;
102
103 // Quota (1400–1499)
104
105 /// The proposed quota allocation would push the sum of all database quotas
106 /// past the configured global ceiling, or the sum of all tenant quotas past
107 /// the database ceiling.
108 QUOTA_OVERCOMMIT = 1400;
109 /// A request was rejected because the calling tenant has exhausted its quota
110 /// (QPS, memory, connections, or storage).
111 TENANT_QUOTA_EXCEEDED = 1401;
112 /// A request was rejected because the target database has exhausted its quota.
113 DATABASE_QUOTA_EXCEEDED = 1402;
114 /// The server is under global resource pressure and cannot accept new requests.
115 SERVER_OVERLOAD = 1403;
116
117 // Clone (1500–1599)
118
119 /// A `CLONE DATABASE` would exceed the maximum clone chain depth of 8.
120 CLONE_DEPTH_EXCEEDED = 1500;
121 /// A mirror database cannot be cloned; promote the mirror first.
122 CANNOT_CLONE_MIRROR = 1501;
123 /// The source database cannot be dropped while clones depend on it.
124 CLONE_DEPENDENCY = 1502;
125 /// A bitemporal `AS OF` query timestamp predates the clone's creation LSN.
126 CLONE_PREDATES_QUERY_TIME = 1503;
127 /// A write targeted a `Shadowed`/`Materializing` clone collection whose
128 /// engine has no copy-on-write support; `MATERIALIZE` the clone first.
129 CLONE_WRITE_REQUIRES_MATERIALIZE = 1504;
130
131 // Mirror (1700–1799)
132
133 /// Write attempted on a mirror database that has not yet been promoted.
134 MIRROR_READ_ONLY = 1700;
135 /// Strong consistency read requested on a mirror; mirrors cannot serve
136 /// strong reads. The client should retry against the source cluster.
137 STALE_READ_NOT_LEADER = 1701;
138 /// Operation requires the mirror to be promoted, but it has not been.
139 MIRROR_NOT_PROMOTED = 1702;
140 /// `DROP DATABASE` targeted the built-in `default` database, which is
141 /// immutable. Shares SQLSTATE `0A000` with `SQL_NOT_ENABLED` and
142 /// `CANNOT_CLONE_MIRROR`, so it must be constructed explicitly rather
143 /// than derived from the bare SQLSTATE string.
144 CANNOT_DROP_DEFAULT_DATABASE = 1710;
145
146 // Move Tenant (1600–1699)
147
148 /// `MOVE TENANT` drain phase timed out; source left unchanged.
149 MOVE_TENANT_DRAIN_TIMEOUT = 1600;
150 /// `MOVE TENANT` pre-flight failed; collection schema incompatibility.
151 MOVE_TENANT_PREFLIGHT_FAILED = 1601;
152 /// `MOVE TENANT` snapshot phase failed; source left unchanged.
153 MOVE_TENANT_SNAPSHOT_FAILED = 1602;
154 /// `MOVE TENANT` cutover phase failed; source still holds the data.
155 MOVE_TENANT_CUTOVER_FAILED = 1603;
156 /// Tenant is already at the target database; `MOVE TENANT` was a no-op.
157 MOVE_TENANT_ALREADY_AT_TARGET = 1604;
158
159 // Backup / Restore (1800–1899)
160 /// RESTORE targeted a tenant different from the one the envelope belongs to.
161 BACKUP_TENANT_MISMATCH = 1800;
162 /// Backup envelope did not decrypt under this server's configured backup KEK.
163 BACKUP_KEY_MISMATCH = 1801;
164
165 // Auth / Security (2000–2099)
166 AUTHORIZATION_DENIED = 2000;
167 AUTH_EXPIRED = 2001;
168 /// Authentication failed: a wrong password, an unknown user, or missing
169 /// credentials. One code for all of them, so a client cannot tell them
170 /// apart.
171 AUTHENTICATION_FAILED = 2002;
172 /// Vector insert or index rejected because the vector dimension exceeds the
173 /// tenant's `max_vector_dim` quota.
174 TENANT_VECTOR_DIM_EXCEEDED = 2010;
175 /// Graph traversal rejected because the requested depth exceeds the tenant's
176 /// `max_graph_depth` quota.
177 TENANT_GRAPH_DEPTH_EXCEEDED = 2011;
178
179 // Protocol handshake (2100–2199)
180 HANDSHAKE_FAILED = 2100;
181
182 // Sync (3000–3099)
183 SYNC_CONNECTION_FAILED = 3000;
184 SYNC_DELTA_REJECTED = 3001;
185 SHAPE_SUBSCRIPTION_FAILED = 3002;
186
187 // Storage (4000–4099)
188 STORAGE = 4000;
189 SEGMENT_CORRUPTED = 4001;
190 COLD_STORAGE = 4002;
191
192 // WAL (4100–4199)
193 WAL = 4100;
194
195 // Serialization (4200–4299)
196 SERIALIZATION = 4200;
197 CODEC = 4201;
198
199 // Config (5000–5099)
200 CONFIG = 5000;
201 BAD_REQUEST = 5001;
202
203 // Cluster (6000–6099)
204 NO_LEADER = 6000;
205 NOT_LEADER = 6001;
206 MIGRATION_IN_PROGRESS = 6002;
207 NODE_UNREACHABLE = 6003;
208 CLUSTER = 6010;
209
210 // Memory (7000–7099)
211 MEMORY_EXHAUSTED = 7000;
212
213 // Encryption (8000–8099)
214 ENCRYPTION = 8000;
215
216 // Internal (9000–9099)
217 INTERNAL = 9000;
218 BRIDGE = 9001;
219 DISPATCH = 9002;
220}
221
222impl fmt::Display for ErrorCode {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "NDB-{:04}", self.0)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn error_code_display() {
234 assert_eq!(ErrorCode::CONSTRAINT_VIOLATION.to_string(), "NDB-1000");
235 assert_eq!(ErrorCode::INTERNAL.to_string(), "NDB-9000");
236 assert_eq!(ErrorCode::WAL.to_string(), "NDB-4100");
237 }
238
239 #[test]
240 fn every_code_has_a_distinct_value() {
241 let mut seen = std::collections::HashSet::new();
242 for code in ErrorCode::ALL {
243 assert!(seen.insert(code.0), "{code} is defined twice");
244 }
245 assert!(ErrorCode::ALL.contains(&ErrorCode::DATABASE_QUOTA_EXCEEDED));
246 assert!(ErrorCode::ALL.contains(&ErrorCode::DISPATCH));
247 }
248}