1use std::borrow::Cow;
4use std::error::Error as StdError;
5use std::fmt;
6
7use thiserror::Error;
8
9use crate::capability::StorageCapability;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum WriterTaskRequestState {
19 NotStarted,
21 TransactionRolledBack,
24 SideEffectsUnknown,
28}
29
30impl fmt::Display for WriterTaskRequestState {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(match self {
33 Self::NotStarted => "not_started",
34 Self::TransactionRolledBack => "transaction_rolled_back",
35 Self::SideEffectsUnknown => "side_effects_unknown",
36 })
37 }
38}
39
40#[derive(Debug, Error)]
42pub enum StorageError {
43 #[error("{capability:?} resource not found: {resource} ({key})")]
44 NotFound {
45 capability: StorageCapability,
46 resource: &'static str,
47 key: String,
48 },
49
50 #[error("{capability:?} resource already exists: {resource} ({key})")]
51 AlreadyExists {
52 capability: StorageCapability,
53 resource: &'static str,
54 key: String,
55 },
56
57 #[error("conflict in {capability:?} during {operation}: {message}")]
58 Conflict {
59 capability: StorageCapability,
60 operation: Cow<'static, str>,
61 message: String,
62 },
63
64 #[error("invalid input for {capability:?} during {operation}: {message}")]
65 InvalidInput {
66 capability: StorageCapability,
67 operation: Cow<'static, str>,
68 message: String,
69 },
70
71 #[error("unsupported operation for {capability:?}: {operation} ({message})")]
72 Unsupported {
73 capability: StorageCapability,
74 operation: Cow<'static, str>,
75 message: String,
76 },
77
78 #[error("pool failure during {operation}: {message}")]
79 Pool {
80 operation: Cow<'static, str>,
81 message: String,
82 },
83
84 #[error("timeout during {operation}")]
85 Timeout { operation: Cow<'static, str> },
86
87 #[error("sql transaction failure during {operation}: {message}")]
88 Transaction {
89 operation: Cow<'static, str>,
90 message: String,
91 },
92
93 #[error("serialization failure in {capability:?}: {message}")]
94 Serialization {
95 capability: StorageCapability,
96 message: String,
97 },
98
99 #[error("index maintenance failure in {capability:?}: {message}")]
100 IndexMaintenance {
101 capability: StorageCapability,
102 message: String,
103 },
104
105 #[error("backend driver error in {capability:?} during {operation}: {source}")]
106 Driver {
107 capability: StorageCapability,
108 operation: Cow<'static, str>,
109 #[source]
110 source: Box<dyn StdError + Send + Sync>,
111 },
112
113 #[error("write queue full: timed out after {timeout_ms}ms waiting for writer task capacity")]
118 WriteQueueFull { timeout_ms: u64 },
119
120 #[error("writer task terminated (request_state={request_state})")]
124 WriterTaskTerminated {
125 request_state: WriterTaskRequestState,
126 },
127
128 #[error("internal storage error: {0}")]
131 Internal(String),
132
133 #[error(
138 "KHIVE_WRITE_QUEUE=1 but no Tokio runtime context is available to spawn the writer task"
139 )]
140 WriterTaskNoRuntime,
141
142 #[error(
146 "refusing write on {capability:?} at {volume}: {available_bytes} bytes available, \
147 below the {floor_bytes}-byte floor"
148 )]
149 CapacityFloor {
150 capability: StorageCapability,
151 volume: String,
152 available_bytes: u64,
153 floor_bytes: u64,
154 },
155}
156
157impl StorageError {
158 pub fn driver(
160 capability: StorageCapability,
161 operation: impl Into<Cow<'static, str>>,
162 source: impl StdError + Send + Sync + 'static,
163 ) -> Self {
164 Self::Driver {
165 capability,
166 operation: operation.into(),
167 source: Box::new(source),
168 }
169 }
170
171 pub fn capability(&self) -> Option<StorageCapability> {
173 match self {
174 Self::NotFound { capability, .. }
175 | Self::AlreadyExists { capability, .. }
176 | Self::Conflict { capability, .. }
177 | Self::InvalidInput { capability, .. }
178 | Self::Unsupported { capability, .. }
179 | Self::Serialization { capability, .. }
180 | Self::IndexMaintenance { capability, .. }
181 | Self::Driver { capability, .. }
182 | Self::CapacityFloor { capability, .. } => Some(*capability),
183 Self::Pool { .. }
184 | Self::Timeout { .. }
185 | Self::Transaction { .. }
186 | Self::WriteQueueFull { .. }
187 | Self::WriterTaskTerminated { .. }
188 | Self::Internal(..)
189 | Self::WriterTaskNoRuntime => None,
190 }
191 }
192
193 pub fn is_retryable(&self) -> bool {
195 matches!(
196 self,
197 Self::Pool { .. }
198 | Self::Timeout { .. }
199 | Self::Transaction { .. }
200 | Self::WriteQueueFull { .. }
201 )
202 }
203
204 pub fn is_fts5_syntax_error(&self) -> bool {
220 let Self::Driver {
221 capability,
222 operation,
223 source,
224 } = self
225 else {
226 return false;
227 };
228 if *capability != StorageCapability::Text || operation.as_ref() != "fts_search" {
229 return false;
230 }
231 let msg = source.to_string();
232 msg.contains("fts5: syntax error")
233 || msg.contains("fts5: parser stack overflow")
234 || msg.contains("fts5: column queries are not supported")
235 || msg.contains("fts5: phrase queries are not supported (detail")
236 || msg.contains("fts5: NEAR queries are not supported (detail")
237 }
238
239 pub fn is_unique_constraint_violation(&self) -> bool {
252 let Self::Driver {
253 capability,
254 operation,
255 source,
256 } = self
257 else {
258 return false;
259 };
260 if *capability != StorageCapability::Sql {
261 return false;
262 }
263 if !matches!(
264 operation.as_ref(),
265 "execute" | "pool_writer.execute" | "tx.execute"
266 ) {
267 return false;
268 }
269 source.to_string().contains("UNIQUE constraint failed")
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use std::fmt;
277
278 #[derive(Debug)]
279 struct FakeSource(String);
280
281 impl fmt::Display for FakeSource {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 write!(f, "{}", self.0)
284 }
285 }
286
287 impl StdError for FakeSource {}
288
289 fn driver_err(operation: &'static str, message: &str) -> StorageError {
290 StorageError::driver(
291 StorageCapability::Text,
292 operation,
293 FakeSource(message.into()),
294 )
295 }
296
297 #[test]
298 fn writer_task_request_state_display_is_stable() {
299 assert_eq!(
300 WriterTaskRequestState::NotStarted.to_string(),
301 "not_started"
302 );
303 assert_eq!(
304 WriterTaskRequestState::TransactionRolledBack.to_string(),
305 "transaction_rolled_back"
306 );
307 assert_eq!(
308 WriterTaskRequestState::SideEffectsUnknown.to_string(),
309 "side_effects_unknown"
310 );
311 }
312
313 #[test]
314 fn writer_task_terminated_is_uncapability_scoped_and_not_retryable() {
315 for request_state in [
316 WriterTaskRequestState::NotStarted,
317 WriterTaskRequestState::TransactionRolledBack,
318 WriterTaskRequestState::SideEffectsUnknown,
319 ] {
320 let error = StorageError::WriterTaskTerminated { request_state };
321 assert_eq!(error.capability(), None);
322 assert!(!error.is_retryable());
323 assert_eq!(
324 error.to_string(),
325 format!("writer task terminated (request_state={request_state})")
326 );
327 }
328 }
329
330 #[test]
331 fn fts5_syntax_error_at_fts_search_is_classified_as_syntax_error() {
332 let e = driver_err("fts_search", "fts5: syntax error near \"@\"");
333 assert!(e.is_fts5_syntax_error());
334 }
335
336 #[test]
337 fn fts5_parser_stack_overflow_is_classified_as_syntax_error() {
338 let e = driver_err("fts_search", "fts5: parser stack overflow");
339 assert!(e.is_fts5_syntax_error());
340 }
341
342 #[test]
343 fn fts5_unsupported_column_query_is_classified_as_syntax_error() {
344 let e = driver_err(
345 "fts_search",
346 "fts5: column queries are not supported (detail=none)",
347 );
348 assert!(e.is_fts5_syntax_error());
349 }
350
351 #[test]
352 fn timeout_is_not_classified_as_syntax_error() {
353 let e = StorageError::Timeout {
354 operation: "fts_search".into(),
355 };
356 assert!(!e.is_fts5_syntax_error());
357 }
358
359 #[test]
360 fn pool_failure_is_not_classified_as_syntax_error() {
361 let e = StorageError::Pool {
362 operation: "fts_search".into(),
363 message: "pool exhausted".into(),
364 };
365 assert!(!e.is_fts5_syntax_error());
366 }
367
368 #[test]
369 fn driver_error_at_non_search_operation_is_not_classified_as_syntax_error() {
370 let e = driver_err("open_fts_reader", "fts5: syntax error near \"@\"");
371 assert!(!e.is_fts5_syntax_error());
372 }
373
374 #[test]
375 fn driver_error_with_unrelated_message_is_not_classified_as_syntax_error() {
376 let e = driver_err("fts_search", "disk I/O error");
377 assert!(!e.is_fts5_syntax_error());
378 }
379
380 #[test]
381 fn fts5_phrase_detail_query_is_classified_as_syntax_error() {
382 let e = driver_err(
383 "fts_search",
384 "fts5: phrase queries are not supported (detail!=full)",
385 );
386 assert!(e.is_fts5_syntax_error());
387 }
388
389 #[test]
390 fn fts5_near_detail_query_is_classified_as_syntax_error() {
391 let e = driver_err(
392 "fts_search",
393 "fts5: NEAR queries are not supported (detail!=full)",
394 );
395 assert!(e.is_fts5_syntax_error());
396 }
397
398 #[test]
399 fn unprefixed_detail_message_is_not_classified_as_syntax_error() {
400 let e = driver_err(
401 "fts_search",
402 "phrase queries are not supported (detail!=full)",
403 );
404 assert!(!e.is_fts5_syntax_error());
405 }
406
407 #[test]
408 fn fts5_shadow_table_corruption_is_not_classified_as_syntax_error() {
409 let e = driver_err(
410 "fts_search",
411 "fts5: error creating shadow table notes_content: no such table",
412 );
413 assert!(!e.is_fts5_syntax_error());
414 }
415
416 #[test]
417 fn non_text_capability_is_not_classified_as_syntax_error() {
418 let e = StorageError::Driver {
419 capability: StorageCapability::Vectors,
420 operation: "fts_search".into(),
421 source: Box::new(FakeSource("fts5: syntax error near \"@\"".into())),
422 };
423 assert!(!e.is_fts5_syntax_error());
424 }
425
426 fn driver_err_sql(operation: &'static str, message: &str) -> StorageError {
427 StorageError::driver(
428 StorageCapability::Sql,
429 operation,
430 FakeSource(message.into()),
431 )
432 }
433
434 #[test]
435 fn unique_constraint_failure_at_execute_sql_capability_is_classified() {
436 let e = driver_err_sql(
437 "execute",
438 "UNIQUE constraint failed: brain_serve_ledger.namespace, \
439 brain_serve_ledger.target_id, brain_serve_ledger.query_class, \
440 brain_serve_ledger.served_at",
441 );
442 assert!(e.is_unique_constraint_violation());
443 }
444
445 #[test]
446 fn unique_constraint_failure_at_pool_writer_execute_is_classified() {
447 let e = driver_err_sql("pool_writer.execute", "UNIQUE constraint failed: t.id");
448 assert!(e.is_unique_constraint_violation());
449 }
450
451 #[test]
452 fn unique_constraint_message_at_non_execute_operation_is_not_classified() {
453 let e = driver_err_sql("query_row", "UNIQUE constraint failed: t.id");
454 assert!(!e.is_unique_constraint_violation());
455 }
456
457 #[test]
458 fn non_unique_driver_error_at_execute_is_not_classified() {
459 let e = driver_err_sql("execute", "disk I/O error");
460 assert!(!e.is_unique_constraint_violation());
461 }
462
463 #[test]
464 fn non_sql_capability_is_not_classified_as_unique_violation() {
465 let e = driver_err("execute", "UNIQUE constraint failed: t.id");
466 assert!(!e.is_unique_constraint_violation());
467 }
468
469 #[test]
470 fn timeout_is_not_classified_as_unique_violation() {
471 let e = StorageError::Timeout {
472 operation: "execute".into(),
473 };
474 assert!(!e.is_unique_constraint_violation());
475 }
476}