diff --git a/Cargo.lock b/Cargo.lock index 514f7ab..947f85f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "bindgen" version = "0.72.1" @@ -82,6 +88,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -94,40 +116,104 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown", + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -139,6 +225,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.85" @@ -149,11 +241,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libsqlite3-sys" @@ -166,6 +264,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -238,6 +342,16 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -256,6 +370,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -300,7 +420,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" dependencies = [ - "hashbrown", + "hashbrown 0.16.1", "thiserror", ] @@ -325,6 +445,19 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -337,6 +470,54 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -354,9 +535,11 @@ name = "sqlite-plugin" version = "0.9.0" dependencies = [ "bindgen", + "libc", "log", "parking_lot", "rusqlite", + "tempfile", ] [[package]] @@ -382,6 +565,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -408,12 +604,36 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -459,8 +679,151 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 2fe82a3..18c0c65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ map-unwrap-or = "warn" rusqlite = { version = "=0.38.0", features = ["blob", "trace", "bundled"] } log = { version = "=0.4.29", features = ["std"] } parking_lot = "=0.12.5" +tempfile = "3" +libc = "0.2" [build-dependencies] bindgen = { version = "0.72", default-features = false } diff --git a/src/vfs.rs b/src/vfs.rs index 4b94ab9..fa1105a 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -212,6 +212,36 @@ pub trait Vfs: Send + Sync { fn shm_unmap(&self, handle: &mut Self::Handle, delete: bool) -> VfsResult<()> { Err(vars::SQLITE_IOERR) } + + /// Memory-mapped page read (xFetch). Return a pointer to `amt` bytes of + /// the file starting at `offset`, or `Ok(None)` to decline and have `SQLite` + /// fall back to `xRead`. + /// + /// The default implementation declines all mmap requests. Override this to + /// enable memory-mapped I/O for your VFS (e.g. mmap the database file). + /// + /// # Safety contract + /// + /// The returned pointer must remain valid until `unfetch` is called with + /// the same offset. `SQLite` may read from the pointer concurrently from + /// multiple threads. + fn fetch( + &self, + handle: &mut Self::Handle, + offset: i64, + amt: usize, + ) -> VfsResult>> { + Ok(None) + } + + /// Release a memory-mapped page previously returned by `fetch`. + /// + /// If `ptr` is null, this is a hint that the VFS should reduce its + /// memory-mapped footprint (`SQLite` calls this when shrinking mmap). + /// The default implementation is a no-op. + fn unfetch(&self, handle: &mut Self::Handle, offset: i64, ptr: *mut u8) -> VfsResult<()> { + Ok(()) + } } #[derive(Clone)] @@ -330,8 +360,8 @@ fn register_inner( xShmLock: Some(x_shm_lock::), xShmBarrier: Some(x_shm_barrier::), xShmUnmap: Some(x_shm_unmap::), - xFetch: None, - xUnfetch: None, + xFetch: Some(x_fetch::), + xUnfetch: Some(x_unfetch::), }; let logger = SqliteLogger::new(sqlite_api.log); @@ -746,6 +776,38 @@ unsafe extern "C" fn x_shm_unmap( }) } +unsafe extern "C" fn x_fetch( + p_file: *mut ffi::sqlite3_file, + i_ofst: ffi::sqlite3_int64, + i_amt: c_int, + pp: *mut *mut c_void, +) -> c_int { + fallible(|| { + let file = unwrap_file!(p_file, T)?; + let vfs = unwrap_vfs!(file.vfs, T)?; + let amt: usize = i_amt.try_into().map_err(|_| vars::SQLITE_IOERR)?; + if let Some(ptr) = vfs.fetch(&mut file.handle, i_ofst, amt)? { + unsafe { *pp = ptr.as_ptr() as *mut c_void } + } else { + unsafe { *pp = null_mut() } + } + Ok(vars::SQLITE_OK) + }) +} + +unsafe extern "C" fn x_unfetch( + p_file: *mut ffi::sqlite3_file, + i_ofst: ffi::sqlite3_int64, + p: *mut c_void, +) -> c_int { + fallible(|| { + let file = unwrap_file!(p_file, T)?; + let vfs = unwrap_vfs!(file.vfs, T)?; + vfs.unfetch(&mut file.handle, i_ofst, p as *mut u8)?; + Ok(vars::SQLITE_OK) + }) +} + // the following functions are wrappers around the base vfs functions unsafe extern "C" fn x_dlopen( diff --git a/tests/fetch_test.rs b/tests/fetch_test.rs new file mode 100644 index 0000000..8f5830a --- /dev/null +++ b/tests/fetch_test.rs @@ -0,0 +1,293 @@ +//! Tests for xFetch/xUnfetch (iVersion 3) support. +//! +//! Implements a minimal file-backed VFS with real mmap-based fetch/unfetch. +//! Each VFS instance has its own atomic counters to prove SQLite calls +//! fetch() and unfetch(), safe for parallel test execution. + +use std::fs::{self, OpenOptions}; +use std::os::unix::fs::FileExt; +use std::os::unix::io::AsRawFd; +use std::path::PathBuf; +use std::ptr::NonNull; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use sqlite_plugin::flags::{AccessFlags, LockLevel, OpenOpts}; +use sqlite_plugin::vars; +use sqlite_plugin::vfs::{RegisterOpts, Vfs, VfsHandle, VfsResult}; + +static VFS_COUNTER: AtomicU64 = AtomicU64::new(1); + +/// Per-VFS counters for fetch/unfetch calls. Returned from setup() so each +/// test gets its own counters, safe for parallel execution. +struct FetchCounters { + fetch: AtomicU64, + unfetch: AtomicU64, +} + +struct Handle { + file: std::fs::File, + #[allow(dead_code)] + path: PathBuf, + mmap_ptr: Option<*mut u8>, + mmap_len: usize, +} + +unsafe impl Send for Handle {} + +impl VfsHandle for Handle { + fn readonly(&self) -> bool { + false + } + fn in_memory(&self) -> bool { + false + } +} + +impl Drop for Handle { + fn drop(&mut self) { + if let Some(ptr) = self.mmap_ptr.take() { + unsafe { + libc::munmap(ptr as *mut libc::c_void, self.mmap_len); + } + } + } +} + +struct FetchVfs { + dir: PathBuf, + counters: Arc, +} + +impl Vfs for FetchVfs { + type Handle = Handle; + + fn open(&self, path: Option<&str>, _: OpenOpts) -> VfsResult { + let p = self.dir.join(path.unwrap_or("temp.db")); + if let Some(d) = p.parent() { + let _ = fs::create_dir_all(d); + } + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&p) + .map_err(|_| vars::SQLITE_CANTOPEN)?; + Ok(Handle { + file, + path: p, + mmap_ptr: None, + mmap_len: 0, + }) + } + + fn delete(&self, path: &str) -> VfsResult<()> { + let _ = fs::remove_file(self.dir.join(path)); + Ok(()) + } + + fn access(&self, path: &str, _: AccessFlags) -> VfsResult { + Ok(self.dir.join(path).exists()) + } + + fn file_size(&self, h: &mut Self::Handle) -> VfsResult { + h.file + .metadata() + .map(|m| m.len() as usize) + .map_err(|_| vars::SQLITE_IOERR) + } + + fn truncate(&self, h: &mut Self::Handle, sz: usize) -> VfsResult<()> { + if let Some(ptr) = h.mmap_ptr.take() { + unsafe { + libc::munmap(ptr as *mut libc::c_void, h.mmap_len); + } + h.mmap_len = 0; + } + h.file.set_len(sz as u64).map_err(|_| vars::SQLITE_IOERR) + } + + fn write(&self, h: &mut Self::Handle, off: usize, data: &[u8]) -> VfsResult { + h.file + .write_at(data, off as u64) + .map_err(|_| vars::SQLITE_IOERR) + } + + fn read(&self, h: &mut Self::Handle, off: usize, buf: &mut [u8]) -> VfsResult { + match h.file.read_at(buf, off as u64) { + Ok(n) => { + buf[n..].fill(0); + Ok(buf.len()) + } + Err(_) => Err(vars::SQLITE_IOERR_READ), + } + } + + fn lock(&self, _: &mut Self::Handle, _: LockLevel) -> VfsResult<()> { + Ok(()) + } + fn unlock(&self, _: &mut Self::Handle, _: LockLevel) -> VfsResult<()> { + Ok(()) + } + fn check_reserved_lock(&self, _: &mut Self::Handle) -> VfsResult { + Ok(false) + } + fn sync(&self, h: &mut Self::Handle) -> VfsResult<()> { + h.file.sync_all().map_err(|_| vars::SQLITE_IOERR_FSYNC) + } + fn close(&self, _: Self::Handle) -> VfsResult<()> { + Ok(()) + } + + fn fetch( + &self, + h: &mut Self::Handle, + offset: i64, + amt: usize, + ) -> VfsResult>> { + self.counters.fetch.fetch_add(1, Ordering::Relaxed); + + let file_len = h.file.metadata().map_err(|_| vars::SQLITE_IOERR)?.len() as usize; + let end = offset as usize + amt; + if end > file_len { + return Ok(None); + } + + if h.mmap_ptr.is_none() || h.mmap_len < end { + if let Some(ptr) = h.mmap_ptr.take() { + unsafe { + libc::munmap(ptr as *mut libc::c_void, h.mmap_len); + } + } + let map_len = file_len; + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + map_len, + libc::PROT_READ, + libc::MAP_SHARED, + h.file.as_raw_fd(), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + return Ok(None); + } + h.mmap_ptr = Some(ptr as *mut u8); + h.mmap_len = map_len; + } + + let base = h.mmap_ptr.expect("just mapped"); + let result = unsafe { base.add(offset as usize) }; + Ok(NonNull::new(result)) + } + + fn unfetch(&self, _h: &mut Self::Handle, _offset: i64, _ptr: *mut u8) -> VfsResult<()> { + self.counters.unfetch.fetch_add(1, Ordering::Relaxed); + Ok(()) + } +} + +fn setup(prefix: &str) -> (tempfile::TempDir, String, Arc) { + let dir = tempfile::tempdir().expect("tmpdir"); + let name = format!("{}_{}", prefix, VFS_COUNTER.fetch_add(1, Ordering::Relaxed)); + let counters = Arc::new(FetchCounters { + fetch: AtomicU64::new(0), + unfetch: AtomicU64::new(0), + }); + let vfs = FetchVfs { + dir: dir.path().to_path_buf(), + counters: Arc::clone(&counters), + }; + sqlite_plugin::vfs::register_static( + std::ffi::CString::new(name.as_str()).expect("name"), + vfs, + RegisterOpts { make_default: false }, + ) + .expect("register"); + (dir, name, counters) +} + +/// fetch() is called by SQLite when mmap_size > 0. +/// Verify data roundtrips correctly through mmap'd reads. +#[test] +fn test_fetch_mmap_reads() { + let (dir, vfs, counters) = setup("mmap"); + let conn = rusqlite::Connection::open_with_flags_and_vfs( + dir.path().join("test.db"), + rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE, + vfs.as_str(), + ) + .expect("open"); + + // Enable mmap -- this is required for SQLite to call xFetch + conn.execute_batch("PRAGMA mmap_size=1048576") + .expect("mmap_size"); + conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", []) + .expect("create"); + + for i in 0..200 { + conn.execute("INSERT INTO t VALUES (?, ?)", (i, format!("value_{i}"))) + .expect("insert"); + } + + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0)) + .expect("count"); + assert_eq!(count, 200); + + let v: String = conn + .query_row("SELECT v FROM t WHERE id=42", [], |r| r.get(0)) + .expect("select"); + assert_eq!(v, "value_42"); + + let fetches = counters.fetch.load(Ordering::Relaxed); + assert!( + fetches > 0, + "fetch() should have been called at least once (got {})", + fetches, + ); + + let unfetches = counters.unfetch.load(Ordering::Relaxed); + assert!( + unfetches > 0, + "unfetch() should have been called at least once (got {})", + unfetches, + ); + + eprintln!( + "fetch called {} times, unfetch called {} times", + fetches, unfetches + ); +} + +/// Enough writes to trigger auto-checkpoint, exercising fetch during checkpoint. +#[test] +fn test_fetch_survives_checkpoint() { + let (dir, vfs, counters) = setup("ckpt"); + let conn = rusqlite::Connection::open_with_flags_and_vfs( + dir.path().join("test.db"), + rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE, + vfs.as_str(), + ) + .expect("open"); + + conn.execute_batch("PRAGMA mmap_size=1048576") + .expect("mmap_size"); + conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, data TEXT)", []) + .expect("create"); + for i in 0..2500 { + conn.execute("INSERT INTO t (data) VALUES (?)", (format!("row_{i}"),)) + .expect("insert"); + } + + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0)) + .expect("count"); + assert_eq!(count, 2500); + + assert!( + counters.fetch.load(Ordering::Relaxed) > 0, + "fetch() should have been called during checkpoint workload", + ); +}