WIP feat(sqlite): create better constructors for SqliteConnectOptions

This commit is contained in:
Austin Bonander 2024-08-02 04:13:49 -07:00
parent 4acecfc636
commit 9d7a42b700
12 changed files with 642 additions and 102 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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`,

View file

@ -79,22 +79,33 @@ where
#[track_caller]
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
try_spawn_blocking(f)
// the compiler doesn't accept `missing_rt()` as a fn-item
// error[E0271]: expected `missing_rt` to be a fn item that returns `JoinHandle<R>`, but it returns `!`
.unwrap_or_else(|f| missing_rt(f))
}
pub fn try_spawn_blocking<F, R>(f: F) -> Result<JoinHandle<R>, 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() {

View file

@ -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

View file

@ -1,16 +1,4 @@
use crate::connection::handle::ConnectionHandle;
use crate::connection::LogSettings;
use crate::connection::{ConnectionState, Statements};
use crate::error::Error;
use crate::{SqliteConnectOptions, SqliteError};
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,
};
use percent_encoding::NON_ALPHANUMERIC;
use sqlx_core::IndexMap;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::{c_void, CStr, CString};
use std::io;
@ -19,6 +7,24 @@ 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 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
// https://doc.rust-lang.org/stable/std/sync/atomic/index.html#portability
@ -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<String>,
},
}
impl EstablishParams {
pub fn from_options(options: &SqliteConnectOptions) -> Result<Self, Error> {
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 = CString::new(filename).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"filename passed to SQLite must not contain nul bytes",
)
})?;
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")
});
EstablishFilename::Temp {
temp: temp.clone(),
query,
}
}
};
let extensions = options
.extensions
@ -187,12 +219,43 @@ impl EstablishParams {
}
pub(crate) fn establish(&self) -> Result<ConnectionState, Error> {
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();
// <https://www.sqlite.org/c3ref/open.html>
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()
})
}
}

View file

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::ffi::CStr;
use std::fmt::Write;
use std::fmt::{self, Debug, Formatter};
use std::fmt::Write;
use std::os::raw::{c_char, c_int, c_void};
use std::panic::catch_unwind;
use std::ptr;
@ -22,11 +22,11 @@ use sqlx_core::error::Error;
use sqlx_core::executor::Executor;
use sqlx_core::transaction::Transaction;
use crate::{Sqlite, SqliteConnectOptions};
use crate::connection::establish::EstablishParams;
use crate::connection::worker::ConnectionWorker;
use crate::options::OptimizeOnClose;
use crate::options::{OptimizeOnClose, SqliteTempPath};
use crate::statement::VirtualStatement;
use crate::{Sqlite, SqliteConnectOptions};
pub(crate) mod collation;
pub(crate) mod describe;
@ -106,6 +106,12 @@ pub(crate) struct ConnectionState {
progress_handler_callback: Option<Handler>,
update_hook_callback: Option<UpdateHookHandler>,
/// (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<SqliteTempPath>,
}
impl ConnectionState {

View file

@ -20,7 +20,6 @@ use crate::connection::establish::EstablishParams;
use crate::connection::execute;
use crate::connection::ConnectionState;
use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement};
// 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

View file

@ -38,6 +38,7 @@ pub use database::Sqlite;
pub use error::SqliteError;
pub use options::{
SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode, SqliteLockingMode, SqliteSynchronous,
SqliteTempPath, SqliteTempPathBuilder
};
pub use query_result::SqliteQueryResult;
pub use row::SqliteRow;

View file

@ -1,4 +1,27 @@
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;
#[cfg(doc)]
use {
crate::SqlitePool,
sqlx_core::pool::{Pool, PoolOptions},
};
use sqlx_core::IndexMap;
pub use auto_vacuum::SqliteAutoVacuum;
pub use journal_mode::SqliteJournalMode;
pub use locking_mode::SqliteLockingMode;
pub use synchronous::SqliteSynchronous;
pub use temp::{SqliteTempPath, SqliteTempPathBuilder};
use crate::common::DebugFn;
use crate::connection::collation::Collation;
use crate::connection::LogSettings;
mod auto_vacuum;
mod connect;
@ -6,19 +29,7 @@ mod journal_mode;
mod locking_mode;
mod parse;
mod synchronous;
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;
use crate::common::DebugFn;
use crate::connection::collation::Collation;
use sqlx_core::IndexMap;
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<u32> },
@ -94,15 +111,12 @@ pub enum OptimizeOnClose {
impl Default for SqliteConnectOptions {
fn default() -> Self {
Self::new()
Self::memory()
}
}
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<Cow<'static, str>, Option<Cow<'static, str>>> = 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<PathBuf>) -> 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<Path>) -> 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

View file

@ -1,36 +1,26 @@
use std::borrow::Cow;
use std::path::Path;
use std::str::FromStr;
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
use url::Url;
use crate::error::Error;
use crate::SqliteConnectOptions;
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use url::Url;
// https://www.sqlite.org/uri.html
static IN_MEMORY_DB_SEQ: AtomicUsize = AtomicUsize::new(0);
impl SqliteConnectOptions {
pub(crate) fn from_db_and_params(database: &str, params: Option<&str>) -> Result<Self, Error> {
let mut options = Self::default();
if database == ":memory:" {
options.in_memory = true;
options.shared_cache = true;
let seqno = IN_MEMORY_DB_SEQ.fetch_add(1, Ordering::Relaxed);
options.filename = Cow::Owned(PathBuf::from(format!("file:sqlx-in-memory-{seqno}")));
let mut options = if database == ":memory:" {
Self::memory()
} else {
// % decode to allow for `?` or `#` in the filename
options.filename = Cow::Owned(
Path::new(
&*percent_decode_str(database)
.decode_utf8()
.map_err(Error::config)?,
)
.to_path_buf(),
);
}
Self::with_path(Path::new(
&*percent_decode_str(database)
.decode_utf8()
.map_err(Error::config)?,
))
};
if let Some(params) = params {
for (key, value) in url::form_urlencoded::parse(params.as_bytes()) {

View file

@ -0,0 +1,321 @@
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<TempPathInner>,
}
/// Builder for [`SqliteTempPath`].
///
/// Created by [`SqliteTempPath::builder()`].
#[derive(Debug)]
pub struct SqliteTempPathBuilder {
inner: TempPathInner,
}
struct TempPathInner {
db_path: OnceCell<PathBuf>,
parent_dir: Option<PathBuf>,
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<Self> {
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> {
// For whatever reason, autoderef fails here
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(Path::new("db.sqlite3")),
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<PathBuf>) -> &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<PathBuf>) -> &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("<not yet created>"),
|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();
}
}
}

View file

@ -48,8 +48,7 @@ async fn test_context(args: &TestArgs) -> Result<TestContext<Sqlite>, Error> {
}
Ok(TestContext {
connect_opts: SqliteConnectOptions::new()
.filename(&db_path)
connect_opts: SqliteConnectOptions::with_path(&db_path)
.create_if_missing(true),
// This doesn't really matter for SQLite as the databases are independent of each other.
// The main limitation is going to be the number of concurrent running tests.