1use std::fmt;
2
3#[derive(Debug, Clone)]
8pub struct ValidationError {
9 pub path: Vec<String>,
10 pub message: String,
11}
12
13#[derive(Debug)]
14pub enum DbError {
15 Io(std::io::Error),
17 Format(FormatError),
19 Schema(SchemaError),
21 Validation(ValidationError),
23 Transaction(TransactionError),
25 Query(QueryError),
27 NotImplemented,
29}
30
31#[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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct QueryError {
332 pub message: String,
333}
334
335#[derive(Debug)]
337pub enum FormatError {
338 BadMagic { got: [u8; 4] },
340 TruncatedHeader { got: usize, expected: usize },
342 UnsupportedVersion { major: u16, minor: u16 },
344 TruncatedSuperblock { got: usize, expected: usize },
346 BadSuperblockMagic { got: [u8; 4] },
348 BadSuperblockChecksum,
350 TruncatedSegmentHeader { got: usize, expected: usize },
352 BadSegmentMagic { got: [u8; 4] },
354 BadSegmentHeaderChecksum,
356 BadSegmentPayloadChecksum,
358 SegmentPayloadPastEof,
360 InvalidCatalogPayload { message: String },
362 TruncatedRecordPayload,
364 RecordPayloadTypeMismatch,
366 InvalidRecordUtf8,
368 RecordPayloadUnsupportedType,
370 UnknownRecordPayloadVersion { got: u16 },
372 TrailingRecordPayload,
374 InvalidTxnPayload { message: String },
376 InvalidCheckpointPayload { message: String },
378 UncleanLogTail {
380 safe_end: u64,
382 reason: &'static str,
383 },
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
388pub enum TransactionError {
389 NestedTransaction,
391 NoActiveTransaction,
393}
394
395#[derive(Debug, Clone)]
397pub enum SchemaError {
398 InvalidFieldPath,
400 DuplicateCollectionName {
402 name: String,
403 },
404 UnknownCollection {
406 id: u32,
407 },
408 UnknownCollectionName {
410 name: String,
411 },
412 InvalidCollectionName,
413 InvalidSchemaVersion {
414 expected: u32,
415 got: u32,
416 },
417 SchemaVersionExhausted,
419 UnexpectedCollectionId {
420 expected: u32,
421 got: u32,
422 },
423 NoPrimaryKey {
425 collection_id: u32,
426 },
427 PrimaryFieldNotFound {
429 name: String,
430 },
431 PrimaryFieldMissingInSchema {
433 name: String,
434 },
435 RowMissingPrimary {
437 name: String,
438 },
439 RowUnknownField {
441 name: String,
442 },
443 RowMissingField {
445 name: String,
446 },
447 UniqueIndexViolation,
449 IncompatibleSchemaChange {
451 message: String,
452 },
453 MigrationRequired {
455 message: String,
456 },
457 IndexRowMissing {
459 collection_id: u32,
460 index_name: String,
461 },
462 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}