Skip to main content

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;