diff --git a/Cargo.lock b/Cargo.lock index 9a0f789308..8160a86a8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,12 +3563,14 @@ dependencies = [ "futures-util", "libsqlite3-sys", "log", + "once_cell", "percent-encoding", "regex", "serde", "serde_urlencoded", "sqlx", "sqlx-core", + "tempfile", "time", "tracing", "url", diff --git a/Cargo.toml b/Cargo.toml index b73630eac0..b0abc8a05e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,6 +141,8 @@ uuid = "1.1.2" # Common utility crates dotenvy = { version = "0.15.0", default-features = false } +tempfile = "3.10.1" +once_cell = { version = "1.19.0", default-features = false, features = ["std"] } # Runtimes [workspace.dependencies.async-std] @@ -176,7 +178,7 @@ url = "2.2.2" rand = "0.8.4" rand_xoshiro = "0.6.0" hex = "0.4.3" -tempfile = "3.10.1" +tempfile = { workspace = true } criterion = { version = "0.5.1", features = ["async_tokio"] } # If this is an unconditional dev-dependency then Cargo will *always* try to build `libsqlite3-sys`, diff --git a/sqlx-core/src/rt/mod.rs b/sqlx-core/src/rt/mod.rs index 43409073ab..6194e28f55 100644 --- a/sqlx-core/src/rt/mod.rs +++ b/sqlx-core/src/rt/mod.rs @@ -79,22 +79,30 @@ where #[track_caller] pub fn spawn_blocking(f: F) -> JoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + try_spawn_blocking(f).unwrap_or_else(missing_rt) +} + +pub fn try_spawn_blocking(f: F) -> Result, F> where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { #[cfg(feature = "_rt-tokio")] if let Ok(handle) = tokio::runtime::Handle::try_current() { - return JoinHandle::Tokio(handle.spawn_blocking(f)); + return Ok(JoinHandle::Tokio(handle.spawn_blocking(f))); } #[cfg(feature = "_rt-async-std")] { - JoinHandle::AsyncStd(async_std::task::spawn_blocking(f)) + Ok(JoinHandle::AsyncStd(async_std::task::spawn_blocking(f))) } #[cfg(not(feature = "_rt-async-std"))] - missing_rt(f) + Err(f) } pub async fn yield_now() { diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index c530d578fe..66edd2463c 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -45,6 +45,9 @@ tracing = { version = "0.1.37", features = ["log"] } serde = { version = "1.0.145", features = ["derive"], optional = true } regex = { version = "1.5.5", optional = true } +tempfile = { workspace = true } +once_cell = { workspace = true } + [dependencies.libsqlite3-sys] version = "0.30.1" default-features = false diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 6438b6b7f4..58cfcaccac 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -1,23 +1,29 @@ -use crate::connection::handle::ConnectionHandle; -use crate::connection::LogSettings; -use crate::connection::{ConnectionState, Statements}; -use crate::error::Error; -use crate::{SqliteConnectOptions, SqliteError}; +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::ffi::{c_void, CStr, CString}; +use std::io; +use std::os::raw::c_int; +use std::ptr::{addr_of_mut, null, null_mut}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + use libsqlite3_sys::{ sqlite3, sqlite3_busy_timeout, sqlite3_db_config, sqlite3_extended_result_codes, sqlite3_free, sqlite3_load_extension, sqlite3_open_v2, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_OK, SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_SHAREDCACHE, + SQLITE_OPEN_URI, }; use percent_encoding::NON_ALPHANUMERIC; + use sqlx_core::IndexMap; -use std::collections::BTreeMap; -use std::ffi::{c_void, CStr, CString}; -use std::io; -use std::os::raw::c_int; -use std::ptr::{addr_of_mut, null, null_mut}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Duration; + +use crate::connection::handle::ConnectionHandle; +use crate::connection::LogSettings; +use crate::connection::{ConnectionState, Statements}; +use crate::error::Error; +use crate::options::{Filename, SqliteTempPath}; +use crate::{SqliteConnectOptions, SqliteError}; // This was originally `AtomicU64` but that's not supported on MIPS (or PowerPC): // https://github.com/launchbadge/sqlx/issues/2859 @@ -42,7 +48,7 @@ impl SqliteLoadExtensionMode { } pub struct EstablishParams { - filename: CString, + filename: EstablishFilename, open_flags: i32, busy_timeout: Duration, statement_cache_capacity: usize, @@ -54,20 +60,16 @@ pub struct EstablishParams { register_regexp_function: bool, } +enum EstablishFilename { + Owned(CString), + Temp { + temp: SqliteTempPath, + query: Option, + }, +} + impl EstablishParams { pub fn from_options(options: &SqliteConnectOptions) -> Result { - let mut filename = options - .filename - .to_str() - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - "filename passed to SQLite must be valid UTF-8", - ) - })? - .to_owned(); - - // By default, we connect to an in-memory database. // [SQLITE_OPEN_NOMUTEX] will instruct [sqlite3_open_v2] to return an error if it // cannot satisfy our wish for a thread-safe, lock-free connection object @@ -105,21 +107,51 @@ impl EstablishParams { query_params.insert("vfs", vfs); } - if !query_params.is_empty() { - filename = format!( - "file:{}?{}", - percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC), - serde_urlencoded::to_string(&query_params).unwrap() - ); - flags |= libsqlite3_sys::SQLITE_OPEN_URI; - } + let filename = match &options.filename { + Filename::Owned(owned) => { + let filename_str = owned.to_str().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "filename passed to SQLite must be valid UTF-8", + ) + })?; + + let filename = if !query_params.is_empty() { + flags |= SQLITE_OPEN_URI; + + format!( + "file:{}?{}", + percent_encoding::percent_encode(filename_str.as_bytes(), NON_ALPHANUMERIC), + serde_urlencoded::to_string(&query_params) + .expect("BUG: failed to URL encode query parameters") + ) + } else { + filename_str.to_string() + }; + + let filename = CString::new(filename).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "filename passed to SQLite must not contain nul bytes", + ) + })?; + + EstablishFilename::Owned(filename) + } + Filename::Temp(temp) => { + let query = (!query_params.is_empty()).then(|| { + flags |= SQLITE_OPEN_URI; + + serde_urlencoded::to_string(&query_params) + .expect("BUG: failed to URL encode query parameters") + }); - let filename = CString::new(filename).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "filename passed to SQLite must not contain nul bytes", - ) - })?; + EstablishFilename::Temp { + temp: temp.clone(), + query, + } + } + }; let extensions = options .extensions @@ -187,12 +219,43 @@ impl EstablishParams { } pub(crate) fn establish(&self) -> Result { + let mut open_flags = self.open_flags; + + let (filename, temp) = match &self.filename { + EstablishFilename::Owned(cstr) => (Cow::Borrowed(&**cstr), None), + EstablishFilename::Temp { temp, query } => { + let path = temp.force_create_blocking()?.to_str().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "filename passed to SQLite must be valid UTF-8", + ) + })?; + + let filename = if let Some(query) = query { + // Ensure the flag is set. + open_flags |= SQLITE_OPEN_URI; + format!("file:{path}?{query}") + } else { + path.to_string() + }; + + ( + Cow::Owned(CString::new(filename).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "filename passed to SQLite must not contain nul bytes", + ) + })?), + Some(temp) + ) + } + }; + let mut handle = null_mut(); // - let mut status = unsafe { - sqlite3_open_v2(self.filename.as_ptr(), &mut handle, self.open_flags, null()) - }; + let mut status = + unsafe { sqlite3_open_v2(filename.as_ptr(), &mut handle, open_flags, null()) }; if handle.is_null() { // Failed to allocate memory @@ -296,6 +359,7 @@ impl EstablishParams { log_settings: self.log_settings.clone(), progress_handler_callback: None, update_hook_callback: None, + _temp: temp.cloned() }) } } diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index 3588b94f82..e4f8583a24 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -6,7 +6,7 @@ use std::os::raw::{c_char, c_int, c_void}; use std::panic::catch_unwind; use std::ptr; use std::ptr::NonNull; - +use std::sync::Arc; use futures_core::future::BoxFuture; use futures_intrusive::sync::MutexGuard; use futures_util::future; @@ -24,7 +24,7 @@ use sqlx_core::transaction::Transaction; use crate::connection::establish::EstablishParams; use crate::connection::worker::ConnectionWorker; -use crate::options::OptimizeOnClose; +use crate::options::{OptimizeOnClose, SqliteTempPath, TempFilename}; use crate::statement::VirtualStatement; use crate::{Sqlite, SqliteConnectOptions}; @@ -106,6 +106,12 @@ pub(crate) struct ConnectionState { progress_handler_callback: Option, update_hook_callback: Option, + + /// (MUST BE LAST) If applicable, hold a strong ref to the temporary directory + /// until the connection is closed. + /// + /// When the last strong ref is dropped, the temporary directory is deleted. + pub(crate) _temp: Option, } impl ConnectionState { diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index a01de2419c..48dad41646 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -20,7 +20,7 @@ use crate::connection::establish::EstablishParams; use crate::connection::execute; use crate::connection::ConnectionState; use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; - +use crate::options::TempFilename; // Each SQLite connection has a dedicated thread. // TODO: Tweak this so that we can use a thread pool per pool of SQLite3 connections to reduce diff --git a/sqlx-sqlite/src/options/mod.rs b/sqlx-sqlite/src/options/mod.rs index 94e37815ec..d9330b5406 100644 --- a/sqlx-sqlite/src/options/mod.rs +++ b/sqlx-sqlite/src/options/mod.rs @@ -1,24 +1,35 @@ -use std::path::Path; +use std::{borrow::Cow, time::Duration}; +use std::cmp::Ordering; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; +use std::sync::Arc; -mod auto_vacuum; -mod connect; -mod journal_mode; -mod locking_mode; -mod parse; -mod synchronous; +#[cfg(doc)] +use { + crate::SqlitePool, + sqlx_core::pool::{Pool, PoolOptions}, +}; + +use sqlx_core::IndexMap; -use crate::connection::LogSettings; pub use auto_vacuum::SqliteAutoVacuum; pub use journal_mode::SqliteJournalMode; pub use locking_mode::SqliteLockingMode; -use std::cmp::Ordering; -use std::sync::Arc; -use std::{borrow::Cow, time::Duration}; pub use synchronous::SqliteSynchronous; +pub use temp::{SqliteTempPath, SqliteTempPathBuilder}; use crate::common::DebugFn; use crate::connection::collation::Collation; -use sqlx_core::IndexMap; +use crate::connection::LogSettings; + + +mod auto_vacuum; +mod connect; +mod journal_mode; +mod locking_mode; +mod parse; +mod synchronous; +mod temp; /// Options and flags which can be used to configure a SQLite connection. /// @@ -54,7 +65,7 @@ use sqlx_core::IndexMap; /// ``` #[derive(Clone, Debug)] pub struct SqliteConnectOptions { - pub(crate) filename: Cow<'static, Path>, + pub(crate) filename: Filename, pub(crate) in_memory: bool, pub(crate) read_only: bool, pub(crate) create_if_missing: bool, @@ -86,6 +97,12 @@ pub struct SqliteConnectOptions { pub(crate) register_regexp_function: bool, } +#[derive(Clone, Debug)] +pub(crate) enum Filename { + Owned(PathBuf), + Temp(SqliteTempPath), +} + #[derive(Clone, Debug)] pub enum OptimizeOnClose { Enabled { analysis_limit: Option }, @@ -99,10 +116,7 @@ impl Default for SqliteConnectOptions { } impl SqliteConnectOptions { - /// Construct `Self` with default options. - /// - /// See the source of this method for the current defaults. - pub fn new() -> Self { + fn with_filename(filename: Filename) -> Self { let mut pragmas: IndexMap, Option>> = IndexMap::new(); // Standard pragmas @@ -186,7 +200,7 @@ impl SqliteConnectOptions { pragmas.insert("analysis_limit".into(), None); Self { - filename: Cow::Borrowed(Path::new(":memory:")), + filename, in_memory: false, read_only: false, create_if_missing: false, @@ -209,19 +223,132 @@ impl SqliteConnectOptions { } } + /// Start building a SQLite connection using a path to a database file, with default settings. + /// + /// See the source file for current defaults. + pub fn with_path(path: impl Into) -> Self { + // `with_filename()` is the common constructor + Self::with_filename(Filename::Owned(path.into())) + } + + /// Start building a SQLite connection to an in-memory database, with default settings. + /// + /// If [shared-cache mode] mode is enabled, this generates a unique temporary path + /// inside [`std::env::temp_dir()`] for the shared-memory file to reside. + /// + /// ## Note: Usage with `Pool` + /// An in-memory database with default settings should **not** be used with + /// [`Pool`] ([`SqlitePool`]), as multiple connections **will not share data**, + /// even with the same `SqliteConnectOptions` instance. + /// + /// You can work around this by enabling [shared-cache mode], but as the + /// [`SQLITE_OPEN_SHAREDCACHE` option is soft-deprecated by SQLite][shared-cache-discouraged] + /// ("is discouraged"), you should probably use a database backed by a tempfile + /// instead ([`Self::temp()`]). + /// + /// If you *do* insist on use this with `Pool`, we recommend the following settings: + /// + /// * Set [`.shared_cache(true)`][shared-cache mode] on this `ConnectOptions`. + /// * Or set [`PoolOptions::max_connections()`] to 1, making the pool effectively a `Mutex`; + /// * Or just wrap a single connection explicitly in an async `Mutex` + /// (both Tokio and `async-std` have mutexes). + /// * Set [`PoolOptions::max_lifetime()`] and [`PoolOptions::idle_timeout()`] to `None`. + /// * This will prevent the pool from reaping connections. + /// * Note that unrecoverable errors will still cause connections to be closed. + /// * Set [`PoolOptions::min_connections()`] to a non-zero value. + /// * This will reduce (but not eliminate) the chance of data loss. + /// + /// Even with these settings, + /// **the contents of the database are not guaranteed to be retained**. + /// Your application should be designed to handle and recover from data loss + /// when using this mode. + /// + /// [shared-cache mode]: Self::shared_cache + /// [shared-cache-discouraged]: https://www.sqlite.org/sharedcache.html#dontuse + pub fn memory() -> Self { + Self::temp_in(SqliteTempPath::lazy_file()).in_memory(true) + } + + /// Start building a SQLite connection that will use a unique path in the OS temp directory. + /// + /// This will create a directory under [`std::env::temp_dir()`] using [`tempfile::TempDir`]. + /// + /// The created directory, and all its contents, will be dropped when this `ConnectOptions` + /// and all created connections have been closed. + /// + /// The path of the directory and database file will not be available until a connection + /// is attempted. If you need the path ahead of time, use [`Self::temp_in()`] + /// with an explicitly created [`SqliteTempPath`]. + pub fn temp() -> Self { + Self::temp_in(SqliteTempPath::lazy_dir()) + } + + + /// Start building a SQLite connection that will use the given temporary path. + /// + /// The given [`SqliteTempPath`] handle will be used to open all connections made with + /// this `ConnectOptions`. + /// + /// All created connections, and clones of this `ConnectOptions`, will retain + /// an instance of the `SqliteTempPath` to ensure it is not deleted until + /// it is no longer being used. + pub fn temp_in(path: SqliteTempPath) -> Self { + Self::with_filename(Filename::Temp(path)) + } + + /// Construct `Self` with default options. + /// + /// See the source file for the current defaults. + #[deprecated = + "Deprecated in favor of specialized constructors; \ + use `SqliteConnectOptions::with_path()`, `::memory()`, or `::temp()`" + ] + pub fn new() -> Self { + Self::memory() + } + /// Sets the name of the database file. /// /// This is a low-level API, and SQLx will apply no special treatment for `":memory:"` as an - /// in-memory database using this method. Using [`SqliteConnectOptions::from_str()`][SqliteConnectOptions#from_str] may be + /// in-memory database using this method. + /// + /// Using [`SqliteConnectOptions::from_str()`][SqliteConnectOptions#from_str] may be /// preferred for simple use cases. + /// + /// ### Note: Discards Temporary Path + /// If this `ConnectOptions` was created with [`Self::temp()`] or [`Self::temp_in()`], + /// this method will discard the [`SqliteTempPath`] instance that was previously held. pub fn filename(mut self, filename: impl AsRef) -> Self { - self.filename = Cow::Owned(filename.as_ref().to_owned()); + self.filename = Filename::Owned(filename.as_ref().into()); self } /// Gets the current name of the database file. + /// + /// ### Panics + /// If this `ConnectOptions` was created with [`Self::temp()`], + /// or [`Self::temp_in()`] with a lazily created path, + /// and a database connection has yet to be opened. + /// + /// See [`.filename_opt()`][Self::filename_opt] for a fallible version. pub fn get_filename(&self) -> &Path { - &self.filename + self.filename_opt() + .expect("failed to create temp file") + } + + /// Gets the current name of the database file. + /// + /// Returns `None` if this `ConnectOptions` was created with [`Self::temp()`], + /// or [`Self::temp_in()`] with a lazily created path, + /// and a database connection has yet to be opened. + pub fn filename_opt(&self) -> Option<&Path> { + match &self.filename { + Filename::Owned(path) => Some(path), + Filename::Temp(temp) => { + temp.get_db_path() + .or_else(|| self.in_memory.then_some(Path::new(":memory:"))) + } + } } /// Set the enforcement of [foreign key constraints](https://www.sqlite.org/pragma.html#pragma_foreign_keys). @@ -243,6 +370,21 @@ impl SqliteConnectOptions { /// Set the [`SQLITE_OPEN_SHAREDCACHE` flag](https://sqlite.org/sharedcache.html). /// /// By default, this is disabled. + /// + /// ### Note: Soft-Deprecated by SQLite + /// [Use of shared-cache mode is soft-deprecated by SQLite][shared-mode-discouraged] + /// ("is discouraged"). + /// + /// Instead, SQLite recommends using [Write-Ahead Log (WAL) mode], which can be enabled + /// by setting [`.journal_mode(SqliteJournalMode::Wal)`][Self::journal_mode]. + /// Note, however, that WAL mode is a persistent setting; it requires locking the database file + /// to change into or out of. + /// + /// WAL mode also cannot be used with in-memory databases + /// ([`Self::memory()`], [`.in_memory()`][Self::in_memory]). + /// + /// [shared-cache-discouraged]: https://www.sqlite.org/sharedcache.html#dontuse + /// [Write-Ahead Log (WAL) mode]: https://www.sqlite.org/wal.html pub fn shared_cache(mut self, on: bool) -> Self { self.shared_cache = on; self diff --git a/sqlx-sqlite/src/options/temp.rs b/sqlx-sqlite/src/options/temp.rs new file mode 100644 index 0000000000..26d0826e5c --- /dev/null +++ b/sqlx-sqlite/src/options/temp.rs @@ -0,0 +1,320 @@ +use std::fmt::{Debug, Formatter}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::{io, mem}; +use std::borrow::Cow; +use std::ffi::OsString; +use once_cell::sync::OnceCell; + +#[cfg(doc)] +use crate::{SqliteConnectOptions, SqliteConnection}; + +/// Handle tracking a named, temporary path for a SQLite database. +/// +/// The path will be deleted when the last handle is dropped. +/// +/// If the path represents a file ([`Self::lazy_file()`], [`Self::builder().file_mode()`]), +/// then only the file itself will be deleted. Any temporary files created by SQLite, +/// if not automatically deleted by SQLite itself, will remain in the parent directory. +/// +/// If the path represents a directory ([`Self::lazy_dir()`], [`Self::builder().dir_mode()`]), +/// then the directory and all its contents, including any other files created by SQLite, +/// will be deleted. +/// +/// The handle can be cloned and shared with other threads. +/// [`SqliteConnectOptions`] will retain a handle, as will its clones, +/// as will any [`SqliteConnection`]s opened with them. +/// +/// [`Self::builder().file_mode()`]: SqliteTempPathBuilder::file_mode +/// [`Self::builder().dir_mode()`]: SqliteTempPathBuilder::file_mode +#[derive(Clone, Debug)] +pub struct SqliteTempPath { + inner: Arc, +} + +/// Builder for [`SqliteTempPath`]. +/// +/// Created by [`SqliteTempPath::builder()`]. +#[derive(Debug)] +pub struct SqliteTempPathBuilder { + inner: TempPathInner, +} + +struct TempPathInner { + db_path: OnceCell, + + parent_dir: Option, + filename: Cow<'static, Path>, + is_dir: bool, + create_missing_parents: bool, +} + +impl SqliteTempPath { + /// Lazily create a temporary file in [`std::env::temp_dir()`]. + /// + /// The file will not be created until the first connection. + /// + /// The file will be deleted when the last instance of this handle is dropped. + /// + /// For advanced configuration, use [`Self::builder()`] to get a [`SqliteTempPathBuilder`]. + pub fn lazy_file() -> Self { + Self::builder().build() + } + + /// Lazily create a temporary directory in [`std::env::temp_dir()`]. + /// + /// The directory will not be created until the first connection. + /// + /// The directory and all its contents, including any other files created by SQLite, + /// will be deleted when the last instance of this handle is dropped. + /// + /// For advanced configuration, use [`Self::builder()`] to get a [`SqliteTempPathBuilder`]. + pub fn lazy_dir() -> Self { + Self::builder().dir_mode().build() + } + + /// Create a temporary directory immediately, returning the handle. + /// + /// This will spawn a blocking task in the current runtime. + /// + /// ### Panics + /// If no runtime is available. + pub async fn create_dir() -> io::Result { + let this = Self::lazy_dir(); + this.force_create().await?; + Ok(this) + } + + /// Get a builder to configure a new temporary path. + /// + /// See [`SqliteTempPathBuilder`] for details. + pub fn builder() -> SqliteTempPathBuilder { + SqliteTempPathBuilder::new() + } + + /// Create the temporary path immediately, returning the path to the database file. + /// + /// If the path has already been created, this returns immediately. + /// + /// This will spawn a blocking task in the current runtime to create the path. + /// + /// ### Panics + /// If no runtime is available. + /// + /// See [`.force_create_blocking()`][Self::force_create_blocking] + /// for a version that blocks instead of spawning a task. + pub async fn force_create(&self) -> io::Result<&Path> { + let this = self.clone(); + + sqlx_core::rt::spawn_blocking(move || this.force_create_blocking().map(|_| ())).await?; + + Ok(self + .inner + .db_path + .get() + .expect("BUG: `self.inner` should be initialized at this point!")) + } + + /// Create the temporary path immediately, returning the path to the database file. + /// + /// If the path has already been created, this returns immediately. + /// + /// ### Blocking + /// This requires touching the filesystem, which may block the current thread. + /// + /// See [`.force_create()`][Self::force_create] for an asynchronous version + /// that uses a background task instead of blocking, but requires an async runtime. + pub fn force_create_blocking(&self) -> io::Result<&Path> { + Ok(self.inner.try_get()?) + } + + /// Return the path to the database file if the path has been created. + /// + /// If this handle represents a directory, the database file may not exist yet. + pub fn get_db_path(&self) -> Option<&Path> { + self.inner.db_path.get().map(|p| p) + } +} + +impl SqliteTempPathBuilder { + fn new() -> Self { + Self { + inner: TempPathInner { + db_path: OnceCell::new(), + parent_dir: None, + filename: Cow::Borrowed("db.sqlite3".into()), + is_dir: false, + create_missing_parents: true, + } + } + } + + /// Configure the builder for creating a temporary file. + /// + /// This is the default. + pub fn file_mode(&mut self) -> &mut Self { + self.inner.is_dir = false; + self + } + + /// Configure the builder for creating a temporary directory. + pub fn dir_mode(&mut self) -> &mut Self { + self.inner.is_dir = true; + self + } + + /// Set the parent directory to use instead of [`std::env::temp_dir()`]. + /// + /// Use [`.create_missing_parents()`][Self::create_missing_parents] to set + /// whether any missing directories in this path are created, or not. + pub fn parent_dir(&mut self, parent_dir: impl Into) -> &mut Self { + self.inner.parent_dir = Some(parent_dir.into()); + self + } + + /// Set `true` to create any missing parent directories, `false` to error. + /// + /// Defaults to `true`. + pub fn create_missing_parents(&mut self, value: bool) -> &mut Self { + self.inner.create_missing_parents = value; + self + } + + /// Set the database filename to use, if building a directory, or filename suffix otherwise. + /// + /// Use of path separators is not recommended. + pub fn filename(&mut self, filename: impl Into) -> &mut Self { + self.inner.filename = Cow::Owned(filename.into()); + self + } + + /// Build a [`SqliteTempPath`] with the given [`tempfile::Tempdir`]. + /// + /// The lifetime of the `TempDir` will be managed by `SqliteTempPath`. + /// + /// This will clear the [`parent_dir`][Self::parent_dir] setting + /// and switch to [`dir_mode`][Self::dir_mode]. + /// + /// The builder may be reused afterward, but is reset to default settings. + pub fn build_with_tempdir(&mut self, tempdir: tempfile::TempDir) -> SqliteTempPath { + let mut inner = self.take_inner(); + + inner.parent_dir = None; + inner.is_dir = true; + + // Panic safety: don't disarm `TempDir` until we've set `db_path`. + inner.db_path.set(tempdir.path().join(&inner.filename)) + .expect("BUG: `db_path` already initialized"); + + mem::forget(tempdir); + + SqliteTempPath { + inner: Arc::new(inner), + } + } + + /// Build a [`SqliteTempPath`] with the given settings. + /// + /// The builder may be reused afterward, but is reset to default settings. + pub fn build(&mut self) -> SqliteTempPath { + SqliteTempPath { + inner: Arc::new(self.take_inner()), + } + } + + fn take_inner(&mut self) -> TempPathInner { + mem::replace(self, Self::new()).inner + } +} + +impl TempPathInner { + fn try_get(&self) -> io::Result<&PathBuf> { + self.db_path.get_or_try_init(move || { + let mut builder = tempfile::Builder::new(); + + builder.prefix("sqlx-sqlite"); + + if self.is_dir { + let mut path = self + .parent_dir + .as_ref() + .map_or_else(|| builder.tempdir(), |parent| builder.tempdir_in(parent))? + .into_path(); + + path.push(&self.filename); + + Ok(path) + } else { + builder.suffix(&*self.filename); + + Ok(self + .parent_dir + .as_ref() + .map_or_else(|| builder.tempfile(), |parent| builder.tempfile_in(parent))? + .into_temp_path() + // Uses `FileSetAttributeW(FILE_ATTRIBUTE_TEMPORARY)` on Windows + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#caching_behavior + .keep()?) + } + }) + } +} + +impl Debug for TempPathInner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TempPathInner") + .field( + "db_path", + &self.db_path.get() + .map_or( + Path::new(""), + |p| p, + ), + ) + .field("parent_dir", &self.parent_dir) + .field("filename", &self.filename) + .field("is_dir", &self.is_dir) + .field("create_missing_parents", &self.create_missing_parents) + .finish() + } +} + +impl Drop for TempPathInner { + fn drop(&mut self) { + let Some(path) = self.db_path.take() else { + return; + }; + + let remove_dir_all = self.is_dir; + + // Drop the path on a blocking task or fall back to executing it synchronously. + let res = sqlx_core::rt::try_spawn_blocking(move || { + let res = if let Some(Some(dir)) = remove_dir_all.then(|| path.parent()) { + std::fs::remove_dir_all(dir) + } else { + std::fs::remove_file(&path) + }; + + match res { + Ok(()) => { + tracing::debug!(remove_dir_all, "successfully deleted SqliteTempPath"); + } + Err(e) if e.kind() == io::ErrorKind::NotFound => { + tracing::debug!( + remove_dir_all, + "did not delete SqliteTempPath, not found (error {e:?})" + ); + } + Err(e) => { + tracing::warn!(remove_dir_all, "error deleting SqliteTempPath: {e:?}"); + } + } + }); + + // If a runtime is not available, it's likely we're shutting down or on a worker thread. + // Either way, we can just block. + if let Err(remove_sync) = res { + remove_sync(); + } + } +}