Snap for 10453563 from 048f509b989d65e59578f2e70bf3b55470c43632 to mainline-art-release

Change-Id: If4f932bbbd00791a26519984c9d17217633bee77
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
diff --git a/Android.bp b/Android.bp
index d5c31aa..3e4e03c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,7 +23,7 @@
     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: [
@@ -31,14 +31,17 @@
         "trace",
     ],
     rustlibs: [
-        "libbitflags",
+        "libbitflags-1.3.2",
         "libfallible_iterator",
         "libfallible_streaming_iterator",
         "libhashlink",
         "liblibsqlite3_sys",
-        "libmemchr",
         "libsmallvec",
     ],
+    apex_available: [
+        "//apex_available:platform",
+        "//apex_available:anyapex",
+    ],
 }
 
 rust_library {
diff --git a/Cargo.toml b/Cargo.toml
index f8b8dda..08b3bc0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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.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 @@
 optional = true
 
 [dependencies.uuid]
-version = "0.8"
+version = "1.0"
 optional = true
 
 [dev-dependencies.bencher]
@@ -135,7 +132,7 @@
 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 = "2.6.0"
 
 [dev-dependencies.uuid]
-version = "0.8"
+version = "1.0"
 features = ["v4"]
 
 [features]
@@ -211,6 +208,7 @@
     "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 @@
 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 @@
 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 @@
 [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 @@
 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 @@
 
 [dependencies.libsqlite3-sys]
 path = "libsqlite3-sys"
-version = "0.24.0"
+version = "0.25.0"
 
 [[test]]
 name = "config_log"
diff --git a/METADATA b/METADATA
index 77fbcb9..ea56c4e 100644
--- a/METADATA
+++ b/METADATA
@@ -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 @@
   }
   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
   }
 }
diff --git a/README.md b/README.md
index 073c464..fdc2381 100644
--- a/README.md
+++ b/README.md
@@ -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 @@
 
     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 @@
     };
     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 @@
 * `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 @@
 * 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/TEST_MAPPING b/TEST_MAPPING
index 91f37bf..5953440 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,19 +1,11 @@
 // Generated by update_crate_tests.py for tests that depend on this crate.
 {
-  "presubmit": [
+  "imports": [
     {
-      "name": "keystore2_test"
+      "path": "system/security/keystore2"
     },
     {
-      "name": "legacykeystore_test"
-    }
-  ],
-  "presubmit-rust": [
-    {
-      "name": "keystore2_test"
-    },
-    {
-      "name": "legacykeystore_test"
+      "path": "system/security/keystore2/legacykeystore"
     }
   ]
 }
diff --git a/patches/Android.bp.diff b/patches/Android.bp.diff
new file mode 100644
index 0000000..34d5397
--- /dev/null
+++ b/patches/Android.bp.diff
@@ -0,0 +1,13 @@
+diff --git a/Android.bp b/Android.bp
+index 51830a3..ca704b4 100644
+--- a/Android.bp
++++ b/Android.bp
+@@ -31,7 +31,7 @@ rust_library {
+         "trace",
+     ],
+     rustlibs: [
+-        "libbitflags",
++        "libbitflags-1.3.2",
+         "libfallible_iterator",
+         "libfallible_streaming_iterator",
+         "libhashlink",
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 @@
     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 @@
         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 @@
     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 @@
 
         #[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 @@
         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 @@
                     // 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 @@
 
     /// 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 @@
     #[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 @@
             }
             #[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<::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 @@
             #[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 @@
 
             #[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 @@
     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 @@
         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 @@
             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 @@
         /// 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 @@
     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 @@
     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 @@
 #[cfg(test)]
 mod test {
     use regex::Regex;
-    use std::f64::EPSILON;
     use std::os::raw::c_double;
 
     #[cfg(feature = "window")]
@@ -812,7 +831,7 @@
         )?;
         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 @@
             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::{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 @@
     }
 }
 
-/// 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 @@
 /// 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 @@
                 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 @@
     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 @@
 }
 
 /// [`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 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 @@
     // 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 @@
             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 @@
         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 @@
             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 @@
     }
 
     #[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 @@
     #[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 @@
 
 #[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);
     }
