Skip to main content

coven_database/
database_builder.rs

1//! Compose locked writers and read-only connections at the database boundary.
2
3use super::*;
4use coven_foundation::files::StoreDirAccess;
5
6use crate::{migration::MigrationOperation, AutoVacuum, NewOperation, OpenError, OpenStep};
7use coven_foundation::progress::{ProgressChannel, ProgressReport, ProgressWatch};
8
9/// Choices needed to open the database part of a store.
10pub struct DatabaseBuilder {
11    directory: StoreDir,
12    tables: Option<Vec<SyncedTable>>,
13    namespaces: Vec<crate::Namespace>,
14    migrations: Option<Vec<Migration>>,
15    migration_operation: Option<Box<MigrationOperation>>,
16    installation: crate::migration_run::Installation,
17    clock: Option<ClockRef>,
18    ids: Option<IdSourceRef>,
19    sqlite: crate::SqliteSettings,
20    functions: crate::SqlFunctions,
21    progress: ProgressChannel<OpenStep>,
22    #[cfg(test)]
23    upkeep_threads: Option<usize>,
24    #[cfg(any(test, feature = "test-utils"))]
25    refuse_close: bool,
26}
27
28impl DatabaseBuilder {
29    /// Begin an open using a directory supplied by the store layout.
30    pub fn new(directory: StoreDir) -> Self {
31        Self {
32            directory,
33            tables: None,
34            namespaces: Vec::new(),
35            migrations: None,
36            migration_operation: None,
37            installation: crate::migration_run::Installation::Own,
38            clock: None,
39            ids: None,
40            sqlite: crate::SqliteSettings::default(),
41            functions: crate::SqlFunctions::default(),
42            progress: ProgressChannel::new(),
43            #[cfg(test)]
44            upkeep_threads: None,
45            #[cfg(any(test, feature = "test-utils"))]
46            refuse_close: false,
47        }
48    }
49
50    /// Have SQLite refuse to close the opened store's writer.
51    #[cfg(any(test, feature = "test-utils"))]
52    pub fn refuse_close(mut self) -> Self {
53        self.refuse_close = true;
54        self
55    }
56
57    /// Let `started` upkeep threads start, and have the system refuse the next.
58    #[cfg(test)]
59    pub(crate) fn refuse_upkeep_thread_after(mut self, started: usize) -> Self {
60        self.upkeep_threads = Some(started);
61        self
62    }
63
64    /// The steps a writable open reports as it runs them, each with how
65    /// much of it is done (§4). A read-only open runs none.
66    pub fn subscribe_progress(&self) -> ProgressWatch<OpenStep> {
67        self.progress.watch()
68    }
69
70    /// The tables that sync, described at the newest app schema version.
71    pub fn synced_tables(mut self, tables: Vec<SyncedTable>) -> Self {
72        self.tables = Some(tables);
73        self
74    }
75
76    /// The file namespaces, each with its kind and cache budget (§17.1).
77    pub fn namespaces(mut self, namespaces: Vec<crate::Namespace>) -> Self {
78        self.namespaces = namespaces;
79        self
80    }
81
82    /// The complete app migration sequence, numbered from one.
83    pub fn migrations(mut self, migrations: Vec<Migration>) -> Self {
84        self.migrations = Some(migrations);
85        self
86    }
87
88    /// Open an installation that loads its store from storage next, for a
89    /// join, restore or reset: its breaking migrations make no migration
90    /// write, since the device isn't registered yet and the load replaces
91    /// what they changed (§18.4).
92    pub fn loading_store(mut self) -> Self {
93        self.installation = crate::migration_run::Installation::Loading;
94        self
95    }
96
97    /// Let sync describe the operation that publishes a breaking migration batch.
98    /// The database commits its step 1 with the schema, converted waiting writes
99    /// and migration write. A factory error rolls all of them back. Standalone
100    /// database users may omit this; the application composition root supplies it.
101    pub fn migration_operation<F>(mut self, operation: F) -> Self
102    where
103        F: Fn(u32) -> Result<NewOperation, DbError> + Send + Sync + 'static,
104    {
105        self.migration_operation = Some(Box::new(operation));
106        self
107    }
108
109    /// The clock used when stamping a local write (§7.2).
110    pub fn clock(mut self, clock: ClockRef) -> Self {
111        self.clock = Some(clock);
112        self
113    }
114
115    /// The source of fresh names for this store's owned file bytes (Appendix C.3).
116    pub fn id_source(mut self, ids: IdSourceRef) -> Self {
117        self.ids = Some(ids);
118        self
119    }
120
121    /// SQLite settings for every connection to the store's database; unset
122    /// ones keep SQLite's defaults, except that `checkpoint_fullfsync` is on
123    /// for macOS and iOS (§4).
124    pub fn sqlite_settings(mut self, settings: crate::SqliteSettings) -> Self {
125        self.sqlite = settings;
126        self
127    }
128
129    /// The app's collations and scalar functions, registered on every
130    /// connection to the store's database before any app SQL runs on it (§4).
131    pub fn sql_functions(mut self, functions: crate::SqlFunctions) -> Self {
132        self.functions = functions;
133        self
134    }
135
136    /// Open one writer under the store lock and four read-only connections.
137    /// Always migrate coven's internal tables, then rebuild the indexes a new
138    /// version of SQLite or of the app's collations and functions changes,
139    /// then migrate the app's schema.
140    pub async fn open(self) -> Result<Database, OpenError> {
141        let lock = self.directory.lock_for_open().await?;
142        self.open_locked(lock).await
143    }
144
145    /// Open under a lock already held by the facade while checking restored
146    /// device identity. The lock must protect this builder's directory.
147    pub async fn open_locked(self, lock: StoreLock) -> Result<Database, OpenError> {
148        let closing = lock.closing_guard();
149        let bugs = self.directory.bugs().clone();
150        let result = finish_blocking(
151            blocking_completing("open the store", bugs, move || self.open_graph(lock)).await,
152        );
153        // Cancellation drops the guard while SQLite can still be opening.
154        // A returned database carries its own guard across its owning clones.
155        closing.disarm();
156        result
157    }
158
159    /// Open read-only connections under a shared deletion guard, without
160    /// migrating or rebuilding, including from a second process while the
161    /// writer is open.
162    pub async fn open_read_only(self) -> Result<Database<ReadOnly>, OpenError> {
163        let bugs = self.directory.bugs().clone();
164        finish_blocking(
165            blocking_completing("open the store read-only", bugs, move || self.open_read_graph())
166                .await,
167        )
168    }
169
170    fn open_graph(self, lock: StoreLock) -> Result<Database, OpenError> {
171        let tables =
172            self.tables.ok_or(OpenError::MissingConfiguration { field: "synced_tables" })?;
173        let migrations =
174            self.migrations.ok_or(OpenError::MissingConfiguration { field: "migrations" })?;
175        // Refuse a directory that is not a store before creating its database.
176        coven_foundation::files::verify_lock(&self.directory, &lock)?;
177        let settings = lock.settings()?;
178        let clock = match self.clock {
179            Some(clock) => clock,
180            None => Arc::new(SystemClock),
181        };
182        let ids = match self.ids {
183            Some(ids) => ids,
184            None => Arc::new(UuidIds),
185        };
186        let started = clock.elapsed();
187        let path = self.directory.database_path();
188        // Commits are reported here once the writer observes them, after
189        // migrating; the transfers in flight report their starts and ends.
190        let observer = CommitObserver::new(self.directory.bugs().clone());
191        #[cfg(test)]
192        observer.refuse_upkeep_thread_after(self.upkeep_threads);
193        let transfers = crate::transfers_in_flight::TransfersInFlight::new(observer.clone());
194        let network = crate::network_facts::NetworkFacts::new(observer.clone());
195        let mut writer = DatabaseConnection::open(
196            &path,
197            false,
198            SqlAuthorization::new(&tables),
199            &self.sqlite,
200            &self.functions,
201        )?;
202        // Before the app's migrations, which may read coven's views.
203        writer.serve_views(settings.device_id, &transfers, &network)?;
204        let (outcomes, write_schema, inputs, mut checkpoints, migrating, migrated, optimized) =
205            writer.with_profile(|writer| -> Result<_, OpenError> {
206                writer.enable_wal()?;
207                let mut progress = ProgressReport::of(&self.progress, clock.clone());
208                let migrating = clock.elapsed();
209                let outcomes = writer.prepare_schema(
210                    &tables,
211                    &migrations,
212                    &self.functions.versions(),
213                    Some(crate::migration_run::MigrationOrigin(
214                        settings.device_id,
215                        clock.now(),
216                        self.installation,
217                    )),
218                    self.migration_operation.as_deref(),
219                    &mut progress,
220                )?;
221                crate::database::file_database::declare_budgets(writer, &self.namespaces)?;
222                crate::file_removals::finish_at_open(writer, &self.directory)?;
223                crate::pending::forget_causes(writer)?;
224                // No connection plans until sync starts in this process.
225                crate::internal_schema::retry_effect::save(
226                    writer,
227                    crate::internal_schema::retry_effect::RetryState::OPENED,
228                )?;
229                let mut write_schema =
230                    crate::write_schema::WriteSchema::read(writer, tables.clone())?;
231                write_schema.conversions = crate::migration_receive::read(writer, &migrations)?;
232                write_schema.namespaces = self.namespaces;
233                crate::write_schema::prepare_evaluators(writer, &write_schema)?;
234                let migrated = clock.elapsed();
235                // After migrating, and before the writer captures commits, on a
236                // connection of its own: one that analyzes a small table loads its
237                // count, and forgetting the statistics afterwards doesn't unload it.
238                // The writer then reads what that connection kept.
239                DatabaseConnection::open(
240                    &path,
241                    false,
242                    SqlAuthorization::new(&tables),
243                    &self.sqlite,
244                    &self.functions,
245                )?
246                .gather_statistics()?;
247                // SQLite reads its statistics again only when told to.
248                writer.batch("ANALYZE sqlite_schema")?;
249                let optimized = clock.elapsed();
250                // The input database holds only coven's tables, which no app setting
251                // or function reaches.
252                let input_path = self.directory.input_database_path();
253                let mut input_writer = coven_only(&input_path, false)?;
254                input_writer.enable_wal()?;
255                let version: u32 =
256                    input_writer.query_row("PRAGMA application_id", [], |r| r.get(0))?;
257                match version {
258                    0 => input_writer.transaction(|db| {
259                        db.batch(&crate::internal_schema::initial_input_schema())
260                    })?,
261                    1 => (),
262                    _ => return Err(DbError::DamagedDatabase.into()),
263                }
264                let mut input_reader = coven_only(&input_path, true)?;
265                input_writer.serve(observer.clone());
266                input_reader.serve(observer.clone());
267                let input_writer = Arc::new(Mutex::new(input_writer));
268                // Coven checkpoints both databases off the commit path from here on,
269                // each on a connection of its own (§4); opening, with no reader yet,
270                // checkpointed as it committed.
271                let checkpoints = vec![crate::wal_checkpoints::WalCheckpointer::start(
272                    coven_only(&input_path, false)?,
273                    &input_writer,
274                    crate::sqlite_settings::CHECKPOINT_PAGES,
275                    clock.clone(),
276                    observer.clone(),
277                )?];
278                let inputs = input_database::InputDatabase::new(input_reader, input_writer)?;
279                Ok((outcomes, write_schema, inputs, checkpoints, migrating, migrated, optimized))
280            })?;
281        writer.observe_commits(observer.clone(), settings.device_id, &tables);
282        #[cfg(any(test, feature = "test-utils"))]
283        if self.refuse_close {
284            writer.leave_statement_open();
285        }
286        let writer = Arc::new(Mutex::new(writer));
287        if let Some(pages) = self.sqlite.checkpoint_pages() {
288            let checkpointing = DatabaseConnection::open(
289                &path,
290                false,
291                SqlAuthorization::new(&[]),
292                &self.sqlite,
293                &crate::SqlFunctions::default(),
294            )?;
295            checkpoints.push(crate::wal_checkpoints::WalCheckpointer::start(
296                checkpointing,
297                &writer,
298                pages,
299                clock.clone(),
300                observer.clone(),
301            )?);
302        }
303        let statistics =
304            crate::upkeep::UpkeepThread::<crate::planner_statistics::PlannerStatistics>::start(
305                DatabaseConnection::open(
306                    &path,
307                    false,
308                    SqlAuthorization::new(&tables),
309                    &self.sqlite,
310                    &self.functions,
311                )?,
312                &writer,
313                observer.clone(),
314            )?;
315        tracing::info!(
316            migrations = migrations.len(),
317            migrating = ?(migrated - migrating),
318            optimizing = ?(optimized - migrated),
319            total = ?(clock.elapsed() - started),
320            "opened the database"
321        );
322        let mut readers = Vec::new();
323        for _ in 0..4 {
324            let mut reader = DatabaseConnection::open(
325                &path,
326                true,
327                SqlAuthorization::new(&tables),
328                &self.sqlite,
329                &self.functions,
330            )?;
331            reader.serve_views(settings.device_id, &transfers, &network)?;
332            reader.serve(observer.clone());
333            readers.push(Mutex::new(reader));
334        }
335        Ok(Database {
336            observer: observer.clone(),
337            closing: Arc::new(lock.closing_guard()),
338            writer: WriterParts {
339                transfers,
340                network,
341                file_tasks: Arc::new(tokio::sync::RwLock::new(())),
342                local_bytes_changes: Arc::new(tokio::sync::watch::channel(0).0),
343                #[cfg(test)]
344                close_waits: Arc::new(Mutex::new(None)),
345                #[cfg(test)]
346                abandoned_stagings: Arc::new(Mutex::new(Vec::new())),
347            },
348            bugs: self.directory.bugs().clone(),
349            inner: Arc::new(RwLock::new(Some(DatabaseInner {
350                inputs,
351                staging: Arc::new(crate::reserved_names::ReservedNames::new(observer.clone())),
352                access: DatabaseAccess {
353                    writer,
354                    readers: ReadPool::new(readers),
355                    observer,
356                    write_schema,
357                    directory: self.directory,
358                    device: settings.device_id,
359                    lock,
360                    #[cfg(test)]
361                    local_open_gate: Mutex::new(None),
362                },
363                checkpoints,
364                statistics,
365                migrations: outcomes,
366                raise_operation: self.migration_operation,
367                clock,
368                ids,
369                copying: Arc::new(Mutex::new(BTreeSet::new())),
370                #[cfg(test)]
371                removal_gate: Mutex::new(None),
372                #[cfg(test)]
373                flush_hook: Mutex::new(None),
374                #[cfg(any(test, feature = "test-utils"))]
375                retry_end_failures: std::sync::atomic::AtomicUsize::new(0),
376                #[cfg(any(test, feature = "test-utils"))]
377                applied_slot_failures: std::sync::atomic::AtomicUsize::new(0),
378            }))),
379        })
380    }
381    fn open_read_graph(self) -> Result<Database<ReadOnly>, OpenError> {
382        let tables =
383            self.tables.ok_or(OpenError::MissingConfiguration { field: "synced_tables" })?;
384        let migrations =
385            self.migrations.ok_or(OpenError::MissingConfiguration { field: "migrations" })?;
386        let lock = self.directory.lock_read_only()?;
387        let settings = self.directory.settings()?;
388        let path = self.directory.database_path();
389        // Only the cache connection commits, and nothing subscribes to it.
390        let observer = CommitObserver::new(self.directory.bugs().clone());
391        // A read-only handle transfers nothing: every file reads as waiting.
392        let transfers = crate::transfers_in_flight::TransfersInFlight::new(observer.clone());
393        // Nor does it watch the network: none is known (api.md E4).
394        let network = crate::network_facts::NetworkFacts::new(observer.clone());
395        let mut first = DatabaseConnection::open(
396            &path,
397            true,
398            SqlAuthorization::read_only(&tables),
399            &self.sqlite,
400            &self.functions,
401        )?;
402        first.serve_views(settings.device_id, &transfers, &network)?;
403        // A read-only open runs no step, so nothing reaches `progress`.
404        let clock = match self.clock {
405            Some(clock) => clock,
406            None => Arc::new(SystemClock),
407        };
408        first.prepare_schema(
409            &tables,
410            &migrations,
411            &self.functions.versions(),
412            None,
413            None,
414            &mut ProgressReport::of(&self.progress, clock),
415        )?;
416        let mut schema = crate::write_schema::WriteSchema::read(&first, tables.clone())?;
417        schema.namespaces = self.namespaces;
418        first.serve(observer.clone());
419        let mut readers = vec![Mutex::new(first)];
420        for _ in 1..4 {
421            let mut reader = DatabaseConnection::open(
422                &path,
423                true,
424                SqlAuthorization::read_only(&tables),
425                &self.sqlite,
426                &self.functions,
427            )?;
428            reader.serve_views(settings.device_id, &transfers, &network)?;
429            reader.serve(observer.clone());
430            readers.push(Mutex::new(reader));
431        }
432        let mut writer = DatabaseConnection::open(
433            &path,
434            false,
435            SqlAuthorization::read_only(&tables),
436            &self.sqlite,
437            &self.functions,
438        )?;
439        writer.serve_views(settings.device_id, &transfers, &network)?;
440        writer.serve(observer.clone());
441        Ok(Database {
442            observer: observer.clone(),
443            closing: Arc::new(lock.closing_guard()),
444            writer: (),
445            bugs: self.directory.bugs().clone(),
446            inner: Arc::new(RwLock::new(Some(DatabaseAccess {
447                writer: Arc::new(Mutex::new(writer)),
448                readers: ReadPool::new(readers),
449                observer,
450                lock,
451                write_schema: schema,
452                directory: self.directory,
453                device: settings.device_id,
454                #[cfg(test)]
455                local_open_gate: Mutex::new(None),
456            }))),
457        })
458    }
459}
460
461/// The operation journal of a database a device reset discarded (§19.3), read
462/// in place and read-only on a blocking thread, never opened as a store. A
463/// journal SQLite reports damaged, or whose records don't decode, is
464/// `DamagedDatabase`. Like `read_store_name`, it reads only coven's table and
465/// needs none of the app's collations or functions. A caller that stops
466/// waiting leaves the read to finish, and its panic goes to `bugs` (E15).
467pub async fn discarded_operations(
468    path: std::path::PathBuf,
469    bugs: &coven_foundation::task::BugSinkRef,
470) -> Result<Vec<crate::OperationRecord>, DbError> {
471    finish_blocking(
472        blocking_completing("read the discarded operations", bugs.clone(), move || {
473            let db = coven_only(&path, true)?;
474            crate::operation::read(&db)
475        })
476        .await,
477    )
478}
479
480/// Read the name for a local store listing without migrations or a writer.
481/// It reads only coven's `_coven_store_state`, and SQLite looks a collation
482/// or function up only for a statement that uses it, so the app's aren't
483/// needed. A caller that stops waiting leaves the read to finish, and its
484/// panic goes to the store's bug sink (E15).
485pub async fn read_store_name(directory: StoreDir) -> Result<String, OpenError> {
486    let bugs = directory.bugs().clone();
487    finish_blocking(
488        blocking_completing("read the store name", bugs, move || {
489            let _lock = directory.lock_read_only()?;
490            let database = coven_only(&directory.database_path(), true)?;
491            Ok(crate::store_state_tables::name(&database)?)
492        })
493        .await,
494    )
495}
496
497/// What a closed store's sync state is computed from, and its unsent work,
498/// in one committed snapshot of its database, read through a connection of
499/// its own while `lock`, the store's writer lock, shows that no handle has it
500/// open (api.md E1, E5). It reads only coven's tables, so the app's
501/// declarations aren't needed; coven's tables must be at this coven's
502/// version, as a read-only open requires. Nothing is sent or downloaded and
503/// no network is watched, as for a read-only handle. A caller that stops
504/// waiting leaves the read to finish, and its panic goes to the store's bug
505/// sink (E15).
506pub async fn closed_sync_facts<P: crate::PendingValue + Clone + Send + 'static>(
507    directory: StoreDir,
508    lock: coven_foundation::files::ClosedStoreLock,
509) -> Result<(crate::SyncFacts<P>, crate::UnsentWork), OpenError> {
510    let bugs = directory.bugs().clone();
511    finish_blocking(
512        blocking_completing("read the closed store's sync status", bugs, move || {
513            let _lock = lock;
514            let settings = directory.settings()?;
515            let mut database = coven_only(&directory.database_path(), true)?;
516            database.prepare_internal_schema()?;
517            let observer = CommitObserver::new(directory.bugs().clone());
518            let transfers = crate::transfers_in_flight::TransfersInFlight::new(observer.clone());
519            let network = crate::network_facts::NetworkFacts::new(observer);
520            database.serve_views(settings.device_id, &transfers, &network)?;
521            database.read_snapshot(|sql| Ok::<_, OpenError>((sql.sync_facts()?, sql.unsent()?)))
522        })
523        .await,
524    )
525}
526
527/// The local creation name in a reset's discarded database. The reset owner
528/// holds its directory lease while reading this setting. Like
529/// `read_store_name`, it reads only `_coven_store_state` and needs none of
530/// the app's collations or functions. A caller that stops waiting leaves
531/// the read to finish, and its panic goes to `bugs` (E15).
532pub async fn discarded_store_name(
533    path: std::path::PathBuf,
534    bugs: &coven_foundation::task::BugSinkRef,
535) -> Result<String, DbError> {
536    finish_blocking(
537        blocking_completing("read the discarded store name", bugs.clone(), move || {
538            let database = coven_only(&path, true)?;
539            crate::store_state_tables::name(&database)
540        })
541        .await,
542    )
543}
544
545/// A connection to a database of coven's own tables only, such as the input
546/// database or a discarded one: no app table, SQLite setting or function
547/// reaches it.
548fn coven_only(path: &std::path::Path, read_only: bool) -> Result<DatabaseConnection, DbError> {
549    DatabaseConnection::open(
550        path,
551        read_only,
552        SqlAuthorization::new(&[]),
553        // The input database is a cache held to its byte budget (§21.1), and
554        // the next received range reuses its freed pages; giving them back
555        // would shrink and regrow the file.
556        &crate::SqliteSettings { auto_vacuum: Some(AutoVacuum::Off), ..Default::default() },
557        &crate::SqlFunctions::default(),
558    )
559}
560
561#[cfg(test)]
562#[path = "database_builder_tests.rs"]
563mod tests;