Skip to main content

coven_database/
sqlite_settings.rs

1//! The SQLite settings an app may choose for its store's connections, the ones
2//! coven keeps fixed, and the WAL commands an app may run (§4, api.md E1).
3
4use std::num::NonZeroU32;
5use std::time::Duration;
6
7use crate::sqlite::DatabaseConnection;
8use crate::DbError;
9
10/// SQLite settings for every connection coven opens to the store's database:
11/// the writer and the readers of a writable handle, and every connection of a
12/// read-only one. A setting left `None` keeps SQLite's own default, except
13/// `checkpoint_fullfsync` on macOS and iOS, which coven turns on, and
14/// `auto_vacuum`, which is `Incremental`. Coven
15/// applies them as each connection opens, before any of its own statements.
16///
17/// The settings coven's guarantees rest on have no field here: the journal
18/// mode, `synchronous`, the locking mode, foreign keys and the rest stay
19/// fixed (§4).
20///
21/// ```compile_fail
22/// let _ = coven_database::SqliteSettings {
23///     journal_mode: None,
24///     ..Default::default()
25/// };
26/// ```
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
28pub struct SqliteSettings {
29    /// How long a connection waits for a lock another connection holds before
30    /// failing with `SQLITE_BUSY` (`PRAGMA busy_timeout`), at most
31    /// `i32::MAX` milliseconds. Unset keeps the 5 seconds rusqlite sets when
32    /// it opens a connection. RESTART and TRUNCATE checkpoints wait this long
33    /// for readers.
34    pub busy_timeout: Option<Duration>,
35    /// The size of each connection's page cache (`PRAGMA cache_size`).
36    pub cache_size: Option<CacheSize>,
37    /// Whether, and past how many cached pages, a transaction writes changed
38    /// pages to the file before it commits (`PRAGMA cache_spill`).
39    pub cache_spill: Option<CacheSpill>,
40    /// Bytes of the database read through memory-mapped I/O (`PRAGMA
41    /// mmap_size`), at most `i64::MAX`. SQLite lowers it to the largest size
42    /// it was built to map.
43    pub mmap_size: Option<u64>,
44    /// Where temporary tables and indices are kept (`PRAGMA temp_store`).
45    /// On Android, coven's SQLite is built to keep them in memory whatever
46    /// this says.
47    pub temp_store: Option<TempStore>,
48    /// Helper threads one statement may use to sort (`PRAGMA threads`), at
49    /// most `i32::MAX`. SQLite lowers it to the most it was built to allow.
50    pub threads: Option<u32>,
51    /// The size SQLite truncates the WAL file to at the first commit after
52    /// a checkpoint lets the WAL start over (`PRAGMA journal_size_limit`).
53    /// Unset is coven's 4 MiB.
54    pub journal_size_limit: Option<JournalSizeLimit>,
55    /// The WAL size at which coven checkpoints it, off the commit path; unset
56    /// is SQLite's 1000 pages (§4). The writer never checkpoints as it
57    /// commits. A read-only handle's own commits, which only its cache
58    /// makes, checkpoint as SQLite's `PRAGMA wal_autocheckpoint` does.
59    pub wal_autocheckpoint: Option<WalAutocheckpoint>,
60    /// Whether the query planner may build a temporary index for one query
61    /// (`PRAGMA automatic_index`).
62    pub automatic_index: Option<bool>,
63    /// Whether SQLite checks each page's cells for damage as it reads them
64    /// (`PRAGMA cell_size_check`).
65    pub cell_size_check: Option<bool>,
66    /// Whether deleted content is overwritten in the file (`PRAGMA
67    /// secure_delete`).
68    pub secure_delete: Option<SecureDelete>,
69    /// Whether a commit's sync on macOS and iOS also flushes the drive's own
70    /// cache, `F_FULLFSYNC` (`PRAGMA fullfsync`). Other platforms ignore it.
71    /// Unset keeps SQLite's default, off, so a power loss can lose the latest
72    /// commits.
73    pub fullfsync: Option<bool>,
74    /// The same for checkpoints (`PRAGMA checkpoint_fullfsync`). Unset is on
75    /// for macOS and iOS, so a power loss can't leave the database file
76    /// holding only part of a checkpoint after the WAL has started over.
77    pub checkpoint_fullfsync: Option<bool>,
78    /// How many rows of each index `ANALYZE` reads (`PRAGMA analysis_limit`).
79    pub analysis_limit: Option<AnalysisLimit>,
80    /// The page size a store's database is created with (`PRAGMA
81    /// page_size`). It is fixed once the file has its first page: an
82    /// existing store keeps the size it was created with.
83    pub page_size: Option<PageSize>,
84    /// The auto-vacuum mode a store's database is created with (`PRAGMA
85    /// auto_vacuum`). Like the page size, an existing store keeps its own.
86    /// Unset is `Incremental`, and coven gives back free pages past 4 MiB
87    /// each time a claim lands (§4).
88    pub auto_vacuum: Option<AutoVacuum>,
89}
90
91/// The SQLite settings one connection to the store's database has in effect,
92/// as SQLite reports them. Every connection has the same, except that a
93/// writable handle's writer never checkpoints as it commits.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct SqliteSettingsInEffect {
96    /// `PRAGMA busy_timeout`, to the millisecond.
97    pub busy_timeout: Duration,
98    /// `PRAGMA cache_size`.
99    pub cache_size: CacheSize,
100    /// `PRAGMA cache_spill`: when on, the page count past which a
101    /// transaction spills, which is never below the cache's own size.
102    pub cache_spill: CacheSpill,
103    /// `PRAGMA mmap_size`.
104    pub mmap_size: u64,
105    /// `PRAGMA temp_store`.
106    pub temp_store: TempStore,
107    /// `PRAGMA threads`.
108    pub threads: u32,
109    /// `PRAGMA journal_size_limit`.
110    pub journal_size_limit: JournalSizeLimit,
111    /// `PRAGMA wal_autocheckpoint`, which on a writable handle's readers is
112    /// the size at which coven checkpoints. Its writer's own is off: it
113    /// never checkpoints as it commits.
114    pub wal_autocheckpoint: WalAutocheckpoint,
115    /// `PRAGMA automatic_index`.
116    pub automatic_index: bool,
117    /// `PRAGMA cell_size_check`.
118    pub cell_size_check: bool,
119    /// `PRAGMA secure_delete`.
120    pub secure_delete: SecureDelete,
121    /// `PRAGMA fullfsync`.
122    pub fullfsync: bool,
123    /// `PRAGMA checkpoint_fullfsync`.
124    pub checkpoint_fullfsync: bool,
125    /// `PRAGMA analysis_limit`.
126    pub analysis_limit: AnalysisLimit,
127    /// `PRAGMA page_size`.
128    pub page_size: PageSize,
129    /// `PRAGMA auto_vacuum`.
130    pub auto_vacuum: AutoVacuum,
131}
132
133/// A page cache's size, which SQLite counts in pages or in kibibytes.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum CacheSize {
136    /// This many pages, at most `i32::MAX`.
137    Pages(u32),
138    /// As many pages as fit in this many kibibytes, at most `i32::MAX`.
139    Kibibytes(u32),
140}
141
142/// Whether a transaction may spill changed pages before it commits.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum CacheSpill {
145    /// Changed pages stay in memory until commit.
146    Off,
147    /// Spill once the cache holds this many pages, at most `i32::MAX`.
148    Pages(NonZeroU32),
149}
150
151/// Where temporary tables and indices are kept.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum TempStore {
154    /// Where SQLite was built to keep them.
155    Default,
156    /// In a temporary file.
157    File,
158    /// In memory.
159    Memory,
160}
161
162/// How large the WAL file may stay after a checkpoint resets it.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum JournalSizeLimit {
165    /// The file keeps its size.
166    Unlimited,
167    /// The file is truncated to this many bytes, at most `i64::MAX`.
168    Bytes(u64),
169}
170
171/// When coven checkpoints the WAL by itself.
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum WalAutocheckpoint {
174    /// Never: the WAL grows until the app checkpoints it.
175    Off,
176    /// Once the WAL holds this many pages, at most `i32::MAX`.
177    Pages(NonZeroU32),
178}
179
180/// Whether deleted content is overwritten.
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182pub enum SecureDelete {
183    /// Deleted content stays in free pages until they are reused.
184    Off,
185    /// Deleted content is overwritten with zeros.
186    On,
187    /// Overwritten only where that adds no I/O.
188    Fast,
189}
190
191/// How many rows of each index `ANALYZE` reads.
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum AnalysisLimit {
194    /// Every row.
195    Unlimited,
196    /// About this many rows, at most `i32::MAX`.
197    Rows(NonZeroU32),
198}
199
200/// A database page size: a power of two from 512 bytes to 64 KiB.
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub enum PageSize {
203    /// 512 bytes.
204    Bytes512,
205    /// 1 KiB.
206    Bytes1024,
207    /// 2 KiB.
208    Bytes2048,
209    /// 4 KiB, SQLite's default.
210    Bytes4096,
211    /// 8 KiB.
212    Bytes8192,
213    /// 16 KiB.
214    Bytes16384,
215    /// 32 KiB.
216    Bytes32768,
217    /// 64 KiB.
218    Bytes65536,
219}
220
221/// What happens to the pages deleted content frees.
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub enum AutoVacuum {
224    /// They stay in the file for reuse.
225    Off,
226    /// Each commit moves them to the end of the file and truncates it.
227    Full,
228    /// They are given back only when the app runs an incremental vacuum.
229    Incremental,
230}
231
232/// SQLite's checkpoint modes (`PRAGMA wal_checkpoint`).
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub enum CheckpointMode {
235    /// Copy nothing: only report how many frames the WAL holds, and how
236    /// many of them are in the database file.
237    Noop,
238    /// Copy what no reader still needs, without waiting.
239    Passive,
240    /// Wait for the writer lock and for readers, then copy the whole WAL.
241    Full,
242    /// As `Full`, then wait for readers to leave the WAL, so the next commit
243    /// writes the WAL from its start.
244    Restart,
245    /// As `Restart`, then truncate the WAL file to zero bytes.
246    Truncate,
247}
248
249/// What a checkpoint did, as SQLite reports it.
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub enum WalCheckpoint {
252    /// SQLite read nothing of the WAL, so it reports no frames: another
253    /// connection was checkpointing the WAL, or was committing to it as
254    /// SQLite read where it ends. SQLite gives up on this at once, without
255    /// waiting out the busy timeout.
256    NotStarted,
257    /// SQLite read the WAL and copied what it could.
258    Ran {
259        /// A `Full`, `Restart` or `Truncate` checkpoint could not finish
260        /// within the busy timeout, because a reader or another writer held
261        /// on.
262        busy: bool,
263        /// Frames in the WAL.
264        log_frames: u32,
265        /// Frames copied into the database, this time or before.
266        checkpointed_frames: u32,
267    },
268}
269
270/// How many free pages an incremental vacuum gives back.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub enum IncrementalVacuum {
273    /// Every free page.
274    All,
275    /// At most this many, at most `i32::MAX`.
276    Pages(NonZeroU32),
277}
278
279impl PageSize {
280    fn bytes(self) -> u32 {
281        match self {
282            PageSize::Bytes512 => 512,
283            PageSize::Bytes1024 => 1024,
284            PageSize::Bytes2048 => 2048,
285            PageSize::Bytes4096 => 4096,
286            PageSize::Bytes8192 => 8192,
287            PageSize::Bytes16384 => 16384,
288            PageSize::Bytes32768 => 32768,
289            PageSize::Bytes65536 => 65536,
290        }
291    }
292
293    fn from_bytes(bytes: i64) -> Result<Self, DbError> {
294        Ok(match bytes {
295            512 => PageSize::Bytes512,
296            1024 => PageSize::Bytes1024,
297            2048 => PageSize::Bytes2048,
298            4096 => PageSize::Bytes4096,
299            8192 => PageSize::Bytes8192,
300            16384 => PageSize::Bytes16384,
301            32768 => PageSize::Bytes32768,
302            65536 => PageSize::Bytes65536,
303            _ => return Err(unreported()),
304        })
305    }
306}
307
308/// Coven's `checkpoint_fullfsync` where the app leaves it unset. On macOS and
309/// iOS a plain `fsync` can leave writes in the drive's cache, in any order. A
310/// checkpoint copies the WAL into the database file and lets the next commit
311/// write the WAL over from its start, so each of its syncs flushes the drive's
312/// cache with `F_FULLFSYNC`: the database file holds the whole checkpoint
313/// before any of the WAL is overwritten (§3). Commits keep a plain `fsync`.
314#[cfg(any(target_os = "macos", target_os = "ios"))]
315const CHECKPOINT_FULLFSYNC: Option<bool> = Some(true);
316/// Elsewhere SQLite's own default stands.
317#[cfg(not(any(target_os = "macos", target_os = "ios")))]
318const CHECKPOINT_FULLFSYNC: Option<bool> = None;
319
320/// Coven's `journal_size_limit` where the app leaves it unset: once a
321/// checkpoint lets the WAL start over, the file shrinks back to 4 MiB, about
322/// the 1000 pages of 4 KiB at which coven checkpoints by default, instead of
323/// keeping the largest size it ever grew to.
324const JOURNAL_SIZE_LIMIT: JournalSizeLimit = JournalSizeLimit::Bytes(4 << 20);
325
326/// Coven's auto-vacuum mode where the app leaves it unset, so coven can give
327/// back the pages its own queue frees.
328const AUTO_VACUUM: AutoVacuum = AutoVacuum::Incremental;
329
330/// The free pages coven leaves in the file for later writes to reuse, the
331/// same 4 MiB it lets the WAL file keep (`JOURNAL_SIZE_LIMIT`). A store's
332/// outgoing queue refills freed pages with its next writes, so vacuuming them
333/// at every landing would only move pages back and forth; the problem is the
334/// peak left behind.
335const KEPT_FREE_BYTES: u64 = 4 << 20;
336
337/// The WAL size at which coven checkpoints where the app leaves it unset:
338/// SQLite's own automatic checkpoint size.
339pub(crate) const CHECKPOINT_PAGES: NonZeroU32 = NonZeroU32::new(1000).expect("nonzero");
340
341impl SqliteSettings {
342    /// The WAL size at which coven checkpoints it, or `None` if it never does.
343    pub(crate) fn checkpoint_pages(&self) -> Option<NonZeroU32> {
344        match self.wal_autocheckpoint {
345            None => Some(CHECKPOINT_PAGES),
346            Some(WalAutocheckpoint::Pages(pages)) => Some(pages),
347            Some(WalAutocheckpoint::Off) => None,
348        }
349    }
350
351    /// The PRAGMAs that set these choices on any connection.
352    fn connection_pragmas(&self) -> Result<Vec<String>, DbError> {
353        let mut pragmas = Vec::new();
354        if let Some(timeout) = self.busy_timeout {
355            let milliseconds = int("busy_timeout", timeout.as_millis())?;
356            pragmas.push(format!("busy_timeout = {milliseconds}"));
357        }
358        if let Some(size) = self.cache_size {
359            pragmas.push(match size {
360                CacheSize::Pages(pages) => format!("cache_size = {}", int("cache_size", pages)?),
361                CacheSize::Kibibytes(kib) => format!("cache_size = -{}", int("cache_size", kib)?),
362            });
363        }
364        if let Some(spill) = self.cache_spill {
365            pragmas.push(match spill {
366                CacheSpill::Off => "cache_spill = OFF".into(),
367                CacheSpill::Pages(pages) => {
368                    format!("cache_spill = {}", int("cache_spill", pages.get())?)
369                }
370            });
371        }
372        if let Some(bytes) = self.mmap_size {
373            pragmas.push(format!("mmap_size = {}", int64("mmap_size", bytes)?));
374        }
375        if let Some(store) = self.temp_store {
376            let store = match store {
377                TempStore::Default => "DEFAULT",
378                TempStore::File => "FILE",
379                TempStore::Memory => "MEMORY",
380            };
381            pragmas.push(format!("temp_store = {store}"));
382        }
383        if let Some(threads) = self.threads {
384            pragmas.push(format!("threads = {}", int("threads", threads)?));
385        }
386        let bytes = match self.journal_size_limit.unwrap_or(JOURNAL_SIZE_LIMIT) {
387            JournalSizeLimit::Unlimited => -1,
388            JournalSizeLimit::Bytes(bytes) => int64("journal_size_limit", bytes)?,
389        };
390        pragmas.push(format!("journal_size_limit = {bytes}"));
391        if let Some(checkpoint) = self.wal_autocheckpoint {
392            let pages = match checkpoint {
393                WalAutocheckpoint::Off => 0,
394                WalAutocheckpoint::Pages(pages) => int("wal_autocheckpoint", pages.get())?,
395            };
396            pragmas.push(format!("wal_autocheckpoint = {pages}"));
397        }
398        for (name, value) in [
399            ("automatic_index", self.automatic_index),
400            ("cell_size_check", self.cell_size_check),
401            ("fullfsync", self.fullfsync),
402            ("checkpoint_fullfsync", self.checkpoint_fullfsync.or(CHECKPOINT_FULLFSYNC)),
403        ] {
404            if let Some(value) = value {
405                pragmas.push(format!("{name} = {}", if value { "ON" } else { "OFF" }));
406            }
407        }
408        if let Some(delete) = self.secure_delete {
409            let delete = match delete {
410                SecureDelete::Off => "OFF",
411                SecureDelete::On => "ON",
412                SecureDelete::Fast => "FAST",
413            };
414            pragmas.push(format!("secure_delete = {delete}"));
415        }
416        if let Some(limit) = self.analysis_limit {
417            let rows = match limit {
418                AnalysisLimit::Unlimited => 0,
419                AnalysisLimit::Rows(rows) => int("analysis_limit", rows.get())?,
420            };
421            pragmas.push(format!("analysis_limit = {rows}"));
422        }
423        Ok(pragmas)
424    }
425
426    /// The PRAGMAs that shape a new database file. The page size comes first:
427    /// setting the auto-vacuum mode writes the file's first page, which fixes it.
428    fn creation_pragmas(&self) -> Vec<String> {
429        let mut pragmas = Vec::new();
430        if let Some(size) = self.page_size {
431            pragmas.push(format!("page_size = {}", size.bytes()));
432        }
433        let vacuum = match self.auto_vacuum.unwrap_or(AUTO_VACUUM) {
434            AutoVacuum::Off => "NONE",
435            AutoVacuum::Full => "FULL",
436            AutoVacuum::Incremental => "INCREMENTAL",
437        };
438        pragmas.push(format!("auto_vacuum = {vacuum}"));
439        pragmas
440    }
441}
442
443/// The fewest rows a table keeps the planner's statistics for (§4). SQLite
444/// 3.53 planned a full scan over a rowid range lookup for a table it counted
445/// at 2 rows, over an index lookup at 2, and over an index range at 8 to 50.
446const SMALL_TABLE: i64 = 100;
447
448impl DatabaseConnection {
449    /// Set the settings coven fixes, then the app's choices, on a connection
450    /// that has run no statement yet. Changing `temp_store` drops temporary
451    /// objects, so this precedes coven's temporary triggers. A writable
452    /// connection to an empty file also gets its creation settings, before
453    /// anything writes the file.
454    pub(crate) fn configure(
455        &self,
456        settings: &SqliteSettings,
457        read_only: bool,
458    ) -> Result<(), DbError> {
459        self.batch(
460            "PRAGMA foreign_keys = ON; PRAGMA recursive_triggers = ON; PRAGMA trusted_schema = OFF;",
461        )?;
462        let mut pragmas = settings.connection_pragmas()?;
463        if !read_only {
464            let pages: i64 = self.query_row("PRAGMA page_count", [], |r| r.get(0))?;
465            if pages == 0 {
466                pragmas.extend(settings.creation_pragmas());
467            }
468        }
469        if pragmas.is_empty() {
470            return Ok(());
471        }
472        self.batch(&format!("PRAGMA {};", pragmas.join("; PRAGMA ")))
473    }
474
475    /// Put the database in WAL mode and make every commit durable (§4).
476    pub(crate) fn enable_wal(&self) -> Result<(), DbError> {
477        let mode: String = self.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?;
478        if !mode.eq_ignore_ascii_case("wal") {
479            return Err(DbError::WalUnavailable { mode });
480        }
481        self.batch("PRAGMA synchronous = FULL;")
482    }
483
484    pub(crate) fn sqlite_settings(&self) -> Result<SqliteSettingsInEffect, DbError> {
485        let read = |pragma: &str| -> Result<i64, DbError> {
486            self.query_row(&format!("PRAGMA {pragma}"), [], |r| r.get(0))
487        };
488        let positive = |value: i64| u32::try_from(value).ok().and_then(NonZeroU32::new);
489        let cache_size = read("cache_size")?;
490        Ok(SqliteSettingsInEffect {
491            busy_timeout: Duration::from_millis(unsigned(read("busy_timeout")?)?),
492            cache_size: if cache_size < 0 {
493                CacheSize::Kibibytes(unsigned(-cache_size)?)
494            } else {
495                CacheSize::Pages(unsigned(cache_size)?)
496            },
497            cache_spill: match read("cache_spill")? {
498                0 => CacheSpill::Off,
499                spill => CacheSpill::Pages(positive(spill).ok_or_else(unreported)?),
500            },
501            mmap_size: unsigned(read("mmap_size")?)?,
502            temp_store: match read("temp_store")? {
503                0 => TempStore::Default,
504                1 => TempStore::File,
505                2 => TempStore::Memory,
506                _ => return Err(unreported()),
507            },
508            threads: unsigned(read("threads")?)?,
509            journal_size_limit: match read("journal_size_limit")? {
510                -1 => JournalSizeLimit::Unlimited,
511                bytes => JournalSizeLimit::Bytes(unsigned(bytes)?),
512            },
513            wal_autocheckpoint: match read("wal_autocheckpoint")? {
514                0 => WalAutocheckpoint::Off,
515                checkpoint => {
516                    WalAutocheckpoint::Pages(positive(checkpoint).ok_or_else(unreported)?)
517                }
518            },
519            automatic_index: read("automatic_index")? != 0,
520            cell_size_check: read("cell_size_check")? != 0,
521            secure_delete: match read("secure_delete")? {
522                0 => SecureDelete::Off,
523                1 => SecureDelete::On,
524                2 => SecureDelete::Fast,
525                _ => return Err(unreported()),
526            },
527            fullfsync: read("fullfsync")? != 0,
528            checkpoint_fullfsync: read("checkpoint_fullfsync")? != 0,
529            analysis_limit: match read("analysis_limit")? {
530                0 => AnalysisLimit::Unlimited,
531                rows => AnalysisLimit::Rows(positive(rows).ok_or_else(unreported)?),
532            },
533            page_size: PageSize::from_bytes(read("page_size")?)?,
534            auto_vacuum: match read("auto_vacuum")? {
535                0 => AutoVacuum::Off,
536                1 => AutoVacuum::Full,
537                2 => AutoVacuum::Incremental,
538                _ => return Err(unreported()),
539            },
540        })
541    }
542
543    /// Checkpoint the WAL outside any transaction, on a connection in WAL
544    /// mode: the writer, or a checkpointer's own. SQLite reports the frames
545    /// unless it read nothing of the WAL, which it reports as busy with no
546    /// frames.
547    pub(crate) fn wal_checkpoint(&self, mode: CheckpointMode) -> Result<WalCheckpoint, DbError> {
548        let mode = match mode {
549            CheckpointMode::Noop => "NOOP",
550            CheckpointMode::Passive => "PASSIVE",
551            CheckpointMode::Full => "FULL",
552            CheckpointMode::Restart => "RESTART",
553            CheckpointMode::Truncate => "TRUNCATE",
554        };
555        let (busy, log, checkpointed): (i64, i64, i64) =
556            self.query_row(&format!("PRAGMA main.wal_checkpoint({mode})"), [], |r| {
557                Ok((r.get(0)?, r.get(1)?, r.get(2)?))
558            })?;
559        if (busy, log, checkpointed) == (1, -1, -1) {
560            return Ok(WalCheckpoint::NotStarted);
561        }
562        let frames = |count: i64| {
563            u32::try_from(count).map_err(|_overflow| DbError::Invariant {
564                what: "a checkpoint that read the WAL reports its frames",
565            })
566        };
567        Ok(WalCheckpoint::Ran {
568            busy: busy != 0,
569            log_frames: frames(log)?,
570            checkpointed_frames: frames(checkpointed)?,
571        })
572    }
573
574    /// Gather the query planner's statistics where they are missing or stale,
575    /// outside any captured transaction, so none records them (§4).
576    /// `0x10000` checks every table, not only those this connection read;
577    /// `0x10` reads at most SQLite's 2,000 rows of each index, or the app's
578    /// own lower `analysis_limit`, and fewer in a schema of over 100 tables
579    /// and indexes; `0x02` analyzes a table never analyzed, or one whose size
580    /// changed tenfold since. Tables whose statistics hold cost no reads.
581    ///
582    /// Then it forgets the statistics of each table under [`SMALL_TABLE`]
583    /// rows, which SQLite may plan to read whole: once such a table grows,
584    /// every statement would read all of it. Without statistics SQLite
585    /// plans each table as a large one, which a small table answers as
586    /// quickly. The connection keeps planning with the counts it found, so a
587    /// writable open runs this on a connection of its own.
588    pub(crate) fn optimize(&self) -> Result<(), DbError> {
589        self.batch("PRAGMA optimize=0x10012")?;
590        let analyzed: bool = self.query_row(
591            "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE name='sqlite_stat1')",
592            [],
593            |r| r.get(0),
594        )?;
595        if !analyzed {
596            return Ok(());
597        }
598        self.internal_execute(
599            "DELETE FROM sqlite_stat1 WHERE tbl IN (SELECT tbl FROM sqlite_stat1
600             GROUP BY tbl HAVING max(CAST(stat AS INTEGER)) < ?1)",
601            [SMALL_TABLE],
602        )?;
603        Ok(())
604    }
605
606    /// Whether a run of [`Self::optimize`] would change statistics a table
607    /// keeps: `PRAGMA optimize`, told to name its work without doing it,
608    /// names each table whose size changed tenfold since its statistics,
609    /// and each one with rows and an index that has none: one never
610    /// analyzed, and every one under [`SMALL_TABLE`] rows. Reading at most
611    /// that many rows of each tells which a run would keep statistics for.
612    pub(crate) fn statistics_wanted(&self) -> Result<bool, DbError> {
613        let named = self.query("PRAGMA optimize=0x10013", [], |r| r.get::<_, String>(0))?;
614        for analyze in named {
615            let table = analyze.strip_prefix("ANALYZE ").ok_or(DbError::Invariant {
616                what: "PRAGMA optimize names its work as ANALYZE statements",
617            })?;
618            let rows: i64 = self.query_row(
619                &format!("SELECT count(*) FROM (SELECT 1 FROM {table} LIMIT ?1)"),
620                [SMALL_TABLE],
621                |r| r.get(0),
622            )?;
623            if rows >= SMALL_TABLE {
624                return Ok(true);
625            }
626        }
627        Ok(false)
628    }
629
630    /// Gather the statistics as [`Self::optimize`] does, on a connection of
631    /// their own while the store stays open, and change the schema in the
632    /// same transaction: SQLite reads the statistics again only as it reads
633    /// the schema, so every connection, the read-only readers among them,
634    /// plans with the new ones from its next statement, this one included,
635    /// which so loads no count of a table whose statistics it forgot.
636    pub(crate) fn gather_statistics_for_every_connection(&mut self) -> Result<(), DbError> {
637        self.transaction(|connection| {
638            connection.optimize()?;
639            connection.batch(
640                "CREATE TABLE _coven_statistics_gathered(gathered);
641                 DROP TABLE _coven_statistics_gathered",
642            )
643        })
644    }
645
646    /// Gather the statistics on this connection of their own, then close it.
647    pub(crate) fn gather_statistics(self) -> Result<(), DbError> {
648        self.optimize()?;
649        self.close()
650    }
651
652    /// Give back free pages of an incrementally vacuumed database. SQLite
653    /// frees one page per result row, so every row is stepped.
654    pub(crate) fn incremental_vacuum(&self, pages: IncrementalVacuum) -> Result<(), DbError> {
655        let sql = match pages {
656            IncrementalVacuum::All => "PRAGMA main.incremental_vacuum".to_owned(),
657            IncrementalVacuum::Pages(pages) => format!(
658                "PRAGMA main.incremental_vacuum({})",
659                int("incremental_vacuum", pages.get())?
660            ),
661        };
662        self.query(&sql, [], |_| Ok(()))?;
663        Ok(())
664    }
665
666    /// Give back the free pages of an incrementally vacuumed database past
667    /// [`KEPT_FREE_BYTES`]; any other auto-vacuum mode is the app's choice
668    /// and keeps its meaning.
669    pub(crate) fn give_back_free_pages(&self) -> Result<(), DbError> {
670        let pragma = |name: &str| -> Result<u64, DbError> {
671            let value: i64 = self.query_row(&format!("PRAGMA main.{name}"), [], |r| r.get(0))?;
672            unsigned(value)
673        };
674        if pragma("auto_vacuum")? != 2 {
675            return Ok(());
676        }
677        let free = pragma("freelist_count")?;
678        let page_size = pragma("page_size")?;
679        let surplus = free.saturating_sub(KEPT_FREE_BYTES / page_size);
680        if let Some(pages) = u32::try_from(surplus).ok().and_then(NonZeroU32::new) {
681            self.incremental_vacuum(IncrementalVacuum::Pages(pages))?;
682        }
683        Ok(())
684    }
685}
686
687/// `value` as one of SQLite's 32-bit integer settings.
688fn int(setting: &'static str, value: impl Into<u128>) -> Result<i32, DbError> {
689    i32::try_from(value.into())
690        .map_err(|_overflow| DbError::SqliteSettingOutOfRange { setting, maximum: i32::MAX as u64 })
691}
692
693/// `value` as one of SQLite's 64-bit integer settings.
694fn int64(setting: &'static str, value: u64) -> Result<i64, DbError> {
695    i64::try_from(value)
696        .map_err(|_overflow| DbError::SqliteSettingOutOfRange { setting, maximum: i64::MAX as u64 })
697}
698
699fn unsigned<T: TryFrom<i64>>(value: i64) -> Result<T, DbError> {
700    T::try_from(value).map_err(|_overflow| unreported())
701}
702
703/// SQLite reported a setting outside the values it documents.
704fn unreported() -> DbError {
705    DbError::Invariant { what: "SQLite reports a setting within its documented values" }
706}
707
708#[cfg(test)]
709#[path = "sqlite_settings_tests.rs"]
710mod tests;