1use std::borrow::Cow;
4use std::error::Error as StdError;
5
6use thiserror::Error;
7
8use crate::capability::StorageCapability;
9
10#[derive(Debug, Error)]
12pub enum StorageError {
13 #[error("{capability:?} resource not found: {resource} ({key})")]
14 NotFound {
15 capability: StorageCapability,
16 resource: &'static str,
17 key: String,
18 },
19
20 #[error("{capability:?} resource already exists: {resource} ({key})")]
21 AlreadyExists {
22 capability: StorageCapability,
23 resource: &'static str,
24 key: String,
25 },
26
27 #[error("conflict in {capability:?} during {operation}: {message}")]
28 Conflict {
29 capability: StorageCapability,
30 operation: Cow<'static, str>,
31 message: String,
32 },
33
34 #[error("invalid input for {capability:?} during {operation}: {message}")]
35 InvalidInput {
36 capability: StorageCapability,
37 operation: Cow<'static, str>,
38 message: String,
39 },
40
41 #[error("unsupported operation for {capability:?}: {operation} ({message})")]
42 Unsupported {
43 capability: StorageCapability,
44 operation: Cow<'static, str>,
45 message: String,
46 },
47
48 #[error("pool failure during {operation}: {message}")]
49 Pool {
50 operation: Cow<'static, str>,
51 message: String,
52 },
53
54 #[error("timeout during {operation}")]
55 Timeout { operation: Cow<'static, str> },
56
57 #[error("sql transaction failure during {operation}: {message}")]
58 Transaction {
59 operation: Cow<'static, str>,
60 message: String,
61 },
62
63 #[error("serialization failure in {capability:?}: {message}")]
64 Serialization {
65 capability: StorageCapability,
66 message: String,
67 },
68
69 #[error("index maintenance failure in {capability:?}: {message}")]
70 IndexMaintenance {
71 capability: StorageCapability,
72 message: String,
73 },
74
75 #[error("backend driver error in {capability:?} during {operation}: {source}")]
76 Driver {
77 capability: StorageCapability,
78 operation: Cow<'static, str>,
79 #[source]
80 source: Box<dyn StdError + Send + Sync>,
81 },
82
83 #[error("write queue full: timed out after {timeout_ms}ms waiting for writer task capacity")]
89 WriteQueueFull { timeout_ms: u64 },
90
91 #[error("internal storage error: {0}")]
96 Internal(String),
97
98 #[error(
108 "KHIVE_WRITE_QUEUE=1 but no Tokio runtime context is available to spawn the writer task"
109 )]
110 WriterTaskNoRuntime,
111}
112
113impl StorageError {
114 pub fn driver(
116 capability: StorageCapability,
117 operation: impl Into<Cow<'static, str>>,
118 source: impl StdError + Send + Sync + 'static,
119 ) -> Self {
120 Self::Driver {
121 capability,
122 operation: operation.into(),
123 source: Box::new(source),
124 }
125 }
126
127 pub fn capability(&self) -> Option<StorageCapability> {
129 match self {
130 Self::NotFound { capability, .. }
131 | Self::AlreadyExists { capability, .. }
132 | Self::Conflict { capability, .. }
133 | Self::InvalidInput { capability, .. }
134 | Self::Unsupported { capability, .. }
135 | Self::Serialization { capability, .. }
136 | Self::IndexMaintenance { capability, .. }
137 | Self::Driver { capability, .. } => Some(*capability),
138 Self::Pool { .. }
139 | Self::Timeout { .. }
140 | Self::Transaction { .. }
141 | Self::WriteQueueFull { .. }
142 | Self::Internal(..)
143 | Self::WriterTaskNoRuntime => None,
144 }
145 }
146
147 pub fn is_retryable(&self) -> bool {
149 matches!(
150 self,
151 Self::Pool { .. }
152 | Self::Timeout { .. }
153 | Self::Transaction { .. }
154 | Self::WriteQueueFull { .. }
155 )
156 }
157
158 pub fn is_fts5_syntax_error(&self) -> bool {
186 let Self::Driver {
187 capability,
188 operation,
189 source,
190 } = self
191 else {
192 return false;
193 };
194 if *capability != StorageCapability::Text || operation.as_ref() != "fts_search" {
195 return false;
196 }
197 let msg = source.to_string();
198 msg.contains("fts5: syntax error")
199 || msg.contains("fts5: parser stack overflow")
200 || msg.contains("fts5: column queries are not supported")
201 || msg.contains("fts5: phrase queries are not supported (detail")
202 || msg.contains("fts5: NEAR queries are not supported (detail")
203 }
204
205 pub fn is_unique_constraint_violation(&self) -> bool {
221 let Self::Driver {
222 capability,
223 operation,
224 source,
225 } = self
226 else {
227 return false;
228 };
229 if *capability != StorageCapability::Sql {
230 return false;
231 }
232 if !matches!(
233 operation.as_ref(),
234 "execute" | "pool_writer.execute" | "tx.execute"
235 ) {
236 return false;
237 }
238 source.to_string().contains("UNIQUE constraint failed")
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use std::fmt;
246
247 #[derive(Debug)]
248 struct FakeSource(String);
249
250 impl fmt::Display for FakeSource {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 write!(f, "{}", self.0)
253 }
254 }
255
256 impl StdError for FakeSource {}
257
258 fn driver_err(operation: &'static str, message: &str) -> StorageError {
259 StorageError::driver(
260 StorageCapability::Text,
261 operation,
262 FakeSource(message.into()),
263 )
264 }
265
266 #[test]
267 fn fts5_syntax_error_at_fts_search_is_classified_as_syntax_error() {
268 let e = driver_err("fts_search", "fts5: syntax error near \"@\"");
269 assert!(e.is_fts5_syntax_error());
270 }
271
272 #[test]
273 fn fts5_parser_stack_overflow_is_classified_as_syntax_error() {
274 let e = driver_err("fts_search", "fts5: parser stack overflow");
275 assert!(e.is_fts5_syntax_error());
276 }
277
278 #[test]
279 fn fts5_unsupported_column_query_is_classified_as_syntax_error() {
280 let e = driver_err(
281 "fts_search",
282 "fts5: column queries are not supported (detail=none)",
283 );
284 assert!(e.is_fts5_syntax_error());
285 }
286
287 #[test]
288 fn timeout_is_not_classified_as_syntax_error() {
289 let e = StorageError::Timeout {
290 operation: "fts_search".into(),
291 };
292 assert!(!e.is_fts5_syntax_error());
293 }
294
295 #[test]
296 fn pool_failure_is_not_classified_as_syntax_error() {
297 let e = StorageError::Pool {
298 operation: "fts_search".into(),
299 message: "pool exhausted".into(),
300 };
301 assert!(!e.is_fts5_syntax_error());
302 }
303
304 #[test]
305 fn driver_error_at_non_search_operation_is_not_classified_as_syntax_error() {
306 let e = driver_err("open_fts_reader", "fts5: syntax error near \"@\"");
310 assert!(!e.is_fts5_syntax_error());
311 }
312
313 #[test]
314 fn driver_error_with_unrelated_message_is_not_classified_as_syntax_error() {
315 let e = driver_err("fts_search", "disk I/O error");
318 assert!(!e.is_fts5_syntax_error());
319 }
320
321 #[test]
322 fn fts5_phrase_detail_query_is_classified_as_syntax_error() {
323 let e = driver_err(
324 "fts_search",
325 "fts5: phrase queries are not supported (detail!=full)",
326 );
327 assert!(e.is_fts5_syntax_error());
328 }
329
330 #[test]
331 fn fts5_near_detail_query_is_classified_as_syntax_error() {
332 let e = driver_err(
333 "fts_search",
334 "fts5: NEAR queries are not supported (detail!=full)",
335 );
336 assert!(e.is_fts5_syntax_error());
337 }
338
339 #[test]
340 fn unprefixed_detail_message_is_not_classified_as_syntax_error() {
341 let e = driver_err(
345 "fts_search",
346 "phrase queries are not supported (detail!=full)",
347 );
348 assert!(!e.is_fts5_syntax_error());
349 }
350
351 #[test]
352 fn fts5_shadow_table_corruption_is_not_classified_as_syntax_error() {
353 let e = driver_err(
356 "fts_search",
357 "fts5: error creating shadow table notes_content: no such table",
358 );
359 assert!(!e.is_fts5_syntax_error());
360 }
361
362 #[test]
363 fn non_text_capability_is_not_classified_as_syntax_error() {
364 let e = StorageError::Driver {
365 capability: StorageCapability::Vectors,
366 operation: "fts_search".into(),
367 source: Box::new(FakeSource("fts5: syntax error near \"@\"".into())),
368 };
369 assert!(!e.is_fts5_syntax_error());
370 }
371
372 fn driver_err_sql(operation: &'static str, message: &str) -> StorageError {
373 StorageError::driver(
374 StorageCapability::Sql,
375 operation,
376 FakeSource(message.into()),
377 )
378 }
379
380 #[test]
381 fn unique_constraint_failure_at_execute_sql_capability_is_classified() {
382 let e = driver_err_sql(
383 "execute",
384 "UNIQUE constraint failed: brain_serve_ledger.namespace, \
385 brain_serve_ledger.target_id, brain_serve_ledger.query_class, \
386 brain_serve_ledger.served_at",
387 );
388 assert!(e.is_unique_constraint_violation());
389 }
390
391 #[test]
392 fn unique_constraint_failure_at_pool_writer_execute_is_classified() {
393 let e = driver_err_sql("pool_writer.execute", "UNIQUE constraint failed: t.id");
397 assert!(e.is_unique_constraint_violation());
398 }
399
400 #[test]
401 fn unique_constraint_message_at_non_execute_operation_is_not_classified() {
402 let e = driver_err_sql("query_row", "UNIQUE constraint failed: t.id");
403 assert!(!e.is_unique_constraint_violation());
404 }
405
406 #[test]
407 fn non_unique_driver_error_at_execute_is_not_classified() {
408 let e = driver_err_sql("execute", "disk I/O error");
409 assert!(!e.is_unique_constraint_violation());
410 }
411
412 #[test]
413 fn non_sql_capability_is_not_classified_as_unique_violation() {
414 let e = driver_err("execute", "UNIQUE constraint failed: t.id");
415 assert!(!e.is_unique_constraint_violation());
416 }
417
418 #[test]
419 fn timeout_is_not_classified_as_unique_violation() {
420 let e = StorageError::Timeout {
421 operation: "execute".into(),
422 };
423 assert!(!e.is_unique_constraint_violation());
424 }
425}