diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 9044e28b7b..0a5461a97b 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -11,6 +11,12 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?; if !exists { + #[cfg(feature = "sqlite")] + sqlx::sqlite::CREATE_DB_WAL.store( + connect_opts.sqlite_create_db_wal, + std::sync::atomic::Ordering::Release, + ); + Any::create_database(&connect_opts.database_url).await?; } diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index e70dbf119b..838fe6b48e 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -221,6 +221,18 @@ pub struct ConnectOpts { /// returning an error. #[clap(long, default_value = "10")] pub connect_timeout: u64, + + /// Set whether or not to create SQLite databases in Write-Ahead Log (WAL) mode: + /// https://www.sqlite.org/wal.html + /// + /// WAL mode is enabled by default for SQLite databases created by `sqlx-cli`. + /// + /// However, if your application sets a `journal_mode` on `SqliteConnectOptions` to something + /// other than `Wal`, then it will have to take the database file out of WAL mode on connecting, + /// which requires an exclusive lock and may return a `database is locked` (`SQLITE_BUSY`) error. + #[cfg(feature = "sqlite")] + #[clap(long, action = clap::ArgAction::Set, default_value = "true")] + pub sqlite_create_db_wal: bool, } /// Argument for automatic confirmation. diff --git a/sqlx-core/src/sqlite/connection/describe.rs b/sqlx-core/src/sqlite/connection/describe.rs index 37f66975cf..ecd5e31bf9 100644 --- a/sqlx-core/src/sqlite/connection/describe.rs +++ b/sqlx-core/src/sqlite/connection/describe.rs @@ -8,7 +8,7 @@ use crate::sqlite::{Sqlite, SqliteColumn}; use either::Either; use std::convert::identity; -pub(super) fn describe(conn: &mut ConnectionState, query: &str) -> Result, Error> { +pub(crate) fn describe(conn: &mut ConnectionState, query: &str) -> Result, Error> { // describing a statement from SQLite can be involved // each SQLx statement is comprised of multiple SQL statements diff --git a/sqlx-core/src/sqlite/connection/execute.rs b/sqlx-core/src/sqlite/connection/execute.rs index a5d6fb3e58..b753fa3f0a 100644 --- a/sqlx-core/src/sqlite/connection/execute.rs +++ b/sqlx-core/src/sqlite/connection/execute.rs @@ -53,6 +53,16 @@ fn bind( Ok(n) } +impl ExecuteIter<'_> { + pub fn finish(&mut self) -> Result<(), Error> { + for res in self { + let _ = res?; + } + + Ok(()) + } +} + impl Iterator for ExecuteIter<'_> { type Item = Result, Error>; diff --git a/sqlx-core/src/sqlite/connection/mod.rs b/sqlx-core/src/sqlite/connection/mod.rs index e75d0d570c..35e5f20012 100644 --- a/sqlx-core/src/sqlite/connection/mod.rs +++ b/sqlx-core/src/sqlite/connection/mod.rs @@ -19,9 +19,9 @@ use crate::sqlite::{Sqlite, SqliteConnectOptions}; use crate::transaction::Transaction; pub(crate) mod collation; -mod describe; -mod establish; -mod execute; +pub(crate) mod describe; +pub(crate) mod establish; +pub(crate) mod execute; mod executor; mod explain; mod handle; diff --git a/sqlx-core/src/sqlite/error.rs b/sqlx-core/src/sqlite/error.rs index b9cd08b1ac..1c507a7839 100644 --- a/sqlx-core/src/sqlite/error.rs +++ b/sqlx-core/src/sqlite/error.rs @@ -39,7 +39,11 @@ impl SqliteError { impl Display for SqliteError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.pad(&self.message) + // We include the code as some produce ambiguous messages: + // SQLITE_BUSY: "database is locked" + // SQLITE_LOCKED: "database table is locked" + // Sadly there's no function to get the string label back from an error code. + write!(f, "(code: {}) {}", self.code, self.message) } } diff --git a/sqlx-core/src/sqlite/migrate.rs b/sqlx-core/src/sqlite/migrate.rs index 02d0841e00..41e1f4d717 100644 --- a/sqlx-core/src/sqlite/migrate.rs +++ b/sqlx-core/src/sqlite/migrate.rs @@ -7,20 +7,32 @@ use crate::migrate::{Migrate, MigrateDatabase}; use crate::query::query; use crate::query_as::query_as; use crate::query_scalar::query_scalar; -use crate::sqlite::{Sqlite, SqliteConnectOptions, SqliteConnection}; +use crate::sqlite::{Sqlite, SqliteConnectOptions, SqliteConnection, SqliteJournalMode}; use futures_core::future::BoxFuture; use sqlx_rt::fs; use std::str::FromStr; +use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; impl MigrateDatabase for Sqlite { fn create_database(url: &str) -> BoxFuture<'_, Result<(), Error>> { Box::pin(async move { + let mut opts = SqliteConnectOptions::from_str(url)?.create_if_missing(true); + + // Since it doesn't make sense to include this flag in the connection URL, + // we just use an `AtomicBool` to pass it. + if super::CREATE_DB_WAL.load(Ordering::Acquire) { + opts = opts.journal_mode(SqliteJournalMode::Wal); + } + // Opening a connection to sqlite creates the database let _ = SqliteConnectOptions::from_str(url)? .create_if_missing(true) .connect() + .await? + // Ensure WAL mode tempfiles are cleaned up + .close() .await?; Ok(()) diff --git a/sqlx-core/src/sqlite/mod.rs b/sqlx-core/src/sqlite/mod.rs index 7810641716..291394204f 100644 --- a/sqlx-core/src/sqlite/mod.rs +++ b/sqlx-core/src/sqlite/mod.rs @@ -16,11 +16,15 @@ pub use options::{ pub use query_result::SqliteQueryResult; pub use row::SqliteRow; pub use statement::SqliteStatement; +use std::sync::atomic::AtomicBool; pub use transaction::SqliteTransactionManager; pub use type_info::SqliteTypeInfo; pub use value::{SqliteValue, SqliteValueRef}; +use crate::describe::Describe; +use crate::error::Error; use crate::executor::Executor; +use crate::sqlite::connection::establish::EstablishParams; mod arguments; mod column; @@ -60,3 +64,24 @@ impl_into_maybe_pool!(Sqlite, SqliteConnection); // required because some databases have a different handling of NULL impl_encode_for_option!(Sqlite); + +/// UNSTABLE: for use by `sqlx-cli` only. +#[doc(hidden)] +pub static CREATE_DB_WAL: AtomicBool = AtomicBool::new(true); + +/// UNSTABLE: for use by `sqlite_macros` only. +#[doc(hidden)] +pub fn describe_blocking( + opts: &SqliteConnectOptions, + query: &str, +) -> Result, Error> { + let params = EstablishParams::from_options(opts)?; + let mut conn = params.establish()?; + + // Execute any ancillary `PRAGMA`s + connection::execute::iter(&mut conn, &opts.pragma_string(), None, false)?.finish()?; + + connection::describe::describe(&mut conn, query) + + // SQLite database is closed immediately when `conn` is dropped +} diff --git a/sqlx-core/src/sqlite/options/connect.rs b/sqlx-core/src/sqlite/options/connect.rs index a087c43c3f..21a61a00ad 100644 --- a/sqlx-core/src/sqlite/options/connect.rs +++ b/sqlx-core/src/sqlite/options/connect.rs @@ -17,25 +17,8 @@ impl ConnectOptions for SqliteConnectOptions { Box::pin(async move { let mut conn = SqliteConnection::establish(self).await?; - // send an initial sql statement comprised of options - let mut init = String::new(); - - // This is a special case for sqlcipher. When the `key` pragma - // is set, we have to make sure it's executed first in order. - if let Some(pragma_key_password) = self.pragmas.get("key") { - write!(init, "PRAGMA key = {}; ", pragma_key_password).ok(); - } - - for (key, value) in &self.pragmas { - // Since we've already written the possible `key` pragma - // above, we shall skip it now. - if key == "key" { - continue; - } - write!(init, "PRAGMA {} = {}; ", key, value).ok(); - } - - conn.execute(&*init).await?; + // Execute PRAGMAs + conn.execute(&*self.pragma_string()).await?; if !self.collations.is_empty() { let mut locked = conn.lock_handle().await?; @@ -59,3 +42,18 @@ impl ConnectOptions for SqliteConnectOptions { self } } + +impl SqliteConnectOptions { + /// Collect all `PRAMGA` commands into a single string + pub(crate) fn pragma_string(&self) -> String { + let mut string = String::new(); + + for (key, opt_value) in &self.pragmas { + if let Some(value) = opt_value { + write!(string, "PRAGMA {} = {}; ", key, value).ok(); + } + } + + string + } +} diff --git a/sqlx-core/src/sqlite/options/mod.rs b/sqlx-core/src/sqlite/options/mod.rs index a0f418e496..72943b7ffe 100644 --- a/sqlx-core/src/sqlite/options/mod.rs +++ b/sqlx-core/src/sqlite/options/mod.rs @@ -63,7 +63,8 @@ pub struct SqliteConnectOptions { pub(crate) busy_timeout: Duration, pub(crate) log_settings: LogSettings, pub(crate) immutable: bool, - pub(crate) pragmas: IndexMap, Cow<'static, str>>, + + pub(crate) pragmas: IndexMap, Option>>, pub(crate) command_channel_size: usize, pub(crate) row_channel_size: usize, @@ -85,32 +86,44 @@ impl SqliteConnectOptions { /// /// See the source of this method for the current defaults. pub fn new() -> Self { - // set default pragmas - let mut pragmas: IndexMap, Cow<'static, str>> = IndexMap::new(); + let mut pragmas: IndexMap, Option>> = IndexMap::new(); + + // Standard pragmas + // + // Most of these don't actually need to be sent because they would be set to their + // default values anyway. See the SQLite documentation for default values of these PRAGMAs: + // https://www.sqlite.org/pragma.html + // + // However, by inserting into the map here, we can ensure that they're set in the proper + // order, even if they're overwritten later by their respective setters or + // directly by `pragma()` - let locking_mode: SqliteLockingMode = Default::default(); - let auto_vacuum: SqliteAutoVacuum = Default::default(); + // SQLCipher special case: if the `key` pragma is set, it must be executed first. + pragmas.insert("key".into(), None); - // page_size must be set before any other action on the database. - pragmas.insert("page_size".into(), "4096".into()); + // Normally, page_size must be set before any other action on the database. + // Defaults to 4096 for new databases. + pragmas.insert("page_size".into(), None); - // Note that locking_mode should be set before journal_mode; see - // https://www.sqlite.org/wal.html#use_of_wal_without_shared_memory . - pragmas.insert("locking_mode".into(), locking_mode.as_str().into()); + // locking_mode should be set before journal_mode: + // https://www.sqlite.org/wal.html#use_of_wal_without_shared_memory + pragmas.insert("locking_mode".into(), None); - pragmas.insert( - "journal_mode".into(), - SqliteJournalMode::Wal.as_str().into(), - ); + // Don't set `journal_mode` unless the user requested it. + // WAL mode is a permanent setting for created databases and changing into or out of it + // requires an exclusive lock that can't be waited on with `sqlite3_busy_timeout()`. + // https://github.com/launchbadge/sqlx/pull/1930#issuecomment-1168165414 + pragmas.insert("journal_mode".into(), None); - pragmas.insert("foreign_keys".into(), "ON".into()); + // We choose to enable foreign key enforcement by default, though SQLite normally + // leaves it off for backward compatibility: https://www.sqlite.org/foreignkeys.html#fk_enable + pragmas.insert("foreign_keys".into(), Some("ON".into())); - pragmas.insert( - "synchronous".into(), - SqliteSynchronous::Full.as_str().into(), - ); + // The `synchronous` pragma defaults to FULL + // https://www.sqlite.org/compile.html#default_synchronous. + pragmas.insert("synchronous".into(), None); - pragmas.insert("auto_vacuum".into(), auto_vacuum.as_str().into()); + pragmas.insert("auto_vacuum".into(), None); Self { filename: Cow::Borrowed(Path::new(":memory:")), @@ -139,13 +152,10 @@ impl SqliteConnectOptions { /// Set the enforcement of [foreign key constraints](https://www.sqlite.org/pragma.html#pragma_foreign_keys). /// - /// By default, this is enabled. + /// SQLx chooses to enable this by default so that foreign keys function as expected, + /// compared to other database flavors. pub fn foreign_keys(mut self, on: bool) -> Self { - self.pragmas.insert( - "foreign_keys".into(), - (if on { "ON" } else { "OFF" }).into(), - ); - self + self.pragma("foreign_keys", if on { "ON" } else { "OFF" }) } /// Set the [`SQLITE_OPEN_SHAREDCACHE` flag](https://sqlite.org/sharedcache.html). @@ -158,21 +168,34 @@ impl SqliteConnectOptions { /// Sets the [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) for the database connection. /// - /// The default journal mode is WAL. For most use cases this can be significantly faster but - /// there are [disadvantages](https://www.sqlite.org/wal.html). + /// Journal modes are ephemeral per connection, with the exception of the + /// [Write-Ahead Log (WAL) mode](https://www.sqlite.org/wal.html). + /// + /// A database created in WAL mode retains the setting and will apply it to all connections + /// opened against it that don't set a `journal_mode`. + /// + /// Opening a connection to a database created in WAL mode with a different `journal_mode` will + /// erase the setting on the database, requiring an exclusive lock to do so. + /// You may get a `database is locked` (corresponding to `SQLITE_BUSY`) error if another + /// connection is accessing the database file at the same time. + /// + /// SQLx does not set a journal mode by default, to avoid unintentionally changing a database + /// into or out of WAL mode. + /// + /// The default journal mode for non-WAL databases is `DELETE`, or `MEMORY` for in-memory + /// databases. + /// + /// For consistency, any commands in `sqlx-cli` which create a SQLite database will create it + /// in WAL mode. pub fn journal_mode(mut self, mode: SqliteJournalMode) -> Self { - self.pragmas - .insert("journal_mode".into(), mode.as_str().into()); - self + self.pragma("journal_mode", mode.as_str()) } /// Sets the [locking mode](https://www.sqlite.org/pragma.html#pragma_locking_mode) for the database connection. /// /// The default locking mode is NORMAL. pub fn locking_mode(mut self, mode: SqliteLockingMode) -> Self { - self.pragmas - .insert("locking_mode".into(), mode.as_str().into()); - self + self.pragma("locking_mode", mode.as_str()) } /// Sets the [access mode](https://www.sqlite.org/c3ref/open.html) to open the database @@ -185,7 +208,7 @@ impl SqliteConnectOptions { /// Sets the [access mode](https://www.sqlite.org/c3ref/open.html) to create the database file /// if the file does not exist. /// - /// By default, a new file **will not be** created if one is not found. + /// By default, a new file **will not be created** if one is not found. pub fn create_if_missing(mut self, create: bool) -> Self { self.create_if_missing = create; self @@ -216,27 +239,28 @@ impl SqliteConnectOptions { /// The default synchronous settings is FULL. However, if durability is not a concern, /// then NORMAL is normally all one needs in WAL mode. pub fn synchronous(mut self, synchronous: SqliteSynchronous) -> Self { - self.pragmas - .insert("synchronous".into(), synchronous.as_str().into()); - self + self.pragma("synchronous", synchronous.as_str()) } /// Sets the [auto_vacuum](https://www.sqlite.org/pragma.html#pragma_auto_vacuum) setting for the database connection. /// /// The default auto_vacuum setting is NONE. + /// + /// For existing databases, a change to this value does not take effect unless a + /// [`VACUUM` command](https://www.sqlite.org/lang_vacuum.html) is executed. pub fn auto_vacuum(mut self, auto_vacuum: SqliteAutoVacuum) -> Self { - self.pragmas - .insert("auto_vacuum".into(), auto_vacuum.as_str().into()); - self + self.pragma("auto_vacuum", auto_vacuum.as_str()) } /// Sets the [page_size](https://www.sqlite.org/pragma.html#pragma_page_size) setting for the database connection. /// /// The default page_size setting is 4096. + /// + /// For existing databases, a change to this value does not take effect unless a + /// [`VACUUM` command](https://www.sqlite.org/lang_vacuum.html) is executed. + /// However, it cannot be changed in WAL mode. pub fn page_size(mut self, page_size: u32) -> Self { - self.pragmas - .insert("page_size".into(), page_size.to_string().into()); - self + self.pragma("page_size", page_size.to_string()) } /// Sets custom initial pragma for the database connection. @@ -245,7 +269,7 @@ impl SqliteConnectOptions { K: Into>, V: Into>, { - self.pragmas.insert(key.into(), value.into()); + self.pragmas.insert(key.into(), Some(value.into())); self } @@ -294,7 +318,8 @@ impl SqliteConnectOptions { /// Sets the [threading mode](https://www.sqlite.org/threadsafe.html) for the database connection. /// - /// The default setting is `false` corersponding to using `OPEN_NOMUTEX`, if `true` then `OPEN_FULLMUTEX`. + /// The default setting is `false` corresponding to using `OPEN_NOMUTEX`. + /// If set to `true` then `OPEN_FULLMUTEX`. /// /// See [open](https://www.sqlite.org/c3ref/open.html) for more details. /// diff --git a/sqlx-core/src/sqlite/types/mod.rs b/sqlx-core/src/sqlite/types/mod.rs index 3059588ad4..a57200ac11 100644 --- a/sqlx-core/src/sqlite/types/mod.rs +++ b/sqlx-core/src/sqlite/types/mod.rs @@ -12,12 +12,25 @@ //! | `u8` | INTEGER | //! | `u16` | INTEGER | //! | `u32` | INTEGER | -//! | `u64` | BIGINT, INT8 | //! | `f32` | REAL | //! | `f64` | REAL | //! | `&str`, [`String`] | TEXT | //! | `&[u8]`, `Vec` | BLOB | //! +//! #### Note: Unsigned Integers +//! The unsigned integer types `u8`, `u16` and `u32` are implemented by zero-extending to the +//! next-larger signed type. So `u8` becomes `i16`, `u16` becomes `i32`, and `u32` becomes `i64` +//! while still retaining their semantic values. +//! +//! Similarly, decoding performs a checked truncation to ensure that overflow does not occur. +//! +//! SQLite stores integers in a variable-width encoding and always handles them in memory as 64-bit +//! signed values, so no space is wasted by this implicit widening. +//! +//! However, there is no corresponding larger type for `u64` in SQLite (it would require a `i128`), +//! and so it is not supported. Bit-casting it to `i64` or storing it as `REAL`, `BLOB` or `TEXT` +//! would change the semantics of the value in SQL and so violates the principle of least surprise. +//! //! ### [`chrono`](https://crates.io/crates/chrono) //! //! Requires the `chrono` Cargo feature flag. diff --git a/sqlx-macros/src/query/data.rs b/sqlx-macros/src/query/data.rs index a010f0b07d..682950be41 100644 --- a/sqlx-macros/src/query/data.rs +++ b/sqlx-macros/src/query/data.rs @@ -24,12 +24,16 @@ impl QueryData { conn: impl Executor<'_, Database = DB>, query: &str, ) -> crate::Result { - Ok(QueryData { + Ok(Self::from_describe(query, conn.describe(query).await?)) + } + + pub fn from_describe(query: &str, describe: Describe) -> Self { + QueryData { query: query.into(), - describe: conn.describe(query).await?, + describe, #[cfg(feature = "offline")] hash: offline::hash_string(query), - }) + } } } diff --git a/sqlx-macros/src/query/mod.rs b/sqlx-macros/src/query/mod.rs index ca401f1ffa..91330e5dfc 100644 --- a/sqlx-macros/src/query/mod.rs +++ b/sqlx-macros/src/query/mod.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::path::PathBuf; +use std::str::FromStr; #[cfg(feature = "offline")] use std::sync::{Arc, Mutex}; @@ -187,68 +188,61 @@ pub fn expand_input(input: QueryMacroInput) -> crate::Result { feature = "sqlite" ))] fn expand_from_db(input: QueryMacroInput, db_url: &str) -> crate::Result { - use sqlx_core::any::AnyConnection; - - let maybe_expanded: crate::Result = block_on(async { - let parsed_db_url = Url::parse(db_url)?; - - match parsed_db_url.scheme() { - // SQLite is not used in the connection cache due to issues with newly created - // databases seemingly being locked for several seconds when journaling is off. This - // isn't a huge issue since the intent of the connection cache was to make connections - // to remote databases much faster. Relevant links: - // - https://github.com/launchbadge/sqlx/pull/1782#issuecomment-1089226716 - // - https://github.com/launchbadge/sqlx/issues/1929 - #[cfg(feature = "sqlite")] - "sqlite" => { - use sqlx_core::connection::ConnectOptions; - use sqlx_core::sqlite::SqliteConnectOptions; - use std::str::FromStr; - - let mut conn = SqliteConnectOptions::from_str(db_url)?.connect().await?; - let data = QueryData::from_db(&mut conn, &input.sql).await?; - conn.close().await?; - expand_with_data(input, data, false) - } - _ => { - static CONNECTION_CACHE: Lazy>> = - Lazy::new(|| AsyncMutex::new(BTreeMap::new())); + use sqlx_core::any::{AnyConnectOptions, AnyConnection}; + use std::str::FromStr; + + let connect_opts = AnyConnectOptions::from_str(db_url)?; + + // SQLite is not used in the connection cache due to issues with newly created + // databases seemingly being locked for several seconds when journaling is off. This + // isn't a huge issue since the intent of the connection cache was to make connections + // to remote databases much faster. Relevant links: + // - https://github.com/launchbadge/sqlx/pull/1782#issuecomment-1089226716 + // - https://github.com/launchbadge/sqlx/issues/1929 + #[cfg(feature = "sqlite")] + if let Some(sqlite_opts) = connect_opts.as_sqlite() { + // Since proc-macros don't benefit from async, we can make a describe call directly + // which also ensures that the database is closed afterwards, regardless of errors. + let describe = sqlx_core::sqlite::describe_blocking(sqlite_opts, &input.sql)?; + let data = QueryData::from_describe(&input.sql, describe); + return expand_with_data(input, data, false); + } - let mut cache = CONNECTION_CACHE.lock().await; + block_on(async { + static CONNECTION_CACHE: Lazy>> = + Lazy::new(|| AsyncMutex::new(BTreeMap::new())); - if !cache.contains_key(db_url) { - let conn = AnyConnection::connect(db_url).await?; - let _ = cache.insert(db_url.to_owned(), conn); - } + let mut cache = CONNECTION_CACHE.lock().await; - let conn_item = cache.get_mut(db_url).expect("Item was just inserted"); - match conn_item.private_get_mut() { - #[cfg(feature = "postgres")] - sqlx_core::any::AnyConnectionKind::Postgres(conn) => { - let data = QueryData::from_db(conn, &input.sql).await?; - expand_with_data(input, data, false) - } - #[cfg(feature = "mssql")] - sqlx_core::any::AnyConnectionKind::Mssql(conn) => { - let data = QueryData::from_db(conn, &input.sql).await?; - expand_with_data(input, data, false) - } - #[cfg(feature = "mysql")] - sqlx_core::any::AnyConnectionKind::MySql(conn) => { - let data = QueryData::from_db(conn, &input.sql).await?; - expand_with_data(input, data, false) - } - // Variants depend on feature flags - #[allow(unreachable_patterns)] - item => { - return Err(format!("Missing expansion needed for: {:?}", item).into()); - } - } - } + if !cache.contains_key(db_url) { + let conn = AnyConnection::connect_with(&connect_opts).await?; + let _ = cache.insert(db_url.to_owned(), conn); } - }); - maybe_expanded.map_err(Into::into) + let conn_item = cache.get_mut(db_url).expect("Item was just inserted"); + match conn_item.private_get_mut() { + #[cfg(feature = "postgres")] + sqlx_core::any::AnyConnectionKind::Postgres(conn) => { + let data = QueryData::from_db(conn, &input.sql).await?; + expand_with_data(input, data, false) + } + #[cfg(feature = "mssql")] + sqlx_core::any::AnyConnectionKind::Mssql(conn) => { + let data = QueryData::from_db(conn, &input.sql).await?; + expand_with_data(input, data, false) + } + #[cfg(feature = "mysql")] + sqlx_core::any::AnyConnectionKind::MySql(conn) => { + let data = QueryData::from_db(conn, &input.sql).await?; + expand_with_data(input, data, false) + } + // Variants depend on feature flags + #[allow(unreachable_patterns)] + item => { + return Err(format!("Missing expansion needed for: {:?}", item).into()); + } + } + }) } #[cfg(feature = "offline")] diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index e4a65989e5..3115cd6938 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -234,7 +234,7 @@ async fn it_fails_to_parse() -> anyhow::Result<()> { let err = res.unwrap_err().to_string(); assert_eq!( - "error returned from database: near \"SEELCT\": syntax error", + "error returned from database: (code: 1) near \"SEELCT\": syntax error", err );