Skip to main content

coven_database/
database.rs

1//! Opening is the composition root; a shared handle owns all connections and
2//! the writer lock. Blocking calls retain these resources after its last owner
3//! drops, while another open waits for their release.
4
5use coven_foundation::locks::{lock_unpoisoned, read_unpoisoned, write_unpoisoned};
6use coven_foundation::task::{blocking_completing, CarriesBug};
7use std::collections::BTreeSet;
8use std::sync::{Arc, Mutex, RwLock};
9
10#[path = "file_database.rs"]
11pub(crate) mod file_database;
12
13#[path = "database_local_bytes.rs"]
14pub(crate) mod local_bytes;
15
16#[path = "database_mode.rs"]
17mod mode;
18pub use mode::{Mode, ReadOnly, Writer};
19
20#[path = "file_staging.rs"]
21mod file_staging;
22pub use file_staging::StagedFiles;
23#[path = "read_pool.rs"]
24mod read_pool;
25use read_pool::ReadPool;
26
27#[path = "input_database.rs"]
28pub(crate) mod input_database;
29
30#[path = "database_operations.rs"]
31mod operations;
32#[path = "database_provider_access.rs"]
33mod provider_access;
34
35#[path = "database_file_retention.rs"]
36pub(crate) mod file_retention;
37#[path = "database_snapshots.rs"]
38mod snapshots;
39
40#[cfg(any(test, feature = "test-utils"))]
41#[path = "database_operations_tests.rs"]
42pub(crate) mod operation_tests;
43
44use coven_foundation::clock::{ClockRef, SystemClock};
45use coven_foundation::files::{StoreDir, StoreLock};
46use coven_foundation::id_source::FileId;
47use coven_foundation::id_source::{DeviceId, IdSourceRef, UuidIds};
48
49use crate::authorization::SqlAuthorization;
50use crate::observation::CommitObserver;
51use crate::read::RunningRead;
52use crate::sqlite::DatabaseConnection;
53use crate::{DbError, Migration, MigrationOutcome, SyncedTable};
54use crate::{LiveQuery, LostValue, Read, ReconfigurableLiveQuery, SqlReadContext};
55
56#[path = "database_builder.rs"]
57mod builder;
58#[path = "database_store.rs"]
59mod store;
60
61#[path = "database_call.rs"]
62mod call;
63#[path = "database_retry.rs"]
64mod retry;
65#[path = "database_sync.rs"]
66mod sync;
67pub use builder::{
68    closed_sync_facts, discarded_operations, discarded_store_name, read_store_name, DatabaseBuilder,
69};
70pub(crate) use call::{finish_blocking, process, start_call};
71
72/// Shared ownership of the database, as its writer or a read-only handle
73/// (`Database<ReadOnly>`), which reads the store through the same calls and
74/// cannot write synced rows or migrate. A read-only handle's connections can
75/// change only local cache metadata, and its shared store lock prevents
76/// deletion until all of them close. Closing any clone closes them all.
77///
78/// ```compile_fail
79/// async fn cannot_write(handle: &coven_database::Database<coven_database::ReadOnly>) {
80///     handle.write(|_| Ok(())).await;
81/// }
82/// ```
83/// ```compile_fail
84/// fn cannot_subscribe(handle: &coven_database::Database<coven_database::ReadOnly>) {
85///     handle.subscribe(|_| Ok::<_, coven_database::DbError>(()));
86/// }
87/// ```
88/// ```compile_fail
89/// fn cannot_reconfigure(handle: &coven_database::Database<coven_database::ReadOnly>) {
90///     handle.subscribe_reconfigurable((), |_, _| Ok::<_, coven_database::DbError>(()));
91/// }
92/// ```
93/// ```compile_fail
94/// fn cannot_subscribe_losses(handle: &coven_database::Database<coven_database::ReadOnly>) {
95///     handle.subscribe_lost_values();
96/// }
97/// ```
98/// ```compile_fail
99/// fn sends_nothing(handle: &coven_database::Database<coven_database::ReadOnly>) {
100///     handle.uploads_in_flight();
101/// }
102/// ```
103pub struct Database<M: Mode = Writer> {
104    /// Where every connection reports its commits (§4); it outlives them, and
105    /// once they close makes only closed subscriptions.
106    observer: CommitObserver,
107    /// Where a bug in the store's work that nobody waits for goes (E15).
108    bugs: coven_foundation::task::BugSinkRef,
109    /// The open store, `None` once closed. Closing takes it before anything
110    /// that can panic, so a panic while it is written leaves it `None`, a
111    /// whole state: the lock is taken as it is (E15).
112    inner: Arc<RwLock<Option<M::Open>>>,
113    /// Marks the store's lock closing, for a wait in this process on its
114    /// release, once closing begins or the last clone drops.
115    closing: Arc<coven_foundation::files::StoreClosingGuard>,
116    /// What only the writer holds beside it.
117    writer: M::WriterParts,
118}
119
120// Written out: `derive(Clone)` would ask the open store to be `Clone`.
121impl<M: Mode> Clone for Database<M> {
122    fn clone(&self) -> Self {
123        Self {
124            observer: self.observer.clone(),
125            bugs: self.bugs.clone(),
126            inner: self.inner.clone(),
127            closing: self.closing.clone(),
128            writer: self.writer.clone(),
129        }
130    }
131}
132
133/// What the store's writer holds beside its open store.
134#[derive(Clone)]
135pub struct WriterParts {
136    /// The files this process is sending now, which coven's views read (§4).
137    transfers: crate::transfers_in_flight::TransfersInFlight,
138    /// The device's network as this process last heard of it, which
139    /// coven's views read (§4).
140    network: crate::network_facts::NetworkFacts,
141    file_tasks: Arc<tokio::sync::RwLock<()>>,
142    /// Counts the changes to the store's directory that its database doesn't
143    /// report, which wake the watch of what the store keeps.
144    local_bytes_changes: Arc<tokio::sync::watch::Sender<u64>>,
145    /// Told when a close finds a call holding the open store and waits for
146    /// it to end.
147    #[cfg(test)]
148    close_waits: Arc<Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
149    /// The removals of snapshot stagings their callers abandoned, for a test
150    /// to wait on.
151    #[cfg(test)]
152    abandoned_stagings: Arc<Mutex<Vec<tests::hooks::StagingRemoval>>>,
153}
154
155/// The writer's open store.
156pub struct DatabaseInner {
157    /// Coven's checkpointers of the input database and, unless the app
158    /// checkpoints it, the store's (§4); stopped before their writers close.
159    checkpoints: Vec<crate::wal_checkpoints::WalCheckpointer>,
160    /// The thread keeping the planner's statistics of the store's database
161    /// while it stays open (§4); stopped before the writer closes.
162    statistics: crate::upkeep::UpkeepThread<crate::planner_statistics::PlannerStatistics>,
163    inputs: input_database::InputDatabase,
164    access: DatabaseAccess<StoreLock>,
165    migrations: Vec<MigrationOutcome>,
166    /// The journal through which passes publish raise captures, when the
167    /// composition root supplied one (`DatabaseBuilder::migration_operation`):
168    /// a circle captured after the migration's journal finished opens it
169    /// again (§18.3).
170    raise_operation: Option<Box<crate::migration::MigrationOperation>>,
171    clock: ClockRef,
172    ids: IdSourceRef,
173    /// The names of copies being written, or written and waiting for the
174    /// write that names them, which no write's removal takes (Appendix C.3):
175    /// staged app bytes and cache files. Shared with what holds them, so
176    /// letting one go never takes the store's own lock. Each change under
177    /// its lock, and `copying`'s, is whole before it can panic, so both are
178    /// taken as they are (E15).
179    staging: Arc<crate::reserved_names::ReservedNames>,
180    /// The paths of the copies the calls making files device-only are
181    /// writing now, which no other write's removal takes (§17.3).
182    copying: Arc<Mutex<BTreeSet<String>>>,
183    /// Runs where released files' bytes are about to go, after the writer is
184    /// free, so a test can stall that disk work.
185    #[cfg(test)]
186    removal_gate: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
187    /// Runs where a write's staged files have been flushed, before its
188    /// commit, so a test can see the order or fail the flush.
189    #[cfg(test)]
190    flush_hook: Mutex<Option<coven_foundation::files::FlushHook>>,
191    /// Fail this many coming `end_retry_waits` transactions with a disk
192    /// error, so a test can show that a failed end leaves every wait.
193    #[cfg(any(test, feature = "test-utils"))]
194    retry_end_failures: std::sync::atomic::AtomicUsize,
195    /// Fail this many coming `applied_slot` reads with a disk error, so a
196    /// test can show what a failed read leaves in memory.
197    #[cfg(any(test, feature = "test-utils"))]
198    applied_slot_failures: std::sync::atomic::AtomicUsize,
199}
200
201/// The connections every handle reads through, and the lock on the store's
202/// directory, which outlives every connection, including when the last
203/// handle is dropped.
204pub struct DatabaseAccess<L> {
205    /// Shared with the store's checkpointer, which holds it while the WAL
206    /// starts over (§4).
207    writer: Arc<Mutex<DatabaseConnection>>,
208    readers: ReadPool,
209    // Every connection reports its commits and handle failures here (§4).
210    observer: CommitObserver,
211    write_schema: crate::write_schema::WriteSchema,
212    directory: StoreDir,
213    device: DeviceId,
214    lock: L,
215    /// Runs once, where the next read of a file on this device only has
216    /// found where its bytes are and is about to open them, so a test can
217    /// commit between the two.
218    #[cfg(test)]
219    local_open_gate: Mutex<Option<Box<dyn FnOnce() + Send>>>,
220}
221
222impl<L> DatabaseAccess<L> {
223    /// Refuse calls once a connection failed the handle. A call whose own
224    /// work failed the handle with a bug returns that bug as `DbError::Bug`:
225    /// its own error, such as a statement the broken authorizer refused,
226    /// says less, and the app's closure may have dropped it. A bug its error
227    /// carries beside that one goes to the sink, so neither is lost. Any
228    /// other failing call keeps its own error, and one that succeeds while
229    /// the handle fails reports the failure (E15).
230    fn call<R, E: From<DbError> + CarriesBug>(
231        &self,
232        run: impl FnOnce() -> Result<R, E>,
233    ) -> Result<R, E> {
234        if let Some(error) = self.observer.error() {
235            return Err(error.into());
236        }
237        let (result, own) = self.observer.calling(run);
238        if let Some(bug) = own {
239            let carried = result.as_ref().err().and_then(CarriesBug::bug);
240            if let Some(other) = carried.filter(|other| **other != bug) {
241                self.observer.report(other);
242            }
243            return Err(DbError::Bug(bug).into());
244        }
245        match (result, self.observer.error()) {
246            (Ok(_), Some(error)) => Err(error.into()),
247            (result, _) => result,
248        }
249    }
250
251    /// Read one committed state on a reader: read-only decisions never take
252    /// the writer (§21.2).
253    fn with_committed<R, E: From<DbError> + CarriesBug>(
254        &self,
255        run: impl FnOnce(&DatabaseConnection) -> Result<R, E>,
256    ) -> Result<R, E> {
257        self.call(|| {
258            self.readers
259                .acquire_reader()
260                .with_reader(|reader| reader.read_transaction(|| run(reader)))
261        })
262    }
263
264    fn with_writer<R, E: From<DbError>>(
265        &self,
266        run: impl FnOnce(&mut DatabaseConnection) -> Result<R, E>,
267    ) -> Result<R, E> {
268        with_locked(&self.writer, |writer| {
269            #[cfg(any(test, feature = "test-utils"))]
270            writer.record_work(|work| work.writer_holds += 1);
271            if let Some(error) = self.observer.error() {
272                return Err(error.into());
273            }
274            #[cfg(any(test, feature = "test-utils"))]
275            let run = |writer| coven_foundation::test_work::writing(|| run(writer));
276            run(writer)
277        })
278    }
279
280    /// Remove released files' bytes now that the writer is free, then forget
281    /// their records under it (§21.2, Appendix C.3). `chosen` is what the writer
282    /// that recorded them chose.
283    fn remove_released(&self, chosen: crate::file_removals::Chosen) -> Vec<DbError> {
284        let released = match chosen {
285            Ok(released) => released,
286            Err(error) => return vec![error],
287        };
288        let (removed, mut failures) = crate::file_removals::remove_bytes(&self.directory, released);
289        if !removed.is_empty() {
290            let forgotten = self.with_writer(|writer| {
291                writer.transaction(|db| crate::file_removals::forget(db, &removed))?;
292                #[cfg(any(test, feature = "test-utils"))]
293                writer.checkpoint(crate::test_utils::WriteCheckpoint::Finished);
294                Ok::<_, DbError>(())
295            });
296            if let Err(error) = forgotten {
297                failures.push(error);
298            }
299        }
300        failures
301    }
302
303    /// The bytes here of a file its record says is on this device only.
304    /// They are found in one committed state and opened after it, and a
305    /// write committing between the two, such as its upload's completion,
306    /// can release them, which coven deletes as that write returns (Appendix
307    /// C.3). Bytes are released only once no record names them, so bytes
308    /// missing or changed are judged by the record as it stands after the
309    /// open: one that no longer names bytes here means the file left this
310    /// device only meanwhile, and `None` sends the read to the cache or
311    /// storage; one that still names them leaves the failure standing.
312    fn open_local_file(
313        &self,
314        reference: &crate::FileRef,
315    ) -> Result<Option<crate::LocalFileStream>, crate::LocalFileError> {
316        let file = reference.id();
317        let source = || {
318            self.readers
319                .acquire_reader()
320                .read_snapshot(|sql| sql.device_only_source(self.device, file))
321        };
322        let Some(found) = source()? else {
323            return Ok(None);
324        };
325        #[cfg(test)]
326        if let Some(gate) = lock_unpoisoned(&self.local_open_gate).take() {
327            gate();
328        }
329        match crate::file_ref::open_source(
330            &self.directory,
331            &file.to_string(),
332            reference.size(),
333            found,
334        ) {
335            Ok(stream) => Ok(Some(stream)),
336            Err(error) if error.judges_bytes() && source()?.is_none() => Ok(None),
337            Err(error) => Err(error),
338        }
339    }
340
341    fn file_ref(&self, file: FileId) -> Result<crate::FileRef, DbError> {
342        self.readers.acquire_reader().read_snapshot(|sql| sql.file_ref(file))
343    }
344
345    fn user_file(&self, file: FileId) -> Result<Option<crate::UserFile>, DbError> {
346        self.readers.acquire_reader().read_snapshot(|sql| sql.user_file(file))
347    }
348
349    fn read<R, E: From<DbError>>(
350        &self,
351        read: impl FnOnce(SqlReadContext<'_>) -> Result<R, E>,
352    ) -> Result<R, E> {
353        self.readers.acquire_reader().read_snapshot(read)
354    }
355
356    fn schema_version(&self) -> Result<u32, DbError> {
357        self.readers.acquire_reader().schema_version()
358    }
359
360    /// Every connection has the same settings, so one reader reports them.
361    fn sqlite_settings(&self) -> Result<crate::SqliteSettingsInEffect, DbError> {
362        self.readers.acquire_reader().with_reader(DatabaseConnection::sqlite_settings)
363    }
364
365    fn close(self) -> Result<(), DbError> {
366        self.observer.close();
367        let mut failures = self.readers.close();
368        // Each checkpointer stopped before, and let go of the writer.
369        match Arc::try_unwrap(self.writer) {
370            Ok(writer) => {
371                if let Err(error) = coven_foundation::locks::into_unpoisoned(writer).close() {
372                    failures.push(error);
373                }
374            }
375            Err(_held) => failures.push(DbError::Invariant {
376                what: "a writer closes after its checkpointer stopped",
377            }),
378        }
379        drop(self.lock);
380        if failures.is_empty() {
381            Ok(())
382        } else {
383            Err(DbError::Closing { failures })
384        }
385    }
386}
387
388/// Run `run` on `connection`, one of the store's SQLite connections. A
389/// panic while one is held leaves it whole: its open transaction rolls back
390/// as the panic unwinds (`write_transaction.rs`), and a failed rollback fails
391/// the handle. So the lock is taken as it is.
392pub(crate) fn with_locked<T, R>(connection: &Mutex<T>, run: impl FnOnce(&mut T) -> R) -> R {
393    run(&mut lock_unpoisoned(connection))
394}
395
396impl DatabaseInner {
397    fn call<R, E: From<DbError> + CarriesBug>(
398        &self,
399        run: impl FnOnce(&Self) -> Result<R, E>,
400    ) -> Result<R, E> {
401        self.access.call(|| run(self))
402    }
403
404    // Their durable removal records remain for the next write or open.
405    fn release_file_reservations(&self, names: &[crate::file_removals::OwnedFile]) {
406        self.staging.release(names);
407    }
408
409    /// Run `operation`, a write, under the writer, then remove the files it
410    /// released once the writer is free (Appendix C.3), and run what its
411    /// steps asked to follow its commit. A panic in it fails it with
412    /// `DbError::Bug` once its transaction has rolled back, and its released
413    /// files go as after any failed write.
414    fn with_files<R, E: crate::WriteFailure>(
415        &self,
416        operation: &'static str,
417        run: impl FnOnce(&mut DatabaseConnection, &crate::file_write::WriteFiles<'_>) -> Result<R, E>,
418    ) -> Result<R, E> {
419        let (result, chosen, committed) = self.access.with_writer(|writer| {
420            let files = crate::file_write::WriteFiles::new(
421                &self.access.write_schema,
422                self.access.device,
423                self.ids.as_ref(),
424                &self.staging,
425                &self.copying,
426            );
427            let result = coven_foundation::task::caught_sync(operation, || run(writer, &files))
428                .unwrap_or_else(|bug| Err(E::from(DbError::Bug(bug))));
429            // After a failed rollback the writer may still hold this write;
430            // removal records wait for the next open (Appendix C.3).
431            let usable = !matches!(self.access.observer.error(), Some(DbError::RollbackFailed));
432            let chosen = usable.then(|| released(writer, self));
433            Ok::<_, E>((result, chosen, files.take_committed()))
434        })?;
435        let result = match chosen {
436            Some(chosen) => self.finish_removals(result, chosen),
437            None => result,
438        };
439        // A write that returns success has committed.
440        if result.is_ok() {
441            committed.into_iter().for_each(|effect| effect());
442        }
443        result
444    }
445
446    /// Remove what `chosen` names, after the writer that chose it is
447    /// released (§21.2, Appendix C.3).
448    fn remove_chosen(&self, chosen: crate::file_removals::Chosen) -> Vec<DbError> {
449        #[cfg(test)]
450        {
451            let gate = lock_unpoisoned(&self.removal_gate).clone();
452            if let Some(gate) = gate {
453                gate();
454            }
455        }
456        self.access.remove_released(chosen)
457    }
458
459    /// `result`, after removing the files it released, as `chosen` names
460    /// them; a failed removal keeps its record for the next write or open.
461    fn finish_removals<R, E: crate::WriteFailure>(
462        &self,
463        result: Result<R, E>,
464        chosen: crate::file_removals::Chosen,
465    ) -> Result<R, E> {
466        crate::write_failure::after_cleanup(result, self.remove_chosen(chosen))
467    }
468}
469
470/// Let `paths` go from the copies a store keeps out of removals, once the
471/// write that brought them has decided (§17.3).
472pub(crate) fn release_copies(copying: &Mutex<BTreeSet<String>>, paths: &[String]) {
473    let mut held = lock_unpoisoned(copying);
474    for path in paths {
475        held.remove(path);
476    }
477}
478
479/// The removals the write that just ran under `writer` released, chosen
480/// with the staging and copying sets held (Appendix C.3).
481fn released(writer: &DatabaseConnection, inner: &DatabaseInner) -> crate::file_removals::Chosen {
482    inner.staging.with_names(|active| {
483        let copying = lock_unpoisoned(&inner.copying);
484        crate::file_removals::choose(writer, active, &copying)
485    })
486}
487
488impl<M: Mode> Database<M> {
489    /// Where a bug in the store's work that nobody waits for goes (E15).
490    pub fn bugs(&self) -> &coven_foundation::task::BugSinkRef {
491        &self.bugs
492    }
493
494    /// Run `run` on the open store on the blocking pool, refused once a
495    /// connection failed the handle.
496    async fn call<R, E>(
497        &self,
498        run: impl FnOnce(&M::Open) -> Result<R, E> + Send + 'static,
499    ) -> Result<R, E>
500    where
501        R: Send + 'static,
502        E: From<DbError> + CarriesBug + Send + 'static,
503    {
504        finish_blocking(
505            start_call("database call", &self.bugs, self.inner.clone(), move |open| {
506                M::access(open).call(|| run(open))
507            })
508            .await,
509        )
510    }
511
512    /// Open the local source of a file still on this device only. No storage
513    /// operation occurs; `None` means the bytes come from the cache or
514    /// storage.
515    pub async fn open_local_file(
516        &self,
517        reference: &crate::FileRef,
518    ) -> Result<Option<crate::LocalFileStream>, crate::LocalFileError> {
519        let reference = reference.clone();
520        self.call(move |open| M::access(open).open_local_file(&reference)).await
521    }
522
523    /// The file this device has a record of under this id, from one
524    /// committed state.
525    pub async fn file_ref(&self, file: FileId) -> Result<crate::FileRef, DbError> {
526        self.call(move |open| M::access(open).file_ref(file)).await
527    }
528
529    /// The recorded original's path, size and modification time; never reread its bytes.
530    pub async fn user_file(&self, file: FileId) -> Result<Option<crate::UserFile>, DbError> {
531        self.call(move |open| M::access(open).user_file(file)).await
532    }
533
534    /// Run ordinary SQL against one consistent snapshot when awaited. The
535    /// read fails with the closure's own error type, and with a failure of
536    /// the database's own as that type's `From<DbError>`.
537    pub fn read<F, R, E>(&self, read: F) -> Read<'_, F, M>
538    where
539        F: FnOnce(SqlReadContext<'_>) -> Result<R, E> + Send + 'static,
540        R: Send + 'static,
541        E: From<DbError> + CarriesBug + Send + 'static,
542    {
543        Read::new(self, read)
544    }
545
546    /// Decode every lost cell and removed row in one snapshot.
547    pub async fn lost_values(&self) -> Result<Vec<LostValue>, DbError> {
548        self.read(|sql| sql.lost_values()).await
549    }
550
551    pub(crate) fn start_read<F, R, E>(&self, read: F) -> RunningRead<R, E>
552    where
553        F: FnOnce(SqlReadContext<'_>) -> Result<R, E> + Send + 'static,
554        R: Send + 'static,
555        E: From<DbError> + CarriesBug + Send + 'static,
556    {
557        RunningRead::new(start_call("read", &self.bugs, self.inner.clone(), move |open| {
558            let access = M::access(open);
559            access.call(|| access.read(read))
560        }))
561    }
562
563    /// The committed app schema version, read on a read-only connection.
564    pub async fn schema_version(&self) -> Result<u32, DbError> {
565        self.call(|open| M::access(open).schema_version()).await
566    }
567
568    /// The SQLite settings the store's connections have in effect (§4).
569    pub async fn sqlite_settings(&self) -> Result<crate::SqliteSettingsInEffect, DbError> {
570        self.call(|open| M::access(open).sqlite_settings()).await
571    }
572}
573
574impl Database {
575    /// Mark this instance as closing for a reopen to wait on, though workers
576    /// still hold clones of it: the facade's owning scope ended (F.4).
577    pub fn begin_close(&self) {
578        self.closing.begin_close();
579    }
580    /// Observe new local writes: rows inserted into the outgoing write queue.
581    /// Upload confirmation removing a row is this device's own bookkeeping
582    /// and wakes nothing (§6.7). Register before the first sync to
583    /// include writes committed while a pass is running.
584    pub fn sync_changes(&self) -> crate::DatabaseChanges {
585        self.table_changes(&["_coven_uploads"], true, true)
586    }
587
588    /// Observe operations as they start: rows inserted into the journal,
589    /// such as an invite, whose expiry the handle wakes to write (§13.2).
590    pub fn started_operations(&self) -> crate::DatabaseChanges {
591        self.table_changes(&["_coven_operations"], true, true)
592    }
593
594    /// The handle's clock could not wait for an invite's expiry: every
595    /// later call and live query fails with `DbError::WallTimer` until the
596    /// store is reopened (§13.2).
597    pub fn fail_expiry_wait(&self, error: coven_foundation::clock::WallTimerError) {
598        self.observer.fail(crate::observation::HandleFailure::WallTimer(error));
599    }
600
601    /// `first` reports the initial state as a change; `inserted_only`
602    /// ignores updates and deletions.
603    fn table_changes(
604        &self,
605        tables: &[&str],
606        first: bool,
607        inserted_only: bool,
608    ) -> crate::DatabaseChanges {
609        let reads: Vec<_> = tables
610            .iter()
611            .map(|table| crate::observation::TableRead {
612                table: (*table).into(),
613                columns: BTreeSet::new(),
614                scope: crate::row_scope::RowScope::All,
615                inserted_only,
616            })
617            .collect();
618        self.observer.changes(reads, first)
619    }
620
621    /// Observe, from now, every commit made outside `attempt`: an operation
622    /// attempt's own commits are no evidence against the wait it ends in,
623    /// and which rows matter is known only when it ends (§6.7).
624    pub fn attempt_commits(&self, attempt: crate::CommitScope) -> crate::AttemptCommits {
625        self.observer.attempt(attempt)
626    }
627
628    /// Observe `rows` from here on, after `attempt`. The result first reports
629    /// whether a commit since the attempt began changed one of them: such a
630    /// commit is evidence against the wait the attempt ends in (IO
631    /// `racing_commit_wakes`).
632    pub fn watch_attempt(
633        &self,
634        attempt: crate::AttemptCommits,
635        rows: &[crate::PrerequisiteRows],
636    ) -> crate::DatabaseChanges {
637        self.observer.watch_attempt(attempt, rows)
638    }
639
640    /// The files this process is sending now: the upload worker marks each
641    /// attempt, and coven's views read them (§4, §17.9).
642    pub fn uploads_in_flight(&self) -> crate::InFlight {
643        self.writer.transfers.uploads()
644    }
645
646    /// The files this process is downloading to keep whole now: each pin's
647    /// download marks its attempt, and `coven_downloading` reads them (§4,
648    /// §17.8).
649    pub fn downloads_in_flight(&self) -> crate::InFlight {
650        self.writer.transfers.downloads()
651    }
652
653    /// The store's watch says the device's network is now `network`:
654    /// coven's views show it, and a live query that read them runs again
655    /// (§4, §6.7).
656    pub fn record_network(&self, network: coven_foundation::network_watch::NetworkState) {
657        self.writer.network.set(network);
658    }
659
660    /// Observe the tables and columns a query reads, and the rows its
661    /// conditions allow. Its results fail with the closure's own error type.
662    pub fn subscribe<F, R, E>(&self, query: F) -> LiveQuery<R, E>
663    where
664        F: Fn(SqlReadContext<'_>) -> Result<R, E> + Send + Sync + 'static,
665        R: Send + 'static,
666        E: From<DbError> + CarriesBug + Send + 'static,
667    {
668        LiveQuery::new(self.subscribe_reconfigurable((), move |(), sql| query(sql)))
669    }
670
671    /// Observe a query whose request can be replaced through a shared handle.
672    pub fn subscribe_reconfigurable<Q, F, R, E>(
673        &self,
674        initial_request: Q,
675        query: F,
676    ) -> ReconfigurableLiveQuery<Q, R, E>
677    where
678        Q: Clone + PartialEq + Send + Sync + 'static,
679        F: Fn(&Q, SqlReadContext<'_>) -> Result<R, E> + Send + Sync + 'static,
680        R: Send + 'static,
681        E: From<DbError> + CarriesBug + Send + 'static,
682    {
683        let commits = self.observer.subscribe();
684        // A subscription retains a database only for its active blocking read.
685        // Idle queries and undelivered results cannot keep a closing lock held.
686        let database = Arc::downgrade(&self.inner);
687        let query = Arc::new(query);
688        let completion = commits.clone();
689        let bugs = self.bugs.clone();
690        ReconfigurableLiveQuery::new(
691            initial_request,
692            Arc::new(move |request| {
693                let database = database.clone();
694                let query = query.clone();
695                let completion = completion.clone();
696                let bugs = bugs.clone();
697                Box::pin(async move {
698                    let database = database.upgrade().ok_or(DbError::StoreClosed)?;
699                    finish_blocking(
700                        start_call("live query", &bugs, database, move |inner| {
701                            inner.call(|inner| {
702                                inner.access.readers.acquire_reader().live_read(
703                                    &mut |reads, lookups| completion.finish_judging(reads, lookups),
704                                    move |sql| query(&request, sql),
705                                )
706                            })
707                        })
708                        .await,
709                    )
710                })
711            }),
712            commits,
713            self.bugs.clone(),
714        )
715    }
716
717    /// Observe the decoded lost cells and removed rows.
718    pub fn subscribe_lost_values<E>(&self) -> LiveQuery<Vec<LostValue>, E>
719    where
720        E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
721    {
722        self.subscribe(|sql| sql.lost_values().map_err(E::from))
723    }
724
725    /// Run app SQL and coven's steps, and commit its unsigned write record
726    /// and merge metadata in one IMMEDIATE transaction (§4). The closure's
727    /// result is returned after commit.
728    pub async fn write<F, R>(&self, sql: F) -> Result<R, DbError>
729    where
730        F: FnOnce(crate::SqlContext<'_, '_>) -> Result<R, DbError> + Send + 'static,
731        R: Send + 'static,
732    {
733        self.write_as(sql).await
734    }
735
736    /// `write`, for a closure whose failures are `E`: callback errors retain
737    /// their type alongside rollback or byte-cleanup failures.
738    pub async fn write_as<F, R, E>(&self, sql: F) -> Result<R, E>
739    where
740        F: FnOnce(crate::SqlContext<'_, '_>) -> Result<R, E> + Send + 'static,
741        R: Send + 'static,
742        E: crate::WriteFailure + Send + 'static,
743    {
744        self.call(move |inner| {
745            inner.with_files("write", |writer, files| {
746                let result = coven_foundation::task::caught_sync("write", || {
747                    writer.local_write(
748                        &inner.access.write_schema,
749                        inner.access.device,
750                        inner.clock.now(),
751                        files,
752                        |files| sql(crate::SqlContext::new(files)),
753                    )
754                });
755                // Staged files the write brought in go free once it has
756                // decided: a write that rolled back leaves their bytes to the
757                // removals its cleanup chooses.
758                inner.release_file_reservations(&files.take_brought());
759                release_copies(&inner.copying, &files.take_held_copies());
760                #[cfg(any(test, feature = "test-utils"))]
761                writer.checkpoint(crate::test_utils::WriteCheckpoint::StagingReleased);
762                result.unwrap_or_else(|bug| Err(E::from(DbError::Bug(bug))))
763            })
764        })
765        .await
766    }
767
768    /// Stage app bytes for a later write (§17.2): `stage` names each file's
769    /// namespace and place and returns what that write's SQL needs, such as
770    /// the ids staging gave them. Their bytes are streamed to coven's own
771    /// copies, each synced, and flushed, before this returns; close waits for
772    /// that, and for its cleanup if it fails or is cancelled. The result
773    /// holds nothing close waits for. A write takes it with `put_staged`.
774    pub async fn stage_files<F, T, E>(&self, stage: F) -> Result<StagedFiles<T>, E>
775    where
776        F: FnOnce(&mut crate::WriteBatch<'_>) -> Result<T, E> + Send + 'static,
777        T: Send + 'static,
778        E: crate::WriteFailure + Send + 'static,
779    {
780        let lease = self.writer.file_tasks.clone().read_owned().await;
781        let database = self.inner.clone();
782        let (staging, value) = finish_blocking(
783            blocking_completing(
784                "stage files",
785                self.bugs.clone(),
786                crate::commit_scope::bind(move || {
787                    file_staging::FileStaging::new(database, lease, stage)
788                }),
789            )
790            .await,
791        )?;
792        let (staging, flushed) =
793            staging.write().await.map_err(|failed| E::from(DbError::from(failed)))?;
794        match flushed {
795            Ok(_flushed) => Ok(staging.into_staged(value)),
796            // A failed copy or flush removes what the staging wrote.
797            // The discard's result is the call's failure, so a bug it
798            // carries reaches the sink too once no caller waits.
799            Err(error) => finish_blocking(
800                blocking_completing(
801                    "discard staged files",
802                    self.bugs.clone(),
803                    crate::commit_scope::bind(move || {
804                        Err::<std::convert::Infallible, E>(staging.discard::<E>(error))
805                    }),
806                )
807                .await,
808            )
809            .map(|never| match never {}),
810        }
811    }
812
813    /// Stage app bytes, then run a write that puts them in its rows, as an
814    /// app does with `stage_files` and `put_staged`, for tests.
815    #[cfg(any(test, feature = "test-utils"))]
816    pub async fn staged_write<F, S, T, R, E>(&self, stage: F, write: S) -> Result<R, E>
817    where
818        F: FnOnce(&mut crate::WriteBatch<'_>) -> Result<T, E> + Send + 'static,
819        S: FnOnce(crate::SqlContext<'_, '_>, T) -> Result<R, E> + Send + 'static,
820        T: Send + 'static,
821        R: Send + 'static,
822        E: crate::WriteFailure + Send + 'static,
823    {
824        let staged = self.stage_files(stage).await?;
825        self.write_as(move |sql| {
826            let value = sql.put_staged(staged)?;
827            write(sql, value)
828        })
829        .await
830    }
831
832    /// Run app SQL that writes device-local tables alone in one IMMEDIATE
833    /// transaction, as plain SQLite: no pre-update hook records its row
834    /// changes, so SQLite empties a table in one step and fills one at its
835    /// own speed (§4). A statement that would write a synced table, itself or
836    /// through a trigger or foreign-key action, fails the write with
837    /// `SyncedTableInBulkWrite`, and everything rolls back. After commit,
838    /// every live query that read a table the write's statements may change
839    /// runs again, whatever rows it read. The closure's result is returned
840    /// after commit.
841    pub async fn write_bulk<F, R, E>(&self, sql: F) -> Result<R, E>
842    where
843        F: FnOnce(crate::BulkSqlContext<'_>) -> Result<R, E> + Send + 'static,
844        R: Send + 'static,
845        E: crate::WriteFailure + Send + 'static,
846    {
847        self.call(move |inner| inner.access.with_writer(|writer| writer.bulk_write(sql))).await
848    }
849
850    /// Checkpoint the store's WAL on the writer, between writes (§4).
851    pub async fn checkpoint(
852        &self,
853        mode: crate::CheckpointMode,
854    ) -> Result<crate::WalCheckpoint, DbError> {
855        self.call(move |inner| inner.access.with_writer(|writer| writer.wal_checkpoint(mode))).await
856    }
857
858    /// Give back free pages of a store created with incremental auto-vacuum,
859    /// on the writer, between writes (§4). On any other store it does nothing.
860    pub async fn incremental_vacuum(&self, pages: crate::IncrementalVacuum) -> Result<(), DbError> {
861        self.call(move |inner| inner.access.with_writer(|writer| writer.incremental_vacuum(pages)))
862            .await
863    }
864
865    /// Give back every connection's cached pages that no statement is using
866    /// (§4). SQLite as coven builds it (`SQLITE_ENABLE_MEMORY_MANAGEMENT`)
867    /// keeps the unused pages of every connection in the process in one
868    /// pool, and one connection's release empties the whole pool: so one
869    /// release on an idle reader frees the writer's, the input database's
870    /// and the checkpointers' too, and never waits for a write.
871    pub async fn release_memory(&self) -> Result<(), DbError> {
872        self.call(|inner| {
873            inner.access.readers.acquire_reader().with_reader(|reader| Ok(reader.release_memory()?))
874        })
875        .await
876    }
877
878    /// The migrations this open committed, classified individually.
879    pub fn applied_migrations(&self) -> Result<Vec<MigrationOutcome>, DbError> {
880        let inner = read_unpoisoned(&self.inner);
881        let inner = inner.as_ref().ok_or(DbError::StoreClosed)?;
882        if let Some(error) = inner.access.observer.error() {
883            return Err(error);
884        }
885        Ok(inner.migrations.clone())
886    }
887
888    /// Wait for database calls, close all connections, then release the writer lock.
889    /// Every subsequent database call, including close, reports `StoreClosed`.
890    pub async fn close(&self) -> Result<(), DbError> {
891        self.closing.begin_close();
892        let database = self.clone();
893        // Closing runs as its own task so that a caller dropping this future
894        // doesn't stop it partway; the runtime dropping that task can.
895        let bugs = self.bugs.clone();
896        coven_foundation::task::spawn_completing("close the store", self.bugs.clone(), async move {
897            let file_tasks = database.writer.file_tasks.clone().write_owned().await;
898            finish_blocking(
899                blocking_completing(
900                    "close the store",
901                    bugs,
902                    crate::commit_scope::bind(move || {
903                        let _file_tasks = file_tasks;
904                        #[cfg(test)]
905                        database.tell_close_wait();
906                        let mut slot = write_unpoisoned(&database.inner);
907                        let Some(inner) = slot.take() else {
908                            return Err(DbError::StoreClosed);
909                        };
910                        let mut failures: Vec<DbError> = inner
911                            .checkpoints
912                            .into_iter()
913                            .filter_map(|checkpoints| checkpoints.stop().err())
914                            .collect();
915                        failures.extend(inner.statistics.stop().err());
916                        failures.extend(inner.inputs.close());
917                        // The statistics follow this session's writes, outside
918                        // any captured transaction. A failed handle's writer may
919                        // still hold a transaction, which closing rolls back.
920                        if inner.access.observer.error().is_none() {
921                            if let Err(error) = lock_unpoisoned(&inner.access.writer).optimize() {
922                                failures.push(error);
923                            }
924                        }
925                        match inner.access.close() {
926                            Err(DbError::Closing { failures: application }) => {
927                                failures.extend(application)
928                            }
929                            Err(error) => failures.push(error),
930                            Ok(()) => (),
931                        }
932                        if failures.is_empty() {
933                            Ok(())
934                        } else {
935                            Err(DbError::Closing { failures })
936                        }
937                    }),
938                )
939                .await,
940            )
941        })
942        .await
943        .map_err(DbError::from)?
944    }
945}
946
947impl Database<ReadOnly> {
948    /// Begin closing every clone of this handle, at once and before its
949    /// connections close: every later call fails with `StoreClosed`, a call
950    /// already running answers `StoreClosed` too, and a deletion in this
951    /// process waits for the readers' lock to be released (E1).
952    pub fn begin_close(&self) {
953        self.observer.close();
954        self.closing.begin_close();
955    }
956
957    /// Wait for active calls and close every clone of this handle.
958    pub async fn close(&self) -> Result<(), DbError> {
959        let handle = self.clone();
960        finish_blocking(
961            blocking_completing("close the store", self.bugs.clone(), move || {
962                let mut slot = write_unpoisoned(&handle.inner);
963                slot.take().ok_or(DbError::StoreClosed)?.close()
964            })
965            .await,
966        )
967    }
968}
969
970#[cfg(test)]
971#[path = "database_tests.rs"]
972mod tests;