diff options
author | Jeff Vander Stoep <jeffv@google.com> | 2023-01-26 21:01:17 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-01-26 21:01:17 +0000 |
commit | 2f63c2611b48ca94a8df88f9195a898174f7f1be (patch) | |
tree | 997e303d260e02f4e7f4175b020c41b9063576ec | |
parent | 87bfaab0946d08a58dbd47664ddbf851ab7a0dcc (diff) | |
parent | 4ca1c7fd214fd21554249a2ed07a2652a4902b8a (diff) | |
download | rusqlite-2f63c2611b48ca94a8df88f9195a898174f7f1be.tar.gz |
Upgrade rusqlite to 0.28.0 am: 8fdab748b3 am: 4ca1c7fd21
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/rusqlite/+/2349462
Change-Id: I43adb64bcc44ea357c75201b998fa287bfb06076
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | Android.bp | 7 | ||||
-rw-r--r-- | Cargo.toml | 16 | ||||
-rw-r--r-- | Cargo.toml.orig | 17 | ||||
-rw-r--r-- | METADATA | 12 | ||||
-rw-r--r-- | README.md | 48 | ||||
-rw-r--r-- | src/busy.rs | 12 | ||||
-rw-r--r-- | src/config.rs | 2 | ||||
-rw-r--r-- | src/context.rs | 7 | ||||
-rw-r--r-- | src/error.rs | 96 | ||||
-rw-r--r-- | src/functions.rs | 31 | ||||
-rw-r--r-- | src/hooks.rs | 14 | ||||
-rw-r--r-- | src/inner_connection.rs | 78 | ||||
-rw-r--r-- | src/lib.rs | 227 | ||||
-rw-r--r-- | src/params.rs | 128 | ||||
-rw-r--r-- | src/pragma.rs | 22 | ||||
-rw-r--r-- | src/raw_statement.rs | 10 | ||||
-rw-r--r-- | src/row.rs | 2 | ||||
-rw-r--r-- | src/session.rs | 2 | ||||
-rw-r--r-- | src/statement.rs | 146 | ||||
-rw-r--r-- | src/trace.rs | 2 | ||||
-rw-r--r-- | src/transaction.rs | 48 | ||||
-rw-r--r-- | src/types/from_sql.rs | 2 | ||||
-rw-r--r-- | src/types/mod.rs | 69 | ||||
-rw-r--r-- | src/types/to_sql.rs | 1 | ||||
-rw-r--r-- | src/types/value_ref.rs | 3 | ||||
-rw-r--r-- | src/util/small_cstr.rs | 2 | ||||
-rw-r--r-- | src/vtab/array.rs | 2 | ||||
-rw-r--r-- | src/vtab/csvtab.rs | 31 | ||||
-rw-r--r-- | src/vtab/mod.rs | 336 | ||||
-rw-r--r-- | src/vtab/series.rs | 11 | ||||
-rw-r--r-- | src/vtab/vtablog.rs | 300 | ||||
-rw-r--r-- | tests/deny_single_threaded_sqlite_config.rs | 11 | ||||
-rw-r--r-- | tests/vtab.rs | 4 |
34 files changed, 1379 insertions, 322 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index de52e1b..96ed8b8 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "8141b5e085bd3a02951588413e5569f1ddf17a8c" + "sha1": "26293a11f595574897e7e5a5b639d1587255c6b9" }, "path_in_vcs": "" }
\ No newline at end of file @@ -23,7 +23,7 @@ rust_library { host_supported: true, crate_name: "rusqlite", cargo_env_compat: true, - cargo_pkg_version: "0.27.0", + cargo_pkg_version: "0.28.0", srcs: ["src/lib.rs"], edition: "2018", features: [ @@ -36,9 +36,12 @@ rust_library { "libfallible_streaming_iterator", "libhashlink", "liblibsqlite3_sys", - "libmemchr", "libsmallvec", ], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], } rust_library { @@ -12,7 +12,7 @@ [package] edition = "2018" name = "rusqlite" -version = "0.27.0" +version = "0.28.0" authors = ["The rusqlite developers"] exclude = [ "/.github/*", @@ -89,17 +89,14 @@ version = "0.2" version = "0.1" [dependencies.hashlink] -version = "0.7" +version = "0.8" [dependencies.lazy_static] version = "1.4" optional = true [dependencies.libsqlite3-sys] -version = "0.24.0" - -[dependencies.memchr] -version = "2.3" +version = "0.25.0" [dependencies.serde_json] version = "1.0" @@ -122,7 +119,7 @@ version = "2.1" optional = true [dependencies.uuid] -version = "0.8" +version = "1.0" optional = true [dev-dependencies.bencher] @@ -135,7 +132,7 @@ version = "0.3" version = "1.4" [dev-dependencies.regex] -version = "1.3" +version = "1.5.5" [dev-dependencies.tempfile] version = "3.1.0" @@ -144,7 +141,7 @@ version = "3.1.0" version = "2.6.0" [dev-dependencies.uuid] -version = "0.8" +version = "1.0" features = ["v4"] [features] @@ -211,6 +208,7 @@ modern-full = [ "window", ] modern_sqlite = ["libsqlite3-sys/bundled_bindings"] +release_memory = ["libsqlite3-sys/min_sqlite_version_3_7_16"] series = ["vtab"] session = [ "libsqlite3-sys/session", diff --git a/Cargo.toml.orig b/Cargo.toml.orig index 6ab1a7e..bd81d44 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,7 @@ [package] name = "rusqlite" -version = "0.27.0" +# Note: Update version in README.md when you change this. +version = "0.28.0" authors = ["The rusqlite developers"] edition = "2018" description = "Ergonomic wrapper for SQLite" @@ -42,6 +43,8 @@ collation = [] functions = ["libsqlite3-sys/min_sqlite_version_3_7_7"] # sqlite3_log: 3.6.23 (2010-03-09) trace = ["libsqlite3-sys/min_sqlite_version_3_6_23"] +# sqlite3_db_release_memory: 3.7.10 (2012-01-16) +release_memory = ["libsqlite3-sys/min_sqlite_version_3_7_16"] bundled = ["libsqlite3-sys/bundled", "modern_sqlite"] bundled-sqlcipher = ["libsqlite3-sys/bundled-sqlcipher", "bundled"] bundled-sqlcipher-vendored-openssl = ["libsqlite3-sys/bundled-sqlcipher-vendored-openssl", "bundled-sqlcipher"] @@ -71,6 +74,7 @@ bundled-windows = ["libsqlite3-sys/bundled-windows"] with-asan = ["libsqlite3-sys/with-asan"] column_decltype = [] wasm32-wasi-vfs = ["libsqlite3-sys/wasm32-wasi-vfs"] +# Note: doesn't support 32-bit. winsqlite3 = ["libsqlite3-sys/winsqlite3"] # Helper feature for enabling most non-build-related optional features @@ -108,7 +112,7 @@ bundled-full = ["modern-full", "bundled"] [dependencies] time = { version = "0.3.0", features = ["formatting", "macros", "parsing"], optional = true } bitflags = "1.2" -hashlink = "0.7" +hashlink = "0.8" chrono = { version = "0.4", optional = true, default-features = false, features = ["clock"] } serde_json = { version = "1.0", optional = true } csv = { version = "1.1", optional = true } @@ -116,16 +120,15 @@ url = { version = "2.1", optional = true } lazy_static = { version = "1.4", optional = true } fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" -memchr = "2.3" -uuid = { version = "0.8", optional = true } +uuid = { version = "1.0", optional = true } smallvec = "1.6.1" [dev-dependencies] doc-comment = "0.3" tempfile = "3.1.0" lazy_static = "1.4" -regex = "1.3" -uuid = { version = "0.8", features = ["v4"] } +regex = "1.5.5" +uuid = { version = "1.0", features = ["v4"] } unicase = "2.6.0" # Use `bencher` over criterion because it builds much faster and we don't have # many benchmarks @@ -133,7 +136,7 @@ bencher = "0.1" [dependencies.libsqlite3-sys] path = "libsqlite3-sys" -version = "0.24.0" +version = "0.25.0" [[test]] name = "config_log" @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update rust/crates/rusqlite +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "rusqlite" description: "Ergonomic wrapper for SQLite" third_party { @@ -7,13 +11,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/rusqlite/rusqlite-0.27.0.crate" + value: "https://static.crates.io/crates/rusqlite/rusqlite-0.28.0.crate" } - version: "0.27.0" + version: "0.28.0" license_type: NOTICE last_upgrade_date { year: 2022 - month: 3 - day: 1 + month: 12 + day: 13 } } @@ -8,11 +8,32 @@ [![Dependency Status](https://deps.rs/repo/github/rusqlite/rusqlite/status.svg)](https://deps.rs/repo/github/rusqlite/rusqlite) [![Discord Chat](https://img.shields.io/discord/927966344266256434.svg?logo=discord)](https://discord.gg/nFYfGPB8g4) -Rusqlite is an ergonomic wrapper for using SQLite from Rust. It attempts to expose -an interface similar to [rust-postgres](https://github.com/sfackler/rust-postgres). +Rusqlite is an ergonomic wrapper for using SQLite from Rust. + +Historically, the API was based on the one from [`rust-postgres`](https://github.com/sfackler/rust-postgres). However, the two have diverged in many ways, and no compatibility between the two is intended. + +## Usage + +In your Cargo.toml: + +```toml +[dependencies] +# `bundled` causes us to automatically compile and link in an up to date +# version of SQLite for you. This avoids many common build issues, and +# avoids depending on the version of SQLite on the users system (or your +# system), which may be old or missing. It's the right choice for most +# programs that control their own SQLite databases. +# +# That said, it's not ideal for all scenarios and in particular, generic +# libraries built around `rusqlite` should probably not enable it, which +# is why it is not a default feature -- it could become hard to disable. +rusqlite = { version = "0.28.0", features = ["bundled"] } +``` + +Simple example usage: ```rust -use rusqlite::{params, Connection, Result}; +use rusqlite::{Connection, Result}; #[derive(Debug)] struct Person { @@ -26,11 +47,11 @@ fn main() -> Result<()> { conn.execute( "CREATE TABLE person ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - data BLOB - )", - [], + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data BLOB + )", + (), // empty list of parameters. )?; let me = Person { id: 0, @@ -39,7 +60,7 @@ fn main() -> Result<()> { }; conn.execute( "INSERT INTO person (name, data) VALUES (?1, ?2)", - params![me.name, me.data], + (&me.name, &me.data), )?; let mut stmt = conn.prepare("SELECT id, name, data FROM person")?; @@ -115,6 +136,7 @@ features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-s * `extra_check` fail when a query passed to execute is readonly or has a column count > 0. * `column_decltype` provides `columns()` method for Statements and Rows; omit if linking to a version of SQLite/SQLCipher compiled with `-DSQLITE_OMIT_DECLTYPE`. * `collation` exposes [`sqlite3_create_collation_v2`](https://sqlite.org/c3ref/create_collation.html). +* `winsqlite3` allows linking against the SQLite present in newer versions of Windows ## Notes on building rusqlite and libsqlite3-sys @@ -127,14 +149,14 @@ You can adjust this behavior in a number of ways: * If you use the `bundled`, `bundled-sqlcipher`, or `bundled-sqlcipher-vendored-openssl` features, `libsqlite3-sys` will use the [cc](https://crates.io/crates/cc) crate to compile SQLite or SQLCipher from source and link against that. This source is embedded in the `libsqlite3-sys` crate and - is currently SQLite 3.38.0 (as of `rusqlite` 0.27.0 / `libsqlite3-sys` - 0.24.0). This is probably the simplest solution to any build problems. You can enable this by adding the following in your `Cargo.toml` file: + is currently SQLite 3.39.0 (as of `rusqlite` 0.28.0 / `libsqlite3-sys` + 0.25.0). This is probably the simplest solution to any build problems. You can enable this by adding the following in your `Cargo.toml` file: ```toml [dependencies.rusqlite] - version = "0.27.0" + version = "0.28.0" features = ["bundled"] ``` -* When using any of the `bundled` features, the build script will honor `SQLITE_MAX_VARIABLE_NUMBER` and `SQLITE_MAX_EXPR_DEPTH` variables. It will also honor a `LIBSQLITE_FLAGS` variable, which can have a format like `"-USQLITE_ALPHA -DSQLITE_BETA SQLITE_GAMMA ..."`. That would disable the `SQLITE_ALPHA` flag, and set the `SQLITE_BETA` and `SQLITE_GAMMA` flags. (The initial `-D` can be omitted, as on the last one.) +* When using any of the `bundled` features, the build script will honor `SQLITE_MAX_VARIABLE_NUMBER` and `SQLITE_MAX_EXPR_DEPTH` variables. It will also honor a `LIBSQLITE3_FLAGS` variable, which can have a format like `"-USQLITE_ALPHA -DSQLITE_BETA SQLITE_GAMMA ..."`. That would disable the `SQLITE_ALPHA` flag, and set the `SQLITE_BETA` and `SQLITE_GAMMA` flags. (The initial `-D` can be omitted, as on the last one.) * When using `bundled-sqlcipher` (and not also using `bundled-sqlcipher-vendored-openssl`), `libsqlite3-sys` will need to link against crypto libraries on the system. If the build script can find a `libcrypto` from OpenSSL or LibreSSL (it will consult `OPENSSL_LIB_DIR`/`OPENSSL_INCLUDE_DIR` and `OPENSSL_DIR` environment variables), it will use that. If building on and for Macs, and none of those variables are set, it will use the system's SecurityFramework instead. diff --git a/src/busy.rs b/src/busy.rs index b394d01..7297f20 100644 --- a/src/busy.rs +++ b/src/busy.rs @@ -90,7 +90,7 @@ mod test { use std::thread; use std::time::Duration; - use crate::{Connection, Error, ErrorCode, Result, TransactionBehavior}; + use crate::{Connection, ErrorCode, Result, TransactionBehavior}; #[test] fn test_default_busy() -> Result<()> { @@ -101,12 +101,10 @@ mod test { let tx1 = db1.transaction_with_behavior(TransactionBehavior::Exclusive)?; let db2 = Connection::open(&path)?; let r: Result<()> = db2.query_row("PRAGMA schema_version", [], |_| unreachable!()); - match r.unwrap_err() { - Error::SqliteFailure(err, _) => { - assert_eq!(err.code, ErrorCode::DatabaseBusy); - } - err => panic!("Unexpected error {}", err), - } + assert_eq!( + r.unwrap_err().sqlite_error_code(), + Some(ErrorCode::DatabaseBusy) + ); tx1.rollback() } diff --git a/src/config.rs b/src/config.rs index b59e5ef..b295d97 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,7 +33,7 @@ pub enum DbConfig { SQLITE_DBCONFIG_TRIGGER_EQP = 1008, // 3.22.0 /// Activates or deactivates the "reset" flag for a database connection. /// Run VACUUM with this flag set to reset the database. - SQLITE_DBCONFIG_RESET_DATABASE = 1009, + SQLITE_DBCONFIG_RESET_DATABASE = 1009, // 3.24.0 /// Activates or deactivates the "defensive" flag for a database connection. SQLITE_DBCONFIG_DEFENSIVE = 1010, // 3.26.0 /// Activates or deactivates the "writable_schema" flag. diff --git a/src/context.rs b/src/context.rs index 5f935fa..bcaefc9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -23,6 +23,7 @@ pub(super) unsafe fn set_result(ctx: *mut sqlite3_context, result: &ToSqlOutput< #[cfg(feature = "blob")] ToSqlOutput::ZeroBlob(len) => { + // TODO sqlite3_result_zeroblob64 // 3.8.11 return ffi::sqlite3_result_zeroblob(ctx, len); } #[cfg(feature = "array")] @@ -42,7 +43,7 @@ pub(super) unsafe fn set_result(ctx: *mut sqlite3_context, result: &ToSqlOutput< ValueRef::Real(r) => ffi::sqlite3_result_double(ctx, r), ValueRef::Text(s) => { let length = s.len(); - if length > c_int::max_value() as usize { + if length > c_int::MAX as usize { ffi::sqlite3_result_error_toobig(ctx); } else { let (c_str, len, destructor) = match str_for_sqlite(s) { @@ -50,16 +51,18 @@ pub(super) unsafe fn set_result(ctx: *mut sqlite3_context, result: &ToSqlOutput< // TODO sqlite3_result_error Err(_) => return ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_MISUSE), }; + // TODO sqlite3_result_text64 // 3.8.7 ffi::sqlite3_result_text(ctx, c_str, len, destructor); } } ValueRef::Blob(b) => { let length = b.len(); - if length > c_int::max_value() as usize { + if length > c_int::MAX as usize { ffi::sqlite3_result_error_toobig(ctx); } else if length == 0 { ffi::sqlite3_result_zeroblob(ctx, 0); } else { + // TODO sqlite3_result_blob64 // 3.8.7 ffi::sqlite3_result_blob( ctx, b.as_ptr().cast::<c_void>(), diff --git a/src/error.rs b/src/error.rs index 129f697..3c264d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,7 +34,7 @@ pub enum Error { /// Error converting a string to a C-compatible string because it contained /// an embedded nul. - NulError(::std::ffi::NulError), + NulError(std::ffi::NulError), /// Error when using SQL named parameters and passing a parameter name not /// present in the SQL. @@ -128,6 +128,19 @@ pub enum Error { #[cfg(feature = "blob")] #[cfg_attr(docsrs, doc(cfg(feature = "blob")))] BlobSizeError, + /// Error referencing a specific token in the input SQL + #[cfg(feature = "modern_sqlite")] // 3.38.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + SqlInputError { + /// error code + error: ffi::Error, + /// error message + msg: String, + /// SQL input + sql: String, + /// byte offset of the start of invalid token + offset: c_int, + }, } impl PartialEq for Error { @@ -172,6 +185,21 @@ impl PartialEq for Error { } #[cfg(feature = "blob")] (Error::BlobSizeError, Error::BlobSizeError) => true, + #[cfg(feature = "modern_sqlite")] + ( + Error::SqlInputError { + error: e1, + msg: m1, + sql: s1, + offset: o1, + }, + Error::SqlInputError { + error: e2, + msg: m2, + sql: s2, + offset: o2, + }, + ) => e1 == e2 && m1 == m2 && s1 == s2 && o1 == o2, (..) => false, } } @@ -184,14 +212,14 @@ impl From<str::Utf8Error> for Error { } } -impl From<::std::ffi::NulError> for Error { +impl From<std::ffi::NulError> for Error { #[cold] - fn from(err: ::std::ffi::NulError) -> Error { + fn from(err: std::ffi::NulError) -> Error { Error::NulError(err) } } -const UNKNOWN_COLUMN: usize = std::usize::MAX; +const UNKNOWN_COLUMN: usize = usize::MAX; /// The conversion isn't precise, but it's convenient to have it /// to allow use of `get_raw(…).as_…()?` in callbacks that take `Error`. @@ -281,9 +309,15 @@ impl fmt::Display for Error { #[cfg(feature = "functions")] Error::GetAuxWrongType => write!(f, "get_aux called with wrong type"), Error::MultipleStatement => write!(f, "Multiple statements provided"), - #[cfg(feature = "blob")] Error::BlobSizeError => "Blob size is insufficient".fmt(f), + #[cfg(feature = "modern_sqlite")] + Error::SqlInputError { + ref msg, + offset, + ref sql, + .. + } => write!(f, "{} in {} at offset {}", msg, sql, offset), } } } @@ -331,14 +365,35 @@ impl error::Error for Error { #[cfg(feature = "blob")] Error::BlobSizeError => None, + #[cfg(feature = "modern_sqlite")] + Error::SqlInputError { ref error, .. } => Some(error), } } } +impl Error { + /// Returns the underlying SQLite error if this is [`Error::SqliteFailure`]. + #[inline] + pub fn sqlite_error(&self) -> Option<&ffi::Error> { + match self { + Self::SqliteFailure(error, _) => Some(error), + _ => None, + } + } + + /// Returns the underlying SQLite error code if this is + /// [`Error::SqliteFailure`]. + #[inline] + pub fn sqlite_error_code(&self) -> Option<ffi::ErrorCode> { + self.sqlite_error().map(|error| error.code) + } +} + // These are public but not re-exported by lib.rs, so only visible within crate. #[cold] pub fn error_from_sqlite_code(code: c_int, message: Option<String>) -> Error { + // TODO sqlite3_error_offset // 3.38.0, #1130 Error::SqliteFailure(ffi::Error::new(code), message) } @@ -352,9 +407,38 @@ pub unsafe fn error_from_handle(db: *mut ffi::sqlite3, code: c_int) -> Error { error_from_sqlite_code(code, message) } +#[cold] +#[cfg(not(all(feature = "modern_sqlite", not(feature = "bundled-sqlcipher"))))] // SQLite >= 3.38.0 +pub unsafe fn error_with_offset(db: *mut ffi::sqlite3, code: c_int, _sql: &str) -> Error { + error_from_handle(db, code) +} + +#[cold] +#[cfg(all(feature = "modern_sqlite", not(feature = "bundled-sqlcipher")))] // SQLite >= 3.38.0 +pub unsafe fn error_with_offset(db: *mut ffi::sqlite3, code: c_int, sql: &str) -> Error { + if db.is_null() { + error_from_sqlite_code(code, None) + } else { + let error = ffi::Error::new(code); + let msg = errmsg_to_string(ffi::sqlite3_errmsg(db)); + if ffi::ErrorCode::Unknown == error.code { + let offset = ffi::sqlite3_error_offset(db); + if offset >= 0 { + return Error::SqlInputError { + error, + msg, + sql: sql.to_owned(), + offset, + }; + } + } + Error::SqliteFailure(error, Some(msg)) + } +} + pub fn check(code: c_int) -> Result<()> { if code != crate::ffi::SQLITE_OK { - Err(crate::error::error_from_sqlite_code(code, None)) + Err(error_from_sqlite_code(code, None)) } else { Ok(()) } diff --git a/src/functions.rs b/src/functions.rs index e613182..138baac 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -162,6 +162,19 @@ impl Context<'_> { unsafe { ValueRef::from_value(arg) } } + /// Returns the subtype of `idx`th argument. + /// + /// # Failure + /// + /// Will panic if `idx` is greater than or equal to + /// [`self.len()`](Context::len). + #[cfg(feature = "modern_sqlite")] // 3.9.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn get_subtype(&self, idx: usize) -> std::os::raw::c_uint { + let arg = self.args[idx]; + unsafe { ffi::sqlite3_value_subtype(arg) } + } + /// Fetch or insert the auxiliary data associated with a particular /// parameter. This is intended to be an easier-to-use way of fetching it /// compared to calling [`get_aux`](Context::get_aux) and @@ -234,6 +247,13 @@ impl Context<'_> { phantom: PhantomData, }) } + + /// Set the Subtype of an SQL function + #[cfg(feature = "modern_sqlite")] // 3.9.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn set_result_subtype(&self, sub_type: std::os::raw::c_uint) { + unsafe { ffi::sqlite3_result_subtype(self.ctx, sub_type) }; + } } /// A reference to a connection handle with a lifetime bound to something. @@ -319,7 +339,7 @@ bitflags::bitflags! { /// Specifies UTF-16 using native byte order as the text encoding this SQL function prefers for its parameters. const SQLITE_UTF16 = ffi::SQLITE_UTF16; /// Means that the function always gives the same output when the input parameters are the same. - const SQLITE_DETERMINISTIC = ffi::SQLITE_DETERMINISTIC; + const SQLITE_DETERMINISTIC = ffi::SQLITE_DETERMINISTIC; // 3.8.3 /// Means that the function may only be invoked from top-level SQL. const SQLITE_DIRECTONLY = 0x0000_0008_0000; // 3.30.0 /// Indicates to SQLite that a function may call `sqlite3_value_subtype()` to inspect the sub-types of its arguments. @@ -617,7 +637,7 @@ unsafe extern "C" fn call_boxed_step<A, D, T>( D: Aggregate<A, T>, T: ToSql, { - let pac = if let Some(pac) = aggregate_context(ctx, ::std::mem::size_of::<*mut A>()) { + let pac = if let Some(pac) = aggregate_context(ctx, std::mem::size_of::<*mut A>()) { pac } else { ffi::sqlite3_result_error_nomem(ctx); @@ -664,7 +684,7 @@ unsafe extern "C" fn call_boxed_inverse<A, W, T>( W: WindowAggregate<A, T>, T: ToSql, { - let pac = if let Some(pac) = aggregate_context(ctx, ::std::mem::size_of::<*mut A>()) { + let pac = if let Some(pac) = aggregate_context(ctx, std::mem::size_of::<*mut A>()) { pac } else { ffi::sqlite3_result_error_nomem(ctx); @@ -787,7 +807,6 @@ where #[cfg(test)] mod test { use regex::Regex; - use std::f64::EPSILON; use std::os::raw::c_double; #[cfg(feature = "window")] @@ -812,7 +831,7 @@ mod test { )?; let result: Result<f64> = db.query_row("SELECT half(6)", [], |r| r.get(0)); - assert!((3f64 - result?).abs() < EPSILON); + assert!((3f64 - result?).abs() < f64::EPSILON); Ok(()) } @@ -826,7 +845,7 @@ mod test { half, )?; let result: Result<f64> = db.query_row("SELECT half(6)", [], |r| r.get(0)); - assert!((3f64 - result?).abs() < EPSILON); + assert!((3f64 - result?).abs() < f64::EPSILON); db.remove_function("half", 1)?; let result: Result<f64> = db.query_row("SELECT half(6)", [], |r| r.get(0)); diff --git a/src/hooks.rs b/src/hooks.rs index f0ae1f3..5058a0c 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -10,7 +10,7 @@ use crate::ffi; use crate::{Connection, InnerConnection}; /// Action Codes -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(i32)] #[non_exhaustive] #[allow(clippy::upper_case_acronyms)] @@ -37,10 +37,10 @@ impl From<i32> for Action { } } -/// The context recieved by an authorizer hook. +/// The context received by an authorizer hook. /// /// See <https://sqlite.org/c3ref/set_authorizer.html> for more info. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct AuthContext<'c> { /// The action to be authorized. pub action: AuthAction<'c>, @@ -57,7 +57,7 @@ pub struct AuthContext<'c> { /// preparation. /// /// See <https://sqlite.org/c3ref/c_alter_table.html> for more info. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] #[allow(missing_docs)] pub enum AuthAction<'c> { @@ -285,7 +285,7 @@ impl<'c> AuthAction<'c> { operation: TransactionOperation::from_str(operation_str), savepoint_name, }, - #[cfg(feature = "modern_sqlite")] + #[cfg(feature = "modern_sqlite")] // 3.8.3 (ffi::SQLITE_RECURSIVE, ..) => Self::Recursive, (code, arg1, arg2) => Self::Unknown { code, arg1, arg2 }, } @@ -296,7 +296,7 @@ pub(crate) type BoxedAuthorizer = Box<dyn for<'c> FnMut(AuthContext<'c>) -> Authorization + Send + 'static>; /// A transaction operation. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] #[allow(missing_docs)] pub enum TransactionOperation { @@ -318,7 +318,7 @@ impl TransactionOperation { } /// [`authorizer`](Connection::authorizer) return code -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Authorization { /// Authorize the action. diff --git a/src/inner_connection.rs b/src/inner_connection.rs index 0ea630e..e5bc3f1 100644 --- a/src/inner_connection.rs +++ b/src/inner_connection.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex}; use super::ffi; use super::str_for_sqlite; use super::{Connection, InterruptHandle, OpenFlags, Result}; -use crate::error::{error_from_handle, error_from_sqlite_code, Error}; +use crate::error::{error_from_handle, error_from_sqlite_code, error_with_offset, Error}; use crate::raw_statement::RawStatement; use crate::statement::Statement; use crate::version::version_number; @@ -25,11 +25,11 @@ pub struct InnerConnection { // interrupt would only acquire the lock after the query's completion. interrupt_lock: Arc<Mutex<*mut ffi::sqlite3>>, #[cfg(feature = "hooks")] - pub free_commit_hook: Option<unsafe fn(*mut ::std::os::raw::c_void)>, + pub free_commit_hook: Option<unsafe fn(*mut std::os::raw::c_void)>, #[cfg(feature = "hooks")] - pub free_rollback_hook: Option<unsafe fn(*mut ::std::os::raw::c_void)>, + pub free_rollback_hook: Option<unsafe fn(*mut std::os::raw::c_void)>, #[cfg(feature = "hooks")] - pub free_update_hook: Option<unsafe fn(*mut ::std::os::raw::c_void)>, + pub free_update_hook: Option<unsafe fn(*mut std::os::raw::c_void)>, #[cfg(feature = "hooks")] pub progress_handler: Option<Box<dyn FnMut() -> bool + Send>>, #[cfg(feature = "hooks")] @@ -208,7 +208,7 @@ impl InnerConnection { Ok(()) } else { let message = super::errmsg_to_string(errmsg); - ffi::sqlite3_free(errmsg.cast::<::std::os::raw::c_void>()); + ffi::sqlite3_free(errmsg.cast::<std::os::raw::c_void>()); Err(error_from_sqlite_code(r, Some(message))) } } @@ -222,6 +222,7 @@ impl InnerConnection { let mut c_stmt = ptr::null_mut(); let (c_sql, len, _) = str_for_sqlite(sql.as_bytes())?; let mut c_tail = ptr::null(); + // TODO sqlite3_prepare_v3 (https://sqlite.org/c3ref/c_prepare_normalize.html) // 3.20.0, #728 #[cfg(not(feature = "unlock_notify"))] let r = unsafe { ffi::sqlite3_prepare_v2( @@ -255,7 +256,9 @@ impl InnerConnection { rc }; // If there is an error, *ppStmt is set to NULL. - self.decode_result(r)?; + if r != ffi::SQLITE_OK { + return Err(unsafe { error_with_offset(self.db, r, sql) }); + } // If the input text contains no SQL (if the input is an empty string or a // comment) then *ppStmt is set to NULL. let c_stmt: *mut ffi::sqlite3_stmt = c_stmt; @@ -276,8 +279,15 @@ impl InnerConnection { } #[inline] - pub fn changes(&self) -> usize { - unsafe { ffi::sqlite3_changes(self.db()) as usize } + pub fn changes(&self) -> u64 { + #[cfg(not(feature = "modern_sqlite"))] + unsafe { + ffi::sqlite3_changes(self.db()) as u64 + } + #[cfg(feature = "modern_sqlite")] // 3.37.0 + unsafe { + ffi::sqlite3_changes64(self.db()) as u64 + } } #[inline] @@ -308,6 +318,56 @@ impl InnerConnection { #[cfg(not(feature = "hooks"))] #[inline] fn remove_hooks(&mut self) {} + + #[cfg(feature = "modern_sqlite")] // 3.7.11 + pub fn db_readonly(&self, db_name: super::DatabaseName<'_>) -> Result<bool> { + let name = db_name.as_cstring()?; + let r = unsafe { ffi::sqlite3_db_readonly(self.db, name.as_ptr()) }; + match r { + 0 => Ok(false), + 1 => Ok(true), + -1 => Err(Error::SqliteFailure( + ffi::Error::new(ffi::SQLITE_MISUSE), + Some(format!("{:?} is not the name of a database", db_name)), + )), + _ => Err(error_from_sqlite_code( + r, + Some("Unexpected result".to_owned()), + )), + } + } + + #[cfg(feature = "modern_sqlite")] // 3.37.0 + pub fn txn_state( + &self, + db_name: Option<super::DatabaseName<'_>>, + ) -> Result<super::transaction::TransactionState> { + let r = if let Some(ref name) = db_name { + let name = name.as_cstring()?; + unsafe { ffi::sqlite3_txn_state(self.db, name.as_ptr()) } + } else { + unsafe { ffi::sqlite3_txn_state(self.db, ptr::null()) } + }; + match r { + 0 => Ok(super::transaction::TransactionState::None), + 1 => Ok(super::transaction::TransactionState::Read), + 2 => Ok(super::transaction::TransactionState::Write), + -1 => Err(Error::SqliteFailure( + ffi::Error::new(ffi::SQLITE_MISUSE), + Some(format!("{:?} is not the name of a valid schema", db_name)), + )), + _ => Err(error_from_sqlite_code( + r, + Some("Unexpected result".to_owned()), + )), + } + } + + #[inline] + #[cfg(feature = "release_memory")] + pub fn release_memory(&self) -> Result<()> { + self.decode_result(unsafe { ffi::sqlite3_db_release_memory(self.db) }) + } } impl Drop for InnerConnection { @@ -340,7 +400,7 @@ fn ensure_safe_sqlite_threading_mode() -> Result<()> { #[cfg(not(any(target_arch = "wasm32")))] fn ensure_safe_sqlite_threading_mode() -> Result<()> { - // Ensure SQLite was compiled in thredsafe mode. + // Ensure SQLite was compiled in threadsafe mode. if unsafe { ffi::sqlite3_threadsafe() == 0 } { return Err(Error::SqliteSingleThreadedMode); } @@ -1,5 +1,9 @@ -//! Rusqlite is an ergonomic wrapper for using SQLite from Rust. It attempts to -//! expose an interface similar to [rust-postgres](https://github.com/sfackler/rust-postgres). +//! Rusqlite is an ergonomic wrapper for using SQLite from Rust. +//! +//! Historically, the API was based on the one from +//! [`rust-postgres`](https://github.com/sfackler/rust-postgres). However, the +//! two have diverged in many ways, and no compatibility between the two is +//! intended. //! //! ```rust //! use rusqlite::{params, Connection, Result}; @@ -16,11 +20,11 @@ //! //! conn.execute( //! "CREATE TABLE person ( -//! id INTEGER PRIMARY KEY, -//! name TEXT NOT NULL, -//! data BLOB -//! )", -//! [], +//! id INTEGER PRIMARY KEY, +//! name TEXT NOT NULL, +//! data BLOB +//! )", +//! (), // empty list of parameters. //! )?; //! let me = Person { //! id: 0, @@ -29,7 +33,7 @@ //! }; //! conn.execute( //! "INSERT INTO person (name, data) VALUES (?1, ?2)", -//! params![me.name, me.data], +//! (&me.name, &me.data), //! )?; //! //! let mut stmt = conn.prepare("SELECT id, name, data FROM person")?; @@ -53,7 +57,6 @@ pub use libsqlite3_sys as ffi; use std::cell::RefCell; -use std::convert; use std::default::Default; use std::ffi::{CStr, CString}; use std::fmt; @@ -145,7 +148,7 @@ const STATEMENT_CACHE_DEFAULT_CAPACITY: usize = 16; #[deprecated = "Use an empty array instead; `stmt.execute(NO_PARAMS)` => `stmt.execute([])`"] pub const NO_PARAMS: &[&dyn ToSql] = &[]; -/// A macro making it more convenient to pass heterogeneous or long lists of +/// A macro making it more convenient to longer lists of /// parameters as a `&[&dyn ToSql]`. /// /// # Example @@ -161,8 +164,7 @@ pub const NO_PARAMS: &[&dyn ToSql] = &[]; /// /// fn add_person(conn: &Connection, person: &Person) -> Result<()> { /// conn.execute( -/// "INSERT INTO person (name, age_in_years, data) -/// VALUES (?1, ?2, ?3)", +/// "INSERT INTO person(name, age_in_years, data) VALUES (?1, ?2, ?3)", /// params![person.name, person.age_in_years, person.data], /// )?; /// Ok(()) @@ -269,7 +271,7 @@ fn str_for_sqlite(s: &[u8]) -> Result<(*const c_char, c_int, ffi::sqlite3_destru // Helper to cast to c_int safely, returning the correct error type if the cast // failed. fn len_as_c_int(len: usize) -> Result<c_int> { - if len >= (c_int::max_value() as usize) { + if len >= (c_int::MAX as usize) { Err(Error::SqliteFailure( ffi::Error::new(ffi::SQLITE_TOOBIG), None, @@ -320,7 +322,7 @@ pub const TEMP_DB: DatabaseName<'static> = DatabaseName::Temp; ))] impl DatabaseName<'_> { #[inline] - fn as_cstring(&self) -> Result<util::SmallCString> { + fn as_cstring(&self) -> Result<SmallCString> { use self::DatabaseName::{Attached, Main, Temp}; match *self { Main => str_to_cstring("main"), @@ -347,27 +349,58 @@ impl Drop for Connection { } impl Connection { - /// Open a new connection to a SQLite database. - /// - /// `Connection::open(path)` is equivalent to - /// `Connection::open_with_flags(path, - /// OpenFlags::SQLITE_OPEN_READ_WRITE | - /// OpenFlags::SQLITE_OPEN_CREATE)`. + /// Open a new connection to a SQLite database. If a database does not exist + /// at the path, one is created. /// /// ```rust,no_run /// # use rusqlite::{Connection, Result}; /// fn open_my_db() -> Result<()> { /// let path = "./my_db.db3"; - /// let db = Connection::open(&path)?; + /// let db = Connection::open(path)?; + /// // Use the database somehow... /// println!("{}", db.is_autocommit()); /// Ok(()) /// } /// ``` /// + /// # Flags + /// + /// `Connection::open(path)` is equivalent to using + /// [`Connection::open_with_flags`] with the default [`OpenFlags`]. That is, + /// it's equivalent to: + /// + /// ```ignore + /// Connection::open_with_flags( + /// path, + /// OpenFlags::SQLITE_OPEN_READ_WRITE + /// | OpenFlags::SQLITE_OPEN_CREATE + /// | OpenFlags::SQLITE_OPEN_URI + /// | OpenFlags::SQLITE_OPEN_NO_MUTEX, + /// ) + /// ``` + /// + /// These flags have the following effects: + /// + /// - Open the database for both reading or writing. + /// - Create the database if one does not exist at the path. + /// - Allow the filename to be interpreted as a URI (see <https://www.sqlite.org/uri.html#uri_filenames_in_sqlite> + /// for details). + /// - Disables the use of a per-connection mutex. + /// + /// Rusqlite enforces thread-safety at compile time, so additional + /// locking is not needed and provides no benefit. (See the + /// documentation on [`OpenFlags::SQLITE_OPEN_FULL_MUTEX`] for some + /// additional discussion about this). + /// + /// Most of these are also the default settings for the C API, although + /// technically the default locking behavior is controlled by the flags used + /// when compiling SQLite -- rather than let it vary, we choose `NO_MUTEX` + /// because it's a fairly clearly the best choice for users of this library. + /// /// # Failure /// - /// Will return `Err` if `path` cannot be converted to a C-compatible - /// string or if the underlying SQLite open call fails. + /// Will return `Err` if `path` cannot be converted to a C-compatible string + /// or if the underlying SQLite open call fails. #[inline] pub fn open<P: AsRef<Path>>(path: P) -> Result<Connection> { let flags = OpenFlags::default(); @@ -561,6 +594,16 @@ impl Connection { self.path.as_deref() } + /// Attempts to free as much heap memory as possible from the database + /// connection. + /// + /// This calls [`sqlite3_db_release_memory`](https://www.sqlite.org/c3ref/db_release_memory.html). + #[inline] + #[cfg(feature = "release_memory")] + pub fn release_memory(&self) -> Result<()> { + self.db.borrow_mut().release_memory() + } + /// Convenience method to prepare and execute a single SQL statement with /// named parameter(s). /// @@ -680,7 +723,7 @@ impl Connection { where P: Params, F: FnOnce(&Row<'_>) -> Result<T, E>, - E: convert::From<Error>, + E: From<Error>, { let mut stmt = self.prepare(sql)?; stmt.check_no_tail()?; @@ -902,8 +945,10 @@ impl Connection { /// Return the number of rows modified, inserted or deleted by the most /// recently completed INSERT, UPDATE or DELETE statement on the database /// connection. + /// + /// See <https://www.sqlite.org/c3ref/changes.html> #[inline] - fn changes(&self) -> usize { + pub fn changes(&self) -> u64 { self.db.borrow().changes() } @@ -928,6 +973,13 @@ impl Connection { pub fn cache_flush(&self) -> Result<()> { self.db.borrow_mut().cache_flush() } + + /// Determine if a database is read-only + #[cfg(feature = "modern_sqlite")] // 3.7.11 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn is_readonly(&self, db_name: DatabaseName<'_>) -> Result<bool> { + self.db.borrow().db_readonly(db_name) + } } impl fmt::Debug for Connection { @@ -1000,40 +1052,81 @@ impl<'conn> Iterator for Batch<'conn, '_> { } bitflags::bitflags! { - /// Flags for opening SQLite database connections. - /// See [sqlite3_open_v2](http://www.sqlite.org/c3ref/open.html) for details. + /// Flags for opening SQLite database connections. See + /// [sqlite3_open_v2](http://www.sqlite.org/c3ref/open.html) for details. + /// + /// The default open flags are `SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_CREATE + /// | SQLITE_OPEN_URI | SQLITE_OPEN_NO_MUTEX`. See [`Connection::open`] for + /// some discussion about these flags. #[repr(C)] pub struct OpenFlags: ::std::os::raw::c_int { /// The database is opened in read-only mode. /// If the database does not already exist, an error is returned. - const SQLITE_OPEN_READ_ONLY = ffi::SQLITE_OPEN_READONLY; + const SQLITE_OPEN_READ_ONLY = ffi::SQLITE_OPEN_READONLY; /// The database is opened for reading and writing if possible, /// or reading only if the file is write protected by the operating system. /// In either case the database must already exist, otherwise an error is returned. - const SQLITE_OPEN_READ_WRITE = ffi::SQLITE_OPEN_READWRITE; + const SQLITE_OPEN_READ_WRITE = ffi::SQLITE_OPEN_READWRITE; /// The database is created if it does not already exist - const SQLITE_OPEN_CREATE = ffi::SQLITE_OPEN_CREATE; + const SQLITE_OPEN_CREATE = ffi::SQLITE_OPEN_CREATE; /// The filename can be interpreted as a URI if this flag is set. - const SQLITE_OPEN_URI = 0x0000_0040; + const SQLITE_OPEN_URI = 0x0000_0040; /// The database will be opened as an in-memory database. - const SQLITE_OPEN_MEMORY = 0x0000_0080; - /// The new database connection will use the "multi-thread" threading mode. - const SQLITE_OPEN_NO_MUTEX = ffi::SQLITE_OPEN_NOMUTEX; - /// The new database connection will use the "serialized" threading mode. - const SQLITE_OPEN_FULL_MUTEX = ffi::SQLITE_OPEN_FULLMUTEX; - /// The database is opened shared cache enabled. - const SQLITE_OPEN_SHARED_CACHE = 0x0002_0000; + const SQLITE_OPEN_MEMORY = 0x0000_0080; + /// The new database connection will not use a per-connection mutex (the + /// connection will use the "multi-thread" threading mode, in SQLite + /// parlance). + /// + /// This is used by default, as proper `Send`/`Sync` usage (in + /// particular, the fact that [`Connection`] does not implement `Sync`) + /// ensures thread-safety without the need to perform locking around all + /// calls. + const SQLITE_OPEN_NO_MUTEX = ffi::SQLITE_OPEN_NOMUTEX; + /// The new database connection will use a per-connection mutex -- the + /// "serialized" threading mode, in SQLite parlance. + /// + /// # Caveats + /// + /// This flag should probably never be used with `rusqlite`, as we + /// ensure thread-safety statically (we implement [`Send`] and not + /// [`Sync`]). That said + /// + /// Critically, even if this flag is used, the [`Connection`] is not + /// safe to use across multiple threads simultaneously. To access a + /// database from multiple threads, you should either create multiple + /// connections, one for each thread (if you have very many threads, + /// wrapping the `rusqlite::Connection` in a mutex is also reasonable). + /// + /// This is both because of the additional per-connection state stored + /// by `rusqlite` (for example, the prepared statement cache), and + /// because not all of SQLites functions are fully thread safe, even in + /// serialized/`SQLITE_OPEN_FULLMUTEX` mode. + /// + /// All that said, it's fairly harmless to enable this flag with + /// `rusqlite`, it will just slow things down while providing no + /// benefit. + const SQLITE_OPEN_FULL_MUTEX = ffi::SQLITE_OPEN_FULLMUTEX; + /// The database is opened with shared cache enabled. + /// + /// This is frequently useful for in-memory connections, but note that + /// broadly speaking it's discouraged by SQLite itself, which states + /// "Any use of shared cache is discouraged" in the official + /// [documentation](https://www.sqlite.org/c3ref/enable_shared_cache.html). + const SQLITE_OPEN_SHARED_CACHE = 0x0002_0000; /// The database is opened shared cache disabled. const SQLITE_OPEN_PRIVATE_CACHE = 0x0004_0000; - /// The database filename is not allowed to be a symbolic link. + /// The database filename is not allowed to be a symbolic link. (3.31.0) const SQLITE_OPEN_NOFOLLOW = 0x0100_0000; - /// Extended result codes. + /// Extended result codes. (3.37.0) const SQLITE_OPEN_EXRESCODE = 0x0200_0000; } } impl Default for OpenFlags { + #[inline] fn default() -> OpenFlags { + // Note: update the `Connection::open` and top-level `OpenFlags` docs if + // you change these. OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_NO_MUTEX @@ -1204,7 +1297,7 @@ mod test { let filename = "no_such_file.db"; let result = Connection::open_with_flags(filename, OpenFlags::SQLITE_OPEN_READ_ONLY); assert!(result.is_err()); - let err = result.err().unwrap(); + let err = result.unwrap_err(); if let Error::SqliteFailure(e, Some(msg)) = err { assert_eq!(ErrorCode::CannotOpen, e.code); assert_eq!(ffi::SQLITE_CANTOPEN, e.extended_code); @@ -1340,8 +1433,9 @@ mod test { fn test_execute_select() { let db = checked_memory_handle(); let err = db.execute("SELECT 1 WHERE 1 < ?", [1i32]).unwrap_err(); - assert!( - err == Error::ExecuteReturnedResults, + assert_eq!( + err, + Error::ExecuteReturnedResults, "Unexpected error: {}", err ); @@ -1509,15 +1603,28 @@ mod test { #[test] fn test_pragma_query_row() -> Result<()> { let db = Connection::open_in_memory()?; - assert_eq!( "memory", db.query_row::<String, _, _>("PRAGMA journal_mode", [], |r| r.get(0))? ); - assert_eq!( - "off", - db.query_row::<String, _, _>("PRAGMA journal_mode=off", [], |r| r.get(0))? - ); + let mode = db.query_row::<String, _, _>("PRAGMA journal_mode=off", [], |r| r.get(0))?; + if cfg!(features = "bundled") { + assert_eq!(mode, "off"); + } else { + // Note: system SQLite on macOS defaults to "off" rather than + // "memory" for the journal mode (which cannot be changed for + // in-memory connections). This seems like it's *probably* legal + // according to the docs below, so we relax this test when not + // bundling: + // + // From https://www.sqlite.org/pragma.html#pragma_journal_mode + // > Note that the journal_mode for an in-memory database is either + // > MEMORY or OFF and can not be changed to a different value. An + // > attempt to change the journal_mode of an in-memory database to + // > any setting other than MEMORY or OFF is ignored. + assert!(mode == "memory" || mode == "off", "Got mode {:?}", mode); + } + Ok(()) } @@ -1632,7 +1739,7 @@ mod test { db.create_scalar_function( "interrupt", 0, - crate::functions::FunctionFlags::default(), + functions::FunctionFlags::default(), move |_| { interrupt_handle.interrupt(); Ok(0) @@ -1644,14 +1751,10 @@ mod test { let result: Result<Vec<i32>> = stmt.query([])?.map(|r| r.get(0)).collect(); - match result.unwrap_err() { - Error::SqliteFailure(err, _) => { - assert_eq!(err.code, ErrorCode::OperationInterrupted); - } - err => { - panic!("Unexpected error {}", err); - } - } + assert_eq!( + result.unwrap_err().sqlite_error_code(), + Some(ErrorCode::OperationInterrupted) + ); Ok(()) } @@ -1995,7 +2098,7 @@ mod test { } #[test] - #[cfg(all(feature = "bundled", not(feature = "bundled-sqlcipher")))] // SQLite >= 3.35.0 + #[cfg(feature = "modern_sqlite")] fn test_returning() -> Result<()> { let db = Connection::open_in_memory()?; db.execute_batch("CREATE TABLE foo(x INTEGER PRIMARY KEY)")?; @@ -2013,4 +2116,12 @@ mod test { let db = Connection::open_in_memory()?; db.cache_flush() } + + #[test] + #[cfg(feature = "modern_sqlite")] + pub fn db_readonly() -> Result<()> { + let db = Connection::open_in_memory()?; + assert!(!db.is_readonly(MAIN_DB)?); + Ok(()) + } } diff --git a/src/params.rs b/src/params.rs index 54aa571..6ab6b5f 100644 --- a/src/params.rs +++ b/src/params.rs @@ -31,12 +31,19 @@ use sealed::Sealed; /// parameters is known at compile time, this can be done in one of the /// following ways: /// +/// - For small lists of parameters up to 16 items, they may alternatively be +/// passed as a tuple, as in `thing.query((1, "foo"))`. +/// +/// This is somewhat inconvenient for a single item, since you need a +/// weird-looking trailing comma: `thing.query(("example",))`. That case is +/// perhaps more cleanly expressed as `thing.query(["example"])`. +/// /// - Using the [`rusqlite::params!`](crate::params!) macro, e.g. /// `thing.query(rusqlite::params![1, "foo", bar])`. This is mostly useful for -/// heterogeneous lists of parameters, or lists where the number of parameters -/// exceeds 32. +/// heterogeneous lists where the number of parameters greater than 16, or +/// homogenous lists of parameters where the number of parameters exceeds 32. /// -/// - For small heterogeneous lists of parameters, they can either be passed as: +/// - For small homogeneous lists of parameters, they can either be passed as: /// /// - an array, as in `thing.query([1i32, 2, 3, 4])` or `thing.query(["foo", /// "bar", "baz"])`. @@ -65,6 +72,9 @@ use sealed::Sealed; /// fn update_rows(conn: &Connection) -> Result<()> { /// let mut stmt = conn.prepare("INSERT INTO test (a, b) VALUES (?, ?)")?; /// +/// // Using a tuple: +/// stmt.execute((0, "foobar"))?; +/// /// // Using `rusqlite::params!`: /// stmt.execute(params![1i32, "blah"])?; /// @@ -127,12 +137,26 @@ use sealed::Sealed; /// /// ## No parameters /// -/// You can just use an empty array literal for no params. The -/// `rusqlite::NO_PARAMS` constant which was so common in previous versions of -/// this library is no longer needed (and is now deprecated). +/// You can just use an empty tuple or the empty array literal to run a query +/// that accepts no parameters. (The `rusqlite::NO_PARAMS` constant which was +/// common in previous versions of this library is no longer needed, and is now +/// deprecated). /// /// ### Example (no parameters) /// +/// The empty tuple: +/// +/// ```rust,no_run +/// # use rusqlite::{Connection, Result, params}; +/// fn delete_all_users(conn: &Connection) -> Result<()> { +/// // You may also use `()`. +/// conn.execute("DELETE FROM users", ())?; +/// Ok(()) +/// } +/// ``` +/// +/// The empty array: +/// /// ```rust,no_run /// # use rusqlite::{Connection, Result, params}; /// fn delete_all_users(conn: &Connection) -> Result<()> { @@ -147,10 +171,11 @@ use sealed::Sealed; /// If you have a number of parameters which is unknown at compile time (for /// example, building a dynamic query at runtime), you have two choices: /// -/// - Use a `&[&dyn ToSql]`, which is nice if you have one otherwise might be -/// annoying. +/// - Use a `&[&dyn ToSql]`. This is often annoying to construct if you don't +/// already have this type on-hand. /// - Use the [`ParamsFromIter`] type. This essentially lets you wrap an -/// iterator some `T: ToSql` with something that implements `Params`. +/// iterator some `T: ToSql` with something that implements `Params`. The +/// usage of this looks like `rusqlite::params_from_iter(something)`. /// /// A lot of the considerations here are similar either way, so you should see /// the [`ParamsFromIter`] documentation for more info / examples. @@ -169,14 +194,23 @@ pub trait Params: Sealed { // Explicitly impl for empty array. Critically, for `conn.execute([])` to be // unambiguous, this must be the *only* implementation for an empty array. This // avoids `NO_PARAMS` being a necessary part of the API. +// +// This sadly prevents `impl<T: ToSql, const N: usize> Params for [T; N]`, which +// forces people to use `params![...]` or `rusqlite::params_from_iter` for long +// homogenous lists of parameters. This is not that big of a deal, but is +// unfortunate, especially because I mostly did it because I wanted a simple +// syntax for no-params that didnt require importing -- the empty tuple fits +// that nicely, but I didn't think of it until much later. +// +// Admittedly, if we did have the generic impl, then we *wouldn't* support the +// empty array literal as a parameter, since the `T` there would fail to be +// inferred. The error message here would probably be quite bad, and so on +// further thought, probably would end up causing *more* surprises, not less. impl Sealed for [&(dyn ToSql + Send + Sync); 0] {} impl Params for [&(dyn ToSql + Send + Sync); 0] { #[inline] fn __bind_in(self, stmt: &mut Statement<'_>) -> Result<()> { - // Note: Can't just return `Ok(())` — `Statement::bind_parameters` - // checks that the right number of params were passed too. - // TODO: we should have tests for `Error::InvalidParameterCount`... - stmt.bind_parameters(&[] as &[&dyn ToSql]) + stmt.ensure_parameter_count(0) } } @@ -196,6 +230,69 @@ impl Params for &[(&str, &dyn ToSql)] { } } +// Manual impls for the empty and singleton tuple, although the rest are covered +// by macros. +impl Sealed for () {} +impl Params for () { + #[inline] + fn __bind_in(self, stmt: &mut Statement<'_>) -> Result<()> { + stmt.ensure_parameter_count(0) + } +} + +// I'm pretty sure you could tweak the `single_tuple_impl` to accept this. +impl<T: ToSql> Sealed for (T,) {} +impl<T: ToSql> Params for (T,) { + #[inline] + fn __bind_in(self, stmt: &mut Statement<'_>) -> Result<()> { + stmt.ensure_parameter_count(1)?; + stmt.raw_bind_parameter(1, self.0)?; + Ok(()) + } +} + +macro_rules! single_tuple_impl { + ($count:literal : $(($field:tt $ftype:ident)),* $(,)?) => { + impl<$($ftype,)*> Sealed for ($($ftype,)*) where $($ftype: ToSql,)* {} + impl<$($ftype,)*> Params for ($($ftype,)*) where $($ftype: ToSql,)* { + fn __bind_in(self, stmt: &mut Statement<'_>) -> Result<()> { + stmt.ensure_parameter_count($count)?; + $({ + debug_assert!($field < $count); + stmt.raw_bind_parameter($field + 1, self.$field)?; + })+ + Ok(()) + } + } + } +} + +// We use a the macro for the rest, but don't bother with trying to implement it +// in a single invocation (it's possible to do, but my attempts were almost the +// same amount of code as just writing it out this way, and much more dense -- +// it is a more complicated case than the TryFrom macro we have for row->tuple). +// +// Note that going up to 16 (rather than the 12 that the impls in the stdlib +// usually support) is just because we did the same in the `TryFrom<Row>` impl. +// I didn't catch that then, but there's no reason to remove it, and it seems +// nice to be consistent here; this way putting data in the database and getting +// data out of the database are more symmetric in a (mostly superficial) sense. +single_tuple_impl!(2: (0 A), (1 B)); +single_tuple_impl!(3: (0 A), (1 B), (2 C)); +single_tuple_impl!(4: (0 A), (1 B), (2 C), (3 D)); +single_tuple_impl!(5: (0 A), (1 B), (2 C), (3 D), (4 E)); +single_tuple_impl!(6: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F)); +single_tuple_impl!(7: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G)); +single_tuple_impl!(8: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H)); +single_tuple_impl!(9: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I)); +single_tuple_impl!(10: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J)); +single_tuple_impl!(11: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K)); +single_tuple_impl!(12: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L)); +single_tuple_impl!(13: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M)); +single_tuple_impl!(14: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N)); +single_tuple_impl!(15: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O)); +single_tuple_impl!(16: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O), (15 P)); + macro_rules! impl_for_array_ref { ($($N:literal)+) => {$( // These are already generic, and there's a shedload of them, so lets @@ -225,9 +322,12 @@ macro_rules! impl_for_array_ref { // Following libstd/libcore's (old) lead, implement this for arrays up to `[_; // 32]`. Note `[_; 0]` is intentionally omitted for coherence reasons, see the // note above the impl of `[&dyn ToSql; 0]` for more information. +// +// Note that this unfortunately means we can't use const generics here, but I +// don't really think it matters -- users who hit that can use `params!` anyway. impl_for_array_ref!( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - 18 19 20 21 22 23 24 25 26 27 29 30 31 32 + 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ); /// Adapter type which allows any iterator over [`ToSql`] values to implement diff --git a/src/pragma.rs b/src/pragma.rs index 1c81c95..673478a 100644 --- a/src/pragma.rs +++ b/src/pragma.rs @@ -406,18 +406,20 @@ mod test { let db = Connection::open_in_memory()?; let journal_mode: String = db.pragma_update_and_check(None, "journal_mode", "OFF", |row| row.get(0))?; - assert_eq!("off", &journal_mode); - // Sanity checks to ensure the move to a generic `ToSql` wasn't breaking - assert_eq!( - "off", - db.pragma_update_and_check(None, "journal_mode", &"OFF", |row| row - .get::<_, String>(0))?, + assert!( + journal_mode == "off" || journal_mode == "memory", + "mode: {:?}", + journal_mode, ); + // Sanity checks to ensure the move to a generic `ToSql` wasn't breaking + let mode = db + .pragma_update_and_check(None, "journal_mode", &"OFF", |row| row.get::<_, String>(0))?; + assert!(mode == "off" || mode == "memory", "mode: {:?}", mode); + let param: &dyn crate::ToSql = &"OFF"; - assert_eq!( - "off", - db.pragma_update_and_check(None, "journal_mode", param, |row| row.get::<_, String>(0))?, - ); + let mode = + db.pragma_update_and_check(None, "journal_mode", param, |row| row.get::<_, String>(0))?; + assert!(mode == "off" || mode == "memory", "mode: {:?}", mode); Ok(()) } diff --git a/src/raw_statement.rs b/src/raw_statement.rs index 8e624dc..f057761 100644 --- a/src/raw_statement.rs +++ b/src/raw_statement.rs @@ -110,7 +110,7 @@ impl RawStatement { #[cfg(feature = "unlock_notify")] pub fn step(&self) -> c_int { use crate::unlock_notify; - let mut db = core::ptr::null_mut::<ffi::sqlite3>(); + let mut db = ptr::null_mut::<ffi::sqlite3>(); loop { unsafe { let mut rc = ffi::sqlite3_step(self.ptr); @@ -224,6 +224,14 @@ impl RawStatement { pub fn tail(&self) -> usize { self.tail } + + #[inline] + #[cfg(feature = "modern_sqlite")] // 3.28.0 + pub fn is_explain(&self) -> i32 { + unsafe { ffi::sqlite3_stmt_isexplain(self.ptr) } + } + + // TODO sqlite3_normalized_sql (https://sqlite.org/c3ref/expanded_sql.html) // 3.27.0 + SQLITE_ENABLE_NORMALIZE } impl Drop for RawStatement { @@ -171,7 +171,7 @@ pub struct AndThenRows<'stmt, F> { impl<T, E, F> Iterator for AndThenRows<'_, F> where - E: convert::From<Error>, + E: From<Error>, F: FnMut(&Row<'_>) -> Result<T, E>, { type Item = Result<T, E>; diff --git a/src/session.rs b/src/session.rs index b02d306..f8aa764 100644 --- a/src/session.rs +++ b/src/session.rs @@ -168,7 +168,7 @@ impl Session<'_> { if r != ffi::SQLITE_OK { let errmsg: *mut c_char = errmsg; let message = errmsg_to_string(&*errmsg); - ffi::sqlite3_free(errmsg as *mut ::std::os::raw::c_void); + ffi::sqlite3_free(errmsg as *mut c_void); return Err(error_from_sqlite_code(r, Some(message))); } } diff --git a/src/statement.rs b/src/statement.rs index 60abd90..ee5e220 100644 --- a/src/statement.rs +++ b/src/statement.rs @@ -3,7 +3,7 @@ use std::os::raw::{c_int, c_void}; #[cfg(feature = "array")] use std::rc::Rc; use std::slice::from_raw_parts; -use std::{convert, fmt, mem, ptr, str}; +use std::{fmt, mem, ptr, str}; use super::ffi; use super::{len_as_c_int, str_for_sqlite}; @@ -33,17 +33,45 @@ impl Statement<'_> { /// ```rust,no_run /// # use rusqlite::{Connection, Result, params}; /// fn update_rows(conn: &Connection) -> Result<()> { - /// let mut stmt = conn.prepare("UPDATE foo SET bar = 'baz' WHERE qux = ?")?; + /// let mut stmt = conn.prepare("UPDATE foo SET bar = ? WHERE qux = ?")?; + /// // For a single parameter, or a parameter where all the values have + /// // the same type, just passing an array is simplest. + /// stmt.execute([2i32])?; /// // The `rusqlite::params!` macro is mostly useful when the parameters do not /// // all have the same type, or if there are more than 32 parameters - /// // at once. + /// // at once, but it can be used in other cases. /// stmt.execute(params![1i32])?; /// // However, it's not required, many cases are fine as: /// stmt.execute(&[&2i32])?; /// // Or even: /// stmt.execute([2i32])?; + /// // If you really want to, this is an option as well. + /// stmt.execute((2i32,))?; + /// Ok(()) + /// } + /// ``` + /// + /// #### Heterogeneous positional parameters + /// + /// ``` + /// use rusqlite::{Connection, Result}; + /// fn store_file(conn: &Connection, path: &str, data: &[u8]) -> Result<()> { + /// # // no need to do it for real. + /// # fn sha256(_: &[u8]) -> [u8; 32] { [0; 32] } + /// let query = "INSERT OR REPLACE INTO files(path, hash, data) VALUES (?, ?, ?)"; + /// let mut stmt = conn.prepare_cached(query)?; + /// let hash: [u8; 32] = sha256(data); + /// // The easiest way to pass positional parameters of have several + /// // different types is by using a tuple. + /// stmt.execute((path, hash, data))?; + /// // Using the `params!` macro also works, and supports longer parameter lists: + /// stmt.execute(rusqlite::params![path, hash, data])?; /// Ok(()) /// } + /// # let c = Connection::open_in_memory().unwrap(); + /// # c.execute_batch("CREATE TABLE files(path TEXT PRIMARY KEY, hash BLOB, data BLOB)").unwrap(); + /// # store_file(&c, "foo/bar.txt", b"bibble").unwrap(); + /// # store_file(&c, "foo/baz.txt", b"bobble").unwrap(); /// ``` /// /// ### Use with named parameters @@ -104,6 +132,7 @@ impl Statement<'_> { /// Will return `Err` if binding parameters fails, the executed statement /// returns rows (in which case `query` should be used instead), or the /// underlying SQLite call fails. + #[doc(hidden)] #[deprecated = "You can use `execute` with named params now."] #[inline] pub fn execute_named(&mut self, params: &[(&str, &dyn ToSql)]) -> Result<usize> { @@ -239,6 +268,7 @@ impl Statement<'_> { /// # Failure /// /// Will return `Err` if binding parameters fails. + #[doc(hidden)] #[deprecated = "You can use `query` with named params now."] pub fn query_named(&mut self, params: &[(&str, &dyn ToSql)]) -> Result<Rows<'_>> { self.query(params) @@ -316,6 +346,7 @@ impl Statement<'_> { /// ## Failure /// /// Will return `Err` if binding parameters fails. + #[doc(hidden)] #[deprecated = "You can use `query_map` with named params now."] pub fn query_map_named<T, F>( &mut self, @@ -386,7 +417,7 @@ impl Statement<'_> { pub fn query_and_then<T, E, P, F>(&mut self, params: P, f: F) -> Result<AndThenRows<'_, F>> where P: Params, - E: convert::From<Error>, + E: From<Error>, F: FnMut(&Row<'_>) -> Result<T, E>, { self.query(params).map(|rows| rows.and_then(f)) @@ -408,6 +439,7 @@ impl Statement<'_> { /// ## Failure /// /// Will return `Err` if binding parameters fails. + #[doc(hidden)] #[deprecated = "You can use `query_and_then` with named params now."] pub fn query_and_then_named<T, E, F>( &mut self, @@ -415,7 +447,7 @@ impl Statement<'_> { f: F, ) -> Result<AndThenRows<'_, F>> where - E: convert::From<Error>, + E: From<Error>, F: FnMut(&Row<'_>) -> Result<T, E>, { self.query_and_then(params, f) @@ -475,6 +507,7 @@ impl Statement<'_> { /// /// Will return `Err` if `sql` cannot be converted to a C-compatible string /// or if the underlying SQLite call fails. + #[doc(hidden)] #[deprecated = "You can use `query_row` with named params now."] pub fn query_row_named<T, F>(&mut self, params: &[(&str, &dyn ToSql)], f: F) -> Result<T> where @@ -567,6 +600,16 @@ impl Statement<'_> { } #[inline] + pub(crate) fn ensure_parameter_count(&self, n: usize) -> Result<()> { + let count = self.parameter_count(); + if count != n { + Err(Error::InvalidParameterCount(n, count)) + } else { + Ok(()) + } + } + + #[inline] pub(crate) fn bind_parameters_named<T: ?Sized + ToSql>( &mut self, params: &[(&str, &T)], @@ -606,9 +649,14 @@ impl Statement<'_> { /// - binding named and positional parameters in the same query. /// - separating parameter binding from query execution. /// - /// Statements that have had their parameters bound this way should be - /// queried or executed by [`Statement::raw_query`] or - /// [`Statement::raw_execute`]. Other functions are not guaranteed to work. + /// In general, statements that have had *any* parameters bound this way + /// should have *all* parameters bound this way, and be queried or executed + /// by [`Statement::raw_query`] or [`Statement::raw_execute`], other usage + /// is unsupported and will likely, probably in surprising ways. + /// + /// That is: Do not mix the "raw" statement functions with the rest of the + /// API, or the results may be surprising, and may even change in future + /// versions without comment. /// /// # Example /// @@ -684,6 +732,7 @@ impl Statement<'_> { #[cfg(feature = "blob")] ToSqlOutput::ZeroBlob(len) => { + // TODO sqlite3_bind_zeroblob64 // 3.8.11 return self .conn .decode_result(unsafe { ffi::sqlite3_bind_zeroblob(ptr, col as c_int, len) }); @@ -707,6 +756,7 @@ impl Statement<'_> { ValueRef::Real(r) => unsafe { ffi::sqlite3_bind_double(ptr, col as c_int, r) }, ValueRef::Text(s) => unsafe { let (c_str, len, destructor) = str_for_sqlite(s)?; + // TODO sqlite3_bind_text64 // 3.8.7 ffi::sqlite3_bind_text(ptr, col as c_int, c_str, len, destructor) }, ValueRef::Blob(b) => unsafe { @@ -714,6 +764,7 @@ impl Statement<'_> { if length == 0 { ffi::sqlite3_bind_zeroblob(ptr, col as c_int, 0) } else { + // TODO sqlite3_bind_blob64 // 3.8.7 ffi::sqlite3_bind_blob( ptr, col as c_int, @@ -732,7 +783,7 @@ impl Statement<'_> { let r = self.stmt.step(); self.stmt.reset(); match r { - ffi::SQLITE_DONE => Ok(self.conn.changes()), + ffi::SQLITE_DONE => Ok(self.conn.changes() as usize), ffi::SQLITE_ROW => Err(Error::ExecuteReturnedResults), _ => Err(self.conn.decode_result(r).unwrap_err()), } @@ -795,6 +846,16 @@ impl Statement<'_> { self.stmt.get_status(status, true) } + /// Returns 1 if the prepared statement is an EXPLAIN statement, + /// or 2 if the statement is an EXPLAIN QUERY PLAN, + /// or 0 if it is an ordinary statement or a NULL pointer. + #[inline] + #[cfg(feature = "modern_sqlite")] // 3.28.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn is_explain(&self) -> i32 { + self.stmt.is_explain() + } + #[cfg(feature = "extra_check")] #[inline] pub(crate) fn check_no_tail(&self) -> Result<()> { @@ -942,15 +1003,15 @@ pub enum StatementStatus { AutoIndex = 3, /// Equivalent to SQLITE_STMTSTATUS_VM_STEP VmStep = 4, - /// Equivalent to SQLITE_STMTSTATUS_REPREPARE + /// Equivalent to SQLITE_STMTSTATUS_REPREPARE (3.20.0) RePrepare = 5, - /// Equivalent to SQLITE_STMTSTATUS_RUN + /// Equivalent to SQLITE_STMTSTATUS_RUN (3.20.0) Run = 6, /// Equivalent to SQLITE_STMTSTATUS_FILTER_MISS FilterMiss = 7, /// Equivalent to SQLITE_STMTSTATUS_FILTER_HIT FilterHit = 8, - /// Equivalent to SQLITE_STMTSTATUS_MEMUSED + /// Equivalent to SQLITE_STMTSTATUS_MEMUSED (3.20.0) MemUsed = 99, } @@ -1266,6 +1327,41 @@ mod test { assert!(!stmt.exists([0i32])?); Ok(()) } + #[test] + fn test_tuple_params() -> Result<()> { + let db = Connection::open_in_memory()?; + let s = db.query_row("SELECT printf('[%s]', ?)", ("abc",), |r| { + r.get::<_, String>(0) + })?; + assert_eq!(s, "[abc]"); + let s = db.query_row( + "SELECT printf('%d %s %d', ?, ?, ?)", + (1i32, "abc", 2i32), + |r| r.get::<_, String>(0), + )?; + assert_eq!(s, "1 abc 2"); + let s = db.query_row( + "SELECT printf('%d %s %d %d', ?, ?, ?, ?)", + (1, "abc", 2i32, 4i64), + |r| r.get::<_, String>(0), + )?; + assert_eq!(s, "1 abc 2 4"); + #[rustfmt::skip] + let bigtup = ( + 0, "a", 1, "b", 2, "c", 3, "d", + 4, "e", 5, "f", 6, "g", 7, "h", + ); + let query = "SELECT printf( + '%d %s | %d %s | %d %s | %d %s || %d %s | %d %s | %d %s | %d %s', + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ? + )"; + let s = db.query_row(query, bigtup, |r| r.get::<_, String>(0))?; + assert_eq!(s, "0 a | 1 b | 2 c | 3 d || 4 e | 5 f | 6 g | 7 h"); + Ok(()) + } #[test] fn test_query_row() -> Result<()> { @@ -1430,4 +1526,30 @@ mod test { assert_eq!(expected, actual); Ok(()) } + + #[test] + #[cfg(feature = "modern_sqlite")] + fn is_explain() -> Result<()> { + let db = Connection::open_in_memory()?; + let stmt = db.prepare("SELECT 1;")?; + assert_eq!(0, stmt.is_explain()); + Ok(()) + } + + #[test] + #[cfg(all(feature = "modern_sqlite", not(feature = "bundled-sqlcipher")))] // SQLite >= 3.38.0 + fn test_error_offset() -> Result<()> { + use crate::ffi::ErrorCode; + let db = Connection::open_in_memory()?; + let r = db.execute_batch("SELECT CURRENT_TIMESTANP;"); + assert!(r.is_err()); + match r.unwrap_err() { + Error::SqlInputError { error, offset, .. } => { + assert_eq!(error.code, ErrorCode::Unknown); + assert_eq!(offset, 7); + } + err => panic!("Unexpected error {}", err), + } + Ok(()) + } } diff --git a/src/trace.rs b/src/trace.rs index 3932976..7fc9090 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -119,6 +119,8 @@ impl Connection { None => unsafe { ffi::sqlite3_profile(c.db(), None, ptr::null_mut()) }, }; } + + // TODO sqlite3_trace_v2 (https://sqlite.org/c3ref/trace_v2.html) // 3.14.0, #977 } #[cfg(test)] diff --git a/src/transaction.rs b/src/transaction.rs index 296b2aa..2c4c6c0 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -87,6 +87,7 @@ pub struct Transaction<'conn> { /// sp.commit() /// } /// ``` +#[derive(Debug)] pub struct Savepoint<'conn> { conn: &'conn Connection, name: String, @@ -375,6 +376,20 @@ impl Drop for Savepoint<'_> { } } +/// Transaction state of a database +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +#[cfg(feature = "modern_sqlite")] // 3.37.0 +#[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] +pub enum TransactionState { + /// Equivalent to SQLITE_TXN_NONE + None, + /// Equivalent to SQLITE_TXN_READ + Read, + /// Equivalent to SQLITE_TXN_WRITE + Write, +} + impl Connection { /// Begin a new transaction with the default behavior (DEFERRED). /// @@ -499,6 +514,16 @@ impl Connection { pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> { Savepoint::with_name(self, name) } + + /// Determine the transaction state of a database + #[cfg(feature = "modern_sqlite")] // 3.37.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn transaction_state( + &self, + db_name: Option<crate::DatabaseName<'_>>, + ) -> Result<TransactionState> { + self.db.borrow().txn_state(db_name) + } } #[cfg(test)] @@ -534,7 +559,7 @@ mod test { } Ok(()) } - fn assert_nested_tx_error(e: crate::Error) { + fn assert_nested_tx_error(e: Error) { if let Error::SqliteFailure(e, Some(m)) = &e { assert_eq!(e.extended_code, crate::ffi::SQLITE_ERROR); // FIXME: Not ideal... @@ -710,4 +735,25 @@ mod test { assert_eq!(x, i); Ok(()) } + + #[test] + #[cfg(feature = "modern_sqlite")] + fn txn_state() -> Result<()> { + use super::TransactionState; + use crate::DatabaseName; + let db = Connection::open_in_memory()?; + assert_eq!( + TransactionState::None, + db.transaction_state(Some(DatabaseName::Main))? + ); + assert_eq!(TransactionState::None, db.transaction_state(None)?); + db.execute_batch("BEGIN")?; + assert_eq!(TransactionState::None, db.transaction_state(None)?); + let _: i32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?; + assert_eq!(TransactionState::Read, db.transaction_state(None)?); + db.pragma_update(None, "user_version", 1)?; + assert_eq!(TransactionState::Write, db.transaction_state(None)?); + db.execute_batch("ROLLBACK")?; + Ok(()) + } } diff --git a/src/types/from_sql.rs b/src/types/from_sql.rs index 88bdd14..b95a378 100644 --- a/src/types/from_sql.rs +++ b/src/types/from_sql.rs @@ -240,7 +240,7 @@ mod test { fn check_ranges<T>(db: &Connection, out_of_range: &[i64], in_range: &[i64]) where - T: Into<i64> + FromSql + ::std::fmt::Debug, + T: Into<i64> + FromSql + std::fmt::Debug, { for n in out_of_range { let err = db diff --git a/src/types/mod.rs b/src/types/mod.rs index 4e524b2..4000ae2 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -110,7 +110,7 @@ pub struct Null; /// SQLite data types. /// See [Fundamental Datatypes](https://sqlite.org/c3ref/c_blob.html). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Type { /// NULL Null, @@ -140,7 +140,6 @@ impl fmt::Display for Type { mod test { use super::Value; use crate::{params, Connection, Error, Result, Statement}; - use std::f64::EPSILON; use std::os::raw::{c_double, c_int}; fn checked_memory_handle() -> Result<Connection> { @@ -264,7 +263,7 @@ mod test { assert_eq!(vec![1, 2], row.get::<_, Vec<u8>>(0)?); assert_eq!("text", row.get::<_, String>(1)?); assert_eq!(1, row.get::<_, c_int>(2)?); - assert!((1.5 - row.get::<_, c_double>(3)?).abs() < EPSILON); + assert!((1.5 - row.get::<_, c_double>(3)?).abs() < f64::EPSILON); assert_eq!(row.get::<_, Option<c_int>>(4)?, None); assert_eq!(row.get::<_, Option<c_double>>(4)?, None); assert_eq!(row.get::<_, Option<String>>(4)?, None); @@ -272,85 +271,67 @@ mod test { // check some invalid types // 0 is actually a blob (Vec<u8>) - assert!(is_invalid_column_type( - row.get::<_, c_int>(0).err().unwrap() - )); - assert!(is_invalid_column_type( - row.get::<_, c_int>(0).err().unwrap() - )); + assert!(is_invalid_column_type(row.get::<_, c_int>(0).unwrap_err())); + assert!(is_invalid_column_type(row.get::<_, c_int>(0).unwrap_err())); assert!(is_invalid_column_type(row.get::<_, i64>(0).err().unwrap())); assert!(is_invalid_column_type( - row.get::<_, c_double>(0).err().unwrap() - )); - assert!(is_invalid_column_type( - row.get::<_, String>(0).err().unwrap() + row.get::<_, c_double>(0).unwrap_err() )); + assert!(is_invalid_column_type(row.get::<_, String>(0).unwrap_err())); #[cfg(feature = "time")] assert!(is_invalid_column_type( - row.get::<_, time::OffsetDateTime>(0).err().unwrap() + row.get::<_, time::OffsetDateTime>(0).unwrap_err() )); assert!(is_invalid_column_type( - row.get::<_, Option<c_int>>(0).err().unwrap() + row.get::<_, Option<c_int>>(0).unwrap_err() )); // 1 is actually a text (String) - assert!(is_invalid_column_type( - row.get::<_, c_int>(1).err().unwrap() - )); + assert!(is_invalid_column_type(row.get::<_, c_int>(1).unwrap_err())); assert!(is_invalid_column_type(row.get::<_, i64>(1).err().unwrap())); assert!(is_invalid_column_type( - row.get::<_, c_double>(1).err().unwrap() + row.get::<_, c_double>(1).unwrap_err() )); assert!(is_invalid_column_type( - row.get::<_, Vec<u8>>(1).err().unwrap() + row.get::<_, Vec<u8>>(1).unwrap_err() )); assert!(is_invalid_column_type( - row.get::<_, Option<c_int>>(1).err().unwrap() + row.get::<_, Option<c_int>>(1).unwrap_err() )); // 2 is actually an integer + assert!(is_invalid_column_type(row.get::<_, String>(2).unwrap_err())); assert!(is_invalid_column_type( - row.get::<_, String>(2).err().unwrap() - )); - assert!(is_invalid_column_type( - row.get::<_, Vec<u8>>(2).err().unwrap() + row.get::<_, Vec<u8>>(2).unwrap_err() )); assert!(is_invalid_column_type( - row.get::<_, Option<String>>(2).err().unwrap() + row.get::<_, Option<String>>(2).unwrap_err() )); // 3 is actually a float (c_double) - assert!(is_invalid_column_type( - row.get::<_, c_int>(3).err().unwrap() - )); + assert!(is_invalid_column_type(row.get::<_, c_int>(3).unwrap_err())); assert!(is_invalid_column_type(row.get::<_, i64>(3).err().unwrap())); + assert!(is_invalid_column_type(row.get::<_, String>(3).unwrap_err())); assert!(is_invalid_column_type( - row.get::<_, String>(3).err().unwrap() - )); - assert!(is_invalid_column_type( - row.get::<_, Vec<u8>>(3).err().unwrap() + row.get::<_, Vec<u8>>(3).unwrap_err() )); assert!(is_invalid_column_type( - row.get::<_, Option<c_int>>(3).err().unwrap() + row.get::<_, Option<c_int>>(3).unwrap_err() )); // 4 is actually NULL - assert!(is_invalid_column_type( - row.get::<_, c_int>(4).err().unwrap() - )); + assert!(is_invalid_column_type(row.get::<_, c_int>(4).unwrap_err())); assert!(is_invalid_column_type(row.get::<_, i64>(4).err().unwrap())); assert!(is_invalid_column_type( - row.get::<_, c_double>(4).err().unwrap() - )); - assert!(is_invalid_column_type( - row.get::<_, String>(4).err().unwrap() + row.get::<_, c_double>(4).unwrap_err() )); + assert!(is_invalid_column_type(row.get::<_, String>(4).unwrap_err())); assert!(is_invalid_column_type( - row.get::<_, Vec<u8>>(4).err().unwrap() + row.get::<_, Vec<u8>>(4).unwrap_err() )); #[cfg(feature = "time")] assert!(is_invalid_column_type( - row.get::<_, time::OffsetDateTime>(4).err().unwrap() + row.get::<_, time::OffsetDateTime>(4).unwrap_err() )); Ok(()) } @@ -373,7 +354,7 @@ mod test { assert_eq!(Value::Text(String::from("text")), row.get::<_, Value>(1)?); assert_eq!(Value::Integer(1), row.get::<_, Value>(2)?); match row.get::<_, Value>(3)? { - Value::Real(val) => assert!((1.5 - val).abs() < EPSILON), + Value::Real(val) => assert!((1.5 - val).abs() < f64::EPSILON), x => panic!("Invalid Value {:?}", x), } assert_eq!(Value::Null, row.get::<_, Value>(4)?); diff --git a/src/types/to_sql.rs b/src/types/to_sql.rs index 2445339..4e0d882 100644 --- a/src/types/to_sql.rs +++ b/src/types/to_sql.rs @@ -363,7 +363,6 @@ mod test { #[test] fn test_i128() -> crate::Result<()> { use crate::Connection; - use std::i128; let db = Connection::open_in_memory()?; db.execute_batch("CREATE TABLE foo (i128 BLOB, desc TEXT)")?; db.execute( diff --git a/src/types/value_ref.rs b/src/types/value_ref.rs index c0d81ca..12806f8 100644 --- a/src/types/value_ref.rs +++ b/src/types/value_ref.rs @@ -257,4 +257,7 @@ impl<'a> ValueRef<'a> { _ => unreachable!("sqlite3_value_type returned invalid value"), } } + + // TODO sqlite3_value_nochange // 3.22.0 & VTab xUpdate + // TODO sqlite3_value_frombind // 3.28.0 } diff --git a/src/util/small_cstr.rs b/src/util/small_cstr.rs index 4543c62..78e43bd 100644 --- a/src/util/small_cstr.rs +++ b/src/util/small_cstr.rs @@ -5,7 +5,7 @@ use std::ffi::{CStr, CString, NulError}; /// small enough. Also guarantees it's input is UTF-8 -- used for cases where we /// need to pass a NUL-terminated string to SQLite, and we have a `&str`. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct SmallCString(smallvec::SmallVec<[u8; 16]>); +pub(crate) struct SmallCString(SmallVec<[u8; 16]>); impl SmallCString { #[inline] diff --git a/src/vtab/array.rs b/src/vtab/array.rs index adfd9c9..f09ac1a 100644 --- a/src/vtab/array.rs +++ b/src/vtab/array.rs @@ -117,7 +117,7 @@ unsafe impl<'vtab> VTab<'vtab> for ArrayTab { Ok(()) } - fn open(&self) -> Result<ArrayTabCursor<'_>> { + fn open(&mut self) -> Result<ArrayTabCursor<'_>> { Ok(ArrayTabCursor::new()) } } diff --git a/src/vtab/csvtab.rs b/src/vtab/csvtab.rs index df3529a..a65db05 100644 --- a/src/vtab/csvtab.rs +++ b/src/vtab/csvtab.rs @@ -11,7 +11,7 @@ //! // Note: This should be done once (usually when opening the DB). //! let db = Connection::open_in_memory()?; //! rusqlite::vtab::csvtab::load_module(&db)?; -//! // Assum3e my_csv.csv +//! // Assume my_csv.csv //! let schema = " //! CREATE VIRTUAL TABLE my_csv_data //! USING csv(filename = 'my_csv.csv') @@ -30,8 +30,8 @@ use std::str; use crate::ffi; use crate::types::Null; use crate::vtab::{ - dequote, escape_double_quote, parse_boolean, read_only_module, Context, CreateVTab, IndexInfo, - VTab, VTabConnection, VTabCursor, Values, + escape_double_quote, parse_boolean, read_only_module, Context, CreateVTab, IndexInfo, VTab, + VTabConfig, VTabConnection, VTabCursor, VTabKind, Values, }; use crate::{Connection, Error, Result}; @@ -74,19 +74,6 @@ impl CsvTab { .from_path(&self.filename) } - fn parameter(c_slice: &[u8]) -> Result<(&str, &str)> { - let arg = str::from_utf8(c_slice)?.trim(); - let mut split = arg.split('='); - if let Some(key) = split.next() { - if let Some(value) = split.next() { - let param = key.trim(); - let value = dequote(value); - return Ok((param, value)); - } - } - Err(Error::ModuleError(format!("illegal argument: '{}'", arg))) - } - fn parse_byte(arg: &str) -> Option<u8> { if arg.len() == 1 { arg.bytes().next() @@ -101,7 +88,7 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { type Cursor = CsvTabCursor<'vtab>; fn connect( - _: &mut VTabConnection, + db: &mut VTabConnection, _aux: Option<&()>, args: &[&[u8]], ) -> Result<(String, CsvTab)> { @@ -122,7 +109,7 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { let args = &args[3..]; for c_slice in args { - let (param, value) = CsvTab::parameter(c_slice)?; + let (param, value) = super::parameter(c_slice)?; match param { "filename" => { if !Path::new(value).exists() { @@ -249,7 +236,7 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { } schema = Some(sql); } - + db.config(VTabConfig::DirectOnly)?; Ok((schema.unwrap(), vtab)) } @@ -259,12 +246,14 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { Ok(()) } - fn open(&self) -> Result<CsvTabCursor<'_>> { + fn open(&mut self) -> Result<CsvTabCursor<'_>> { Ok(CsvTabCursor::new(self.reader()?)) } } -impl CreateVTab<'_> for CsvTab {} +impl CreateVTab<'_> for CsvTab { + const KIND: VTabKind = VTabKind::Default; +} /// A cursor for the CSV virtual table #[repr(C)] diff --git a/src/vtab/mod.rs b/src/vtab/mod.rs index bdb6509..07008f3 100644 --- a/src/vtab/mod.rs +++ b/src/vtab/mod.rs @@ -57,6 +57,23 @@ use crate::{str_to_cstring, Connection, Error, InnerConnection, Result}; // ffi::sqlite3_vtab => VTab // ffi::sqlite3_vtab_cursor => VTabCursor +/// Virtual table kind +pub enum VTabKind { + /// Non-eponymous + Default, + /// [`create`](CreateVTab::create) == [`connect`](VTab::connect) + /// + /// See [SQLite doc](https://sqlite.org/vtab.html#eponymous_virtual_tables) + Eponymous, + /// No [`create`](CreateVTab::create) / [`destroy`](CreateVTab::destroy) or + /// not used + /// + /// SQLite >= 3.9.0 + /// + /// See [SQLite doc](https://sqlite.org/vtab.html#eponymous_only_virtual_tables) + EponymousOnly, +} + /// Virtual table module /// /// (See [SQLite doc](https://sqlite.org/c3ref/module.html)) @@ -84,31 +101,26 @@ const ZERO_MODULE: ffi::sqlite3_module = unsafe { .module }; -/// Create a read-only virtual table implementation. -/// -/// Step 2 of [Creating New Virtual Table Implementations](https://sqlite.org/vtab.html#creating_new_virtual_table_implementations). -#[must_use] -pub fn read_only_module<'vtab, T: CreateVTab<'vtab>>() -> &'static Module<'vtab, T> { - // The xConnect and xCreate methods do the same thing, but they must be - // different so that the virtual table is not an eponymous virtual table. +macro_rules! module { + ($lt:lifetime, $vt:ty, $ct:ty, $xc:expr, $xd:expr, $xu:expr) => { #[allow(clippy::needless_update)] &Module { base: ffi::sqlite3_module { // We don't use V3 - iVersion: 2, // We don't use V2 or V3 features in read_only_module types - xCreate: Some(rust_create::<T>), - xConnect: Some(rust_connect::<T>), - xBestIndex: Some(rust_best_index::<T>), - xDisconnect: Some(rust_disconnect::<T>), - xDestroy: Some(rust_destroy::<T>), - xOpen: Some(rust_open::<T>), - xClose: Some(rust_close::<T::Cursor>), - xFilter: Some(rust_filter::<T::Cursor>), - xNext: Some(rust_next::<T::Cursor>), - xEof: Some(rust_eof::<T::Cursor>), - xColumn: Some(rust_column::<T::Cursor>), - xRowid: Some(rust_rowid::<T::Cursor>), - xUpdate: None, + iVersion: 2, + xCreate: $xc, + xConnect: Some(rust_connect::<$vt>), + xBestIndex: Some(rust_best_index::<$vt>), + xDisconnect: Some(rust_disconnect::<$vt>), + xDestroy: $xd, + xOpen: Some(rust_open::<$vt>), + xClose: Some(rust_close::<$ct>), + xFilter: Some(rust_filter::<$ct>), + xNext: Some(rust_next::<$ct>), + xEof: Some(rust_eof::<$ct>), + xColumn: Some(rust_column::<$ct>), + xRowid: Some(rust_rowid::<$ct>), // FIXME optional + xUpdate: $xu, xBegin: None, xSync: None, xCommit: None, @@ -120,7 +132,46 @@ pub fn read_only_module<'vtab, T: CreateVTab<'vtab>>() -> &'static Module<'vtab, xRollbackTo: None, ..ZERO_MODULE }, - phantom: PhantomData::<&'vtab T>, + phantom: PhantomData::<&$lt $vt>, + } + }; +} + +/// Create an modifiable virtual table implementation. +/// +/// Step 2 of [Creating New Virtual Table Implementations](https://sqlite.org/vtab.html#creating_new_virtual_table_implementations). +#[must_use] +pub fn update_module<'vtab, T: UpdateVTab<'vtab>>() -> &'static Module<'vtab, T> { + match T::KIND { + VTabKind::EponymousOnly => { + module!('vtab, T, T::Cursor, None, None, Some(rust_update::<T>)) + } + VTabKind::Eponymous => { + module!('vtab, T, T::Cursor, Some(rust_connect::<T>), Some(rust_disconnect::<T>), Some(rust_update::<T>)) + } + _ => { + module!('vtab, T, T::Cursor, Some(rust_create::<T>), Some(rust_destroy::<T>), Some(rust_update::<T>)) + } + } +} + +/// Create a read-only virtual table implementation. +/// +/// Step 2 of [Creating New Virtual Table Implementations](https://sqlite.org/vtab.html#creating_new_virtual_table_implementations). +#[must_use] +pub fn read_only_module<'vtab, T: CreateVTab<'vtab>>() -> &'static Module<'vtab, T> { + match T::KIND { + VTabKind::EponymousOnly => eponymous_only_module(), + VTabKind::Eponymous => { + // A virtual table is eponymous if its xCreate method is the exact same function + // as the xConnect method + module!('vtab, T, T::Cursor, Some(rust_connect::<T>), Some(rust_disconnect::<T>), None) + } + _ => { + // The xConnect and xCreate methods may do the same thing, but they must be + // different so that the virtual table is not an eponymous virtual table. + module!('vtab, T, T::Cursor, Some(rust_create::<T>), Some(rust_destroy::<T>), None) + } } } @@ -129,49 +180,36 @@ pub fn read_only_module<'vtab, T: CreateVTab<'vtab>>() -> &'static Module<'vtab, /// Step 2 of [Creating New Virtual Table Implementations](https://sqlite.org/vtab.html#creating_new_virtual_table_implementations). #[must_use] pub fn eponymous_only_module<'vtab, T: VTab<'vtab>>() -> &'static Module<'vtab, T> { - // A virtual table is eponymous if its xCreate method is the exact same function - // as the xConnect method For eponymous-only virtual tables, the xCreate - // method is NULL - #[allow(clippy::needless_update)] - &Module { - base: ffi::sqlite3_module { - // We don't use V3 - iVersion: 2, - xCreate: None, - xConnect: Some(rust_connect::<T>), - xBestIndex: Some(rust_best_index::<T>), - xDisconnect: Some(rust_disconnect::<T>), - xDestroy: None, - xOpen: Some(rust_open::<T>), - xClose: Some(rust_close::<T::Cursor>), - xFilter: Some(rust_filter::<T::Cursor>), - xNext: Some(rust_next::<T::Cursor>), - xEof: Some(rust_eof::<T::Cursor>), - xColumn: Some(rust_column::<T::Cursor>), - xRowid: Some(rust_rowid::<T::Cursor>), - xUpdate: None, - xBegin: None, - xSync: None, - xCommit: None, - xRollback: None, - xFindFunction: None, - xRename: None, - xSavepoint: None, - xRelease: None, - xRollbackTo: None, - ..ZERO_MODULE - }, - phantom: PhantomData::<&'vtab T>, - } + // For eponymous-only virtual tables, the xCreate method is NULL + module!('vtab, T, T::Cursor, None, None, None) +} + +/// Virtual table configuration options +#[repr(i32)] +#[non_exhaustive] +#[cfg(feature = "modern_sqlite")] // 3.7.7 +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum VTabConfig { + /// Equivalent to SQLITE_VTAB_CONSTRAINT_SUPPORT + ConstraintSupport = 1, + /// Equivalent to SQLITE_VTAB_INNOCUOUS + Innocuous = 2, + /// Equivalent to SQLITE_VTAB_DIRECTONLY + DirectOnly = 3, } /// `feature = "vtab"` pub struct VTabConnection(*mut ffi::sqlite3); impl VTabConnection { - // TODO sqlite3_vtab_config (http://sqlite.org/c3ref/vtab_config.html) + /// Configure various facets of the virtual table interface + #[cfg(feature = "modern_sqlite")] // 3.7.7 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn config(&mut self, config: VTabConfig) -> Result<()> { + crate::error::check(unsafe { ffi::sqlite3_vtab_config(self.0, config as c_int) }) + } - // TODO sqlite3_vtab_on_conflict (http://sqlite.org/c3ref/vtab_on_conflict.html) + // TODO sqlite3_vtab_on_conflict (http://sqlite.org/c3ref/vtab_on_conflict.html) & xUpdate /// Get access to the underlying SQLite database connection handle. /// @@ -191,7 +229,7 @@ impl VTabConnection { } } -/// Virtual table instance trait. +/// Eponymous-only virtual table instance trait. /// /// # Safety /// @@ -229,13 +267,17 @@ pub unsafe trait VTab<'vtab>: Sized { /// Create a new cursor used for accessing a virtual table. /// (See [SQLite doc](https://sqlite.org/vtab.html#the_xopen_method)) - fn open(&'vtab self) -> Result<Self::Cursor>; + fn open(&'vtab mut self) -> Result<Self::Cursor>; } -/// Non-eponymous virtual table instance trait. +/// Read-only virtual table instance trait. /// /// (See [SQLite doc](https://sqlite.org/c3ref/vtab.html)) pub trait CreateVTab<'vtab>: VTab<'vtab> { + /// For [`EponymousOnly`](VTabKind::EponymousOnly), + /// [`create`](CreateVTab::create) and [`destroy`](CreateVTab::destroy) are + /// not called + const KIND: VTabKind; /// Create a new instance of a virtual table in response to a CREATE VIRTUAL /// TABLE statement. The `db` parameter is a pointer to the SQLite /// database connection that is executing the CREATE VIRTUAL TABLE @@ -261,9 +303,26 @@ pub trait CreateVTab<'vtab>: VTab<'vtab> { } } +/// Writable virtual table instance trait. +/// +/// (See [SQLite doc](https://sqlite.org/vtab.html#xupdate)) +pub trait UpdateVTab<'vtab>: CreateVTab<'vtab> { + /// Delete rowid or PK + fn delete(&mut self, arg: ValueRef<'_>) -> Result<()>; + /// Insert: `args[0] == NULL: old rowid or PK, args[1]: new rowid or PK, + /// args[2]: ...` + /// + /// Return the new rowid. + // TODO Make the distinction between argv[1] == NULL and argv[1] != NULL ? + fn insert(&mut self, args: &Values<'_>) -> Result<i64>; + /// Update: `args[0] != NULL: old rowid or PK, args[1]: new row id or PK, + /// args[2]: ...` + fn update(&mut self, args: &Values<'_>) -> Result<()>; +} + /// Index constraint operator. /// See [Virtual Table Constraint Operator Codes](https://sqlite.org/c3ref/c_index_constraint_eq.html) for details. -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, PartialEq)] #[allow(non_snake_case, non_camel_case_types, missing_docs)] #[allow(clippy::upper_case_acronyms)] pub enum IndexConstraintOp { @@ -310,10 +369,24 @@ impl From<u8> for IndexConstraintOp { } } +#[cfg(feature = "modern_sqlite")] // 3.9.0 +bitflags::bitflags! { + /// Virtual table scan flags + /// See [Function Flags](https://sqlite.org/c3ref/c_index_scan_unique.html) for details. + #[repr(C)] + pub struct IndexFlags: ::std::os::raw::c_int { + /// Default + const NONE = 0; + /// Scan visits at most 1 row. + const SQLITE_INDEX_SCAN_UNIQUE = ffi::SQLITE_INDEX_SCAN_UNIQUE; + } +} + /// Pass information into and receive the reply from the /// [`VTab::best_index`] method. /// /// (See [SQLite doc](http://sqlite.org/c3ref/index_info.html)) +#[derive(Debug)] pub struct IndexInfo(*mut ffi::sqlite3_index_info); impl IndexInfo { @@ -376,6 +449,14 @@ impl IndexInfo { } } + /// String used to identify the index + pub fn set_idx_str(&mut self, idx_str: &str) { + unsafe { + (*self.0).idxStr = alloc(idx_str); + (*self.0).needToFreeIdxStr = 1; + } + } + /// True if output is already ordered #[inline] pub fn set_order_by_consumed(&mut self, order_by_consumed: bool) { @@ -402,10 +483,60 @@ impl IndexInfo { } } - // TODO idxFlags - // TODO colUsed + /// Mask of SQLITE_INDEX_SCAN_* flags. + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.9.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + #[inline] + pub fn set_idx_flags(&mut self, flags: IndexFlags) { + unsafe { (*self.0).idxFlags = flags.bits() }; + } - // TODO sqlite3_vtab_collation (http://sqlite.org/c3ref/vtab_collation.html) + /// Mask of columns used by statement + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.10.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + #[inline] + pub fn col_used(&self) -> u64 { + unsafe { (*self.0).colUsed } + } + + /// Determine the collation for a virtual table constraint + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.22.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn collation(&self, constraint_idx: usize) -> Result<&str> { + use std::ffi::CStr; + let idx = constraint_idx as c_int; + let collation = unsafe { ffi::sqlite3_vtab_collation(self.0, idx) }; + if collation.is_null() { + return Err(Error::SqliteFailure( + ffi::Error::new(ffi::SQLITE_MISUSE), + Some(format!("{} is out of range", constraint_idx)), + )); + } + Ok(unsafe { CStr::from_ptr(collation) }.to_str()?) + } + + /*/// Determine if a virtual table query is DISTINCT + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.38.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn distinct(&self) -> c_int { + unsafe { ffi::sqlite3_vtab_distinct(self.0) } + } + + /// Constraint values + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.38.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn set_rhs_value(&mut self, constraint_idx: c_int, value: ValueRef) -> Result<()> { + // TODO ValueRef to sqlite3_value + crate::error::check(unsafe { ffi::sqlite3_vtab_rhs_value(self.O, constraint_idx, value) }) + } + + /// Identify and handle IN constraints + #[cfg(feature = "modern_sqlite")] // SQLite >= 3.38.0 + #[cfg_attr(docsrs, doc(cfg(feature = "modern_sqlite")))] + pub fn set_in_constraint(&mut self, constraint_idx: c_int, b_handle: c_int) -> bool { + unsafe { ffi::sqlite3_vtab_in(self.0, constraint_idx, b_handle) != 0 } + } // TODO sqlite3_vtab_in_first / sqlite3_vtab_in_next https://sqlite.org/c3ref/vtab_in_first.html + */ } /// Iterate on index constraint and its associated usage. @@ -583,7 +714,7 @@ impl Context { Ok(()) } - // TODO sqlite3_vtab_nochange (http://sqlite.org/c3ref/vtab_nochange.html) + // TODO sqlite3_vtab_nochange (http://sqlite.org/c3ref/vtab_nochange.html) // 3.22.0 & xColumn } /// Wrapper to [`VTabCursor::filter`] arguments, the values @@ -651,6 +782,7 @@ impl Values<'_> { iter: self.args.iter(), } } + // TODO sqlite3_vtab_in_first / sqlite3_vtab_in_next https://sqlite.org/c3ref/vtab_in_first.html & 3.38.0 } impl<'a> IntoIterator for &'a Values<'a> { @@ -707,6 +839,13 @@ impl InnerConnection { module: &'static Module<'vtab, T>, aux: Option<T::Aux>, ) -> Result<()> { + use crate::version; + if version::version_number() < 3_009_000 && module.base.xCreate.is_none() { + return Err(Error::ModuleError(format!( + "Eponymous-only virtual table not supported by SQLite version {}", + version::version() + ))); + } let c_name = str_to_cstring(module_name)?; let r = match aux { Some(aux) => { @@ -754,7 +893,7 @@ pub fn dequote(s: &str) -> &str { } match s.bytes().next() { Some(b) if b == b'"' || b == b'\'' => match s.bytes().rev().next() { - Some(e) if e == b => &s[1..s.len() - 1], + Some(e) if e == b => &s[1..s.len() - 1], // FIXME handle inner escaped quote(s) _ => s, }, _ => s, @@ -784,6 +923,20 @@ pub fn parse_boolean(s: &str) -> Option<bool> { } } +/// `<param_name>=['"]?<param_value>['"]?` => `(<param_name>, <param_value>)` +pub fn parameter(c_slice: &[u8]) -> Result<(&str, &str)> { + let arg = std::str::from_utf8(c_slice)?.trim(); + let mut split = arg.split('='); + if let Some(key) = split.next() { + if let Some(value) = split.next() { + let param = key.trim(); + let value = dequote(value); + return Ok((param, value)); + } + } + Err(Error::ModuleError(format!("illegal argument: '{}'", arg))) +} + // FIXME copy/paste from function.rs unsafe extern "C" fn free_boxed_value<T>(p: *mut c_void) { drop(Box::from_raw(p.cast::<T>())); @@ -810,7 +963,7 @@ where .map(|&cs| CStr::from_ptr(cs).to_bytes()) // FIXME .to_str() -> Result<&str, Utf8Error> .collect::<Vec<_>>(); match T::create(&mut conn, aux.as_ref(), &vec[..]) { - Ok((sql, vtab)) => match ::std::ffi::CString::new(sql) { + Ok((sql, vtab)) => match std::ffi::CString::new(sql) { Ok(c_sql) => { let rc = ffi::sqlite3_declare_vtab(db, c_sql.as_ptr()); if rc == ffi::SQLITE_OK { @@ -862,7 +1015,7 @@ where .map(|&cs| CStr::from_ptr(cs).to_bytes()) // FIXME .to_str() -> Result<&str, Utf8Error> .collect::<Vec<_>>(); match T::connect(&mut conn, aux.as_ref(), &vec[..]) { - Ok((sql, vtab)) => match ::std::ffi::CString::new(sql) { + Ok((sql, vtab)) => match std::ffi::CString::new(sql) { Ok(c_sql) => { let rc = ffi::sqlite3_declare_vtab(db, c_sql.as_ptr()); if rc == ffi::SQLITE_OK { @@ -1061,6 +1214,49 @@ where } } +unsafe extern "C" fn rust_update<'vtab, T: 'vtab>( + vtab: *mut ffi::sqlite3_vtab, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + p_rowid: *mut ffi::sqlite3_int64, +) -> c_int +where + T: UpdateVTab<'vtab>, +{ + assert!(argc >= 1); + let args = slice::from_raw_parts_mut(argv, argc as usize); + let vt = vtab.cast::<T>(); + let r = if args.len() == 1 { + (*vt).delete(ValueRef::from_value(args[0])) + } else if ffi::sqlite3_value_type(args[0]) == ffi::SQLITE_NULL { + // TODO Make the distinction between argv[1] == NULL and argv[1] != NULL ? + let values = Values { args }; + match (*vt).insert(&values) { + Ok(rowid) => { + *p_rowid = rowid; + Ok(()) + } + Err(e) => Err(e), + } + } else { + let values = Values { args }; + (*vt).update(&values) + }; + match r { + Ok(_) => ffi::SQLITE_OK, + Err(Error::SqliteFailure(err, s)) => { + if let Some(err_msg) = s { + set_err_msg(vtab, &err_msg); + } + err.extended_code + } + Err(err) => { + set_err_msg(vtab, &err.to_string()); + ffi::SQLITE_ERROR + } + } +} + /// Virtual table cursors can set an error message by assigning a string to /// `zErrMsg`. #[cold] @@ -1138,6 +1334,8 @@ pub mod csvtab; #[cfg(feature = "series")] #[cfg_attr(docsrs, doc(cfg(feature = "series")))] pub mod series; // SQLite >= 3.9.0 +#[cfg(test)] +mod vtablog; #[cfg(test)] mod test { diff --git a/src/vtab/series.rs b/src/vtab/series.rs index f26212a..fffbd4d 100644 --- a/src/vtab/series.rs +++ b/src/vtab/series.rs @@ -10,8 +10,8 @@ use std::os::raw::c_int; use crate::ffi; use crate::types::Type; use crate::vtab::{ - eponymous_only_module, Context, IndexConstraintOp, IndexInfo, VTab, VTabConnection, VTabCursor, - Values, + eponymous_only_module, Context, IndexConstraintOp, IndexInfo, VTab, VTabConfig, VTabConnection, + VTabCursor, Values, }; use crate::{Connection, Error, Result}; @@ -57,13 +57,14 @@ unsafe impl<'vtab> VTab<'vtab> for SeriesTab { type Cursor = SeriesTabCursor<'vtab>; fn connect( - _: &mut VTabConnection, + db: &mut VTabConnection, _aux: Option<&()>, _args: &[&[u8]], ) -> Result<(String, SeriesTab)> { let vtab = SeriesTab { base: ffi::sqlite3_vtab::default(), }; + db.config(VTabConfig::Innocuous)?; Ok(( "CREATE TABLE x(value,start hidden,stop hidden,step hidden)".to_owned(), vtab, @@ -103,6 +104,8 @@ unsafe impl<'vtab> VTab<'vtab> for SeriesTab { let mut constraint_usage = info.constraint_usage(*j); constraint_usage.set_argv_index(n_arg); constraint_usage.set_omit(true); + #[cfg(all(test, feature = "modern_sqlite"))] + debug_assert_eq!(Ok("BINARY"), info.collation(*j)); } if !(unusable_mask & !idx_num).is_empty() { return Err(Error::SqliteFailure( @@ -150,7 +153,7 @@ unsafe impl<'vtab> VTab<'vtab> for SeriesTab { Ok(()) } - fn open(&self) -> Result<SeriesTabCursor<'_>> { + fn open(&mut self) -> Result<SeriesTabCursor<'_>> { Ok(SeriesTabCursor::new()) } } diff --git a/src/vtab/vtablog.rs b/src/vtab/vtablog.rs new file mode 100644 index 0000000..bc2e01f --- /dev/null +++ b/src/vtab/vtablog.rs @@ -0,0 +1,300 @@ +///! Port of C [vtablog](http://www.sqlite.org/cgi/src/finfo?name=ext/misc/vtablog.c) +use std::default::Default; +use std::marker::PhantomData; +use std::os::raw::c_int; +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::vtab::{ + update_module, Context, CreateVTab, IndexInfo, UpdateVTab, VTab, VTabConnection, VTabCursor, + VTabKind, Values, +}; +use crate::{ffi, ValueRef}; +use crate::{Connection, Error, Result}; + +/// Register the "vtablog" module. +pub fn load_module(conn: &Connection) -> Result<()> { + let aux: Option<()> = None; + conn.create_module("vtablog", update_module::<VTabLog>(), aux) +} + +/// An instance of the vtablog virtual table +#[repr(C)] +struct VTabLog { + /// Base class. Must be first + base: ffi::sqlite3_vtab, + /// Number of rows in the table + n_row: i64, + /// Instance number for this vtablog table + i_inst: usize, + /// Number of cursors created + n_cursor: usize, +} + +impl VTabLog { + fn connect_create( + _: &mut VTabConnection, + _: Option<&()>, + args: &[&[u8]], + is_create: bool, + ) -> Result<(String, VTabLog)> { + static N_INST: AtomicUsize = AtomicUsize::new(1); + let i_inst = N_INST.fetch_add(1, Ordering::SeqCst); + println!( + "VTabLog::{}(tab={}, args={:?}):", + if is_create { "create" } else { "connect" }, + i_inst, + args, + ); + let mut schema = None; + let mut n_row = None; + + let args = &args[3..]; + for c_slice in args { + let (param, value) = super::parameter(c_slice)?; + match param { + "schema" => { + if schema.is_some() { + return Err(Error::ModuleError(format!( + "more than one '{}' parameter", + param + ))); + } + schema = Some(value.to_owned()) + } + "rows" => { + if n_row.is_some() { + return Err(Error::ModuleError(format!( + "more than one '{}' parameter", + param + ))); + } + if let Ok(n) = i64::from_str(value) { + n_row = Some(n) + } + } + _ => { + return Err(Error::ModuleError(format!( + "unrecognized parameter '{}'", + param + ))); + } + } + } + if schema.is_none() { + return Err(Error::ModuleError("no schema defined".to_owned())); + } + let vtab = VTabLog { + base: ffi::sqlite3_vtab::default(), + n_row: n_row.unwrap_or(10), + i_inst, + n_cursor: 0, + }; + Ok((schema.unwrap(), vtab)) + } +} + +impl Drop for VTabLog { + fn drop(&mut self) { + println!("VTabLog::drop({})", self.i_inst); + } +} + +unsafe impl<'vtab> VTab<'vtab> for VTabLog { + type Aux = (); + type Cursor = VTabLogCursor<'vtab>; + + fn connect( + db: &mut VTabConnection, + aux: Option<&Self::Aux>, + args: &[&[u8]], + ) -> Result<(String, Self)> { + VTabLog::connect_create(db, aux, args, false) + } + + fn best_index(&self, info: &mut IndexInfo) -> Result<()> { + println!("VTabLog::best_index({})", self.i_inst); + info.set_estimated_cost(500.); + info.set_estimated_rows(500); + Ok(()) + } + + fn open(&'vtab mut self) -> Result<Self::Cursor> { + self.n_cursor += 1; + println!( + "VTabLog::open(tab={}, cursor={})", + self.i_inst, self.n_cursor + ); + Ok(VTabLogCursor { + base: ffi::sqlite3_vtab_cursor::default(), + i_cursor: self.n_cursor, + row_id: 0, + phantom: PhantomData, + }) + } +} + +impl<'vtab> CreateVTab<'vtab> for VTabLog { + const KIND: VTabKind = VTabKind::Default; + + fn create( + db: &mut VTabConnection, + aux: Option<&Self::Aux>, + args: &[&[u8]], + ) -> Result<(String, Self)> { + VTabLog::connect_create(db, aux, args, true) + } + + fn destroy(&self) -> Result<()> { + println!("VTabLog::destroy({})", self.i_inst); + Ok(()) + } +} + +impl<'vtab> UpdateVTab<'vtab> for VTabLog { + fn delete(&mut self, arg: ValueRef<'_>) -> Result<()> { + println!("VTabLog::delete({}, {:?})", self.i_inst, arg); + Ok(()) + } + + fn insert(&mut self, args: &Values<'_>) -> Result<i64> { + println!( + "VTabLog::insert({}, {:?})", + self.i_inst, + args.iter().collect::<Vec<ValueRef<'_>>>() + ); + Ok(self.n_row as i64) + } + + fn update(&mut self, args: &Values<'_>) -> Result<()> { + println!( + "VTabLog::update({}, {:?})", + self.i_inst, + args.iter().collect::<Vec<ValueRef<'_>>>() + ); + Ok(()) + } +} + +/// A cursor for the Series virtual table +#[repr(C)] +struct VTabLogCursor<'vtab> { + /// Base class. Must be first + base: ffi::sqlite3_vtab_cursor, + /// Cursor number + i_cursor: usize, + /// The rowid + row_id: i64, + phantom: PhantomData<&'vtab VTabLog>, +} + +impl VTabLogCursor<'_> { + fn vtab(&self) -> &VTabLog { + unsafe { &*(self.base.pVtab as *const VTabLog) } + } +} + +impl Drop for VTabLogCursor<'_> { + fn drop(&mut self) { + println!( + "VTabLogCursor::drop(tab={}, cursor={})", + self.vtab().i_inst, + self.i_cursor + ); + } +} + +unsafe impl VTabCursor for VTabLogCursor<'_> { + fn filter(&mut self, _: c_int, _: Option<&str>, _: &Values<'_>) -> Result<()> { + println!( + "VTabLogCursor::filter(tab={}, cursor={})", + self.vtab().i_inst, + self.i_cursor + ); + self.row_id = 0; + Ok(()) + } + + fn next(&mut self) -> Result<()> { + println!( + "VTabLogCursor::next(tab={}, cursor={}): rowid {} -> {}", + self.vtab().i_inst, + self.i_cursor, + self.row_id, + self.row_id + 1 + ); + self.row_id += 1; + Ok(()) + } + + fn eof(&self) -> bool { + let eof = self.row_id >= self.vtab().n_row; + println!( + "VTabLogCursor::eof(tab={}, cursor={}): {}", + self.vtab().i_inst, + self.i_cursor, + eof, + ); + eof + } + + fn column(&self, ctx: &mut Context, i: c_int) -> Result<()> { + let value = if i < 26 { + format!( + "{}{}", + "abcdefghijklmnopqrstuvwyz".chars().nth(i as usize).unwrap(), + self.row_id + ) + } else { + format!("{}{}", i, self.row_id) + }; + println!( + "VTabLogCursor::column(tab={}, cursor={}, i={}): {}", + self.vtab().i_inst, + self.i_cursor, + i, + value, + ); + ctx.set_result(&value) + } + + fn rowid(&self) -> Result<i64> { + println!( + "VTabLogCursor::rowid(tab={}, cursor={}): {}", + self.vtab().i_inst, + self.i_cursor, + self.row_id, + ); + Ok(self.row_id) + } +} + +#[cfg(test)] +mod test { + use crate::{Connection, Result}; + #[test] + fn test_module() -> Result<()> { + let db = Connection::open_in_memory()?; + super::load_module(&db)?; + + db.execute_batch( + "CREATE VIRTUAL TABLE temp.log USING vtablog( + schema='CREATE TABLE x(a,b,c)', + rows=25 + );", + )?; + let mut stmt = db.prepare("SELECT * FROM log;")?; + let mut rows = stmt.query([])?; + while rows.next()?.is_some() {} + db.execute("DELETE FROM log WHERE a = ?", ["a1"])?; + db.execute( + "INSERT INTO log (a, b, c) VALUES (?, ?, ?)", + ["a", "b", "c"], + )?; + db.execute( + "UPDATE log SET b = ?, c = ? WHERE a = ?", + ["bn", "cn", "a1"], + )?; + Ok(()) + } +} diff --git a/tests/deny_single_threaded_sqlite_config.rs b/tests/deny_single_threaded_sqlite_config.rs index f6afdd5..adfc8e5 100644 --- a/tests/deny_single_threaded_sqlite_config.rs +++ b/tests/deny_single_threaded_sqlite_config.rs @@ -5,17 +5,16 @@ use rusqlite::ffi; use rusqlite::Connection; #[test] -#[should_panic] fn test_error_when_singlethread_mode() { // put SQLite into single-threaded mode unsafe { + // Note: macOS system SQLite seems to return an error if you attempt to + // reconfigure to single-threaded mode. if ffi::sqlite3_config(ffi::SQLITE_CONFIG_SINGLETHREAD) != ffi::SQLITE_OK { return; } - if ffi::sqlite3_initialize() != ffi::SQLITE_OK { - return; - } + assert_eq!(ffi::sqlite3_initialize(), ffi::SQLITE_OK); } - - let _ = Connection::open_in_memory().unwrap(); + let res = Connection::open_in_memory(); + assert!(res.is_err()); } diff --git a/tests/vtab.rs b/tests/vtab.rs index fa26459..6558206 100644 --- a/tests/vtab.rs +++ b/tests/vtab.rs @@ -39,7 +39,7 @@ fn test_dummy_module() -> rusqlite::Result<()> { Ok(()) } - fn open(&'vtab self) -> Result<DummyTabCursor<'vtab>> { + fn open(&'vtab mut self) -> Result<DummyTabCursor<'vtab>> { Ok(DummyTabCursor::default()) } } @@ -88,7 +88,7 @@ fn test_dummy_module() -> rusqlite::Result<()> { db.create_module::<DummyTab>("dummy", module, None)?; let version = version_number(); - if version < 3_008_012 { + if version < 3_009_000 { return Ok(()); } |