diff --git a/src/lib.rs b/src/lib.rs
index 1fa7a33..89f133e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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 @@
 #[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 @@
 ///
 /// 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 @@
 // 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 @@
 ))]
 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 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 @@
         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 @@
     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 @@
     /// 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 @@
     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 @@
 }
 
 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 @@
         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 @@
     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 @@
     #[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 @@
         db.create_scalar_function(
             "interrupt",
             0,
-            crate::functions::FunctionFlags::default(),
+            functions::FunctionFlags::default(),
             move |_| {
                 interrupt_handle.interrupt();
                 Ok(0)
@@ -1644,14 +1751,10 @@
 
         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 @@
     }
 
     #[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 @@
         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 @@
 /// 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 @@
 /// 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 @@
 ///
 /// ## 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 @@
 /// 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 @@
 // 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 @@
     }
 }
 
+// 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 @@
 // 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 @@
         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);
+        assert!(
+            journal_mode == "off" || journal_mode == "memory",
+            "mode: {:?}",
+            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))?,
-        );
+        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 @@
     #[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 @@
     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 {
diff --git a/src/row.rs b/src/row.rs
index c766e50..221905a 100644
--- a/src/row.rs
+++ b/src/row.rs
@@ -171,7 +171,7 @@
 
 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 @@
             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 @@
 #[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,19 +33,47 @@
     /// ```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
     ///
     /// ```rust,no_run
@@ -104,6 +132,7 @@
     /// 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 @@
     /// # 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 @@
     /// ## 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 @@
     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 @@
     /// ## 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 @@
         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 @@
     ///
     /// 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 @@
     }
 
     #[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 @@
     /// - 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 @@
 
             #[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 @@
             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 @@
                 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 @@
         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 @@
         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 @@
     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 @@
         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 @@
         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 @@
             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 @@
 ///     sp.commit()
 /// }
 /// ```
+#[derive(Debug)]
 pub struct Savepoint<'conn> {
     conn: &'conn Connection,
     name: String,
@@ -375,6 +376,20 @@
     }
 }
 
+/// 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 @@
     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 @@
         }
         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 @@
         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 @@
 
         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 @@
 
 /// 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 @@
 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 @@
         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 @@
         // 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()
+            row.get::<_, c_double>(0).unwrap_err()
         ));
-        assert!(is_invalid_column_type(
-            row.get::<_, String>(0).err().unwrap()
-        ));
+        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()
+            row.get::<_, Vec<u8>>(2).unwrap_err()
         ));
         assert!(is_invalid_column_type(
-            row.get::<_, Vec<u8>>(2).err().unwrap()
-        ));
-        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()
+            row.get::<_, Vec<u8>>(3).unwrap_err()
         ));
         assert!(is_invalid_column_type(
-            row.get::<_, Vec<u8>>(3).err().unwrap()
-        ));
-        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()
+            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::<_, String>(4).err().unwrap()
-        ));
-        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 @@
         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 @@
     #[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 @@
             _ => 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 @@
 /// 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 @@
         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 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 @@
             .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 @@
     type Cursor = CsvTabCursor<'vtab>;
 
     fn connect(
-        _: &mut VTabConnection,
+        db: &mut VTabConnection,
         _aux: Option<&()>,
         args: &[&[u8]],
     ) -> Result<(String, CsvTab)> {
@@ -122,7 +109,7 @@
 
         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 @@
             }
             schema = Some(sql);
         }
-
+        db.config(VTabConfig::DirectOnly)?;
         Ok((schema.unwrap(), vtab))
     }
 
@@ -259,12 +246,14 @@
         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 @@
 // 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 @@
     .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 @@
             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 @@
 /// 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 @@
     }
 }
 
-/// Virtual table instance trait.
+/// Eponymous-only virtual table instance trait.
 ///
 /// # Safety
 ///
@@ -229,13 +267,17 @@
 
     /// 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 @@
     }
 }
 
+/// 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 @@
     }
 }
 
+#[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 @@
         }
     }
 
+    /// 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 @@
         }
     }
 
-    // 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 @@
         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 @@
             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 @@
         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 @@
     }
     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 @@
     }
 }
 
+/// `<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 @@
         .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 @@
         .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 @@
     }
 }
 
+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 @@
 #[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 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 @@
     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 @@
             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 @@
         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::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 @@
             Ok(())
         }
 
-        fn open(&'vtab self) -> Result<DummyTabCursor<'vtab>> {
+        fn open(&'vtab mut self) -> Result<DummyTabCursor<'vtab>> {
             Ok(DummyTabCursor::default())
         }
     }
@@ -88,7 +88,7 @@
     db.create_module::<DummyTab>("dummy", module, None)?;
 
     let version = version_number();
-    if version < 3_008_012 {
+    if version < 3_009_000 {
         return Ok(());
     }