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")]
88 WriteQueueFull { timeout_ms: u64 },
89
90 #[error("internal storage error: {0}")]
95 Internal(String),
96
97 #[error(
102 "KHIVE_WRITE_QUEUE=1 but no Tokio runtime context is available to spawn the writer task"
103 )]
104 WriterTaskNoRuntime,
105
106 #[error(
110 "refusing write on {capability:?} at {volume}: {available_bytes} bytes available, \
111 below the {floor_bytes}-byte floor"
112 )]
113 CapacityFloor {
114 capability: StorageCapability,
115 volume: String,
116 available_bytes: u64,
117 floor_bytes: u64,
118 },
119}
120
121impl StorageError {
122 pub fn driver(
124 capability: StorageCapability,
125 operation: impl Into<Cow<'static, str>>,
126 source: impl StdError + Send + Sync + 'static,
127 ) -> Self {
128 Self::Driver {
129 capability,
130 operation: operation.into(),
131 source: Box::new(source),
132 }
133 }
134
135 pub fn capability(&self) -> Option<StorageCapability> {
137 match self {
138 Self::NotFound { capability, .. }
139 | Self::AlreadyExists { capability, .. }
140 | Self::Conflict { capability, .. }
141 | Self::InvalidInput { capability, .. }
142 | Self::Unsupported { capability, .. }
143 | Self::Serialization { capability, .. }
144 | Self::IndexMaintenance { capability, .. }
145 | Self::Driver { capability, .. }
146 | Self::CapacityFloor { capability, .. } => Some(*capability),
147 Self::Pool { .. }
148 | Self::Timeout { .. }
149 | Self::Transaction { .. }
150 | Self::WriteQueueFull { .. }
151 | Self::Internal(..)
152 | Self::WriterTaskNoRuntime => None,
153 }
154 }
155
156 pub fn is_retryable(&self) -> bool {
158 matches!(
159 self,
160 Self::Pool { .. }
161 | Self::Timeout { .. }
162 | Self::Transaction { .. }
163 | Self::WriteQueueFull { .. }
164 )
165 }
166
167 pub fn is_fts5_syntax_error(&self) -> bool {
183 let Self::Driver {
184 capability,
185 operation,
186 source,
187 } = self
188 else {
189 return false;
190 };
191 if *capability != StorageCapability::Text || operation.as_ref() != "fts_search" {
192 return false;
193 }
194 let msg = source.to_string();
195 msg.contains("fts5: syntax error")
196 || msg.contains("fts5: parser stack overflow")
197 || msg.contains("fts5: column queries are not supported")
198 || msg.contains("fts5: phrase queries are not supported (detail")
199 || msg.contains("fts5: NEAR queries are not supported (detail")
200 }
201
202 pub fn is_unique_constraint_violation(&self) -> bool {
215 let Self::Driver {
216 capability,
217 operation,
218 source,
219 } = self
220 else {
221 return false;
222 };
223 if *capability != StorageCapability::Sql {
224 return false;
225 }
226 if !matches!(
227 operation.as_ref(),
228 "execute" | "pool_writer.execute" | "tx.execute"
229 ) {
230 return false;
231 }
232 source.to_string().contains("UNIQUE constraint failed")
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use std::fmt;
240
241 #[derive(Debug)]
242 struct FakeSource(String);
243
244 impl fmt::Display for FakeSource {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 write!(f, "{}", self.0)
247 }
248 }
249
250 impl StdError for FakeSource {}
251
252 fn driver_err(operation: &'static str, message: &str) -> StorageError {
253 StorageError::driver(
254 StorageCapability::Text,
255 operation,
256 FakeSource(message.into()),
257 )
258 }
259
260 #[test]
261 fn fts5_syntax_error_at_fts_search_is_classified_as_syntax_error() {
262 let e = driver_err("fts_search", "fts5: syntax error near \"@\"");
263 assert!(e.is_fts5_syntax_error());
264 }
265
266 #[test]
267 fn fts5_parser_stack_overflow_is_classified_as_syntax_error() {
268 let e = driver_err("fts_search", "fts5: parser stack overflow");
269 assert!(e.is_fts5_syntax_error());
270 }
271
272 #[test]
273 fn fts5_unsupported_column_query_is_classified_as_syntax_error() {
274 let e = driver_err(
275 "fts_search",
276 "fts5: column queries are not supported (detail=none)",
277 );
278 assert!(e.is_fts5_syntax_error());
279 }
280
281 #[test]
282 fn timeout_is_not_classified_as_syntax_error() {
283 let e = StorageError::Timeout {
284 operation: "fts_search".into(),
285 };
286 assert!(!e.is_fts5_syntax_error());
287 }
288
289 #[test]
290 fn pool_failure_is_not_classified_as_syntax_error() {
291 let e = StorageError::Pool {
292 operation: "fts_search".into(),
293 message: "pool exhausted".into(),
294 };
295 assert!(!e.is_fts5_syntax_error());
296 }
297
298 #[test]
299 fn driver_error_at_non_search_operation_is_not_classified_as_syntax_error() {
300 let e = driver_err("open_fts_reader", "fts5: syntax error near \"@\"");
301 assert!(!e.is_fts5_syntax_error());
302 }
303
304 #[test]
305 fn driver_error_with_unrelated_message_is_not_classified_as_syntax_error() {
306 let e = driver_err("fts_search", "disk I/O error");
307 assert!(!e.is_fts5_syntax_error());
308 }
309
310 #[test]
311 fn fts5_phrase_detail_query_is_classified_as_syntax_error() {
312 let e = driver_err(
313 "fts_search",
314 "fts5: phrase queries are not supported (detail!=full)",
315 );
316 assert!(e.is_fts5_syntax_error());
317 }
318
319 #[test]
320 fn fts5_near_detail_query_is_classified_as_syntax_error() {
321 let e = driver_err(
322 "fts_search",
323 "fts5: NEAR queries are not supported (detail!=full)",
324 );
325 assert!(e.is_fts5_syntax_error());
326 }
327
328 #[test]
329 fn unprefixed_detail_message_is_not_classified_as_syntax_error() {
330 let e = driver_err(
331 "fts_search",
332 "phrase queries are not supported (detail!=full)",
333 );
334 assert!(!e.is_fts5_syntax_error());
335 }
336
337 #[test]
338 fn fts5_shadow_table_corruption_is_not_classified_as_syntax_error() {
339 let e = driver_err(
340 "fts_search",
341 "fts5: error creating shadow table notes_content: no such table",
342 );
343 assert!(!e.is_fts5_syntax_error());
344 }
345
346 #[test]
347 fn non_text_capability_is_not_classified_as_syntax_error() {
348 let e = StorageError::Driver {
349 capability: StorageCapability::Vectors,
350 operation: "fts_search".into(),
351 source: Box::new(FakeSource("fts5: syntax error near \"@\"".into())),
352 };
353 assert!(!e.is_fts5_syntax_error());
354 }
355
356 fn driver_err_sql(operation: &'static str, message: &str) -> StorageError {
357 StorageError::driver(
358 StorageCapability::Sql,
359 operation,
360 FakeSource(message.into()),
361 )
362 }
363
364 #[test]
365 fn unique_constraint_failure_at_execute_sql_capability_is_classified() {
366 let e = driver_err_sql(
367 "execute",
368 "UNIQUE constraint failed: brain_serve_ledger.namespace, \
369 brain_serve_ledger.target_id, brain_serve_ledger.query_class, \
370 brain_serve_ledger.served_at",
371 );
372 assert!(e.is_unique_constraint_violation());
373 }
374
375 #[test]
376 fn unique_constraint_failure_at_pool_writer_execute_is_classified() {
377 let e = driver_err_sql("pool_writer.execute", "UNIQUE constraint failed: t.id");
378 assert!(e.is_unique_constraint_violation());
379 }
380
381 #[test]
382 fn unique_constraint_message_at_non_execute_operation_is_not_classified() {
383 let e = driver_err_sql("query_row", "UNIQUE constraint failed: t.id");
384 assert!(!e.is_unique_constraint_violation());
385 }
386
387 #[test]
388 fn non_unique_driver_error_at_execute_is_not_classified() {
389 let e = driver_err_sql("execute", "disk I/O error");
390 assert!(!e.is_unique_constraint_violation());
391 }
392
393 #[test]
394 fn non_sql_capability_is_not_classified_as_unique_violation() {
395 let e = driver_err("execute", "UNIQUE constraint failed: t.id");
396 assert!(!e.is_unique_constraint_violation());
397 }
398
399 #[test]
400 fn timeout_is_not_classified_as_unique_violation() {
401 let e = StorageError::Timeout {
402 operation: "execute".into(),
403 };
404 assert!(!e.is_unique_constraint_violation());
405 }
406}