mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 06:24:16 +00:00
WIP feat(sqlite): create better constructors for SqliteConnectOptions
This commit is contained in:
parent
4acecfc636
commit
9d7a42b700
12 changed files with 642 additions and 102 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) {
|
||||
|
|
321
sqlx-sqlite/src/options/temp.rs
Normal file
321
sqlx-sqlite/src/options/temp.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue