Skip to content

Commit bc3e705

Browse files
authored
sqlite improvements (#1965)
* use direct blocking calls for SQLite in `sqlx_macros` * this also ensures the database is closed properly, cleaning up tempfiles * don't send `PRAGMA journal_mode` unless set * this previously defaulted to WAL mode which is a permanent setting on databases which doesn't necessarily apply to all use-cases * changing into or out of WAL mode acquires an exclusive lock on the database that can't be waited on by `sqlite3_busy_timeout()` * for consistency, `sqlx-cli` commands that create databases will still create SQLite databases in WAL mode; added a flag to disable this. * in general, don't send `PRAGMA`s unless different than default * we were sending a bunch of `PRAGMA`s with their default values just to enforce an execution order on them, but we can also do this by inserting empty slots for their keys into the `IndexMap` * add error code to `SqliteError` printout * document why `u64` is not supported
1 parent d9fd21c commit bc3e705

File tree

14 files changed

+236
-133
lines changed

14 files changed

+236
-133
lines changed

sqlx-cli/src/database.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> {
1111
let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?;
1212

1313
if !exists {
14+
#[cfg(feature = "sqlite")]
15+
sqlx::sqlite::CREATE_DB_WAL.store(
16+
connect_opts.sqlite_create_db_wal,
17+
std::sync::atomic::Ordering::Release,
18+
);
19+
1420
Any::create_database(&connect_opts.database_url).await?;
1521
}
1622

sqlx-cli/src/opt.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ pub struct ConnectOpts {
221221
/// returning an error.
222222
#[clap(long, default_value = "10")]
223223
pub connect_timeout: u64,
224+
225+
/// Set whether or not to create SQLite databases in Write-Ahead Log (WAL) mode:
226+
/// https://www.sqlite.org/wal.html
227+
///
228+
/// WAL mode is enabled by default for SQLite databases created by `sqlx-cli`.
229+
///
230+
/// However, if your application sets a `journal_mode` on `SqliteConnectOptions` to something
231+
/// other than `Wal`, then it will have to take the database file out of WAL mode on connecting,
232+
/// which requires an exclusive lock and may return a `database is locked` (`SQLITE_BUSY`) error.
233+
#[cfg(feature = "sqlite")]
234+
#[clap(long, action = clap::ArgAction::Set, default_value = "true")]
235+
pub sqlite_create_db_wal: bool,
224236
}
225237

226238
/// Argument for automatic confirmation.

sqlx-core/src/sqlite/connection/describe.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::sqlite::{Sqlite, SqliteColumn};
88
use either::Either;
99
use std::convert::identity;
1010

11-
pub(super) fn describe(conn: &mut ConnectionState, query: &str) -> Result<Describe<Sqlite>, Error> {
11+
pub(crate) fn describe(conn: &mut ConnectionState, query: &str) -> Result<Describe<Sqlite>, Error> {
1212
// describing a statement from SQLite can be involved
1313
// each SQLx statement is comprised of multiple SQL statements
1414

sqlx-core/src/sqlite/connection/execute.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ fn bind(
5353
Ok(n)
5454
}
5555

56+
impl ExecuteIter<'_> {
57+
pub fn finish(&mut self) -> Result<(), Error> {
58+
for res in self {
59+
let _ = res?;
60+
}
61+
62+
Ok(())
63+
}
64+
}
65+
5666
impl Iterator for ExecuteIter<'_> {
5767
type Item = Result<Either<SqliteQueryResult, SqliteRow>, Error>;
5868

sqlx-core/src/sqlite/connection/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use crate::sqlite::{Sqlite, SqliteConnectOptions};
1919
use crate::transaction::Transaction;
2020

2121
pub(crate) mod collation;
22-
mod describe;
23-
mod establish;
24-
mod execute;
22+
pub(crate) mod describe;
23+
pub(crate) mod establish;
24+
pub(crate) mod execute;
2525
mod executor;
2626
mod explain;
2727
mod handle;

sqlx-core/src/sqlite/error.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ impl SqliteError {
3939

4040
impl Display for SqliteError {
4141
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
42-
f.pad(&self.message)
42+
// We include the code as some produce ambiguous messages:
43+
// SQLITE_BUSY: "database is locked"
44+
// SQLITE_LOCKED: "database table is locked"
45+
// Sadly there's no function to get the string label back from an error code.
46+
write!(f, "(code: {}) {}", self.code, self.message)
4347
}
4448
}
4549

sqlx-core/src/sqlite/migrate.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,32 @@ use crate::migrate::{Migrate, MigrateDatabase};
77
use crate::query::query;
88
use crate::query_as::query_as;
99
use crate::query_scalar::query_scalar;
10-
use crate::sqlite::{Sqlite, SqliteConnectOptions, SqliteConnection};
10+
use crate::sqlite::{Sqlite, SqliteConnectOptions, SqliteConnection, SqliteJournalMode};
1111
use futures_core::future::BoxFuture;
1212
use sqlx_rt::fs;
1313
use std::str::FromStr;
14+
use std::sync::atomic::Ordering;
1415
use std::time::Duration;
1516
use std::time::Instant;
1617

1718
impl MigrateDatabase for Sqlite {
1819
fn create_database(url: &str) -> BoxFuture<'_, Result<(), Error>> {
1920
Box::pin(async move {
21+
let mut opts = SqliteConnectOptions::from_str(url)?.create_if_missing(true);
22+
23+
// Since it doesn't make sense to include this flag in the connection URL,
24+
// we just use an `AtomicBool` to pass it.
25+
if super::CREATE_DB_WAL.load(Ordering::Acquire) {
26+
opts = opts.journal_mode(SqliteJournalMode::Wal);
27+
}
28+
2029
// Opening a connection to sqlite creates the database
2130
let _ = SqliteConnectOptions::from_str(url)?
2231
.create_if_missing(true)
2332
.connect()
33+
.await?
34+
// Ensure WAL mode tempfiles are cleaned up
35+
.close()
2436
.await?;
2537

2638
Ok(())

sqlx-core/src/sqlite/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ pub use options::{
1616
pub use query_result::SqliteQueryResult;
1717
pub use row::SqliteRow;
1818
pub use statement::SqliteStatement;
19+
use std::sync::atomic::AtomicBool;
1920
pub use transaction::SqliteTransactionManager;
2021
pub use type_info::SqliteTypeInfo;
2122
pub use value::{SqliteValue, SqliteValueRef};
2223

24+
use crate::describe::Describe;
25+
use crate::error::Error;
2326
use crate::executor::Executor;
27+
use crate::sqlite::connection::establish::EstablishParams;
2428

2529
mod arguments;
2630
mod column;
@@ -60,3 +64,24 @@ impl_into_maybe_pool!(Sqlite, SqliteConnection);
6064

6165
// required because some databases have a different handling of NULL
6266
impl_encode_for_option!(Sqlite);
67+
68+
/// UNSTABLE: for use by `sqlx-cli` only.
69+
#[doc(hidden)]
70+
pub static CREATE_DB_WAL: AtomicBool = AtomicBool::new(true);
71+
72+
/// UNSTABLE: for use by `sqlite_macros` only.
73+
#[doc(hidden)]
74+
pub fn describe_blocking(
75+
opts: &SqliteConnectOptions,
76+
query: &str,
77+
) -> Result<Describe<Sqlite>, Error> {
78+
let params = EstablishParams::from_options(opts)?;
79+
let mut conn = params.establish()?;
80+
81+
// Execute any ancillary `PRAGMA`s
82+
connection::execute::iter(&mut conn, &opts.pragma_string(), None, false)?.finish()?;
83+
84+
connection::describe::describe(&mut conn, query)
85+
86+
// SQLite database is closed immediately when `conn` is dropped
87+
}

sqlx-core/src/sqlite/options/connect.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,8 @@ impl ConnectOptions for SqliteConnectOptions {
1717
Box::pin(async move {
1818
let mut conn = SqliteConnection::establish(self).await?;
1919

20-
// send an initial sql statement comprised of options
21-
let mut init = String::new();
22-
23-
// This is a special case for sqlcipher. When the `key` pragma
24-
// is set, we have to make sure it's executed first in order.
25-
if let Some(pragma_key_password) = self.pragmas.get("key") {
26-
write!(init, "PRAGMA key = {}; ", pragma_key_password).ok();
27-
}
28-
29-
for (key, value) in &self.pragmas {
30-
// Since we've already written the possible `key` pragma
31-
// above, we shall skip it now.
32-
if key == "key" {
33-
continue;
34-
}
35-
write!(init, "PRAGMA {} = {}; ", key, value).ok();
36-
}
37-
38-
conn.execute(&*init).await?;
20+
// Execute PRAGMAs
21+
conn.execute(&*self.pragma_string()).await?;
3922

4023
if !self.collations.is_empty() {
4124
let mut locked = conn.lock_handle().await?;
@@ -59,3 +42,18 @@ impl ConnectOptions for SqliteConnectOptions {
5942
self
6043
}
6144
}
45+
46+
impl SqliteConnectOptions {
47+
/// Collect all `PRAMGA` commands into a single string
48+
pub(crate) fn pragma_string(&self) -> String {
49+
let mut string = String::new();
50+
51+
for (key, opt_value) in &self.pragmas {
52+
if let Some(value) = opt_value {
53+
write!(string, "PRAGMA {} = {}; ", key, value).ok();
54+
}
55+
}
56+
57+
string
58+
}
59+
}

0 commit comments

Comments
 (0)