Skip to main content

nodedb_types/error/
details.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Machine-matchable structured error details.
4
5use serde::{Deserialize, Serialize};
6
7/// Structured error details for programmatic matching.
8///
9/// Clients match on the variant to determine the error category, then
10/// extract structured fields. The `message` on [`crate::error::NodeDbError`]
11/// carries the human-readable explanation.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "kind", rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum ErrorDetails {
16    // Write path
17    #[serde(rename = "constraint_violation")]
18    ConstraintViolation {
19        collection: String,
20        /// The constraint kind (`"not_null"`, `"unique"`, ...). Drives
21        /// SQLSTATE selection (23502 vs 23505) on every hop, local or remote.
22        /// Named `constraint`, not `kind`, because the enum's own
23        /// `#[serde(tag = "kind")]` already owns that JSON key.
24        constraint: String,
25    },
26    #[serde(rename = "write_conflict")]
27    WriteConflict {
28        collection: String,
29        document_id: String,
30    },
31    #[serde(rename = "deadline_exceeded")]
32    DeadlineExceeded,
33    #[serde(rename = "prevalidation_rejected")]
34    PrevalidationRejected { constraint: String },
35    #[serde(rename = "append_only_violation")]
36    AppendOnlyViolation { collection: String },
37    #[serde(rename = "balance_violation")]
38    BalanceViolation { collection: String },
39    #[serde(rename = "period_locked")]
40    PeriodLocked { collection: String },
41    #[serde(rename = "period_lock_misconfigured")]
42    PeriodLockMisconfigured {
43        collection: String,
44        ref_table: String,
45        status_column: String,
46    },
47    #[serde(rename = "state_transition_violation")]
48    StateTransitionViolation { collection: String },
49    #[serde(rename = "transition_check_violation")]
50    TransitionCheckViolation { collection: String },
51    #[serde(rename = "type_guard_violation")]
52    TypeGuardViolation { collection: String },
53    #[serde(rename = "retention_violation")]
54    RetentionViolation { collection: String },
55    #[serde(rename = "legal_hold_active")]
56    LegalHoldActive { collection: String },
57    #[serde(rename = "type_mismatch")]
58    TypeMismatch { collection: String },
59    #[serde(rename = "overflow")]
60    Overflow { collection: String },
61    #[serde(rename = "insufficient_balance")]
62    InsufficientBalance { collection: String },
63    #[serde(rename = "rate_exceeded")]
64    RateExceeded { gate: String },
65    /// A transaction rolled back for a reason other than a serialization
66    /// conflict. `detail` names the reason. The client retries it.
67    #[serde(rename = "transaction_rollback")]
68    TransactionRollback { detail: String },
69    /// The statement cannot run inside an explicit transaction block.
70    /// `detail` names the statement or the refused operation.
71    #[serde(rename = "active_sql_transaction")]
72    ActiveSqlTransaction { detail: String },
73
74    // Read path
75    #[serde(rename = "collection_not_found")]
76    CollectionNotFound { collection: String },
77    /// The named database does not exist.
78    #[serde(rename = "database_not_found")]
79    DatabaseNotFound { database: String },
80    #[serde(rename = "document_not_found")]
81    DocumentNotFound {
82        collection: String,
83        document_id: String,
84    },
85    /// A named catalog object other than a collection or database was not
86    /// found (type, role, index, alert, tenant, …).
87    #[serde(rename = "undefined_object")]
88    UndefinedObject { object: String },
89    /// A named catalog object already exists under that name.
90    #[serde(rename = "already_exists")]
91    AlreadyExists { object: String },
92    /// The target object exists but is not in a state that accepts this
93    /// operation (locked, busy, mid-transition).
94    #[serde(rename = "object_not_ready")]
95    ObjectNotReady { object: String },
96    /// A requested value/record does not exist, outside the
97    /// collection/document shape of `DocumentNotFound`.
98    #[serde(rename = "not_found")]
99    NotFound { detail: String },
100    /// A drop or revoke refused because other objects still depend on
101    /// `object`. The message lists the dependents.
102    #[serde(rename = "dependent_objects_exist")]
103    DependentObjectsExist { object: String },
104    #[serde(rename = "collection_draining")]
105    CollectionDraining { collection: String },
106    #[serde(rename = "collection_deactivated")]
107    CollectionDeactivated {
108        collection: String,
109        /// Wall-clock nanoseconds when retention elapses and the
110        /// collection becomes unrecoverable. Clients can render a
111        /// human-readable countdown.
112        retention_expires_at_ns: u64,
113        /// Copy-pasteable SQL the user can run to restore the
114        /// collection. Populated with the actual name, so the error
115        /// is actionable without further lookup.
116        undrop_hint: String,
117    },
118
119    // Query
120    #[serde(rename = "plan_error")]
121    PlanError { phase: String, detail: String },
122    #[serde(rename = "sql_not_enabled")]
123    SqlNotEnabled,
124    /// A function call names no registered scalar/aggregate/window function.
125    #[serde(rename = "undefined_function")]
126    UndefinedFunction { name: String },
127    /// A column reference names no column of any relation in scope.
128    #[serde(rename = "undefined_column")]
129    UndefinedColumn { column: String },
130    /// A bare column name resolves against more than one relation in scope.
131    #[serde(rename = "ambiguous_column")]
132    AmbiguousColumn { column: String },
133    /// Expression evaluation divided or took a modulus by zero.
134    #[serde(rename = "division_by_zero")]
135    DivisionByZero,
136    /// A function received a value it cannot compute on. `detail` names
137    /// the function and the offending value.
138    #[serde(rename = "data_exception")]
139    DataException { detail: String },
140    /// A statement exceeded a server limit on its own size or depth.
141    /// `detail` names the limit.
142    #[serde(rename = "program_limit_exceeded")]
143    ProgramLimitExceeded { detail: String },
144    /// A LIMIT/OFFSET/FETCH bound resolved outside `[0, usize::MAX]`.
145    #[serde(rename = "invalid_limit_value")]
146    InvalidLimitValue { clause: String, value: String },
147
148    // Auth
149    #[serde(rename = "authorization_denied")]
150    AuthorizationDenied { resource: String },
151    #[serde(rename = "auth_expired")]
152    AuthExpired,
153    /// Credentials were rejected. Carries nothing that tells a wrong
154    /// password from an unknown user.
155    #[serde(rename = "authentication_failed")]
156    AuthenticationFailed,
157    /// Tenant quota: vector dimension exceeds `max_vector_dim`.
158    #[serde(rename = "tenant_vector_dim_exceeded")]
159    TenantVectorDimExceeded { dim: u32, limit: u32 },
160    /// Tenant quota: graph traversal depth exceeds `max_graph_depth`.
161    #[serde(rename = "tenant_graph_depth_exceeded")]
162    TenantGraphDepthExceeded { depth: u32, limit: u32 },
163
164    // Protocol handshake
165    #[serde(rename = "handshake_failed")]
166    HandshakeFailed {
167        /// Numeric error code sent by the server (0=BadMagic, 1=VersionMismatch, 2=Malformed).
168        server_code: u8,
169    },
170
171    // Sync
172    #[serde(rename = "sync_connection_failed")]
173    SyncConnectionFailed,
174    #[serde(rename = "sync_delta_rejected")]
175    SyncDeltaRejected {
176        compensation: Option<crate::sync::compensation::CompensationHint>,
177    },
178    #[serde(rename = "shape_subscription_failed")]
179    ShapeSubscriptionFailed { shape_id: String },
180
181    // Storage (opaque infrastructure)
182    #[serde(rename = "storage")]
183    Storage {
184        component: String,
185        op: String,
186        detail: String,
187    },
188    #[serde(rename = "segment_corrupted")]
189    SegmentCorrupted {
190        segment_id: u64,
191        corruption: String,
192        detail: String,
193    },
194    #[serde(rename = "cold_storage")]
195    ColdStorage {
196        backend: String,
197        op: String,
198        detail: String,
199    },
200    #[serde(rename = "wal")]
201    Wal { stage: String, detail: String },
202
203    // Serialization
204    #[serde(rename = "serialization")]
205    Serialization { format: String },
206    #[serde(rename = "codec")]
207    Codec {
208        codec: String,
209        op: String,
210        detail: String,
211    },
212
213    // Config
214    #[serde(rename = "config")]
215    Config,
216    #[serde(rename = "bad_request")]
217    BadRequest,
218
219    // Cluster
220    #[serde(rename = "no_leader")]
221    NoLeader,
222    #[serde(rename = "not_leader")]
223    NotLeader { leader_addr: String },
224    #[serde(rename = "migration_in_progress")]
225    MigrationInProgress,
226    #[serde(rename = "node_unreachable")]
227    NodeUnreachable,
228    #[serde(rename = "cluster")]
229    Cluster,
230
231    // Memory
232    #[serde(rename = "memory_exhausted")]
233    MemoryExhausted { engine: String },
234
235    // Encryption
236    #[serde(rename = "encryption")]
237    Encryption { cipher: String, detail: String },
238
239    // Engine ops
240    #[serde(rename = "array")]
241    Array { array: String },
242
243    // Quota
244    #[serde(rename = "quota_overcommit")]
245    QuotaOvercommit { field: String },
246    #[serde(rename = "quota_exceeded")]
247    QuotaExceeded { scope: String },
248    #[serde(rename = "server_overload")]
249    ServerOverload,
250
251    // Clone DDL
252    #[serde(rename = "clone_depth_exceeded")]
253    CloneDepthExceeded { depth: u32, limit: u32 },
254    #[serde(rename = "cannot_clone_mirror")]
255    CannotCloneMirror { database: String },
256    #[serde(rename = "clone_dependency")]
257    CloneDependency { dependents: Vec<String> },
258    #[serde(rename = "clone_predates_query_time")]
259    ClonePredatesQueryTime { as_of_lsn: u64, created_at_lsn: u64 },
260    /// Write refused: the collection's engine has no copy-on-write support,
261    /// so a `Shadowed`/`Materializing` clone cannot safely accept writes.
262    #[serde(rename = "clone_write_requires_materialize")]
263    CloneWriteRequiresMaterialize {
264        collection: String,
265        engine: String,
266        database: String,
267    },
268
269    // Backup / Restore
270    /// RESTORE targeted `expected` but the envelope belongs to `actual`.
271    #[serde(rename = "backup_tenant_mismatch")]
272    BackupTenantMismatch { expected: u64, actual: u64 },
273    /// The envelope did not decrypt under this server's configured backup KEK.
274    #[serde(rename = "backup_key_mismatch")]
275    BackupKeyMismatch,
276
277    // Mirror DDL
278    /// Write attempted on a mirror database that has not been promoted.
279    #[serde(rename = "mirror_read_only")]
280    MirrorReadOnly { database: String },
281    /// Strong read requested on a mirror; the client should contact the source.
282    #[serde(rename = "stale_read_not_leader")]
283    StaleReadNotLeader {
284        database: String,
285        /// Hint: source cluster endpoint the client should redirect to.
286        source_cluster: String,
287    },
288    /// Operation requires the database to be a promoted mirror.
289    #[serde(rename = "mirror_not_promoted")]
290    MirrorNotPromoted { database: String },
291    /// `DROP DATABASE` targeted the built-in `default` database.
292    #[serde(rename = "cannot_drop_default_database")]
293    CannotDropDefaultDatabase,
294
295    // Move Tenant DDL
296    #[serde(rename = "move_tenant_drain_timeout")]
297    MoveTenantDrainTimeout { tenant: String, source_db: String },
298    #[serde(rename = "move_tenant_preflight_failed")]
299    MoveTenantPreflightFailed { tenant: String, detail: String },
300    #[serde(rename = "move_tenant_snapshot_failed")]
301    MoveTenantSnapshotFailed { tenant: String, detail: String },
302    #[serde(rename = "move_tenant_cutover_failed")]
303    MoveTenantCutoverFailed { tenant: String, detail: String },
304    #[serde(rename = "move_tenant_already_at_target")]
305    MoveTenantAlreadyAtTarget { tenant: String, target_db: String },
306
307    // Bridge / Dispatch / Internal
308    #[serde(rename = "bridge")]
309    Bridge {
310        plane: String,
311        op: String,
312        detail: String,
313    },
314    #[serde(rename = "dispatch")]
315    Dispatch { stage: String, detail: String },
316    #[serde(rename = "internal")]
317    Internal { component: String, detail: String },
318}