From 84b8d6039e5ac3774b467b92cdcf9eaab635be87 Mon Sep 17 00:00:00 2001 From: Luther Monson Date: Tue, 14 Jul 2026 17:01:49 -0700 Subject: [PATCH 1/3] feat(backend): experimental Turso Database engine backend (litewire-turso) New crate crates/litewire-turso implementing Backend/BackendConn over the Turso Database engine (the Rust rewrite of SQLite), pinned exactly at turso =0.7.0 (first non-pre release of the 0.7 line, Beta upstream). - Async-native: no spawn_blocking hop; per-session tokio Mutex mirrors the rusqlite backend's one-statement-at-a-time session semantics. - Per-connection isolation via one turso::Database factory handing each wire session its own turso::Connection (busy timeout 5000ms default). - Busy errors tagged with SQLite phrasing so the mysql/postgres error_map classifiers produce retryable lock-wait codes. - VACUUM rejected with a clear unsupported error; multi-process access, ATTACH/DETACH, and non-UTF-8 TEXT caveats documented in crate docs. - Facade: feature "turso", off by default; re-exports litewire_turso and Turso. - Tests mirror the rusqlite backend suite (17 unit tests incl. isolation) plus a litewire-level integration test (sessions, transactions, last_insert_rowid, affected rows, describe_columns). --- Cargo.lock | 1075 ++++++++++++++++- Cargo.toml | 7 + README.md | 1 + crates/litewire-turso/Cargo.toml | 17 + crates/litewire-turso/src/lib.rs | 671 ++++++++++ .../litewire-turso/tests/litewire_basics.rs | 140 +++ crates/litewire/Cargo.toml | 4 + crates/litewire/src/lib.rs | 9 + 8 files changed, 1884 insertions(+), 40 deletions(-) create mode 100644 crates/litewire-turso/Cargo.toml create mode 100644 crates/litewire-turso/src/lib.rs create mode 100644 crates/litewire-turso/tests/litewire_basics.rs diff --git a/Cargo.lock b/Cargo.lock index a7d6080..6e9b3c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,51 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aegis" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e07d39d15384924b35b70d7b8fa1f9a2934101dd3fa4722ede163cc4f9b7b960" +dependencies = [ + "cc", + "softaes", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -46,6 +91,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -105,6 +156,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "antithesis_sdk" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08410fcac93669a476c006cd6c4512ac1e2b30fd117231a5d55d8a2c76599b82" +dependencies = [ + "libc", + "libloading", + "linkme", + "once_cell", + "rand 0.8.5", + "rustc_version_runtime", + "serde", + "serde_json", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -120,6 +187,35 @@ dependencies = [ "object", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "aristo" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdeefa800110050103e2459a2f8dbdbd560ad046e30f1f87e24ce19c2cb8bde" +dependencies = [ + "aristo-macros", +] + +[[package]] +name = "aristo-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64a66d21a80182b35b1741997a6d2456911f54b7eb1918aa4e2382fc205268d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "array-init" version = "2.1.0" @@ -132,6 +228,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "async-trait" version = "0.1.89" @@ -268,6 +370,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex 1.3.0", + "syn 2.0.117", + "which", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -277,12 +402,12 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash", - "shlex", + "rustc-hash 2.1.2", + "shlex 1.3.0", "syn 2.0.117", ] @@ -346,6 +471,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "branches" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e426eb5cc1900033930ec955317b302e68f19f326cc7bb0c8a86865a826cdf0c" +dependencies = [ + "rustc_version", +] + [[package]] name = "btoi" version = "0.4.3" @@ -383,6 +517,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -397,14 +551,14 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.58" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -428,6 +582,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cfg_block" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487" + [[package]] name = "chacha20" version = "0.10.0" @@ -453,6 +613,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -525,6 +695,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "connection-string" version = "0.2.0" @@ -571,6 +750,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -643,6 +831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -655,6 +844,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.0" @@ -793,6 +991,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -827,6 +1045,23 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "siphasher", +] + +[[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.9" @@ -1021,6 +1256,36 @@ dependencies = [ "slab", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "genawaiter-macro", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1072,6 +1337,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -1121,7 +1396,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "equivalent", "foldhash", ] @@ -1147,6 +1422,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1162,6 +1443,15 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.4.0" @@ -1303,57 +1593,108 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbdf98e5e0aa827770acee171ebb568aaab975a36b56afdb5fd0e2f525750bb" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" + [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1365,18 +1706,20 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1429,6 +1772,35 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + +[[package]] +name = "io-uring" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1451,6 +1823,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1526,6 +1907,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1585,6 +1972,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linkme" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83272d46373fb8decca684579ac3e7c8f3d71d4cc3aa693df8759e260ae41cf" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d59e20403c7d08fe62b4376edfe5c7fb2ef1e6b1465379686d0f21c8df444b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -1604,6 +2023,7 @@ dependencies = [ "litewire-postgres", "litewire-tds", "litewire-translate", + "litewire-turso", "mysql_async", "tiberius", "tokio", @@ -1696,6 +2116,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "litewire-turso" +version = "0.1.0" +dependencies = [ + "async-trait", + "litewire-backend", + "tokio", + "tracing", + "turso", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1711,6 +2142,19 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.12.5" @@ -1763,6 +2207,37 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "mime" version = "0.3.17" @@ -1842,7 +2317,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tokio-util", - "twox-hash", + "twox-hash 1.6.3", "url", "webpki", "webpki-roots 0.26.11", @@ -1856,7 +2331,7 @@ checksum = "478b0ff3f7d67b79da2b96f56f334431aef65e15ba4b29dd74a4236e29582bdc" dependencies = [ "base64 0.21.7", "bigdecimal", - "bindgen", + "bindgen 0.72.1", "bitflags", "bitvec", "btoi", @@ -1980,6 +2455,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "opensrv-mysql" version = "0.7.0" @@ -2002,6 +2483,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pack1" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b7bb0ecf2e447b1f20ee94ee79ef6eed1e9d4b3c36ce1903b9dea3bf205523" +dependencies = [ + "bytemuck", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2025,6 +2521,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pem" version = "3.0.6" @@ -2122,6 +2624,32 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "postgres-protocol" version = "0.6.11" @@ -2159,6 +2687,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -2233,6 +2763,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "psm" version = "0.1.30" @@ -2274,7 +2827,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.2", "rustls 0.23.37", "socket2 0.6.3", "thiserror 2.0.18", @@ -2294,7 +2847,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.2", "rustls 0.23.37", "rustls-pki-types", "slab", @@ -2421,6 +2974,24 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + [[package]] name = "recursive" version = "0.1.1" @@ -2576,6 +3147,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "roaring" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +dependencies = [ + "bytemuck", + "byteorder", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -2609,10 +3190,61 @@ dependencies = [ ] [[package]] -name = "rustc-hash" -version = "2.1.2" +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[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 0.12.1", + "windows-sys 0.61.2", +] [[package]] name = "rustls" @@ -2756,6 +3388,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2884,6 +3522,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -2921,6 +3565,32 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "shuttle" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab17edba38d63047f46780cf7360acf7467fec2c048928689a5c1dd1c2b4e31" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "generator", + "hex", + "owo-colors", + "rand 0.8.5", + "rand_core 0.6.4", + "rand_pcg", + "scoped-tls", + "smallvec", + "tracing", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2943,6 +3613,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simsimd" +version = "6.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fb3bc3cdce07a7d7d4caa4c54f8aa967f6be41690482b54b24100a2253fa70" +dependencies = [ + "cc", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2981,6 +3660,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "softaes" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea" + [[package]] name = "sqlparser" version = "0.57.0" @@ -3034,6 +3719,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + [[package]] name = "subprocess" version = "0.2.15" @@ -3050,6 +3757,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -3098,6 +3811,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3190,8 +3916,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", + "serde_core", "time-core", "time-macros", ] @@ -3214,11 +3942,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -3424,6 +4153,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -3480,6 +4222,197 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "turso" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac8d131650c1bf6a2721202208b3dfba218be25c975f48bebda766173fbdc3b" +dependencies = [ + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sync_sdk_kit", +] + +[[package]] +name = "turso_core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77f2106de5a3014261be18283999fde0d06c24ae5d4cb85a6eff1aaeaff453d" +dependencies = [ + "aegis", + "aes", + "aes-gcm", + "allocator-api2 0.4.0", + "antithesis_sdk", + "arc-swap", + "aristo", + "bigdecimal", + "bitflags", + "branches", + "bumpalo", + "bytemuck", + "cfg_aliases", + "cfg_block", + "chrono", + "crc32c", + "crossbeam-epoch", + "crossbeam-utils", + "either", + "fallible-iterator 0.3.0", + "fastbloom", + "hex", + "icu_collator", + "icu_locale", + "intrusive-collections", + "io-uring", + "libc", + "libloading", + "libm", + "loom", + "miette", + "num-bigint", + "num-traits", + "pack1", + "parking_lot", + "pastey", + "polling", + "rand 0.9.2", + "rapidhash", + "regex", + "regex-syntax", + "roaring", + "rustc-hash 2.1.2", + "rustix 1.1.4", + "ryu", + "serde_json", + "shuttle", + "simsimd", + "smallvec", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", + "turso_ext", + "turso_macros", + "turso_parser", + "twox-hash 2.1.2", + "uncased", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "turso_ext" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d367d863b095a9893690fc6c52bbf27edf422c135a58ba5ed2b03dbec8faac" +dependencies = [ + "chrono", + "getrandom 0.4.2", + "turso_macros", +] + +[[package]] +name = "turso_macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a817815d9ca218cb971ed2a0a41a89a5160966b5b4bd103c82792fd2f230f1c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "turso_parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad90c0e6eea7ab131214804c70edad5372acecdcad8d4ccc75b0ded55b0df8f" +dependencies = [ + "bitflags", + "memchr", + "miette", + "strum", + "strum_macros", + "thiserror 2.0.18", + "turso_macros", +] + +[[package]] +name = "turso_sdk_kit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1abf8f42cf5c40f9777982c8dfda776242397250eb48f99d524c383b1b641a" +dependencies = [ + "bindgen 0.69.5", + "env_logger", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_ext", + "turso_sdk_kit_macros", +] + +[[package]] +name = "turso_sdk_kit_macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372ce1889d2729223886f805638a4357f389756d6b64e26487af5a78b3e92e2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "turso_sync_engine" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4157da3af3730e3cf5109668dc29b58b673f99fbbac3db3a682a4b87d56b9c4a" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc32c", + "genawaiter", + "http", + "libc", + "prost", + "roaring", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "turso_core", + "turso_parser", + "uuid", +] + +[[package]] +name = "turso_sync_sdk_kit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa3b4dee7f50c671ca757d1ac1ea7c99a7a9dec819a98bb1ad6a055e7d0234f" +dependencies = [ + "bindgen 0.69.5", + "env_logger", + "genawaiter", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sdk_kit_macros", + "turso_sync_engine", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -3491,12 +4424,30 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +dependencies = [ + "rand 0.9.2", +] + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3524,12 +4475,28 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -3554,6 +4521,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3572,7 +4545,9 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ + "getrandom 0.4.2", "js-sys", + "sha1_smol", "wasm-bindgen", ] @@ -3783,6 +4758,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "2.1.1" @@ -4080,6 +5067,12 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.2" @@ -4097,9 +5090,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4108,9 +5101,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4167,21 +5160,23 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -4189,9 +5184,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 929d126..84c8d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/litewire-postgres", "crates/litewire-tds", "crates/litewire-hrana", + "crates/litewire-turso", ] [workspace.package] @@ -24,6 +25,7 @@ litewire-mysql = { path = "crates/litewire-mysql" } litewire-postgres = { path = "crates/litewire-postgres" } litewire-tds = { path = "crates/litewire-tds" } litewire-hrana = { path = "crates/litewire-hrana" } +litewire-turso = { path = "crates/litewire-turso" } # Async runtime tokio = { version = "1", features = ["full"] } @@ -39,6 +41,11 @@ sqlparser = { version = "0.57", features = ["serde"] } # to fill in `Column.decltype` on empty result sets (an unexecuted prepared # SELECT). Without it, wire-frontend type inference has to LIMIT-0 probe. rusqlite = { version = "0.32", features = ["bundled", "column_decltype"] } +# Turso Database — the ground-up Rust rewrite of SQLite +# (github.com/tursodatabase/turso). Pinned exactly: the engine is Beta and +# API/behavior move between releases. 0.7.0 is the first non-pre release on +# the 0.7 line (July 2026). Bump deliberately, never via `cargo update`. +turso = { version = "=0.7.0", default-features = false } # HTTP (Hrana) axum = "0.8" diff --git a/README.md b/README.md index 5efd417..a1db812 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ async fn main() -> anyhow::Result<()> { |---------|-------------|----------| | `Rusqlite` | `backend-rusqlite` | Direct in-process SQLite | | `HranaClient` | `backend-hrana-client` | Remote SQLite via the Hrana HTTP protocol (sqld / Turso) | +| `Turso` | `turso` | **Experimental** — [Turso Database](https://github.com/tursodatabase/turso) engine (Rust rewrite of SQLite, Beta upstream; pinned `=0.7.0`). See `crates/litewire-turso` docs for limitations (no `VACUUM`, no multi-process access) | | Custom | implement `Backend` trait | Bring your own | The `HranaClient` backend connects to [sqld](https://github.com/tursodatabase/libsql) via HTTP, enabling embedded replicas and distributed SQLite clusters. diff --git a/crates/litewire-turso/Cargo.toml b/crates/litewire-turso/Cargo.toml new file mode 100644 index 0000000..acdb54f --- /dev/null +++ b/crates/litewire-turso/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "litewire-turso" +description = "Experimental Turso Database (Rust SQLite rewrite) backend for litewire" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +litewire-backend = { workspace = true } +async-trait = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +turso = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/crates/litewire-turso/src/lib.rs b/crates/litewire-turso/src/lib.rs new file mode 100644 index 0000000..661019b --- /dev/null +++ b/crates/litewire-turso/src/lib.rs @@ -0,0 +1,671 @@ +//! **Experimental** in-process backend using [Turso Database], the +//! ground-up Rust rewrite of SQLite. +//! +//! [Turso Database]: https://github.com/tursodatabase/turso +//! +//! # Status +//! +//! The Turso engine is **Beta** (pinned here at `turso = "=0.7.0"`, the +//! first non-pre release of the 0.7 line, July 2026). Upstream does not +//! yet position it as a production SQLite replacement. This backend exists +//! to gather evidence (benchmarks, file-format round-trips, crash-recovery +//! smokes) behind an explicit opt-in — it is not a default anywhere. +//! +//! # Async-native +//! +//! Unlike the `rusqlite` backend, which wraps every call in +//! [`tokio::task::spawn_blocking`], the Turso engine is natively async: +//! `query`/`execute` futures are polled on the calling task with no +//! thread-pool hop. A per-session [`tokio::sync::Mutex`] serializes +//! statements on the *same* [`TursoConn`] (one wire client cannot issue +//! two overlapping statements anyway), matching the rusqlite backend's +//! per-session semantics. +//! +//! # Per-connection isolation +//! +//! [`Turso`] is a **factory**: it owns one [`turso::Database`] and hands +//! each wire session its own [`turso::Connection`] via +//! [`Backend::connect`]. Transaction state is per-connection. Concurrent +//! writers are coordinated by the engine (MVCC / `BEGIN CONCURRENT` is a +//! Turso feature; plain busy-handling applies to classic transactions) with +//! a configurable busy timeout (default 5000 ms, mirroring the rusqlite +//! backend). +//! +//! # In-memory databases +//! +//! `":memory:"` maps to one shared in-memory database owned by the +//! [`turso::Database`] object; every [`Backend::connect`] session sees the +//! same data. (No temp-file workaround is needed here, unlike the rusqlite +//! backend, because connections derive from a single engine instance.) +//! +//! # Known-unsupported operations (Turso 0.7.0) +//! +//! Returned as clear [`BackendError`]s rather than silent misbehavior: +//! +//! - **`VACUUM`** — rejected by this backend with an "unsupported" error. +//! Upstream support is incomplete and gated behind an experimental +//! builder flag we do not enable. +//! - **Multi-process access** — the engine does not support a second +//! *process* opening the same database file (multiprocess WAL is an +//! experimental upstream flag, not enabled here). Do not point another +//! process at the same file while this backend owns it. +//! - **`ATTACH` / `DETACH`** — gated behind an experimental upstream flag, +//! not enabled; the engine returns its own error. +//! - **Non-UTF-8 `TEXT`** — the engine's Rust API surfaces `TEXT` as +//! `String`; unlike the rusqlite backend (which returns such cells as +//! `Blob`), byte-exact round-trips of invalid-UTF-8 text are not +//! guaranteed. +//! +//! Anything else the engine cannot do surfaces as the engine's own error +//! text mapped into [`BackendError::Sqlite`], which the wire frontends' +//! error classifiers already understand (SQLite-style message shapes). + +use std::time::Duration; + +use litewire_backend::{ + Backend, BackendConn, BackendError, Column, ExecuteResult, ResultSet, Value, +}; +use tokio::sync::Mutex; + +/// Builder for [`Turso`]. Use [`Turso::open`] / [`Turso::memory`] for the +/// default configuration; use [`TursoBuilder`] to tune the busy timeout. +#[derive(Clone, Debug)] +pub struct TursoBuilder { + path: String, + busy_timeout_ms: u32, +} + +impl TursoBuilder { + /// Set the busy timeout (milliseconds) applied to every per-session + /// connection. Mirrors the rusqlite backend's `busy_timeout` PRAGMA + /// (default 5000 ms). + #[must_use] + pub fn busy_timeout_ms(mut self, ms: u32) -> Self { + self.busy_timeout_ms = ms; + self + } + + /// Finalize the builder: open (or create) the database with the Turso + /// engine. The engine is WAL-native; no journal-mode bootstrap is + /// required. + /// + /// # Errors + /// + /// Returns [`BackendError::Sqlite`] if the database cannot be opened + /// (bad path, corrupt file, unsupported format). + pub async fn build(self) -> Result { + let db = turso::Builder::new_local(&self.path) + .build() + .await + .map_err(map_turso_err)?; + Ok(Turso { + db, + busy_timeout_ms: self.busy_timeout_ms, + }) + } +} + +/// **Experimental** in-process backend via the Turso Database engine. +/// +/// This type is a **factory**: it opens a fresh [`turso::Connection`] for +/// every wire-protocol session via [`Backend::connect`]. See the module +/// docs for status and limitations. +pub struct Turso { + db: turso::Database, + busy_timeout_ms: u32, +} + +impl Turso { + /// Open (or create) a file-backed database at `path` with the default + /// configuration (busy timeout 5000 ms). + /// + /// # Errors + /// + /// Returns an error if the database cannot be opened. + pub async fn open(path: impl AsRef) -> Result { + Self::builder(path).build().await + } + + /// Open a shared in-memory database. All sessions from this factory + /// see the same data; the database vanishes when the [`Turso`] is + /// dropped. + /// + /// # Errors + /// + /// Returns an error if the engine cannot create the database. + pub async fn memory() -> Result { + Self::builder(":memory:").build().await + } + + /// Start a [`TursoBuilder`] to override defaults. + #[must_use] + pub fn builder(path: impl AsRef) -> TursoBuilder { + TursoBuilder { + path: path.as_ref().to_string(), + busy_timeout_ms: 5000, + } + } +} + +/// Per-session Turso handle. +/// +/// Owns exactly one [`turso::Connection`]. A [`tokio::sync::Mutex`] +/// serializes statements on the same session (held across `.await`, which +/// is why this is the tokio mutex and not a std/parking_lot one). +pub struct TursoConn { + conn: Mutex, +} + +#[async_trait::async_trait] +impl Backend for Turso { + async fn connect(&self) -> Result, BackendError> { + let conn = self.db.connect().map_err(map_turso_err)?; + conn.busy_timeout(Duration::from_millis(u64::from(self.busy_timeout_ms))) + .map_err(map_turso_err)?; + Ok(Box::new(TursoConn { + conn: Mutex::new(conn), + })) + } +} + +/// Map a [`turso::Error`] into litewire's stringly-typed [`BackendError`]. +/// +/// Busy conditions are tagged with the classic SQLite phrasing +/// ("database is locked (SQLITE_BUSY)") so the wire frontends' substring +/// classifiers (`litewire-mysql`/`-postgres` `error_map`) map them to the +/// retryable lock-wait error codes clients expect. +fn map_turso_err(e: turso::Error) -> BackendError { + match e { + turso::Error::Busy(m) | turso::Error::BusySnapshot(m) => { + BackendError::Sqlite(format!("database is locked (SQLITE_BUSY): {m}")) + } + other => BackendError::Sqlite(other.to_string()), + } +} + +/// Convert litewire values into Turso positional params. +/// +/// `turso::params::Params` is `#[doc(hidden)]` but public and implements +/// `IntoParams`; the crate's own tests construct it directly. Acceptable +/// under an exact version pin (`=0.7.0`). +fn to_params(params: &[Value]) -> turso::params::Params { + turso::params::Params::Positional( + params + .iter() + .map(|v| match v { + Value::Null => turso::Value::Null, + Value::Integer(i) => turso::Value::Integer(*i), + Value::Float(f) => turso::Value::Real(*f), + Value::Text(s) => turso::Value::Text(s.clone()), + Value::Blob(b) => turso::Value::Blob(b.clone()), + }) + .collect(), + ) +} + +/// Convert a Turso value into a litewire value. +fn from_turso(v: turso::Value) -> Value { + match v { + turso::Value::Null => Value::Null, + turso::Value::Integer(i) => Value::Integer(i), + turso::Value::Real(f) => Value::Float(f), + turso::Value::Text(s) => Value::Text(s), + turso::Value::Blob(b) => Value::Blob(b), + } +} + +/// Convert Turso column metadata into litewire columns. +fn to_columns(cols: &[turso::Column]) -> Vec { + cols.iter() + .map(|c| Column { + name: c.name().to_string(), + decltype: c.decl_type().map(str::to_string), + }) + .collect() +} + +/// Reject statements the Turso engine cannot execute correctly yet, with +/// an error that says so instead of an opaque engine failure. +fn reject_unsupported(sql: &str) -> Result<(), BackendError> { + let first = sql.trim_start().get(..6).unwrap_or_default(); + if first.eq_ignore_ascii_case("VACUUM") { + return Err(BackendError::Other( + "VACUUM is not supported by the experimental Turso backend \ + (incomplete upstream in Turso 0.7.0)" + .into(), + )); + } + Ok(()) +} + +#[async_trait::async_trait] +impl BackendConn for TursoConn { + async fn query(&self, sql: &str, params: &[Value]) -> Result { + reject_unsupported(sql)?; + let conn = self.conn.lock().await; + // `prepare_cached` interns the parsed statement in the engine's + // per-connection cache, mirroring the rusqlite backend. + let mut stmt = conn.prepare_cached(sql).await.map_err(map_turso_err)?; + + let columns = to_columns(&stmt.columns()); + let col_count = columns.len(); + + let mut rows = stmt.query(to_params(params)).await.map_err(map_turso_err)?; + let mut result_rows = Vec::new(); + while let Some(row) = rows.next().await.map_err(map_turso_err)? { + let mut values = Vec::with_capacity(col_count); + for i in 0..col_count { + values.push(from_turso(row.get_value(i).map_err(map_turso_err)?)); + } + result_rows.push(values); + } + + Ok(ResultSet { + columns, + rows: result_rows, + }) + } + + async fn execute(&self, sql: &str, params: &[Value]) -> Result { + reject_unsupported(sql)?; + let conn = self.conn.lock().await; + let mut stmt = conn.prepare_cached(sql).await.map_err(map_turso_err)?; + let affected = stmt + .execute(to_params(params)) + .await + .map_err(map_turso_err)?; + let last_id = conn.last_insert_rowid(); + + Ok(ExecuteResult { + affected_rows: affected, + last_insert_rowid: if last_id != 0 { Some(last_id) } else { None }, + }) + } + + async fn describe_columns(&self, sql: &str) -> Result, BackendError> { + let conn = self.conn.lock().await; + // Prepare without executing -- same trick as the rusqlite backend, + // no LIMIT-0 probe needed. + let stmt = conn.prepare_cached(sql).await.map_err(map_turso_err)?; + Ok(to_columns(&stmt.columns())) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + + // Mirrors the rusqlite backend's test suite so behavior differences + // between the two engines show up as test failures here, not as + // production surprises. + + #[tokio::test] + async fn basic_crud() { + let backend = Turso::memory().await.unwrap(); + + backend + .execute( + "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)", + &[], + ) + .await + .unwrap(); + + let result = backend + .execute( + "INSERT INTO users (name) VALUES (?1)", + &[Value::Text("Alice".into())], + ) + .await + .unwrap(); + assert_eq!(result.affected_rows, 1); + assert_eq!(result.last_insert_rowid, Some(1)); + + let result = backend + .execute( + "INSERT INTO users (name) VALUES (?1)", + &[Value::Text("Bob".into())], + ) + .await + .unwrap(); + assert_eq!(result.last_insert_rowid, Some(2)); + + let rs = backend + .query("SELECT id, name FROM users ORDER BY id", &[]) + .await + .unwrap(); + assert_eq!(rs.columns.len(), 2); + assert_eq!(rs.columns[0].name, "id"); + assert_eq!(rs.columns[1].name, "name"); + assert_eq!(rs.rows.len(), 2); + assert_eq!(rs.rows[0][1], Value::Text("Alice".into())); + assert_eq!(rs.rows[1][1], Value::Text("Bob".into())); + + let rs = backend + .query("SELECT * FROM users WHERE id = ?1", &[Value::Integer(1)]) + .await + .unwrap(); + assert_eq!(rs.rows.len(), 1); + assert_eq!(rs.rows[0][1], Value::Text("Alice".into())); + } + + #[tokio::test] + async fn types_roundtrip() { + let backend = Turso::memory().await.unwrap(); + backend + .execute( + "CREATE TABLE typed (i INTEGER, r REAL, t TEXT, b BLOB)", + &[], + ) + .await + .unwrap(); + + backend + .execute( + "INSERT INTO typed VALUES (?1, ?2, ?3, ?4)", + &[ + Value::Integer(42), + Value::Float(2.72), + Value::Text("hello".into()), + Value::Blob(vec![0xDE, 0xAD]), + ], + ) + .await + .unwrap(); + + let rs = backend.query("SELECT * FROM typed", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(42)); + assert_eq!(rs.rows[0][1], Value::Float(2.72)); + assert_eq!(rs.rows[0][2], Value::Text("hello".into())); + assert_eq!(rs.rows[0][3], Value::Blob(vec![0xDE, 0xAD])); + } + + #[tokio::test] + async fn null_handling() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (v TEXT)", &[]) + .await + .unwrap(); + backend + .execute("INSERT INTO t VALUES (?1)", &[Value::Null]) + .await + .unwrap(); + + let rs = backend.query("SELECT * FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Null); + } + + #[tokio::test] + async fn empty_table_query() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (id INTEGER, name TEXT)", &[]) + .await + .unwrap(); + + let rs = backend.query("SELECT * FROM t", &[]).await.unwrap(); + assert_eq!(rs.columns.len(), 2); + assert!(rs.rows.is_empty()); + } + + #[tokio::test] + async fn multiple_params() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (a INTEGER, b TEXT, c REAL)", &[]) + .await + .unwrap(); + backend + .execute( + "INSERT INTO t VALUES (?1, ?2, ?3)", + &[ + Value::Integer(1), + Value::Text("hello".into()), + Value::Float(9.99), + ], + ) + .await + .unwrap(); + + let rs = backend + .query( + "SELECT * FROM t WHERE a = ?1 AND b = ?2", + &[Value::Integer(1), Value::Text("hello".into())], + ) + .await + .unwrap(); + assert_eq!(rs.rows.len(), 1); + assert_eq!(rs.rows[0][0], Value::Integer(1)); + } + + #[tokio::test] + async fn affected_rows_count() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)", &[]) + .await + .unwrap(); + + for i in 0..5 { + backend + .execute( + "INSERT INTO t VALUES (?1, ?2)", + &[Value::Integer(i), Value::Text(format!("v{i}"))], + ) + .await + .unwrap(); + } + + let result = backend + .execute("DELETE FROM t WHERE id >= ?1", &[Value::Integer(3)]) + .await + .unwrap(); + assert_eq!(result.affected_rows, 2); + } + + #[tokio::test] + async fn query_error_on_bad_sql() { + let backend = Turso::memory().await.unwrap(); + let result = backend.query("DEFINITELY NOT SQL !!!", &[]).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn execute_error_on_bad_sql() { + let backend = Turso::memory().await.unwrap(); + let result = backend.execute("DEFINITELY NOT SQL !!!", &[]).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn blob_roundtrip() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (data BLOB)", &[]) + .await + .unwrap(); + + let data = vec![0x00, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF]; + backend + .execute("INSERT INTO t VALUES (?1)", &[Value::Blob(data.clone())]) + .await + .unwrap(); + + let rs = backend.query("SELECT * FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Blob(data)); + } + + #[tokio::test] + async fn column_names_preserved() { + let backend = Turso::memory().await.unwrap(); + backend + .execute( + "CREATE TABLE users (id INTEGER, name TEXT, email TEXT)", + &[], + ) + .await + .unwrap(); + + let rs = backend + .query("SELECT id, name, email FROM users", &[]) + .await + .unwrap(); + assert_eq!(rs.columns[0].name, "id"); + assert_eq!(rs.columns[1].name, "name"); + assert_eq!(rs.columns[2].name, "email"); + } + + #[tokio::test] + async fn query_with_alias() { + let backend = Turso::memory().await.unwrap(); + let rs = backend + .query("SELECT 1 AS num, 'hello' AS greeting", &[]) + .await + .unwrap(); + assert_eq!(rs.columns[0].name, "num"); + assert_eq!(rs.columns[1].name, "greeting"); + assert_eq!(rs.rows[0][0], Value::Integer(1)); + assert_eq!(rs.rows[0][1], Value::Text("hello".into())); + } + + #[tokio::test] + async fn describe_columns_returns_decltypes() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE users (id INTEGER, name TEXT, tags BLOB)", &[]) + .await + .unwrap(); + + let cols = backend + .describe_columns("SELECT id, name, tags FROM users") + .await + .unwrap(); + assert_eq!(cols[0].name, "id"); + assert_eq!(cols[0].decltype.as_deref(), Some("INTEGER")); + assert_eq!(cols[1].name, "name"); + assert_eq!(cols[1].decltype.as_deref(), Some("TEXT")); + assert_eq!(cols[2].name, "tags"); + assert_eq!(cols[2].decltype.as_deref(), Some("BLOB")); + } + + #[tokio::test] + async fn vacuum_rejected_with_clear_error() { + let backend = Turso::memory().await.unwrap(); + let err = backend.execute("VACUUM", &[]).await.unwrap_err(); + assert!( + err.to_string().contains("not supported"), + "expected clear unsupported error, got: {err}" + ); + } + + // -- Isolation tests (mirroring the rusqlite backend) ----------------- + + #[tokio::test] + async fn per_conn_transaction_isolation() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", &[]) + .await + .unwrap(); + + let a = backend.connect().await.unwrap(); + let b = backend.connect().await.unwrap(); + + a.execute("BEGIN", &[]).await.unwrap(); + a.execute("INSERT INTO t VALUES (1, 'from-a')", &[]) + .await + .unwrap(); + + let rs = b.query("SELECT COUNT(*) FROM t", &[]).await.unwrap(); + assert_eq!( + rs.rows[0][0], + Value::Integer(0), + "B saw A's uncommitted row" + ); + + let _ = b.execute("ROLLBACK", &[]).await; + + a.execute("COMMIT", &[]).await.unwrap(); + + let rs = b.query("SELECT COUNT(*) FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(1)); + let rs = a.query("SELECT v FROM t WHERE id=1", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Text("from-a".into())); + } + + #[tokio::test] + async fn per_conn_rollback_stays_local() { + let backend = Turso::memory().await.unwrap(); + backend + .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", &[]) + .await + .unwrap(); + + let a = backend.connect().await.unwrap(); + let b = backend.connect().await.unwrap(); + + a.execute("BEGIN", &[]).await.unwrap(); + a.execute("INSERT INTO t VALUES (42, 'ghost')", &[]) + .await + .unwrap(); + a.execute("ROLLBACK", &[]).await.unwrap(); + + let rs = b.query("SELECT COUNT(*) FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(0)); + let rs = a.query("SELECT COUNT(*) FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(0)); + } + + #[tokio::test] + async fn per_conn_concurrent_readers() { + let backend = Arc::new(Turso::memory().await.unwrap()); + backend + .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", &[]) + .await + .unwrap(); + for i in 0..100 { + backend + .execute( + "INSERT INTO t VALUES (?1, ?2)", + &[Value::Integer(i), Value::Text(format!("row-{i}"))], + ) + .await + .unwrap(); + } + + let mut handles = Vec::new(); + for _ in 0..16 { + let be = Arc::clone(&backend); + handles.push(tokio::spawn(async move { + let conn = be.connect().await.unwrap(); + for i in 0..50 { + let rs = conn + .query("SELECT v FROM t WHERE id=?1", &[Value::Integer(i % 100)]) + .await + .unwrap(); + assert_eq!(rs.rows.len(), 1); + } + })); + } + for h in handles { + h.await.unwrap(); + } + } + + #[tokio::test] + async fn per_conn_shared_memory_visibility() { + let backend = Turso::memory().await.unwrap(); + + let a = backend.connect().await.unwrap(); + let b = backend.connect().await.unwrap(); + + a.execute("CREATE TABLE t (id INTEGER)", &[]).await.unwrap(); + a.execute("INSERT INTO t VALUES (7)", &[]).await.unwrap(); + + let rs = b.query("SELECT id FROM t", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(7)); + } +} diff --git a/crates/litewire-turso/tests/litewire_basics.rs b/crates/litewire-turso/tests/litewire_basics.rs new file mode 100644 index 0000000..f41a438 --- /dev/null +++ b/crates/litewire-turso/tests/litewire_basics.rs @@ -0,0 +1,140 @@ +//! Integration test: drive the Turso backend through the `Backend` / +//! `BackendConn` trait objects exactly the way litewire's wire frontends +//! do — schema init, prepared statements with positional params, +//! transactions, `last_insert_rowid`, affected rows, and `describe_columns` +//! (the `COM_STMT_PREPARE` path). + +use std::sync::Arc; + +use litewire_backend::{SharedBackend, Value}; +use litewire_turso::Turso; + +async fn shared_backend() -> SharedBackend { + Arc::new(Turso::memory().await.expect("open turso memory db")) +} + +#[tokio::test] +async fn wire_session_lifecycle() { + let backend = shared_backend().await; + + // Schema init (what ephpm / app bootstrap does via the stateless API). + backend + .execute( + "CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + views INTEGER NOT NULL DEFAULT 0 + )", + &[], + ) + .await + .unwrap(); + backend + .execute("CREATE INDEX idx_posts_title ON posts(title)", &[]) + .await + .unwrap(); + + // A wire session: connect once, run prepared statements. + let session = backend.connect().await.unwrap(); + + // COM_STMT_PREPARE metadata without execution. + let cols = session + .describe_columns("SELECT id, title, views FROM posts") + .await + .unwrap(); + assert_eq!(cols.len(), 3); + assert_eq!(cols[0].name, "id"); + assert_eq!(cols[0].decltype.as_deref(), Some("INTEGER")); + + // INSERT with params: last_insert_rowid + affected rows. + let r = session + .execute( + "INSERT INTO posts (title) VALUES (?1)", + &[Value::Text("hello world".into())], + ) + .await + .unwrap(); + assert_eq!(r.affected_rows, 1); + assert_eq!(r.last_insert_rowid, Some(1)); + + // Transaction: multi-statement, committed. + session.execute("BEGIN", &[]).await.unwrap(); + for i in 0..10 { + session + .execute( + "INSERT INTO posts (title, views) VALUES (?1, ?2)", + &[Value::Text(format!("post-{i}")), Value::Integer(i * 10)], + ) + .await + .unwrap(); + } + session.execute("COMMIT", &[]).await.unwrap(); + + // Point SELECT (the ephpm db.php fixture pattern). + let rs = session + .query( + "SELECT title, views FROM posts WHERE id = ?1", + &[Value::Integer(5)], + ) + .await + .unwrap(); + assert_eq!(rs.rows.len(), 1); + assert_eq!(rs.rows[0][0], Value::Text("post-3".into())); + + // UPDATE affected rows. + let r = session + .execute( + "UPDATE posts SET views = views + 1 WHERE views >= ?1", + &[Value::Integer(50)], + ) + .await + .unwrap(); + assert_eq!(r.affected_rows, 5); + + // Aggregate over the index. + let rs = session + .query("SELECT COUNT(*) AS n FROM posts", &[]) + .await + .unwrap(); + assert_eq!(rs.columns[0].name, "n"); + assert_eq!(rs.rows[0][0], Value::Integer(11)); + + // Rollback leaves no trace. + session.execute("BEGIN", &[]).await.unwrap(); + session.execute("DELETE FROM posts", &[]).await.unwrap(); + session.execute("ROLLBACK", &[]).await.unwrap(); + let rs = session + .query("SELECT COUNT(*) FROM posts", &[]) + .await + .unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(11)); +} + +#[tokio::test] +async fn two_sessions_do_not_share_transactions() { + let backend = shared_backend().await; + backend + .execute("CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)", &[]) + .await + .unwrap(); + + let a = backend.connect().await.unwrap(); + let b = backend.connect().await.unwrap(); + + a.execute("BEGIN", &[]).await.unwrap(); + a.execute( + "INSERT INTO kv VALUES (?1, ?2)", + &[Value::Text("k1".into()), Value::Text("uncommitted".into())], + ) + .await + .unwrap(); + + // B must not see A's open transaction. + let rs = b.query("SELECT COUNT(*) FROM kv", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(0)); + + a.execute("COMMIT", &[]).await.unwrap(); + + let rs = b.query("SELECT COUNT(*) FROM kv", &[]).await.unwrap(); + assert_eq!(rs.rows[0][0], Value::Integer(1)); +} diff --git a/crates/litewire/Cargo.toml b/crates/litewire/Cargo.toml index 3a0e0a6..97f8505 100644 --- a/crates/litewire/Cargo.toml +++ b/crates/litewire/Cargo.toml @@ -18,6 +18,7 @@ litewire-mysql = { workspace = true, optional = true } litewire-postgres = { workspace = true, optional = true } litewire-tds = { workspace = true, optional = true } litewire-hrana = { workspace = true, optional = true } +litewire-turso = { workspace = true, optional = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, optional = true } @@ -33,6 +34,9 @@ tds = ["dep:litewire-tds"] hrana = ["dep:litewire-hrana"] backend-rusqlite = ["litewire-backend/rusqlite"] backend-hrana-client = ["litewire-backend/hrana-client"] +# EXPERIMENTAL: Turso Database engine backend (Beta upstream). Off by +# default; see crates/litewire-turso for status and limitations. +turso = ["dep:litewire-turso"] cli = ["dep:clap", "dep:tracing-subscriber"] [dev-dependencies] diff --git a/crates/litewire/src/lib.rs b/crates/litewire/src/lib.rs index ac928e4..91ab0b5 100644 --- a/crates/litewire/src/lib.rs +++ b/crates/litewire/src/lib.rs @@ -32,6 +32,15 @@ pub use litewire_postgres; #[cfg(feature = "tds")] pub use litewire_tds; +/// EXPERIMENTAL: Turso Database engine backend (feature `turso`, off by +/// default). The engine is Beta upstream — see the `litewire-turso` crate +/// docs for status and known-unsupported operations. +#[cfg(feature = "turso")] +pub use litewire_turso; + +#[cfg(feature = "turso")] +pub use litewire_turso::Turso; + use std::net::SocketAddr; use std::sync::Arc; From fbbf10f37d1ee63cbf30bf576e6b41bf15bf6bae Mon Sep 17 00:00:00 2001 From: Luther Monson Date: Tue, 14 Jul 2026 17:20:28 -0700 Subject: [PATCH 2/3] feat(turso): Phase 1 gate-evidence harness (examples/phase1_gates.rs) Self-contained runner for the ePHPm Turso-engine roadmap decision gates: - bench: point SELECT / INSERT / 10-query page p50-p99, turso vs rusqlite - writers: N concurrent BackendConns inserting (MVCC vs WAL+busy_timeout), throughput + busy/error counts - gate3: SQLite<->Turso file-format round-trip with checksums and integrity_check in both directions - crash: SIGKILL a turso-engine writer child mid-loop, reopen, verify with both engines (N iterations) Also: litewire-backend becomes a default-features=false path dep at runtime (the trait is all the crate needs); rusqlite is dev-only for the comparison harness. --- crates/litewire-turso/Cargo.toml | 7 +- .../litewire-turso/examples/phase1_gates.rs | 531 ++++++++++++++++++ 2 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 crates/litewire-turso/examples/phase1_gates.rs diff --git a/crates/litewire-turso/Cargo.toml b/crates/litewire-turso/Cargo.toml index acdb54f..8e8ff35 100644 --- a/crates/litewire-turso/Cargo.toml +++ b/crates/litewire-turso/Cargo.toml @@ -7,11 +7,16 @@ rust-version.workspace = true license.workspace = true [dependencies] -litewire-backend = { workspace = true } +# Path dep (not workspace) so default features can be disabled: the trait +# is all this crate needs at runtime; rusqlite stays out of the build. +litewire-backend = { path = "../litewire-backend", version = "0.1.0", default-features = false } async-trait = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } turso = { workspace = true } [dev-dependencies] +# rusqlite backend enabled for tests/examples only: the Phase 1 gate +# harness (examples/phase1_gates.rs) compares both engines side by side. +litewire-backend = { workspace = true, features = ["rusqlite"] } tokio = { workspace = true, features = ["full"] } diff --git a/crates/litewire-turso/examples/phase1_gates.rs b/crates/litewire-turso/examples/phase1_gates.rs new file mode 100644 index 0000000..90234b5 --- /dev/null +++ b/crates/litewire-turso/examples/phase1_gates.rs @@ -0,0 +1,531 @@ +//! Phase 1 gate-evidence harness for the experimental Turso backend. +//! +//! Produces the evidence for the ePHPm Turso-engine roadmap decision gates +//! 2-4, comparing the Turso engine against the rusqlite (genuine SQLite C +//! engine) backend through the exact same `Backend`/`BackendConn` seam the +//! wire frontends use. +//! +//! ```text +//! cargo run --release -p litewire-turso --example phase1_gates -- bench +//! cargo run --release -p litewire-turso --example phase1_gates -- writers +//! cargo run --release -p litewire-turso --example phase1_gates -- gate3 +//! cargo run --release -p litewire-turso --example phase1_gates -- crash +//! ``` +//! +//! `crash` re-executes this binary with the internal `crash-writer` +//! subcommand as a child process, SIGKILLs it mid-write-loop, then reopens +//! and integrity-checks the database with both engines. + +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use litewire_backend::rusqlite_backend::Rusqlite; +use litewire_backend::{Backend, BackendConn, Value}; +use litewire_turso::Turso; + +fn usage() -> ! { + eprintln!( + "usage: phase1_gates | writers | gate3 | crash >" + ); + std::process::exit(2); +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("bench") => bench(args.get(2).unwrap_or_else(|| usage())).await, + Some("writers") => { + let dir = args.get(2).unwrap_or_else(|| usage()); + let n: usize = args + .get(3) + .unwrap_or_else(|| usage()) + .parse() + .expect("n_conns"); + writers(dir, n).await; + } + Some("gate3") => gate3(args.get(2).unwrap_or_else(|| usage())).await, + Some("crash") => { + let dir = args.get(2).unwrap_or_else(|| usage()); + let iters: usize = args + .get(3) + .unwrap_or_else(|| usage()) + .parse() + .expect("iters"); + crash(dir, iters).await; + } + Some("crash-writer") => crash_writer(args.get(2).unwrap_or_else(|| usage())).await, + _ => usage(), + } +} + +async fn open_engine(engine: &str, path: &str) -> Arc { + match engine { + "rusqlite" => Arc::new(Rusqlite::open(path).expect("open rusqlite")), + "turso" => Arc::new(Turso::open(path).await.expect("open turso")), + _ => unreachable!(), + } +} + +fn pctl(sorted_ns: &[u128], p: f64) -> f64 { + let idx = ((sorted_ns.len() as f64) * p).ceil() as usize; + sorted_ns[idx.saturating_sub(1).min(sorted_ns.len() - 1)] as f64 / 1000.0 +} + +fn report(label: &str, engine: &str, mut samples_ns: Vec) { + samples_ns.sort_unstable(); + println!( + "| {label} | {engine} | {:.1} | {:.1} | {:.1} | n={} |", + pctl(&samples_ns, 0.50), + pctl(&samples_ns, 0.95), + pctl(&samples_ns, 0.99), + samples_ns.len() + ); +} + +// ── Gate 2: latency matrix ────────────────────────────────────────────────── + +async fn bench(dir: &str) { + println!("| op | engine | p50 us | p95 us | p99 us | samples |"); + println!("|----|--------|--------|--------|--------|---------|"); + for engine in ["rusqlite", "turso"] { + let path = format!("{dir}/bench-{engine}.db"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(format!("{path}-wal")); + let _ = std::fs::remove_file(format!("{path}-shm")); + let be = open_engine(engine, &path).await; + + be.execute("CREATE TABLE bench (id INTEGER PRIMARY KEY, v TEXT)", &[]) + .await + .unwrap(); + let seed_conn = be.connect().await.unwrap(); + seed_conn.execute("BEGIN", &[]).await.unwrap(); + for i in 0..1000 { + seed_conn + .execute( + "INSERT INTO bench VALUES (?1, ?2)", + &[ + Value::Integer(i), + Value::Text(format!("row-{i}-{}", "x".repeat(64))), + ], + ) + .await + .unwrap(); + } + seed_conn.execute("COMMIT", &[]).await.unwrap(); + + let conn = be.connect().await.unwrap(); + + // Warmup. + for i in 0..500 { + conn.query( + "SELECT v FROM bench WHERE id=?1", + &[Value::Integer(i % 1000)], + ) + .await + .unwrap(); + } + + // Point SELECT. + let mut samples = Vec::with_capacity(5000); + for i in 0..5000i64 { + let t = Instant::now(); + let rs = conn + .query( + "SELECT v FROM bench WHERE id=?1", + &[Value::Integer(i % 1000)], + ) + .await + .unwrap(); + samples.push(t.elapsed().as_nanos()); + assert_eq!(rs.rows.len(), 1); + } + report("point SELECT", engine, samples); + + // Autocommit INSERT. + be.execute( + "CREATE TABLE bench_ins (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)", + &[], + ) + .await + .unwrap(); + let mut samples = Vec::with_capacity(2000); + for i in 0..2000 { + let t = Instant::now(); + conn.execute( + "INSERT INTO bench_ins (v) VALUES (?1)", + &[Value::Text(format!("ins-{i}-{}", "y".repeat(64)))], + ) + .await + .unwrap(); + samples.push(t.elapsed().as_nanos()); + } + report("INSERT (autocommit)", engine, samples); + + // 10-query page (the ephpm db.php fixture pattern). + let mut samples = Vec::with_capacity(500); + for i in 0..500i64 { + let t = Instant::now(); + for j in 0..10 { + let rs = conn + .query( + "SELECT v FROM bench WHERE id=?1", + &[Value::Integer((i * 10 + j) % 1000)], + ) + .await + .unwrap(); + assert_eq!(rs.rows.len(), 1); + } + samples.push(t.elapsed().as_nanos()); + } + report("10-query page", engine, samples); + } +} + +// ── Gate 2: concurrent writers ────────────────────────────────────────────── + +async fn writers(dir: &str, n_conns: usize) { + const INSERTS_PER_CONN: usize = 250; + println!( + "| engine | conns | total inserts | wall s | inserts/s | busy errors | other errors |" + ); + println!( + "|--------|-------|---------------|--------|-----------|-------------|--------------|" + ); + for engine in ["rusqlite", "turso"] { + let path = format!("{dir}/writers-{engine}.db"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(format!("{path}-wal")); + let _ = std::fs::remove_file(format!("{path}-shm")); + let be = open_engine(engine, &path).await; + be.execute( + "CREATE TABLE w (id INTEGER PRIMARY KEY AUTOINCREMENT, tag TEXT, payload TEXT)", + &[], + ) + .await + .unwrap(); + + let payload = "z".repeat(200); + let start = Instant::now(); + let mut handles = Vec::new(); + for c in 0..n_conns { + let be = Arc::clone(&be); + let payload = payload.clone(); + handles.push(tokio::spawn(async move { + let conn: Box = be.connect().await.unwrap(); + let mut busy = 0u64; + let mut other = 0u64; + for i in 0..INSERTS_PER_CONN { + let r = conn + .execute( + "INSERT INTO w (tag, payload) VALUES (?1, ?2)", + &[ + Value::Text(format!("c{c}-i{i}")), + Value::Text(payload.clone()), + ], + ) + .await; + if let Err(e) = r { + let msg = e.to_string().to_ascii_lowercase(); + if msg.contains("busy") || msg.contains("locked") { + busy += 1; + } else { + other += 1; + eprintln!("[{engine}] writer error: {e}"); + } + } + } + (busy, other) + })); + } + let mut busy_total = 0u64; + let mut other_total = 0u64; + for h in handles { + let (b, o) = h.await.unwrap(); + busy_total += b; + other_total += o; + } + let wall = start.elapsed(); + + let rs = be.query("SELECT COUNT(*) FROM w", &[]).await.unwrap(); + let rows = match rs.rows[0][0] { + Value::Integer(n) => n, + _ => panic!("count"), + }; + let expected = (n_conns * INSERTS_PER_CONN) as i64; + assert_eq!( + rows + (busy_total + other_total) as i64, + expected, + "row count + failures must equal attempts" + ); + println!( + "| {engine} | {n_conns} | {rows} | {:.2} | {:.0} | {busy_total} | {other_total} |", + wall.as_secs_f64(), + rows as f64 / wall.as_secs_f64() + ); + } +} + +// ── Gate 3: file-format round-trip ────────────────────────────────────────── + +async fn checksum(conn: &dyn BackendConn, sql: &str) -> u64 { + let rs = conn.query(sql, &[]).await.unwrap(); + let mut h = DefaultHasher::new(); + for row in &rs.rows { + for v in row { + match v { + Value::Null => "NULL".hash(&mut h), + Value::Integer(i) => i.hash(&mut h), + // Hash the bit pattern; both engines must return the same f64. + Value::Float(f) => f.to_bits().hash(&mut h), + Value::Text(s) => s.hash(&mut h), + Value::Blob(b) => b.hash(&mut h), + } + } + } + h.finish() +} + +async fn integrity_check(engine: &str, path: &str) -> String { + let be = open_engine(engine, path).await; + match be.query("PRAGMA integrity_check", &[]).await { + Ok(rs) => rs + .rows + .iter() + .flat_map(|r| r.iter().map(std::string::ToString::to_string)) + .collect::>() + .join(","), + Err(e) => format!("UNSUPPORTED/FAILED: {e}"), + } +} + +const CHECKSUM_SQL: &str = "SELECT i, r, t, b FROM typed WHERE id <= 500 ORDER BY id"; + +async fn seed_typed(conn: &dyn BackendConn) { + conn.execute( + "CREATE TABLE typed ( + id INTEGER PRIMARY KEY, + i INTEGER, r REAL, t TEXT, b BLOB + )", + &[], + ) + .await + .unwrap(); + conn.execute("CREATE INDEX idx_typed_i ON typed(i)", &[]) + .await + .unwrap(); + conn.execute("CREATE UNIQUE INDEX idx_typed_t ON typed(t)", &[]) + .await + .unwrap(); + conn.execute("BEGIN", &[]).await.unwrap(); + for id in 1..=500i64 { + let (i, r, t, b) = varied_row(id); + conn.execute( + "INSERT INTO typed VALUES (?1, ?2, ?3, ?4, ?5)", + &[Value::Integer(id), i, r, t, b], + ) + .await + .unwrap(); + } + conn.execute("COMMIT", &[]).await.unwrap(); +} + +fn varied_row(id: i64) -> (Value, Value, Value, Value) { + let i = match id % 5 { + 0 => Value::Null, + 1 => Value::Integer(i64::MAX - id), + 2 => Value::Integer(i64::MIN + id), + 3 => Value::Integer(0), + _ => Value::Integer(-id * 7919), + }; + let r = match id % 4 { + 0 => Value::Float(0.1 + id as f64), + 1 => Value::Float(-1e308 / id as f64), + 2 => Value::Float(id as f64 * 1e-10), + _ => Value::Null, + }; + let t = Value::Text(format!( + "row-{id}-\u{00e9}\u{4e2d}\u{6587}-{}", + "t".repeat((id % 40) as usize) + )); + let b = if id % 3 == 0 { + Value::Null + } else { + Value::Blob((0..(id % 64) as u8).collect()) + }; + (i, r, t, b) +} + +async fn gate3(dir: &str) { + for (writer, reader) in [("rusqlite", "turso"), ("turso", "rusqlite")] { + let path = format!("{dir}/roundtrip-{writer}-to-{reader}.db"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(format!("{path}-wal")); + let _ = std::fs::remove_file(format!("{path}-shm")); + println!("=== {writer} writes -> {reader} reads/writes -> {writer} reopens ==="); + + // 1. Writer engine creates + seeds the database (WAL mode). + let sum_original; + { + let be = open_engine(writer, &path).await; + let conn = be.connect().await.unwrap(); + seed_typed(conn.as_ref()).await; + sum_original = checksum(conn.as_ref(), CHECKSUM_SQL).await; + println!("{writer} wrote 500 rows, checksum={sum_original:#018x}"); + } + + // 2. Reader engine opens the same file: read, verify, write. + { + let be = open_engine(reader, &path).await; + let conn = be.connect().await.unwrap(); + let sum_read = checksum(conn.as_ref(), CHECKSUM_SQL).await; + println!( + "{reader} read checksum={sum_read:#018x} -> {}", + if sum_read == sum_original { + "MATCH" + } else { + "MISMATCH" + } + ); + for id in 501..=600i64 { + let (i, r, t, b) = varied_row(id); + conn.execute( + "INSERT INTO typed VALUES (?1, ?2, ?3, ?4, ?5)", + &[Value::Integer(id), i, r, t, b], + ) + .await + .unwrap(); + } + conn.execute( + "UPDATE typed SET i = i + 1 WHERE id % 5 = 4 AND id <= 500", + &[], + ) + .await + .unwrap(); + conn.execute( + "UPDATE typed SET i = i - 1 WHERE id % 5 = 4 AND id <= 500", + &[], + ) + .await + .unwrap(); + println!("{reader} inserted rows 501..=600 and ran balanced UPDATEs"); + } + + // 3. Writer engine reopens: integrity + checksum + new-row visibility. + { + let be = open_engine(writer, &path).await; + let conn = be.connect().await.unwrap(); + let sum_back = checksum(conn.as_ref(), CHECKSUM_SQL).await; + let rs = conn.query("SELECT COUNT(*) FROM typed", &[]).await.unwrap(); + println!( + "{writer} reopened: checksum {} (rows={}, expect 600)", + if sum_back == sum_original { + "MATCH" + } else { + "MISMATCH" + }, + rs.rows[0][0] + ); + } + println!( + "integrity_check (rusqlite): {}", + integrity_check("rusqlite", &path).await + ); + println!( + "integrity_check (turso): {}", + integrity_check("turso", &path).await + ); + println!(); + } +} + +// ── Gate 4: crash smoke ───────────────────────────────────────────────────── + +/// Child-process mode: open the DB with the Turso engine and insert in a +/// tight loop until killed. Prints `READY` once the schema exists so the +/// parent knows writes have started. +async fn crash_writer(path: &str) { + let be = Turso::open(path).await.expect("open turso"); + let conn = be.connect().await.expect("connect"); + conn.execute( + "CREATE TABLE IF NOT EXISTS crash (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)", + &[], + ) + .await + .expect("create"); + println!("READY"); + let mut i = 0u64; + loop { + conn.execute( + "INSERT INTO crash (v) VALUES (?1)", + &[Value::Text(format!("crash-{i}-{}", "k".repeat(128)))], + ) + .await + .expect("insert"); + i += 1; + } +} + +async fn crash(dir: &str, iterations: usize) { + let exe = std::env::current_exe().expect("current_exe"); + let path = format!("{dir}/crash.db"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(format!("{path}-wal")); + let _ = std::fs::remove_file(format!("{path}-shm")); + + let mut prev_rows = 0i64; + for iter in 1..=iterations { + let mut child = std::process::Command::new(&exe) + .args(["crash-writer", &path]) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("spawn crash-writer"); + + // Wait for READY, then let it write for a varied interval before + // the SIGKILL lands mid-write-loop. + { + use std::io::{BufRead, BufReader}; + let stdout = child.stdout.take().expect("child stdout"); + let mut line = String::new(); + BufReader::new(stdout) + .read_line(&mut line) + .expect("read READY"); + assert_eq!(line.trim(), "READY"); + } + std::thread::sleep(Duration::from_millis(300 + (iter as u64 * 137) % 900)); + child.kill().expect("SIGKILL child"); + let _ = child.wait(); + + // Reopen with the Turso engine: count + write must work. + let (rows, reopen_write) = { + let be = open_engine("turso", &path).await; + let conn = be.connect().await.unwrap(); + let rs = conn.query("SELECT COUNT(*) FROM crash", &[]).await.unwrap(); + let rows = match rs.rows[0][0] { + Value::Integer(n) => n, + _ => panic!("count"), + }; + let w = conn + .execute("INSERT INTO crash (v) VALUES ('post-crash-probe')", &[]) + .await; + let ok = w.is_ok(); + if ok { + conn.execute("DELETE FROM crash WHERE v = 'post-crash-probe'", &[]) + .await + .unwrap(); + } + (rows, ok) + }; + + // Cross-check the on-disk state with the genuine SQLite C engine. + let integrity = integrity_check("rusqlite", &path).await; + + println!( + "iter {iter:>2}: rows={rows} (prev {prev_rows}, monotonic={}) turso-reopen-write={} sqlite-integrity={integrity}", + rows >= prev_rows, + if reopen_write { "ok" } else { "FAILED" }, + ); + prev_rows = rows; + } +} From 1cff10501350af64ef38f7fb82066a2b1a886d34 Mon Sep 17 00:00:00 2001 From: Luther Monson Date: Tue, 14 Jul 2026 17:25:14 -0700 Subject: [PATCH 3/3] feat(turso): per-session synchronous PRAGMA (default NORMAL, rusqlite-backend parity; engine default is FULL) --- crates/litewire-turso/src/lib.rs | 52 ++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/litewire-turso/src/lib.rs b/crates/litewire-turso/src/lib.rs index 661019b..57ee346 100644 --- a/crates/litewire-turso/src/lib.rs +++ b/crates/litewire-turso/src/lib.rs @@ -29,7 +29,9 @@ //! writers are coordinated by the engine (MVCC / `BEGIN CONCURRENT` is a //! Turso feature; plain busy-handling applies to classic transactions) with //! a configurable busy timeout (default 5000 ms, mirroring the rusqlite -//! backend). +//! backend). Each session also sets `PRAGMA synchronous = NORMAL` by +//! default — the engine's own default is `FULL`; `NORMAL` matches the +//! rusqlite backend's documented per-session behavior. //! //! # In-memory databases //! @@ -67,12 +69,41 @@ use litewire_backend::{ }; use tokio::sync::Mutex; +/// `synchronous` setting applied to every per-session connection. +/// +/// Mirrors the rusqlite backend's `Synchronous` enum. The engine's own +/// default is `FULL`; this backend defaults to [`Synchronous::Normal`] to +/// match the rusqlite backend's documented per-session behavior (the +/// WAL-appropriate default: durable across power loss for committed +/// transactions, higher write throughput than `FULL`). +#[derive(Clone, Copy, Debug)] +pub enum Synchronous { + /// Fastest, unsafe against power loss. + Off, + /// WAL-appropriate default, matches the rusqlite backend. + Normal, + /// Fully synchronous (the Turso engine's own default). Slowest. + Full, +} + +impl Synchronous { + fn as_pragma_str(self) -> &'static str { + match self { + Self::Off => "OFF", + Self::Normal => "NORMAL", + Self::Full => "FULL", + } + } +} + /// Builder for [`Turso`]. Use [`Turso::open`] / [`Turso::memory`] for the -/// default configuration; use [`TursoBuilder`] to tune the busy timeout. +/// default configuration; use [`TursoBuilder`] to tune the per-session +/// busy timeout and `synchronous` mode. #[derive(Clone, Debug)] pub struct TursoBuilder { path: String, busy_timeout_ms: u32, + synchronous: Synchronous, } impl TursoBuilder { @@ -85,6 +116,15 @@ impl TursoBuilder { self } + /// Set the `synchronous` PRAGMA applied to every per-session + /// connection (default [`Synchronous::Normal`], mirroring the rusqlite + /// backend). + #[must_use] + pub fn synchronous(mut self, s: Synchronous) -> Self { + self.synchronous = s; + self + } + /// Finalize the builder: open (or create) the database with the Turso /// engine. The engine is WAL-native; no journal-mode bootstrap is /// required. @@ -101,6 +141,7 @@ impl TursoBuilder { Ok(Turso { db, busy_timeout_ms: self.busy_timeout_ms, + synchronous: self.synchronous, }) } } @@ -113,6 +154,7 @@ impl TursoBuilder { pub struct Turso { db: turso::Database, busy_timeout_ms: u32, + synchronous: Synchronous, } impl Turso { @@ -143,6 +185,7 @@ impl Turso { TursoBuilder { path: path.as_ref().to_string(), busy_timeout_ms: 5000, + synchronous: Synchronous::Normal, } } } @@ -162,6 +205,11 @@ impl Backend for Turso { let conn = self.db.connect().map_err(map_turso_err)?; conn.busy_timeout(Duration::from_millis(u64::from(self.busy_timeout_ms))) .map_err(map_turso_err)?; + // The engine's own default is synchronous=FULL; bring the session + // to parity with the rusqlite backend (NORMAL by default). + conn.pragma_update("synchronous", self.synchronous.as_pragma_str()) + .await + .map_err(map_turso_err)?; Ok(Box::new(TursoConn { conn: Mutex::new(conn), }))