Skip to main content

modelvault_core/
error.rs

1use std::fmt;
2
3/// Top-level error for [`crate::db::Database`] and storage: I/O, on-disk layout, or schema rules.
4///
5/// Convert from [`std::io::Error`] via `?` for convenience on file operations.
6/// Structured validation failure (0.6+): nested path and human-readable detail.
7#[derive(Debug, Clone)]
8pub struct ValidationError {
9    pub path: Vec<String>,
10    pub message: String,
11}
12
13#[derive(Debug)]
14pub enum DbError {
15    /// Failed to access the database file or path.
16    Io(std::io::Error),
17    /// Failed to parse or validate the on-disk format (header, superblock, segments, payloads).
18    Format(FormatError),
19    /// Catalog or row did not satisfy schema invariants.
20    Schema(SchemaError),
21    /// Row value failed type or constraint checks before persistence.
22    Validation(ValidationError),
23    /// Transaction nesting or API misuse (0.8+).
24    Transaction(TransactionError),
25    /// Query construction, parsing, or execution error (SQL adapter and query planner).
26    Query(QueryError),
27    /// Requested capability is not implemented in this release (e.g. nested field paths in rows).
28    NotImplemented,
29}
30
31/// Stable classification of core errors (suitable for matching in higher-level bindings).
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DbErrorKind {
34    Io,
35    Format,
36    Schema,
37    Validation,
38    Transaction,
39    Query,
40    NotImplemented,
41}
42
43impl DbErrorKind {
44    /// Stable snake_case code for bindings and logging.
45    pub const fn as_str(self) -> &'static str {
46        match self {
47            DbErrorKind::Io => "io",
48            DbErrorKind::Format => "format",
49            DbErrorKind::Schema => "schema",
50            DbErrorKind::Validation => "validation",
51            DbErrorKind::Transaction => "transaction",
52            DbErrorKind::Query => "query",
53            DbErrorKind::NotImplemented => "not_implemented",
54        }
55    }
56}
57
58impl DbError {
59    pub fn kind(&self) -> DbErrorKind {
60        match self {
61            DbError::Io(_) => DbErrorKind::Io,
62            DbError::Format(_) => DbErrorKind::Format,
63            DbError::Schema(_) => DbErrorKind::Schema,
64            DbError::Validation(_) => DbErrorKind::Validation,
65            DbError::Transaction(_) => DbErrorKind::Transaction,
66            DbError::Query(_) => DbErrorKind::Query,
67            DbError::NotImplemented => DbErrorKind::NotImplemented,
68        }
69    }
70
71    /// Machine-readable key/value pairs for the specific error variant (empty for I/O).
72    pub fn details(&self) -> std::collections::BTreeMap<String, String> {
73        match self {
74            DbError::Io(_) => std::collections::BTreeMap::new(),
75            DbError::Format(e) => format_error_details(e),
76            DbError::Schema(e) => schema_error_details(e),
77            DbError::Validation(e) => {
78                let mut m = std::collections::BTreeMap::new();
79                if !e.path.is_empty() {
80                    m.insert("path".to_string(), e.path.join("."));
81                }
82                m.insert("message".to_string(), e.message.clone());
83                m
84            }
85            DbError::Transaction(e) => transaction_error_details(e),
86            DbError::Query(e) => {
87                let mut m = std::collections::BTreeMap::new();
88                m.insert("message".to_string(), e.message.clone());
89                m
90            }
91            DbError::NotImplemented => std::collections::BTreeMap::new(),
92        }
93    }
94}
95
96fn format_error_details(e: &FormatError) -> std::collections::BTreeMap<String, String> {
97    use std::collections::BTreeMap;
98    let mut m = BTreeMap::new();
99    match e {
100        FormatError::BadMagic { got } => {
101            m.insert("variant".to_string(), "bad_magic".to_string());
102            m.insert("got".to_string(), format!("{got:02x?}"));
103        }
104        FormatError::TruncatedHeader { got, expected } => {
105            m.insert("variant".to_string(), "truncated_header".to_string());
106            m.insert("got".to_string(), got.to_string());
107            m.insert("expected".to_string(), expected.to_string());
108        }
109        FormatError::UnsupportedVersion { major, minor } => {
110            m.insert("variant".to_string(), "unsupported_version".to_string());
111            m.insert("major".to_string(), major.to_string());
112            m.insert("minor".to_string(), minor.to_string());
113        }
114        FormatError::TruncatedSuperblock { got, expected } => {
115            m.insert("variant".to_string(), "truncated_superblock".to_string());
116            m.insert("got".to_string(), got.to_string());
117            m.insert("expected".to_string(), expected.to_string());
118        }
119        FormatError::BadSuperblockMagic { got } => {
120            m.insert("variant".to_string(), "bad_superblock_magic".to_string());
121            m.insert("got".to_string(), format!("{got:02x?}"));
122        }
123        FormatError::BadSuperblockChecksum => {
124            m.insert("variant".to_string(), "bad_superblock_checksum".to_string());
125        }
126        FormatError::TruncatedSegmentHeader { got, expected } => {
127            m.insert(
128                "variant".to_string(),
129                "truncated_segment_header".to_string(),
130            );
131            m.insert("got".to_string(), got.to_string());
132            m.insert("expected".to_string(), expected.to_string());
133        }
134        FormatError::BadSegmentMagic { got } => {
135            m.insert("variant".to_string(), "bad_segment_magic".to_string());
136            m.insert("got".to_string(), format!("{got:02x?}"));
137        }
138        FormatError::BadSegmentHeaderChecksum => {
139            m.insert(
140                "variant".to_string(),
141                "bad_segment_header_checksum".to_string(),
142            );
143        }
144        FormatError::BadSegmentPayloadChecksum => {
145            m.insert(
146                "variant".to_string(),
147                "bad_segment_payload_checksum".to_string(),
148            );
149        }
150        FormatError::SegmentPayloadPastEof => {
151            m.insert(
152                "variant".to_string(),
153                "segment_payload_past_eof".to_string(),
154            );
155        }
156        FormatError::InvalidCatalogPayload { message } => {
157            m.insert("variant".to_string(), "invalid_catalog_payload".to_string());
158            m.insert("message".to_string(), message.clone());
159        }
160        FormatError::TruncatedRecordPayload => {
161            m.insert(
162                "variant".to_string(),
163                "truncated_record_payload".to_string(),
164            );
165        }
166        FormatError::RecordPayloadTypeMismatch => {
167            m.insert(
168                "variant".to_string(),
169                "record_payload_type_mismatch".to_string(),
170            );
171        }
172        FormatError::InvalidRecordUtf8 => {
173            m.insert("variant".to_string(), "invalid_record_utf8".to_string());
174        }
175        FormatError::RecordPayloadUnsupportedType => {
176            m.insert(
177                "variant".to_string(),
178                "record_payload_unsupported_type".to_string(),
179            );
180        }
181        FormatError::UnknownRecordPayloadVersion { got } => {
182            m.insert(
183                "variant".to_string(),
184                "unknown_record_payload_version".to_string(),
185            );
186            m.insert("got".to_string(), got.to_string());
187        }
188        FormatError::TrailingRecordPayload => {
189            m.insert("variant".to_string(), "trailing_record_payload".to_string());
190        }
191        FormatError::InvalidTxnPayload { message } => {
192            m.insert("variant".to_string(), "invalid_txn_payload".to_string());
193            m.insert("message".to_string(), message.clone());
194        }
195        FormatError::InvalidCheckpointPayload { message } => {
196            m.insert(
197                "variant".to_string(),
198                "invalid_checkpoint_payload".to_string(),
199            );
200            m.insert("message".to_string(), message.clone());
201        }
202        FormatError::UncleanLogTail { safe_end, reason } => {
203            m.insert("variant".to_string(), "unclean_log_tail".to_string());
204            m.insert("safe_end".to_string(), safe_end.to_string());
205            m.insert("reason".to_string(), (*reason).to_string());
206        }
207    }
208    m
209}
210
211fn schema_error_details(e: &SchemaError) -> std::collections::BTreeMap<String, String> {
212    use std::collections::BTreeMap;
213    let mut m = BTreeMap::new();
214    match e {
215        SchemaError::InvalidFieldPath => {
216            m.insert("variant".to_string(), "invalid_field_path".to_string());
217        }
218        SchemaError::DuplicateCollectionName { name } => {
219            m.insert(
220                "variant".to_string(),
221                "duplicate_collection_name".to_string(),
222            );
223            m.insert("name".to_string(), name.clone());
224        }
225        SchemaError::UnknownCollection { id } => {
226            m.insert("variant".to_string(), "unknown_collection".to_string());
227            m.insert("id".to_string(), id.to_string());
228        }
229        SchemaError::UnknownCollectionName { name } => {
230            m.insert("variant".to_string(), "unknown_collection_name".to_string());
231            m.insert("name".to_string(), name.clone());
232        }
233        SchemaError::InvalidCollectionName => {
234            m.insert("variant".to_string(), "invalid_collection_name".to_string());
235        }
236        SchemaError::InvalidSchemaVersion { expected, got } => {
237            m.insert("variant".to_string(), "invalid_schema_version".to_string());
238            m.insert("expected".to_string(), expected.to_string());
239            m.insert("got".to_string(), got.to_string());
240        }
241        SchemaError::SchemaVersionExhausted => {
242            m.insert(
243                "variant".to_string(),
244                "schema_version_exhausted".to_string(),
245            );
246        }
247        SchemaError::UnexpectedCollectionId { expected, got } => {
248            m.insert(
249                "variant".to_string(),
250                "unexpected_collection_id".to_string(),
251            );
252            m.insert("expected".to_string(), expected.to_string());
253            m.insert("got".to_string(), got.to_string());
254        }
255        SchemaError::NoPrimaryKey { collection_id } => {
256            m.insert("variant".to_string(), "no_primary_key".to_string());
257            m.insert("collection_id".to_string(), collection_id.to_string());
258        }
259        SchemaError::PrimaryFieldNotFound { name } => {
260            m.insert("variant".to_string(), "primary_field_not_found".to_string());
261            m.insert("name".to_string(), name.clone());
262        }
263        SchemaError::PrimaryFieldMissingInSchema { name } => {
264            m.insert(
265                "variant".to_string(),
266                "primary_field_missing_in_schema".to_string(),
267            );
268            m.insert("name".to_string(), name.clone());
269        }
270        SchemaError::RowMissingPrimary { name } => {
271            m.insert("variant".to_string(), "row_missing_primary".to_string());
272            m.insert("name".to_string(), name.clone());
273        }
274        SchemaError::RowUnknownField { name } => {
275            m.insert("variant".to_string(), "row_unknown_field".to_string());
276            m.insert("name".to_string(), name.clone());
277        }
278        SchemaError::RowMissingField { name } => {
279            m.insert("variant".to_string(), "row_missing_field".to_string());
280            m.insert("name".to_string(), name.clone());
281        }
282        SchemaError::UniqueIndexViolation => {
283            m.insert("variant".to_string(), "unique_index_violation".to_string());
284        }
285        SchemaError::IncompatibleSchemaChange { message } => {
286            m.insert(
287                "variant".to_string(),
288                "incompatible_schema_change".to_string(),
289            );
290            m.insert("message".to_string(), message.clone());
291        }
292        SchemaError::MigrationRequired { message } => {
293            m.insert("variant".to_string(), "migration_required".to_string());
294            m.insert("message".to_string(), message.clone());
295        }
296        SchemaError::IndexRowMissing {
297            collection_id,
298            index_name,
299        } => {
300            m.insert("variant".to_string(), "index_row_missing".to_string());
301            m.insert("collection_id".to_string(), collection_id.to_string());
302            m.insert("index_name".to_string(), index_name.clone());
303        }
304        SchemaError::PrimaryKeyTypeMismatch { collection_id } => {
305            m.insert(
306                "variant".to_string(),
307                "primary_key_type_mismatch".to_string(),
308            );
309            m.insert("collection_id".to_string(), collection_id.to_string());
310        }
311    }
312    m
313}
314
315fn transaction_error_details(e: &TransactionError) -> std::collections::BTreeMap<String, String> {
316    use std::collections::BTreeMap;
317    let mut m = BTreeMap::new();
318    match e {
319        TransactionError::NestedTransaction => {
320            m.insert("variant".to_string(), "nested_transaction".to_string());
321        }
322        TransactionError::NoActiveTransaction => {
323            m.insert("variant".to_string(), "no_active_transaction".to_string());
324        }
325    }
326    m
327}
328
329/// Query errors: unsupported query forms, bad syntax, or invalid paths.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct QueryError {
332    pub message: String,
333}
334
335/// Low-level decode/validation failures for bytes read from the store.
336#[derive(Debug)]
337pub enum FormatError {
338    /// File magic was not `TDB0`.
339    BadMagic { got: [u8; 4] },
340    /// Fewer bytes than expected for a fixed-size header region.
341    TruncatedHeader { got: usize, expected: usize },
342    /// Header or manifest reported an unsupported format or manifest version.
343    UnsupportedVersion { major: u16, minor: u16 },
344    /// Superblock slice shorter than [`crate::superblock::SUPERBLOCK_SIZE`].
345    TruncatedSuperblock { got: usize, expected: usize },
346    /// Superblock magic was not `TSB0`.
347    BadSuperblockMagic { got: [u8; 4] },
348    /// Superblock CRC did not match payload.
349    BadSuperblockChecksum,
350    /// Segment header slice shorter than expected.
351    TruncatedSegmentHeader { got: usize, expected: usize },
352    /// Segment header magic was not `TSG0`.
353    BadSegmentMagic { got: [u8; 4] },
354    /// Header CRC32C did not match header bytes.
355    BadSegmentHeaderChecksum,
356    /// Payload CRC32C did not match segment body.
357    BadSegmentPayloadChecksum,
358    /// Declared payload length would extend past the file end.
359    SegmentPayloadPastEof,
360    /// Invalid catalog segment payload (binary layout).
361    InvalidCatalogPayload { message: String },
362    /// Record segment payload truncated or malformed.
363    TruncatedRecordPayload,
364    /// Record payload type tag did not match schema.
365    RecordPayloadTypeMismatch,
366    /// UTF-8 in a record string field was invalid.
367    InvalidRecordUtf8,
368    /// Record payload used a composite type not supported in v1 row encoding.
369    RecordPayloadUnsupportedType,
370    /// Record payload version not supported.
371    UnknownRecordPayloadVersion { got: u16 },
372    /// Extra bytes after a decoded record payload.
373    TrailingRecordPayload,
374    /// Transaction marker segment payload was malformed.
375    InvalidTxnPayload { message: String },
376    /// Checkpoint payload references a replay offset before the checkpoint segment end.
377    InvalidCheckpointPayload { message: String },
378    /// On-disk log ends with an incomplete transaction or torn write; strict open refuses to modify.
379    UncleanLogTail {
380        /// First byte offset that may be discarded to reach a committed prefix (truncate target).
381        safe_end: u64,
382        reason: &'static str,
383    },
384}
385
386/// Transaction session errors (0.8+).
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub enum TransactionError {
389    /// `Database::transaction` was called while a transaction is already active.
390    NestedTransaction,
391    /// `commit_transaction` was called with no active transaction.
392    NoActiveTransaction,
393}
394
395/// Schema and row-level validation errors (catalog replay, registration, insert/get).
396#[derive(Debug, Clone)]
397pub enum SchemaError {
398    /// Field path had no segments or an empty segment.
399    InvalidFieldPath,
400    /// Another collection already uses this name.
401    DuplicateCollectionName {
402        name: String,
403    },
404    /// No collection registered with this id.
405    UnknownCollection {
406        id: u32,
407    },
408    /// No collection registered under this name.
409    UnknownCollectionName {
410        name: String,
411    },
412    InvalidCollectionName,
413    InvalidSchemaVersion {
414        expected: u32,
415        got: u32,
416    },
417    /// `u32` schema version counter cannot be incremented further.
418    SchemaVersionExhausted,
419    UnexpectedCollectionId {
420        expected: u32,
421        got: u32,
422    },
423    /// Collection was created without a primary key (catalog v1); inserts are not supported.
424    NoPrimaryKey {
425        collection_id: u32,
426    },
427    /// Declared primary field is not a single top-level segment or not present in fields.
428    PrimaryFieldNotFound {
429        name: String,
430    },
431    /// New schema version drops or renames the primary-key field.
432    PrimaryFieldMissingInSchema {
433        name: String,
434    },
435    /// Insert row did not include the primary key field.
436    RowMissingPrimary {
437        name: String,
438    },
439    /// Insert row referenced an unknown field name.
440    RowUnknownField {
441        name: String,
442    },
443    /// Insert row omitted a non-primary field.
444    RowMissingField {
445        name: String,
446    },
447    /// Unique secondary index was violated (key already mapped to another primary key).
448    UniqueIndexViolation,
449    /// Proposed schema update is not compatible with the existing schema.
450    IncompatibleSchemaChange {
451        message: String,
452    },
453    /// Proposed schema update is supported, but requires an explicit migration step.
454    MigrationRequired {
455        message: String,
456    },
457    /// Secondary index references a primary key with no row in `latest`.
458    IndexRowMissing {
459        collection_id: u32,
460        index_name: String,
461    },
462    /// Primary key scalar type does not match the collection primary field type.
463    PrimaryKeyTypeMismatch {
464        collection_id: u32,
465    },
466}
467
468impl fmt::Display for ValidationError {
469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470        if self.path.is_empty() {
471            return write!(f, "validation error: {}", self.message);
472        }
473        write!(
474            f,
475            "validation error at {}: {}",
476            self.path.join("."),
477            self.message
478        )
479    }
480}
481
482impl fmt::Display for DbError {
483    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484        match self {
485            DbError::Io(e) => write!(f, "i/o error: {e}"),
486            DbError::Format(e) => write!(f, "format error: {e}"),
487            DbError::Schema(e) => write!(f, "schema error: {e}"),
488            DbError::Validation(e) => write!(f, "{e}"),
489            DbError::Transaction(e) => write!(f, "transaction error: {e}"),
490            DbError::Query(e) => write!(f, "query error: {}", e.message),
491            DbError::NotImplemented => write!(f, "not implemented"),
492        }
493    }
494}
495
496impl fmt::Display for TransactionError {
497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498        match self {
499            TransactionError::NestedTransaction => {
500                write!(f, "nested transactions are not supported")
501            }
502            TransactionError::NoActiveTransaction => {
503                write!(f, "no active transaction")
504            }
505        }
506    }
507}
508
509impl fmt::Display for FormatError {
510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511        match self {
512            FormatError::BadMagic { got } => {
513                write!(f, "bad magic bytes: expected \"TDB0\", got {:02x?}", got)
514            }
515            FormatError::TruncatedHeader { got, expected } => {
516                write!(f, "truncated header: got {got} bytes, expected {expected}")
517            }
518            FormatError::UnsupportedVersion { major, minor } => {
519                write!(f, "unsupported format version {major}.{minor}")
520            }
521            FormatError::TruncatedSuperblock { got, expected } => {
522                write!(
523                    f,
524                    "truncated superblock: got {got} bytes, expected {expected}"
525                )
526            }
527            FormatError::BadSuperblockMagic { got } => {
528                write!(
529                    f,
530                    "bad superblock magic bytes: expected \"TSB0\", got {:02x?}",
531                    got
532                )
533            }
534            FormatError::BadSuperblockChecksum => write!(f, "superblock checksum mismatch"),
535            FormatError::TruncatedSegmentHeader { got, expected } => {
536                write!(
537                    f,
538                    "truncated segment header: got {got} bytes, expected {expected}"
539                )
540            }
541            FormatError::BadSegmentMagic { got } => {
542                write!(
543                    f,
544                    "bad segment magic bytes: expected \"TSG0\", got {:02x?}",
545                    got
546                )
547            }
548            FormatError::BadSegmentHeaderChecksum => write!(f, "segment header checksum mismatch"),
549            FormatError::BadSegmentPayloadChecksum => {
550                write!(f, "segment payload checksum mismatch")
551            }
552            FormatError::SegmentPayloadPastEof => {
553                write!(f, "segment payload extends past end of file")
554            }
555            FormatError::InvalidCatalogPayload { message } => {
556                write!(f, "invalid catalog payload: {message}")
557            }
558            FormatError::TruncatedRecordPayload => write!(f, "truncated record payload"),
559            FormatError::RecordPayloadTypeMismatch => {
560                write!(f, "record payload type does not match schema")
561            }
562            FormatError::InvalidRecordUtf8 => write!(f, "invalid UTF-8 in record string"),
563            FormatError::RecordPayloadUnsupportedType => {
564                write!(f, "unsupported type in record payload v1")
565            }
566            FormatError::UnknownRecordPayloadVersion { got } => {
567                write!(f, "unknown record payload version {got}")
568            }
569            FormatError::TrailingRecordPayload => write!(f, "trailing bytes in record payload"),
570            FormatError::InvalidTxnPayload { message } => {
571                write!(f, "invalid transaction marker payload: {message}")
572            }
573            FormatError::InvalidCheckpointPayload { message } => {
574                write!(f, "invalid checkpoint payload: {message}")
575            }
576            FormatError::UncleanLogTail { safe_end, reason } => {
577                write!(
578                    f,
579                    "unclean log tail (strict open): {reason}; safe truncate end offset {safe_end}"
580                )
581            }
582        }
583    }
584}
585
586impl fmt::Display for SchemaError {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        match self {
589            SchemaError::InvalidFieldPath => write!(f, "invalid field path"),
590            SchemaError::DuplicateCollectionName { name } => {
591                write!(f, "duplicate collection name: {name:?}")
592            }
593            SchemaError::UnknownCollection { id } => {
594                write!(f, "unknown collection id {id}")
595            }
596            SchemaError::UnknownCollectionName { name } => {
597                write!(f, "unknown collection name {name:?}")
598            }
599            SchemaError::InvalidCollectionName => write!(f, "invalid collection name"),
600            SchemaError::InvalidSchemaVersion { expected, got } => {
601                write!(f, "invalid schema version: expected {expected}, got {got}")
602            }
603            SchemaError::SchemaVersionExhausted => {
604                write!(
605                    f,
606                    "schema version limit reached (u32::MAX); cannot register another schema version"
607                )
608            }
609            SchemaError::UnexpectedCollectionId { expected, got } => {
610                write!(
611                    f,
612                    "unexpected collection id in catalog replay: expected {expected}, got {got}"
613                )
614            }
615            SchemaError::NoPrimaryKey { collection_id } => {
616                write!(
617                    f,
618                    "collection {collection_id} has no primary key (upgrade catalog or re-register)"
619                )
620            }
621            SchemaError::PrimaryFieldNotFound { name } => {
622                write!(f, "primary field {name:?} not found as a top-level field")
623            }
624            SchemaError::PrimaryFieldMissingInSchema { name } => {
625                write!(
626                    f,
627                    "schema update must retain top-level primary field {name:?}"
628                )
629            }
630            SchemaError::RowMissingPrimary { name } => {
631                write!(f, "insert row missing primary key field {name:?}")
632            }
633            SchemaError::RowUnknownField { name } => {
634                write!(f, "insert row has unknown field {name:?}")
635            }
636            SchemaError::RowMissingField { name } => {
637                write!(f, "insert row missing field {name:?}")
638            }
639            SchemaError::UniqueIndexViolation => write!(f, "unique index violation"),
640            SchemaError::IncompatibleSchemaChange { message } => {
641                write!(f, "incompatible schema change: {message}")
642            }
643            SchemaError::MigrationRequired { message } => {
644                write!(f, "migration required: {message}")
645            }
646            SchemaError::IndexRowMissing {
647                collection_id,
648                index_name,
649            } => {
650                write!(
651                    f,
652                    "index {index_name:?} on collection {collection_id} references missing row"
653                )
654            }
655            SchemaError::PrimaryKeyTypeMismatch { collection_id } => {
656                write!(
657                    f,
658                    "primary key type mismatch for collection {collection_id}"
659                )
660            }
661        }
662    }
663}
664
665impl std::error::Error for DbError {
666    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
667        match self {
668            DbError::Io(e) => Some(e),
669            DbError::Format(_) => None,
670            DbError::Schema(_) => None,
671            DbError::Validation(_) => None,
672            DbError::Transaction(_) => None,
673            DbError::Query(_) => None,
674            DbError::NotImplemented => None,
675        }
676    }
677}
678
679impl From<std::io::Error> for DbError {
680    fn from(value: std::io::Error) -> Self {
681        DbError::Io(value)
682    }
683}