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;