coven_database/error.rs
1//! Typed opening, authorization and migration failures; the schema rules
2//! are `schema_error`'s.
3
4use coven_foundation::files::{SettingsError, StoreLockError};
5use coven_foundation::task::{Bug, CarriesBug};
6
7impl CarriesBug for DbError {
8 fn bug(&self) -> Option<&Bug> {
9 match self {
10 Self::Bug(bug) => Some(bug),
11 Self::Disk(error) => error.bug(),
12 Self::Lock(error) => error.bug(),
13 Self::StatePersistence(cause)
14 | Self::CheckpointFailed(cause)
15 | Self::StatisticsFailed(cause) => cause.bug(),
16 Self::Rollback { operation, .. } => operation.bug(),
17 Self::FileCleanup { write, failures } => write
18 .as_ref()
19 .err()
20 .and_then(|write| write.bug())
21 .or_else(|| failures.iter().find_map(DbError::bug)),
22 Self::StagingCleanup { operation, cleanup } => {
23 operation.bug().or_else(|| cleanup.bug())
24 }
25 Self::Closing { failures } => failures.iter().find_map(DbError::bug),
26 // An earlier bug, which reached its one reporter as it happened.
27 Self::StoreFailed(_) => None,
28 _ => None,
29 }
30 }
31}
32
33/// A failed database call or a write the database refuses (§4, §9, §15, §17).
34#[derive(Debug, thiserror::Error)]
35pub enum DbError {
36 /// Another claim must settle before this device reserves a new number.
37 #[error("a claim is already pending")]
38 ClaimPending,
39 /// No current claim exists for this operation.
40 #[error("there is no current claim")]
41 NoClaim,
42 /// A request to a provider account is between its target being fixed and
43 /// its reply, so a revocation queued now could precede the grants it
44 /// must follow (§20.9). The call is made again once the request ends.
45 #[error("a request to a provider account is running")]
46 ProviderRequestRunning,
47 /// Uncertain publication cannot be abandoned or resealed; a landed writes
48 /// batch cannot finish until its consumed state has committed.
49 #[error("the current claim has not settled or its writes have not applied")]
50 ClaimUnsettled,
51 /// The installation has exhausted its claim-number representation.
52 #[error("claim numbers exhausted")]
53 ClaimNumberExhausted,
54 /// This installation has exhausted its positive original-write identities.
55 #[error("write numbers exhausted")]
56 WriteNumberExhausted,
57 /// The supplied floor evidence does not cover the attempted slot.
58 #[error("floor evidence does not cover the claim")]
59 InvalidClaimCoverage,
60 /// Slot application must extend the previously committed prefix.
61 #[error("slot {received:?} does not follow applied slot {applied}")]
62 SequenceGap {
63 /// Previously committed coverage.
64 applied: u64,
65 /// The received slot.
66 received: coven_merge::Slot,
67 },
68 /// A slot's readable original awaits an external prerequisite.
69 #[error("write awaits {0:?}")]
70 WriteWaiting(crate::WriteWait),
71 /// A pending record, its subject, the sync status or an operation's
72 /// data could not be encoded, or a stored pending record could not be
73 /// decoded as the requested record type (E5).
74 #[error("invalid sync state data: {0}")]
75 StateEncoding(#[source] serde_json::Error),
76 /// Saving pending state or the sync status failed. The call that lost
77 /// the update and every later call and live query fail with this same
78 /// cause until reopened (E5).
79 #[error("saving sync state failed: {0}")]
80 StatePersistence(#[source] std::sync::Arc<DbError>),
81 /// An earlier call's rollback failed, so a connection may still be inside
82 /// that transaction, holding its uncommitted changes or its old snapshot;
83 /// this handle, read-only or not, refuses calls until reopened (§4).
84 #[error("an earlier rollback failed; reopen the store")]
85 RollbackFailed,
86 /// One of coven's own WAL checkpoints failed with this cause, other than
87 /// on a busy or locked database, so the store's checkpointing stopped.
88 /// Rather than stay open while its WAL grows without bound, this handle
89 /// and every live query fail with this same cause until reopened (§4).
90 #[error("a WAL checkpoint failed: {0}")]
91 CheckpointFailed(#[source] std::sync::Arc<DbError>),
92 /// Checking or gathering the query planner's statistics while the store
93 /// stays open failed with this cause, other than on a busy or locked
94 /// database, so keeping them stopped. Rather than stay open while the
95 /// planner plans with statistics that no longer hold, this handle and
96 /// every live query fail with this same cause until reopened (§4).
97 #[error("gathering the planner's statistics failed: {0}")]
98 StatisticsFailed(#[source] std::sync::Arc<DbError>),
99 /// The handle's clock could not wait for an open invite's expiry, so
100 /// the invite would outlive its lifetime. Rather than stay open
101 /// without writing expiries, this handle and every live query fail with
102 /// this same cause until reopened, and opening writes each expiry
103 /// already past (§13.2).
104 #[error(transparent)]
105 WallTimer(coven_foundation::clock::WallTimerError),
106 /// The system refused a thread of coven's upkeep, the one that
107 /// checkpoints a WAL or the one that keeps the planner's statistics, so
108 /// the store did not open (§4).
109 #[error("an upkeep thread could not start: {0}")]
110 UpkeepThread(#[source] std::io::Error),
111 /// Reading a claim's sealed bytes to retain them failed, and the claim's
112 /// transaction did not commit (§11).
113 #[error("reading a claim's bytes failed: {0}")]
114 ClaimRead(#[source] std::io::Error),
115 /// This handle or a clone has closed the store (E1).
116 #[error("store is closed")]
117 StoreClosed,
118 /// The call's runtime is shutting down and dropped its work (E15). A
119 /// call the blocking pool dropped unstarted changed nothing
120 /// (`ShutdownOutcome::NotRun`); closing, which runs as its own task, may
121 /// have closed some connections (`ShutdownOutcome::Unknown`).
122 #[error(transparent)]
123 Shutdown(#[from] coven_foundation::task::Shutdown),
124 /// The call's work panicked: a bug in coven or in the app's callback,
125 /// which the caller reports (E15). A write that panicked rolled back,
126 /// and the bytes it released or staged are removed as after any failed
127 /// write.
128 #[error(transparent)]
129 Bug(#[from] coven_foundation::task::Bug),
130 /// An earlier bug may have left this handle's state wrong, so the
131 /// handle failed with it, and every call and live query fails with it
132 /// until the store is reopened (E15). The call whose work hit the bug
133 /// returned it as `Bug`, or the store's bug sink heard it, so the app
134 /// shows this and doesn't report it again.
135 #[error("the store's handle failed on an earlier bug; reopen the store: {0}")]
136 StoreFailed(coven_foundation::task::Bug),
137 /// SQLite refused or failed a statement, preserving its error.
138 #[error(transparent)]
139 Sqlite(rusqlite::Error),
140 /// A statement met damage SQLite reports (`SQLITE_CORRUPT`,
141 /// `SQLITE_NOTADB`), decoding stored facts found damage (§19.3), or a
142 /// snapshot's staged records differ from a second staging's (§19.1).
143 #[error("damaged database")]
144 DamagedDatabase,
145 /// Coven's own code broke a rule it relies on, such as a caller's
146 /// contract or a step its state machine cannot reach: a bug, not damage.
147 /// Sync reports it as `Failed(LocalFailure::Bug)` (§19.1).
148 #[error("coven invariant failed: {what}")]
149 Invariant {
150 /// The broken rule.
151 what: &'static str,
152 },
153 /// Directory settings disagree with a store's creation entry (§10).
154 #[error("store settings disagree with creation of {store}")]
155 StoreSettingsMismatch {
156 /// The id in the creation entry.
157 store: coven_foundation::id_source::StoreId,
158 },
159 /// A synced table declaration or schema is invalid.
160 #[error(transparent)]
161 Schema(#[from] crate::SchemaError),
162 /// A write was given files staged, or a pin prepared, in another open
163 /// store, or in this one before it closed: the copies they name aren't
164 /// reserved here (§17.3, §17.8).
165 #[error("these files were not prepared in this open store")]
166 PreparedElsewhere,
167 /// A write made a file device-only and also removed its last reference,
168 /// so the copy kept as its original would name no file (§17.3). Nothing
169 /// committed.
170 #[error("file {file} made device-only has no reference left in the same write")]
171 DeviceOnlyUnreferenced {
172 /// The file.
173 file: coven_foundation::id_source::FileId,
174 },
175 /// A pin prepared over a whole copy already kept found that copy gone
176 /// when its write committed it, evicted or unpinned since: nothing was pinned, and
177 /// preparing again downloads it. A prepare whose partial download the
178 /// app's unpin ended meanwhile fails with it too (§17.8).
179 #[error("the kept copy of file {file} changed since its pin was prepared")]
180 PinChanged {
181 /// The file.
182 file: coven_foundation::id_source::FileId,
183 },
184 /// One of coven's steps in this write failed, and the write's closure
185 /// went on to return success: the write rolls back, since a failed step
186 /// may have made only part of its change (§4).
187 #[error("a step of this write failed")]
188 StepFailed,
189 /// One of the app's statements in this write failed, and the write's
190 /// closure went on to return success: the write rolls back, since SQLite
191 /// may have kept part of the statement's change or undone rows coven saw
192 /// it change (§4).
193 #[error("a statement of this write failed")]
194 StatementFailed,
195 /// App SQL read, changed or defined one of coven's internal objects (§4).
196 #[error("app SQL cannot access internal object {table}")]
197 InternalTable {
198 /// The reserved object.
199 table: String,
200 },
201 /// A local trigger wrote a synced table or a shared trigger a local one (§9.4).
202 #[error("trigger {trigger} cannot write table {table}")]
203 TriggerTarget {
204 /// The trigger doing the write.
205 trigger: String,
206 /// The target table.
207 table: String,
208 },
209 /// A bulk write's statement, or a trigger or foreign-key action it would
210 /// run, writes a synced table (§4, E3). Nothing committed.
211 #[error("a bulk write cannot write synced table {table}")]
212 SyncedTableInBulkWrite {
213 /// The synced table.
214 table: String,
215 },
216 /// SQLite rolled the owned transaction back itself, so it can't go on (E3, E13).
217 #[error("SQLite ended the owned transaction")]
218 TransactionEnded,
219 /// App SQL tried transaction control, a PRAGMA, ATTACH, loading an
220 /// extension, changing a hidden rowid, or changing the schema outside a
221 /// migration (§4, E13).
222 #[error("app SQL cannot perform {operation}")]
223 StatementForbidden {
224 /// The refused operation.
225 operation: &'static str,
226 },
227 /// SQLite kept another journal mode instead of WAL (§4).
228 #[error("SQLite kept journal mode {mode} instead of WAL")]
229 WalUnavailable {
230 /// The journal mode SQLite selected.
231 mode: String,
232 },
233 /// An app's SQLite setting is larger than SQLite can hold (api.md E1).
234 #[error("SQLite setting {setting} exceeds {maximum}")]
235 SqliteSettingOutOfRange {
236 /// The setting's PRAGMA name.
237 setting: &'static str,
238 /// The largest value SQLite holds for it.
239 maximum: u64,
240 },
241 /// An app collation has the name of one of SQLite's own (api.md E1).
242 #[error("collation {name} is SQLite's own")]
243 ReservedCollation {
244 /// The name as the app gave it.
245 name: String,
246 },
247 /// The app gave two collations one name, which SQLite compares without
248 /// regard to ASCII case (api.md E1).
249 #[error("collation {name} is registered twice")]
250 DuplicateCollation {
251 /// The second registration's name.
252 name: String,
253 },
254 /// An app function has the name of one SQLite or coven already has, or
255 /// coven's reserved prefix (api.md E1).
256 #[error("function {name} is SQLite's or coven's own")]
257 ReservedFunction {
258 /// The name as the app gave it.
259 name: String,
260 },
261 /// The app gave two functions one name, which SQLite compares without
262 /// regard to ASCII case (api.md E1).
263 #[error("function {name} is registered twice")]
264 DuplicateFunction {
265 /// The second registration's name.
266 name: String,
267 },
268 /// An app function is deterministic and takes no arguments, which SQLite
269 /// evaluates while planning in a way coven can't receive (api.md E1).
270 #[error("function {name} is deterministic and takes no arguments")]
271 ConstantFunction {
272 /// The function's name.
273 name: String,
274 },
275 /// A writable open found a new version of an app collation or function
276 /// that a CHECK or generated column uses. Coven rebuilds only indexes,
277 /// so the open changed nothing (§4).
278 #[error("{name} has a new version, and a CHECK or generated column of {table} uses it")]
279 VersionChangeRefused {
280 /// The collation's or function's name, ASCII lowercase.
281 name: String,
282 /// The table whose CHECK or generated column uses it.
283 table: String,
284 },
285 /// A read-only open found the store's indexes built with another
286 /// version of an app collation or function than the one registered.
287 /// Only a writable open rebuilds them (§4).
288 #[error("the store's indexes use version {built} of {name}, not {registered}")]
289 VersionNotRebuilt {
290 /// The collation's or function's name, ASCII lowercase.
291 name: String,
292 /// The version the store's indexes were built with.
293 built: u32,
294 /// The version this open registered.
295 registered: u32,
296 },
297 /// The wall clock or next timestamp exceeds its representation (§7.2).
298 #[error("clock is outside the timestamp range")]
299 ClockOutOfRange,
300 /// Protected remote input fills the configured disk budget. No input or
301 /// budget change was committed; increase the budget or finish pending work.
302 #[error("retained input requires {required} bytes within a {maximum} byte budget")]
303 InputBudgetExceeded {
304 /// Bytes required after evicting every eligible range.
305 required: u64,
306 /// Configured encrypted-input limit.
307 maximum: u64,
308 },
309 /// A value or row exceeds the format (format.md D1, D2, D5), or a write's
310 /// plaintext and sealing key ids cannot fit in its SQLite queue row (§6.3).
311 #[error("{field} length {actual} exceeds {maximum}")]
312 TooLarge {
313 /// The bounded field or collection.
314 field: &'static str,
315 /// Its actual length.
316 actual: u64,
317 /// Its maximum length.
318 maximum: u64,
319 },
320 /// A reference reaches an audience the source row's readers cannot read (§15.3).
321 #[error("reference {table}.{column} at {key:?} reaches another audience")]
322 ReferenceAudience {
323 /// The referencing table.
324 table: String,
325 /// Its row's primary key.
326 key: crate::RowKey,
327 /// The referencing column.
328 column: String,
329 },
330 /// A write targets a circle whose deletion has been applied (§15.9).
331 #[error("circle {0} has been deleted")]
332 DeletedCircle(coven_foundation::id_source::CircleId),
333 /// A local write targets a circle this member is not in or the store does not have (§15).
334 #[error("not a member of circle {0}")]
335 NotInCircle(coven_foundation::id_source::CircleId),
336 /// This audience still exposes its previous data while its selected boundary reloads (§16.5).
337 #[error("audience {0:?} is reloading")]
338 AudienceReloading(coven_merge::Audience),
339 /// The write directly changed another member's row of an `owner_only()`
340 /// table: a statement, a shared trigger, or coven copying or dismissing
341 /// it for the app (§9.6). The transaction, including its upload record,
342 /// did not commit.
343 #[error("{table} row {key:?} belongs to member {owner:?}")]
344 NotOwner {
345 /// The owned row's table.
346 table: String,
347 /// Its primary key, in declared key order.
348 key: crate::RowKey,
349 /// The member who originally created it.
350 owner: coven_foundation::id_source::MemberId,
351 },
352 /// The write's foreign-key actions on this row are not the schema's exact
353 /// consequence of its direct changes, so every receiver would refuse
354 /// them (§9.6). The transaction, including its upload record, did not
355 /// commit.
356 #[error("{table} row {key:?} is not an exact foreign-key effect")]
357 InvalidForeignKeyEffect {
358 /// The row's table.
359 table: String,
360 /// Its primary key, in declared key order.
361 key: crate::RowKey,
362 },
363 /// An inserted row's independent key holds no UUID (§9.2).
364 #[error("independent key {key:?} of {table} does not contain a canonical lowercase UUIDv4 or UUIDv7")]
365 KeyNotUuid {
366 /// The inserted row's table.
367 table: String,
368 /// Its primary key, in declared key order.
369 key: crate::RowKey,
370 },
371 /// A root row's audience cell is neither `store` nor a circle id in
372 /// canonical lowercase form (§15).
373 #[error("audience cell of {table} at {key:?} is neither `store` nor a canonical lowercase circle id")]
374 InvalidAudience {
375 /// The written row's table.
376 table: String,
377 /// Its primary key, in declared key order.
378 key: crate::RowKey,
379 },
380 /// A downloaded write fails the merge's checks and is never applied (§19.1).
381 #[error("invalid write {write:?}: {error}")]
382 InvalidWrite {
383 /// The refused write's device and write number.
384 write: coven_merge::WriteId,
385 /// The merge rule it broke.
386 #[source]
387 error: coven_merge::MergeError,
388 },
389 /// Original row-change evidence is unavailable. Receipt waits without
390 /// consuming the position; local authoring rolls back instead of guessing.
391 #[error("missing original write sources: {0:?}")]
392 MissingWriteSources(Vec<coven_format::write_sources::ChangeId>),
393 /// A waiting record or its conversion violates the write format (E13).
394 #[error(transparent)]
395 WriteFormat(coven_format::Error),
396 /// A migration's declared kind disagrees with its runtime classification.
397 #[error("migration {version} declared the wrong kind")]
398 MigrationKindMismatch {
399 /// The refused migration's version.
400 version: u32,
401 },
402 /// A conversion changed or introduced a reference whose generation it cannot know (E13).
403 #[error("conversion cannot change reference {table}.{column}")]
404 MigrationReference {
405 /// The referencing table in the converted change.
406 table: String,
407 /// The referencing column in the converted change.
408 column: String,
409 },
410 /// A converted row names the same column twice.
411 #[error("migration conversion repeats {table}.{column}")]
412 MigrationDuplicateColumn {
413 /// The converted table.
414 table: String,
415 /// The repeated column.
416 column: String,
417 },
418 /// A converted column is absent from the target schema.
419 #[error("converted column {table}.{column} is absent from the schema")]
420 MigrationColumnMissing {
421 /// The converted table.
422 table: String,
423 /// The absent column.
424 column: String,
425 },
426 /// A converted table has no primary index.
427 #[error("converted table {table} has no primary index")]
428 MigrationPrimaryKeyMissing {
429 /// The converted table.
430 table: String,
431 },
432 /// A converted key has the wrong number of values.
433 #[error("converted key of {table} has {actual} values, expected {expected}")]
434 MigrationKeyArity {
435 /// The converted table.
436 table: String,
437 /// The primary index width.
438 expected: usize,
439 /// The supplied key width.
440 actual: usize,
441 },
442 /// A converted row or reference names an absent table.
443 #[error("converted table {table} is absent from the schema")]
444 MigrationTableMissing {
445 /// The absent table.
446 table: String,
447 },
448 /// Distinct original references became the same converted reference.
449 #[error("converted references of {table} collide at {reference:?}")]
450 MigrationReferenceCollision {
451 /// The original referencing table.
452 table: String,
453 /// The repeated converted reference.
454 reference: coven_merge::ForeignKey,
455 },
456 /// A write dismissed a removed row, deleting it for good, and also
457 /// changed that row or one its delete cascades to (§8.4).
458 #[error("a write dismissing {table} at {key:?} also changes it")]
459 DismissedRowChanged {
460 /// The declared table.
461 table: String,
462 /// The row's key.
463 key: crate::RowKey,
464 },
465 /// A write changed a write-once file reference that named a file (§17.2).
466 #[error("file reference on {table} at {key:?} cannot change")]
467 FileWriteOnce {
468 /// The declared table.
469 table: String,
470 /// The row's key.
471 key: crate::RowKey,
472 },
473 /// The user's original disappeared.
474 #[error("user file is missing: {}", path.display())]
475 UserFileMissing {
476 /// The original path.
477 path: std::path::PathBuf,
478 },
479 /// The user's original changed during preparation or since it was prepared.
480 #[error("user file changed: {}", path.display())]
481 UserFileChanged {
482 /// The original path.
483 path: std::path::PathBuf,
484 },
485 /// No namespace of this name was declared when the store opened (E1).
486 #[error("file namespace {namespace} is not declared")]
487 UndeclaredNamespace {
488 /// The supplied namespace.
489 namespace: String,
490 },
491 /// A file was staged into a namespace of the other kind: app bytes into a
492 /// user namespace, or a user's original into an app one (§17.1); or a
493 /// bound reference names a user's file (§17.4).
494 #[error("file namespace {namespace} holds {kind:?} files")]
495 NamespaceKind {
496 /// The supplied namespace.
497 namespace: String,
498 /// The kind the namespace was declared with.
499 kind: crate::Provenance,
500 },
501 /// The id source reused the physical name of a kept file: a cache
502 /// file's name, a staged copy's `<folder>/<copy>`, or a staging's folder
503 /// (Appendix C.3).
504 #[error("file name {name:?} is already kept by this device")]
505 FileNameReused {
506 /// The refused physical name, as this device's records hold it.
507 name: String,
508 },
509 /// A file the write staged is referenced by none of its rows (§17.2).
510 #[error("no row references staged file {file} in {namespace}")]
511 FileUnreferenced {
512 /// The staged file's namespace.
513 namespace: String,
514 /// The staged file's id.
515 file: coven_foundation::id_source::FileId,
516 },
517 /// A reference names no file this device has a record of in its row's
518 /// audience (§17.2).
519 #[error("{table}.{column} names {file}, which is no file of its row's audience")]
520 FileUnknown {
521 /// The referencing table.
522 table: String,
523 /// The reference column.
524 column: String,
525 /// The value the column holds.
526 file: String,
527 },
528 /// This device has no record of the file (§17.2): one to move, to make
529 /// device-only, or to stage a new file over.
530 #[error("no file {file} here")]
531 FileAbsent {
532 /// The requested file.
533 file: coven_foundation::id_source::FileId,
534 },
535 /// The file is on another device only, and only that device has its
536 /// bytes: one to move to the cloud (§17.3), or one a write makes a new
537 /// file over (§17.2).
538 #[error("file {file} is on another device")]
539 NotThisDevice {
540 /// The file.
541 file: coven_foundation::id_source::FileId,
542 },
543 /// The file to move is in the cloud already (§17.3).
544 #[error("file {file} is not on this device only")]
545 NotDeviceOnlyHere {
546 /// The file.
547 file: coven_foundation::id_source::FileId,
548 },
549 /// The file to move is in a move already, moving or failed: a file is in
550 /// one move at a time, and the app cancels a failed one to move its files
551 /// again (§17.3).
552 #[error("file {file} is in move {id:?}")]
553 FileInMove {
554 /// The file.
555 file: coven_foundation::id_source::FileId,
556 /// The move that holds it.
557 id: crate::MoveId,
558 },
559 /// A bound reference names a file larger than its limit (§17.4).
560 #[error("{table}.{column} names a file of {size} bytes, over its limit of {limit}")]
561 BoundFileTooLarge {
562 /// The referencing table.
563 table: String,
564 /// The bound reference column.
565 column: String,
566 /// The file's size in bytes.
567 size: u64,
568 /// The column's limit in bytes.
569 limit: u64,
570 },
571 /// A bound reference names a file on one device only that no bound
572 /// upload stores (§17.4).
573 #[error("file {file} is on one device only, with no bound upload")]
574 DeviceOnly {
575 /// The file.
576 file: coven_foundation::id_source::FileId,
577 },
578 /// This device never made the move (§17.3).
579 #[error("no move {id:?} here")]
580 MoveUnknown {
581 /// The move.
582 id: crate::MoveId,
583 },
584 /// The move finished: its files are in the cloud (§17.3).
585 #[error("move {id:?} finished")]
586 MoveFinished {
587 /// The move.
588 id: crate::MoveId,
589 },
590 /// The move is a bound file's: its upload holds a write until the file
591 /// is stored, so it can't be cancelled (§17.4).
592 #[error("move {id:?} stores a bound file")]
593 MoveBound {
594 /// The move.
595 id: crate::MoveId,
596 },
597 /// The file to make device-only is on one device only already, so
598 /// storage holds no bytes of it to download (§17.3).
599 #[error("file {file} is not in the cloud")]
600 NotInCloud {
601 /// The file.
602 file: coven_foundation::id_source::FileId,
603 },
604 /// No record here names the stored bytes a file reference was taken
605 /// over any more: the record went, or a switch took it off them, as
606 /// making the file device-only does. The cache keeps bytes only while a
607 /// record names them, so a pin fetching them keeps nothing (§17.8).
608 #[error("file {file} is no longer stored where its reference names")]
609 NoLongerStored {
610 /// The file.
611 file: coven_foundation::id_source::FileId,
612 },
613 /// Another call making files device-only is writing a copy at this path
614 /// (§17.3).
615 #[error("a copy is being written at {}", path.display())]
616 CopyPathTaken {
617 /// The copy's path.
618 path: std::path::PathBuf,
619 },
620 /// Reading or keeping file bytes failed.
621 #[error(transparent)]
622 Disk(coven_foundation::files::FileError),
623 /// A write queueing a file's upload, as staging for the cloud or a move
624 /// does, could not create the file's random key (§17.3).
625 #[error("creating a file key failed: {0}")]
626 FileKey(#[source] coven_crypto::CryptoError),
627 /// Retaining the store while staging file bytes failed.
628 #[error(transparent)]
629 Lock(StoreLockError),
630 /// File cleanup failed, retaining every failure: on opening (`Ok`), or
631 /// after a failed write, whose error it keeps (Appendix C.3).
632 #[error("file cleanup failed: {failures:?}; write outcome: {write:?}")]
633 FileCleanup {
634 /// `Ok` on opening; otherwise the failed write's error.
635 write: Result<(), Box<DbError>>,
636 /// Each failed byte removal or pending-record update.
637 failures: Vec<DbError>,
638 },
639 /// A snapshot load or file check failed, and so did removing the records
640 /// it staged, which its failure ends (§16.2); both failures are kept,
641 /// and sync judges the load's or check's own as it would alone (§19.1).
642 #[error("removing staged snapshot records failed: {cleanup}; after: {operation}")]
643 StagingCleanup {
644 /// The load's or check's own failure.
645 operation: Box<DbError>,
646 /// Removing the staged records' tables.
647 cleanup: Box<DbError>,
648 },
649 /// An operation no longer has the state expected by its caller.
650 #[error("operation {0:?} changed or is absent")]
651 OperationChanged(crate::OperationId),
652 /// The operation the app acknowledges is still running: it has no
653 /// outcome yet (§11.4).
654 #[error("operation {0:?} is still running")]
655 OperationRunning(crate::OperationId),
656 /// `coven_operations` lists no operation with this id: the app's writes
657 /// never journaled it, or its outcome was acknowledged already (§11.4).
658 #[error("no operation {0:?} is listed")]
659 OperationUnknown(crate::OperationId),
660 /// Self-replacement has paused app writes while queued objects settle
661 /// and the new binding and custody are installed (§12.3, E9).
662 #[error("member keys are changing; app writes are paused")]
663 MemberKeysChanging,
664 /// The prepared raise no longer names the current counted prefix.
665 #[error("raise capture changed before snapshot creation")]
666 CaptureChanged,
667 /// An ordinary snapshot of an audience whose raise this device captured
668 /// and has not published: it would carry a schema version the history
669 /// doesn't hold yet (§16.2).
670 #[error("this device's raise of the audience has not landed")]
671 RaiseWaiting,
672 /// Loading plaintext supplied by sync failed without changing the database.
673 #[error(transparent)]
674 Snapshot(#[from] crate::SnapshotError),
675 /// Closing failed for these connections, after every one was tried (E1).
676 #[error("closing database connections failed: {failures:?}")]
677 Closing {
678 /// Failures in connection-close order.
679 failures: Vec<DbError>,
680 },
681 /// A transaction failed and rolling it back failed too. Later calls fail
682 /// with `RollbackFailed` until the store is reopened (§4).
683 #[error("{operation}; rollback failed: {rollback}")]
684 Rollback {
685 /// The operation that required rollback.
686 operation: Box<DbError>,
687 /// SQLite's rollback failure.
688 #[source]
689 rollback: rusqlite::Error,
690 },
691}
692
693impl DbError {
694 /// Whether the disk under the store caused this failure: its files
695 /// could not be read or kept, or SQLite found the disk full, failing,
696 /// read-only or damaged. A failure of any other kind, and one that
697 /// names no disk error, is not the disk's.
698 pub fn is_disk(&self) -> bool {
699 match self {
700 Self::Disk(_) => true,
701 Self::Sqlite(error) => sqlite_error_is_disk(error),
702 Self::Rollback { operation, .. } => operation.is_disk(),
703 _ => false,
704 }
705 }
706}
707
708/// Whether SQLite failed because of the disk under it: full, failing, gone
709/// read-only, or damaged.
710pub fn sqlite_error_is_disk(error: &rusqlite::Error) -> bool {
711 matches!(
712 error.sqlite_error_code(),
713 Some(
714 rusqlite::ErrorCode::DiskFull
715 | rusqlite::ErrorCode::SystemIoFailure
716 | rusqlite::ErrorCode::CannotOpen
717 | rusqlite::ErrorCode::ReadOnly
718 | rusqlite::ErrorCode::DatabaseCorrupt
719 | rusqlite::ErrorCode::NotADatabase
720 )
721 )
722}
723
724impl From<coven_foundation::task::TaskFailed> for DbError {
725 fn from(failed: coven_foundation::task::TaskFailed) -> Self {
726 failed.into_error()
727 }
728}
729
730impl From<coven_foundation::files::FileError> for DbError {
731 fn from(error: coven_foundation::files::FileError) -> Self {
732 match error {
733 coven_foundation::files::FileError::Bug(bug) => Self::Bug(bug),
734 error => Self::Disk(error),
735 }
736 }
737}
738
739impl From<StoreLockError> for DbError {
740 fn from(error: StoreLockError) -> Self {
741 match error {
742 StoreLockError::Bug(bug) => Self::Bug(bug),
743 error => Self::Lock(error),
744 }
745 }
746}
747
748impl From<coven_foundation::files::WriterError> for DbError {
749 fn from(error: coven_foundation::files::WriterError) -> Self {
750 match error {
751 coven_foundation::files::WriterError::Lock(error) => error.into(),
752 coven_foundation::files::WriterError::File(error) => error.into(),
753 }
754 }
755}
756
757impl From<coven_foundation::files::StagingFolderError> for DbError {
758 fn from(error: coven_foundation::files::StagingFolderError) -> Self {
759 match error {
760 coven_foundation::files::StagingFolderError::Taken(folder) => {
761 Self::FileNameReused { name: folder.as_str().to_owned() }
762 }
763 coven_foundation::files::StagingFolderError::Writer(error) => error.into(),
764 }
765 }
766}
767
768impl From<coven_format::Error> for DbError {
769 fn from(error: coven_format::Error) -> Self {
770 match error {
771 coven_format::Error::Limit { field, actual, maximum } => {
772 Self::TooLarge { field, actual: actual as u64, maximum: maximum as u64 }
773 }
774 error => Self::WriteFormat(error),
775 }
776 }
777}
778
779impl From<rusqlite::Error> for DbError {
780 fn from(error: rusqlite::Error) -> Self {
781 match error {
782 rusqlite::Error::UserFunctionError(source) => match source.downcast::<Self>() {
783 Ok(error) => *error,
784 Err(source) => Self::Sqlite(rusqlite::Error::UserFunctionError(source)),
785 },
786 // A column coven reads into one of its types, such as a `RowKey`,
787 // fails with what that type found.
788 rusqlite::Error::FromSqlConversionFailure(index, kind, source) => {
789 match source.downcast::<Self>() {
790 Ok(error) => *error,
791 Err(source) => {
792 Self::Sqlite(rusqlite::Error::FromSqlConversionFailure(index, kind, source))
793 }
794 }
795 }
796 // SQLite found a page it can't read as what it should hold, or a
797 // file that isn't a database: the damage, wherever a statement
798 // met it, is the store's, not the statement's (§19.3).
799 rusqlite::Error::SqliteFailure(failure, _)
800 if matches!(
801 failure.code,
802 rusqlite::ErrorCode::DatabaseCorrupt | rusqlite::ErrorCode::NotADatabase
803 ) =>
804 {
805 Self::DamagedDatabase
806 }
807 error => Self::Sqlite(error),
808 }
809 }
810}
811
812/// Opening the database failed: its writer or read-only connections, its
813/// lock, its settings or its migrations (§4, E1, E13).
814#[derive(Debug, thiserror::Error)]
815pub enum OpenError {
816 /// The database failed.
817 #[error(transparent)]
818 Database(#[from] DbError),
819 /// An app migration failed or cannot run on this schema.
820 #[error(transparent)]
821 Migration(#[from] MigrationError),
822 /// Coven's internal schema is unsupported or its migration cannot run.
823 #[error(transparent)]
824 CovenMigration(#[from] CovenMigrationError),
825 /// The directory's settings could not be read.
826 #[error(transparent)]
827 Settings(SettingsError),
828 /// The store's lock could not be taken.
829 #[error(transparent)]
830 Lock(StoreLockError),
831 /// A required builder choice was not supplied (E1).
832 #[error("opening requires {field}")]
833 MissingConfiguration {
834 /// The missing choice.
835 field: &'static str,
836 },
837 /// Taking the store's lock panicked: a bug, which the caller reports
838 /// (E15). Opening's database work that panicked is `DbError::Bug`.
839 #[error(transparent)]
840 Bug(#[from] coven_foundation::task::Bug),
841}
842
843impl CarriesBug for OpenError {
844 fn bug(&self) -> Option<&Bug> {
845 match self {
846 Self::Bug(bug) => Some(bug),
847 Self::Database(error) => error.bug(),
848 Self::Lock(error) => error.bug(),
849 Self::Settings(error) => error.bug(),
850 Self::Migration(MigrationError::Failed { source, .. })
851 | Self::CovenMigration(CovenMigrationError::Failed { source }) => source.bug(),
852 Self::Migration(
853 MigrationError::NotContiguous { .. } | MigrationError::SchemaTooNew { .. },
854 )
855 | Self::CovenMigration(
856 CovenMigrationError::ReadOnly | CovenMigrationError::SchemaTooNew { .. },
857 )
858 | Self::MissingConfiguration { .. } => None,
859 }
860 }
861}
862
863impl From<SettingsError> for OpenError {
864 fn from(error: SettingsError) -> Self {
865 match error {
866 SettingsError::Bug(bug) => Self::Bug(bug),
867 error => Self::Settings(error),
868 }
869 }
870}
871
872impl From<StoreLockError> for OpenError {
873 fn from(error: StoreLockError) -> Self {
874 match error {
875 StoreLockError::Bug(bug) => Self::Bug(bug),
876 error => Self::Lock(error),
877 }
878 }
879}
880
881impl From<rusqlite::Error> for OpenError {
882 fn from(error: rusqlite::Error) -> Self {
883 Self::Database(error.into())
884 }
885}
886
887/// Coven's local tables cannot be used at this version (Appendix C.1, E1).
888#[derive(Debug, thiserror::Error)]
889pub enum CovenMigrationError {
890 /// The read-only connection cannot migrate coven's tables.
891 #[error("read-only open cannot migrate coven's schema")]
892 ReadOnly,
893 /// The internal schema requires a newer coven.
894 #[error("coven schema version {current} is newer than supported version {supported}")]
895 SchemaTooNew {
896 /// The database's internal schema version.
897 current: u32,
898 /// The internal schema version this coven supports.
899 supported: u32,
900 },
901 /// A migration failed and its transaction rolled back.
902 #[error("coven migration failed: {source}")]
903 Failed {
904 /// The failure, including any rollback error.
905 #[source]
906 source: Box<DbError>,
907 },
908}
909
910/// The application migration run could not complete.
911#[derive(Debug, thiserror::Error)]
912pub enum MigrationError {
913 /// The list is not numbered from one without gaps.
914 #[error("migration at {position} has version {found}, expected {expected}")]
915 NotContiguous {
916 /// Zero-based position in the list.
917 position: usize,
918 /// Declared version.
919 found: u32,
920 /// Required version.
921 expected: u32,
922 },
923 /// The database requires a newer app.
924 #[error("schema version {current} is newer than supported version {supported}")]
925 SchemaTooNew {
926 /// The database version.
927 current: u32,
928 /// The last supplied migration.
929 supported: u32,
930 },
931 /// The migration or final validation failed; the whole run rolled back.
932 #[error("migration {version} ({name}) failed: {source}")]
933 Failed {
934 /// Migration being run, or the final migration for validation errors.
935 version: u32,
936 /// Its name.
937 name: &'static str,
938 /// The original failure.
939 #[source]
940 source: Box<DbError>,
941 },
942}
943
944#[cfg(test)]
945#[path = "error_tests.rs"]
946mod tests;