Skip to main content

coven_database/
sqlite_temporary.rs

1//! The folder SQLite keeps temporary tables in, for every store this
2//! process opens (§21.2).
3//!
4//! Temporary tables spill to files there once a connection's cache fills,
5//! so a staged snapshot waits on disk. SQLite finds a folder for itself
6//! everywhere but Android, where an app names one, such as its cache folder.
7
8use coven_foundation::files::TemporaryFolder;
9use coven_foundation::locks::lock_unpoisoned;
10use std::path::{Path, PathBuf};
11use std::sync::Mutex;
12
13/// What this process has told SQLite, and whether a connection may be using it.
14struct Temporary {
15    directory: Option<TemporaryFolder>,
16    opened: bool,
17}
18
19static TEMPORARY: Mutex<Temporary> = Mutex::new(Temporary { directory: None, opened: false });
20
21/// Why the folder for temporary tables could not be named (E1).
22#[derive(Debug, thiserror::Error)]
23pub enum TemporaryDirectoryError {
24    /// A store's database opened in this process before the folder was named;
25    /// SQLite reads it without a lock, so it is named first.
26    #[error("the temporary folder is named before any store opens")]
27    AfterOpening,
28    /// Another folder was named already. Naming the same one again is allowed.
29    #[error("the temporary folder is already {}", directory.display())]
30    AlreadyNamed {
31        /// The folder named first.
32        directory: PathBuf,
33    },
34    /// The path is not an existing folder, or SQLite cannot hold it: it is not
35    /// UTF-8, or contains a NUL.
36    #[error("{} is not a folder SQLite can keep temporary tables in", directory.display())]
37    NotAFolder {
38        /// The path given.
39        directory: PathBuf,
40    },
41}
42
43/// Keep SQLite's temporary tables in `directory`, for every store this
44/// process opens from now on. Call it before opening any store.
45pub fn set_temporary_directory(directory: &Path) -> Result<(), TemporaryDirectoryError> {
46    let mut temporary = lock_unpoisoned(&TEMPORARY);
47    if let Some(named) = &temporary.directory {
48        if named.path() == directory {
49            return Ok(());
50        }
51        return Err(TemporaryDirectoryError::AlreadyNamed { directory: named.path().to_owned() });
52    }
53    if temporary.opened {
54        return Err(TemporaryDirectoryError::AfterOpening);
55    }
56    let unusable = || TemporaryDirectoryError::NotAFolder { directory: directory.to_owned() };
57    let folder = TemporaryFolder::at(directory).ok_or_else(unusable)?;
58    let text = folder.path().to_str().ok_or_else(unusable)?;
59    let text = std::ffi::CString::new(text).map_err(|_nul| unusable())?;
60    // SAFETY: no connection exists yet, so nothing reads the variable while it
61    // changes, and later connections only read it. SQLite requires its value
62    // to come from sqlite3_mprintf.
63    unsafe {
64        let copy = rusqlite::ffi::sqlite3_mprintf(c"%s".as_ptr(), text.as_ptr());
65        if copy.is_null() {
66            return Err(unusable());
67        }
68        rusqlite::ffi::sqlite3_temp_directory = copy;
69    }
70    temporary.directory = Some(folder);
71    Ok(())
72}
73
74/// Every connection opens after this: the folder can no longer change. It
75/// refuses a SQLite built to keep temporary tables in memory unless a
76/// connection asks for files (`SQLITE_TEMP_STORE=2`) or whatever it asks
77/// (`=3`, libsqlite3-sys's Android default): coven's own temporary tables,
78/// such as a streamed write's staged rows, would then hold all of it in
79/// memory. It refuses one built without `SQLITE_ENABLE_MEMORY_MANAGEMENT`
80/// too: each connection would keep its unused pages apart, and releasing
81/// memory on one connection would leave every other's (§4).
82pub(crate) fn opening() -> Result<(), crate::DbError> {
83    // SAFETY: the option is a NUL-terminated string that outlives the call.
84    if unsafe { rusqlite::ffi::sqlite3_compileoption_used(c"ENABLE_MEMORY_MANAGEMENT".as_ptr()) }
85        == 0
86    {
87        return Err(crate::DbError::Invariant {
88            what: "SQLite keeps every connection's unused pages in one pool (SQLITE_ENABLE_MEMORY_MANAGEMENT)",
89        });
90    }
91    for option in [c"TEMP_STORE=2", c"TEMP_STORE=3"] {
92        // SAFETY: the option is a NUL-terminated string that outlives the call.
93        if unsafe { rusqlite::ffi::sqlite3_compileoption_used(option.as_ptr()) } != 0 {
94            return Err(crate::DbError::Invariant {
95                what: "SQLite keeps temporary tables in files unless a connection asks for memory (SQLITE_TEMP_STORE below 2)",
96            });
97        }
98    }
99    lock_unpoisoned(&TEMPORARY).opened = true;
100    Ok(())
101}