diff --git a/Cargo.lock b/Cargo.lock index 16bae15..05a2993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,9 +49,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "atk" @@ -111,9 +111,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bindgen" @@ -132,7 +132,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -159,22 +159,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2", ] [[package]] @@ -200,15 +191,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -218,9 +209,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -252,9 +243,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -279,7 +270,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -289,14 +280,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.47" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -344,17 +335,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "num-traits", @@ -545,7 +530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -555,7 +540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -579,7 +564,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -590,7 +575,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -624,7 +609,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -673,7 +658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -684,14 +669,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -701,13 +686,13 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -727,9 +712,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -770,7 +755,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -857,15 +842,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -912,7 +897,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1007,7 +992,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1171,9 +1156,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1192,6 +1177,19 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gio" version = "0.18.4" @@ -1258,7 +1256,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1337,7 +1335,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1491,14 +1489,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1515,9 +1512,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1539,9 +1536,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png", @@ -1595,9 +1592,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1609,9 +1606,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1628,6 +1625,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1668,9 +1671,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1695,9 +1698,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1714,9 +1717,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "javascriptcore-rs" @@ -1765,9 +1768,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1814,7 +1817,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.12.1", + "indexmap 2.13.0", "selectors", ] @@ -1833,6 +1836,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1859,9 +1868,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libloading" @@ -1885,19 +1894,19 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -1935,9 +1944,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mac" @@ -1967,7 +1976,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1988,9 +1997,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2025,9 +2034,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2044,14 +2053,14 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -2134,9 +2143,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2187,23 +2196,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", + "syn 2.0.114", ] [[package]] @@ -2223,9 +2216,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.10.0", - "block2 0.6.2", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -2233,8 +2226,8 @@ dependencies = [ "objc2-core-image", "objc2-core-text", "objc2-core-video", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -2244,8 +2237,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2255,8 +2248,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2267,7 +2260,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", "dispatch2", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -2278,7 +2271,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.10.0", "dispatch2", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", "objc2-io-surface", ] @@ -2289,8 +2282,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2300,7 +2293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", "objc2-core-graphics", ] @@ -2312,7 +2305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -2333,18 +2326,6 @@ dependencies = [ "cc", ] -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - [[package]] name = "objc2-foundation" version = "0.3.2" @@ -2352,9 +2333,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", - "block2 0.6.2", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2365,7 +2346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2375,35 +2356,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2411,8 +2367,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -2422,7 +2379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2433,9 +2390,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2445,11 +2402,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ "bitflags 2.10.0", - "block2 0.6.2", - "objc2 0.6.3", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-javascript-core", "objc2-security", ] @@ -2515,7 +2472,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2639,7 +2596,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2666,7 +2623,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", ] [[package]] @@ -2715,7 +2672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.1", + "indexmap 2.13.0", "quick-xml", "serde", "time", @@ -2764,6 +2721,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2790,7 +2757,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -2825,9 +2792,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2843,9 +2810,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2916,7 +2883,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2952,15 +2919,24 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2980,7 +2956,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2997,9 +2973,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3008,15 +2984,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", @@ -3033,7 +3009,6 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tokio-util", @@ -3049,9 +3024,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -3103,9 +3078,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3145,9 +3120,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3164,7 +3139,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3240,7 +3215,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3251,20 +3226,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3275,7 +3250,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3289,9 +3264,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -3310,17 +3285,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3329,14 +3304,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3358,7 +3333,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3410,10 +3385,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3429,9 +3405,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -3441,15 +3417,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3462,9 +3438,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3472,24 +3448,24 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3568,7 +3544,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -3577,7 +3553,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -3595,7 +3571,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3618,7 +3594,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.111", + "syn 2.0.114", "tokio", "url", ] @@ -3660,7 +3636,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "whoami", @@ -3698,7 +3674,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "whoami", @@ -3723,7 +3699,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "url", @@ -3736,40 +3712,46 @@ dependencies = [ "serde", "sqlx", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] -name = "sqlx-sqlite-conn-mgr" +name = "sqlx-sqlite-observer" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1ee0837ee4c49036a64632adf814c1799ec54cc0ba31712d263ce12c09ad45" dependencies = [ - "serde", + "futures", + "libsqlite3-sys", + "parking_lot", + "regex", "sqlx", - "thiserror 2.0.17", + "sqlx-sqlite-conn-mgr", + "tempfile", + "thiserror 2.0.18", "tokio", + "tokio-stream", "tracing", + "tracing-subscriber", ] [[package]] -name = "sqlx-sqlite-observer" +name = "sqlx-sqlite-toolkit" version = "0.8.6" dependencies = [ - "futures", - "libsqlite3-sys", - "parking_lot", - "regex", + "base64 0.22.1", + "indexmap 2.13.0", + "serde", + "serde_json", "sqlx", - "sqlx-sqlite-conn-mgr 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlx-sqlite-conn-mgr", + "sqlx-sqlite-observer", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", + "time", "tokio", - "tokio-stream", "tracing", - "tracing-subscriber", + "uuid", ] [[package]] @@ -3850,9 +3832,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -3876,7 +3858,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3899,7 +3881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.10.0", - "block2 0.6.2", + "block2", "core-foundation", "core-graphics", "crossbeam-channel", @@ -3916,9 +3898,9 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", @@ -3940,7 +3922,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3951,9 +3933,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.3" +version = "2.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8" +checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" dependencies = [ "anyhow", "bytes", @@ -3971,9 +3953,9 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -3990,7 +3972,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", @@ -4002,9 +3984,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.2" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d" +checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" dependencies = [ "anyhow", "cargo_toml", @@ -4018,15 +4000,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f" +checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" dependencies = [ "base64 0.22.1", "brotli", @@ -4040,9 +4022,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.111", + "syn 2.0.114", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4051,23 +4033,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d" +checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.1" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" dependencies = [ "anyhow", "glob", @@ -4076,7 +4058,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] @@ -4085,16 +4067,17 @@ name = "tauri-plugin-sqlite" version = "0.1.0" dependencies = [ "base64 0.22.1", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "serde", "serde_json", "sqlx", - "sqlx-sqlite-conn-mgr 0.8.6", + "sqlx-sqlite-conn-mgr", + "sqlx-sqlite-toolkit", "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tracing", @@ -4103,23 +4086,23 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.3", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4128,17 +4111,17 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -4155,9 +4138,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" dependencies = [ "anyhow", "brotli", @@ -4183,8 +4166,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", - "toml 0.9.8", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4199,17 +4182,17 @@ checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ "dunce", "embed-resource", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4237,11 +4220,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4252,18 +4235,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4277,30 +4260,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4356,14 +4339,14 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -4373,9 +4356,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4398,14 +4381,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -4422,9 +4405,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -4435,7 +4418,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4446,7 +4429,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4455,36 +4438,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.12.1", - "toml_datetime 0.7.3", + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ "winnow 0.7.14", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4497,9 +4480,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", @@ -4545,7 +4528,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4585,23 +4568,23 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -4672,9 +4655,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-normalization" @@ -4697,16 +4680,23 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4735,13 +4725,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -4822,9 +4812,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", ] @@ -4837,9 +4836,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -4850,11 +4849,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4863,9 +4863,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4873,31 +4873,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" 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 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -4906,11 +4928,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -4918,9 +4952,9 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -4942,9 +4976,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -4962,9 +4996,9 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -4976,22 +5010,22 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5043,10 +5077,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5119,7 +5153,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5130,7 +5164,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5527,9 +5561,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +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 0.5.0", + "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 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "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 2.0.114", + "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 2.10.0", + "indexmap 2.13.0", + "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 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -5539,12 +5655,12 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.5" +version = "0.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0" dependencies = [ "base64 0.22.1", - "block2 0.6.2", + "block2", "cookie", "crossbeam-channel", "dirs", @@ -5559,10 +5675,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5571,7 +5687,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -5622,28 +5738,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5663,7 +5779,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] @@ -5703,5 +5819,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/Cargo.toml b/Cargo.toml index 9f85c6a..ec7e00d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "3" members = [ "crates/sqlx-sqlite-conn-mgr", "crates/sqlx-sqlite-observer", + "crates/sqlx-sqlite-toolkit", ] [package] @@ -33,6 +34,9 @@ sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio # Connection manager sqlx-sqlite-conn-mgr = { path = "crates/sqlx-sqlite-conn-mgr" } +# High-level toolkit (builders, transactions, decoding) +sqlx-sqlite-toolkit = { path = "crates/sqlx-sqlite-toolkit" } + [build-dependencies] tauri-plugin = { version = "2.5.1", features = ["build"] } diff --git a/README.md b/README.md index da35bde..076af21 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,42 @@ SQLite database interface for Tauri applications using * **Type Safety**: Full TypeScript bindings * **Migration Support**: SQLx's migration framework * **Resource Management**: Proper cleanup on application exit + * **Optional Change Notifications**: SQLite hooks for reactive change notifications ## Architecture +The plugin is built from three standalone Rust crates, each usable independently +without Tauri: + +```text +┌───────────────────────────────────────────────────────────────┐ +│ tauri-plugin-sqlite (src/) │ +│ Tauri commands, state management, permissions │ +├───────────────────────────────────────────────────────────────┤ +│ sqlx-sqlite-toolkit (crate) │ +│ DatabaseWrapper, builders, transactions │ +│ JSON decoding, optional observer integration │ +├───────────────────────────────────────────────────────────────┤ +│ sqlx-sqlite-conn-mgr (crate) │ sqlx-sqlite-observer (crate) │ +│ Connection pools, │ Change notifications │ +│ single writer, │ via SQLite hooks │ +│ WAL mode, attached │ broadcast streams │ +│ databases │ (optional) │ +└───────────────────────────────┴───────────────────────────────┘ +``` + + * **[`sqlx-sqlite-conn-mgr`](crates/sqlx-sqlite-conn-mgr/)** — Low-level connection + management: read pool, exclusive writer, WAL mode, attached databases + * **[`sqlx-sqlite-observer`](crates/sqlx-sqlite-observer/)** — Reactive change + notifications using SQLite's native preupdate/commit/rollback hooks + * **[`sqlx-sqlite-toolkit`](crates/sqlx-sqlite-toolkit/)** — High-level API: + `DatabaseWrapper`, builder-pattern queries, interruptible transactions, JSON + type decoding. Optionally integrates the observer behind a feature flag. + * **`tauri-plugin-sqlite` (this package)** — Thin Tauri layer: IPC commands, path + resolution, state management, permissions + +### Query Routing + | Operation Type | Method | Pool Used | Concurrency | | -------------------- | --------------- | ---------------- | ------------------- | | SELECT (multiple) | `fetchAll()` | Read pool | Multiple concurrent | @@ -31,8 +64,7 @@ SQLite database interface for Tauri applications using | INSERT/UPDATE/DELETE | `execute()` | Write connection | Serialized | | DDL (CREATE, etc.) | `execute()` | Write connection | Serialized | -See [`crates/sqlx-sqlite-conn-mgr/README.md`](crates/sqlx-sqlite-conn-mgr/README.md) for -connection manager internals. +See individual crate READMEs for detailed API documentation. ## Installation @@ -85,10 +117,10 @@ Register the plugin in your Tauri application: ```rust fn main() { - tauri::Builder::default() - .plugin(tauri_plugin_sqlite::Builder::new().build()) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .plugin(tauri_plugin_sqlite::Builder::new().build()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } ``` @@ -112,14 +144,14 @@ Register migrations using SQLx's `migrate!()` macro, which embeds them at compil use tauri_plugin_sqlite::Builder; fn main() { - tauri::Builder::default() - .plugin( - Builder::new() - .add_migrations("main.db", sqlx::migrate!("./migrations")) - .build() - ) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .plugin( + Builder::new() + .add_migrations("main.db", sqlx::migrate!("./migrations")) + .build() + ) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } ``` @@ -133,16 +165,16 @@ tracked in `_sqlx_migrations` — re-running is safe and idempotent. Use `getMigrationEvents()` to retrieve cached events: ```typescript -import Database from '@silvermine/tauri-plugin-sqlite' +import Database from '@silvermine/tauri-plugin-sqlite'; -const db = await Database.load('mydb.db') +const db = await Database.load('mydb.db'); // Get all migration events (including ones emitted before listener could be registered) -const events = await db.getMigrationEvents() +const events = await db.getMigrationEvents(); for (const event of events) { - console.info(`${event.status}: ${event.dbPath}`) + console.info(`${event.status}: ${event.dbPath}`); if (event.status === 'failed') { - console.error(`Migration error: ${event.error}`) + console.error(`Migration error: ${event.error}`); } } ``` @@ -151,31 +183,31 @@ for (const event of events) { layer completing some or all migrations before the frontend subscription initializes. ```typescript -import { listen } from '@tauri-apps/api/event' -import type { MigrationEvent } from '@silvermine/tauri-plugin-sqlite' +import { listen } from '@tauri-apps/api/event'; +import type { MigrationEvent } from '@silvermine/tauri-plugin-sqlite'; await listen('sqlite:migration', (event) => { - const { dbPath, status, migrationCount, error } = event.payload - console.info(`Migration ${status} for ${dbPath}: ${migrationCount} migrations`, error) -}) + const { dbPath, status, migrationCount, error } = event.payload; + console.info(`Migration ${status} for ${dbPath}: ${migrationCount} migrations`, error); +}); ``` ### Connecting ```typescript -import Database from '@silvermine/tauri-plugin-sqlite' +import Database from '@silvermine/tauri-plugin-sqlite'; // Path is relative to app config directory (no sqlite: prefix needed) -let db = await Database.load('mydb.db') +let db = await Database.load('mydb.db'); // With custom configuration db = await Database.load('mydb.db', { maxReadConnections: 10, // default: 6 idleTimeoutSecs: 60 // default: 30 -}) +}); // Lazy initialization (connects on first query) -db = Database.get('mydb.db') +db = Database.get('mydb.db'); ``` ### Parameter Binding @@ -183,7 +215,7 @@ db = Database.get('mydb.db') All query methods use `$1`, `$2`, etc. syntax with `SqlValue` types: ```typescript -type SqlValue = string | number | boolean | null | Uint8Array +type SqlValue = string | number | boolean | null | Uint8Array; ``` | SQLite Type | TypeScript Type | Notes | @@ -205,34 +237,34 @@ Use `execute()` for INSERT, UPDATE, DELETE, CREATE, etc.: ```typescript await db.execute( 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)' -) +); const result = await db.execute( 'INSERT INTO users (name, email) VALUES ($1, $2)', ['Alice', 'alice@example.com'] -) -console.info(`Inserted ${result.rowsAffected} row(s), ID: ${result.lastInsertId}`) +); +console.info(`Inserted ${result.rowsAffected} row(s), ID: ${result.lastInsertId}`); ``` ### Read Operations ```typescript -type User = { id: number; name: string; email: string } +type User = { id: number; name: string; email: string }; // Multiple rows const users = await db.fetchAll( 'SELECT * FROM users WHERE email LIKE $1', ['%@example.com'] -) -console.info(`Found ${users.length} users`) +); +console.info(`Found ${users.length} users`); // Single row (returns undefined if not found, throws if multiple rows) const user = await db.fetchOne( 'SELECT * FROM users WHERE id = $1', [42] -) +); if (user) { - console.info(`Found user: ${user.name}`) + console.info(`Found user: ${user.name}`); } ``` @@ -245,8 +277,8 @@ const results = await db.executeTransaction([ ['UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]], ['UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]], ['INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)', [1, 2, 100]] -]) -console.info(`Transaction completed: ${results.length} statements executed`) +]); +console.info(`Transaction completed: ${results.length} statements executed`); ``` Transactions use `BEGIN IMMEDIATE`, commit on success, and rollback on any failure. @@ -259,30 +291,30 @@ generated ID or other computed values, then using that data in subsequent writes ```typescript // Assuming userId, productId, itemTotal are defined in your application context -const userId = 123 -const productId = 456 -const itemTotal = 99.99 +const userId = 123; +const productId = 456; +const itemTotal = 99.99; // Begin transaction with initial insert let tx = await db.beginInterruptibleTransaction([ ['INSERT INTO orders (user_id, total) VALUES ($1, $2)', [userId, 0]] -]) +]); // Read the uncommitted data to get the generated order ID const orders = await tx.read>( 'SELECT id FROM orders WHERE user_id = $1 ORDER BY id DESC LIMIT 1', [userId] -) -const orderId = orders[0].id +); +const orderId = orders[0].id; // Continue transaction with the order ID tx = await tx.continueWith([ ['INSERT INTO order_items (order_id, product_id) VALUES ($1, $2)', [orderId, productId]], ['UPDATE orders SET total = $1 WHERE id = $2', [itemTotal, orderId]] -]) +]); // Commit the transaction -await tx.commit() +await tx.commit(); ``` **Important:** @@ -296,7 +328,7 @@ await tx.commit() To rollback instead of committing: ```typescript -await tx.rollback() +await tx.rollback(); ``` ### Cross-Database Queries @@ -320,8 +352,8 @@ const results = await db.fetchAll( schemaName: 'orders', mode: 'readOnly' } -]) -console.info(`Found ${results.length} results from cross-database query`) +]); +console.info(`Found ${results.length} results from cross-database query`); // Update main database using data from attached database await db.execute( @@ -333,12 +365,12 @@ await db.execute( schemaName: 'archive', mode: 'readOnly' } -]) +]); // Atomic writes across multiple databases // Assuming userId and total are defined in your application context -const userId = 123 -const total = 99.99 +const userId = 123; +const total = 99.99; await db.executeTransaction([ ['INSERT INTO main.orders (user_id, total) VALUES ($1, $2)', [userId, total]], @@ -349,7 +381,7 @@ await db.executeTransaction([ schemaName: 'stats', mode: 'readWrite' } -]) +]); ``` **Attached Database Modes:** @@ -368,13 +400,13 @@ await db.executeTransaction([ ### Error Handling ```typescript -import type { SqliteError } from '@silvermine/tauri-plugin-sqlite' +import type { SqliteError } from '@silvermine/tauri-plugin-sqlite'; try { - await db.execute('INSERT INTO users (id) VALUES ($1)', [1]) + await db.execute('INSERT INTO users (id) VALUES ($1)', [1]); } catch (err) { - const error = err as SqliteError - console.error(error.code, error.message) + const error = err as SqliteError; + console.error(error.code, error.message); } ``` @@ -391,9 +423,9 @@ Common error codes: ### Closing and Removing ```typescript -await db.close() // Close this connection -await Database.close_all() // Close all connections -await db.remove() // Close and DELETE database file(s) - irreversible! +await db.close(); // Close this connection +await Database.close_all(); // Close all connections +await db.remove(); // Close and DELETE database file(s) - irreversible! ``` ## API Reference @@ -441,24 +473,24 @@ return builders that are directly awaitable and support method chaining: ```typescript interface WriteQueryResult { - rowsAffected: number - lastInsertId: number // 0 for WITHOUT ROWID tables + rowsAffected: number; + lastInsertId: number; // 0 for WITHOUT ROWID tables } interface CustomConfig { - maxReadConnections?: number // default: 6 - idleTimeoutSecs?: number // default: 30 + maxReadConnections?: number; // default: 6 + idleTimeoutSecs?: number; // default: 30 } interface AttachedDatabaseSpec { - databasePath: string // Path relative to app config directory - schemaName: string // Schema name for accessing tables (e.g., 'orders') - mode: 'readOnly' | 'readWrite' + databasePath: string; // Path relative to app config directory + schemaName: string; // Schema name for accessing tables (e.g., 'orders') + mode: 'readOnly' | 'readWrite'; } interface SqliteError { - code: string - message: string + code: string; + message: string; } ``` @@ -479,8 +511,8 @@ let mut db = DatabaseWrapper::load(PathBuf::from("/path/to/mydb.db"), None).awai // With custom configuration use tauri_plugin_sqlite::CustomConfig; let config = CustomConfig { - max_read_connections: Some(10), - idle_timeout_secs: Some(60), + max_read_connections: Some(10), + idle_timeout_secs: Some(60), }; db = DatabaseWrapper::load(PathBuf::from("/path/to/mydb.db"), Some(config)).await?; ``` @@ -492,28 +524,28 @@ use serde_json::json; // Write operations let result = db.execute( - "INSERT INTO users (name, email) VALUES (?, ?)".into(), - vec![json!("Alice"), json!("alice@example.com")] + "INSERT INTO users (name, email) VALUES (?, ?)".into(), + vec![json!("Alice"), json!("alice@example.com")] ).await?; println!("Inserted row {}", result.last_insert_id); // Read multiple rows let users = db.fetch_all( - "SELECT * FROM users WHERE active = ?".into(), - vec![json!(true)] + "SELECT * FROM users WHERE active = ?".into(), + vec![json!(true)] ).await?; println!("Found {} users", users.len()); // Read single row let user = db.fetch_one( - "SELECT * FROM users WHERE id = ?".into(), - vec![json!(42)] + "SELECT * FROM users WHERE id = ?".into(), + vec![json!(42)] ).await?; if let Some(user_data) = user { - println!("Found user: {:?}", user_data); + println!("Found user: {:?}", user_data); } ``` @@ -523,9 +555,9 @@ Use `execute_transaction()` for atomic execution of multiple statements: ```rust let results = db.execute_transaction(vec![ - ("UPDATE accounts SET balance = balance - ? WHERE id = ?", vec![json!(100), json!(1)]), - ("UPDATE accounts SET balance = balance + ? WHERE id = ?", vec![json!(100), json!(2)]), - ("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)", vec![json!(1), json!(2), json!(100)]), + ("UPDATE accounts SET balance = balance - ? WHERE id = ?", vec![json!(100), json!(1)]), + ("UPDATE accounts SET balance = balance + ? WHERE id = ?", vec![json!(100), json!(2)]), + ("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)", vec![json!(1), json!(2), json!(100)]), ]).await?; println!("Transaction completed: {} statements executed", results.len()); @@ -545,23 +577,23 @@ let item_total = 99.99; // Begin transaction with initial statements let mut tx = db.begin_interruptible_transaction() - .execute(vec![ - ("INSERT INTO orders (user_id, total) VALUES (?, ?)", vec![json!(user_id), json!(0)]), - ]) - .await?; + .execute(vec![ + ("INSERT INTO orders (user_id, total) VALUES (?, ?)", vec![json!(user_id), json!(0)]), + ]) + .await?; // Read uncommitted data let orders = tx.read( - "SELECT id FROM orders WHERE user_id = ? ORDER BY id DESC LIMIT 1".into(), - vec![json!(user_id)] + "SELECT id FROM orders WHERE user_id = ? ORDER BY id DESC LIMIT 1".into(), + vec![json!(user_id)] ).await?; let order_id = orders[0].get("id").unwrap().as_i64().unwrap(); // Continue with more statements tx.continue_with(vec![ - ("INSERT INTO order_items (order_id, product_id) VALUES (?, ?)", vec![json!(order_id), json!(product_id)]), - ("UPDATE orders SET total = ? WHERE id = ?", vec![json!(item_total), json!(order_id)]), + ("INSERT INTO order_items (order_id, product_id) VALUES (?, ?)", vec![json!(order_id), json!(product_id)]), + ("UPDATE orders SET total = ? WHERE id = ?", vec![json!(item_total), json!(order_id)]), ]).await?; // Commit (or rollback) @@ -585,15 +617,15 @@ let stats_db = DatabaseWrapper::load("/path/to/stats.db".into(), None).await?; // Create attached spec using the inner database reference let stats_spec = AttachedSpec { - database: Arc::clone(stats_db.inner()), - schema_name: "stats".to_string(), - mode: AttachedMode::ReadWrite, + database: Arc::clone(stats_db.inner()), + schema_name: "stats".to_string(), + mode: AttachedMode::ReadWrite, }; // Simple transaction with attached database let results = main_db.execute_transaction(vec![ - ("INSERT INTO main.orders (user_id) VALUES (?)", vec![json!(1)]), - ("UPDATE stats.order_count SET count = count + 1", vec![]), + ("INSERT INTO main.orders (user_id) VALUES (?)", vec![json!(1)]), + ("UPDATE stats.order_count SET count = count + 1", vec![]), ]) .attach(vec![stats_spec]) .await?; @@ -605,20 +637,20 @@ let inventory_db = DatabaseWrapper::load("/path/to/inventory.db".into(), None).a // Create spec for inventory database let inv_spec = AttachedSpec { - database: Arc::clone(inventory_db.inner()), - schema_name: "inv".to_string(), - mode: AttachedMode::ReadWrite, + database: Arc::clone(inventory_db.inner()), + schema_name: "inv".to_string(), + mode: AttachedMode::ReadWrite, }; // Assuming product_id is defined in your application context let product_id = 789; let _tx = main_db.begin_interruptible_transaction() - .attach(vec![inv_spec]) - .execute(vec![ - ("UPDATE inv.stock SET quantity = quantity - ? WHERE product_id = ?", vec![json!(1), json!(product_id)]), - ]) - .await?; + .attach(vec![inv_spec]) + .execute(vec![ + ("UPDATE inv.stock SET quantity = quantity - ? WHERE product_id = ?", vec![json!(1), json!(product_id)]), + ]) + .await?; // Continue with transaction operations... ``` @@ -669,22 +701,22 @@ tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } ```rust #[cfg(debug_assertions)] fn init_tracing() { - use tracing_subscriber::{fmt, EnvFilter}; - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("trace")); + use tracing_subscriber::{fmt, EnvFilter}; + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("trace")); - fmt().with_env_filter(filter).compact().init(); + fmt().with_env_filter(filter).compact().init(); } #[cfg(not(debug_assertions))] fn init_tracing() {} fn main() { - init_tracing(); - tauri::Builder::default() - .plugin(tauri_plugin_sqlite::Builder::new().build()) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + init_tracing(); + tauri::Builder::default() + .plugin(tauri_plugin_sqlite::Builder::new().build()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } ``` diff --git a/api-iife.js b/api-iife.js index d313ce7..2304c8b 100644 --- a/api-iife.js +++ b/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_SQLITE__=function(t){"use strict";async function e(t,e={},a){return window.__TAURI_INTERNALS__.invoke(t,e,a)}"function"==typeof SuppressedError&&SuppressedError;class a{constructor(t,e){this._dbPath=t,this._transactionId=e}async read(t,a){return await e("plugin:sqlite|transaction_read",{token:{dbPath:this._dbPath,transactionId:this._transactionId},query:t,values:a??[]})}async continue(t){const n=await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Continue",statements:t.map(([t,e])=>({query:t,values:e??[]}))}});return new a(n.dbPath,n.transactionId)}async commit(){await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Commit"}})}async rollback(){await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Rollback"}})}}class n{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|fetch_all",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null})}}class s{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|fetch_one",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null})}}class i{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){const[t,a]=await e("plugin:sqlite|execute",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null});return{lastInsertId:a,rowsAffected:t}}}class r{constructor(t,e,a=[]){this._db=t,this._statements=e,this._attached=a}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|execute_transaction",{db:this._db.path,statements:this._statements.map(([t,e])=>({query:t,values:e??[]})),attached:this._attached.length>0?this._attached:null})}}class h{constructor(t){this.path=t}static async load(t,a){const n=await e("plugin:sqlite|load",{db:t,customConfig:a});return new h(n)}static get(t){return new h(t)}static async close_all(){await e("plugin:sqlite|close_all")}execute(t,e){return new i(this,t,e??[])}executeTransaction(t){return new r(this,t)}fetchAll(t,e){return new n(this,t,e??[])}fetchOne(t,e){return new s(this,t,e??[])}async close(){return await e("plugin:sqlite|close",{db:this.path})}async remove(){return await e("plugin:sqlite|remove",{db:this.path})}async executeInterruptibleTransaction(t){const n=await e("plugin:sqlite|execute_interruptible_transaction",{db:this.path,initialStatements:t.map(([t,e])=>({query:t,values:e??[]}))});return new a(n.dbPath,n.transactionId)}async getMigrationEvents(){return await e("plugin:sqlite|get_migration_events",{db:this.path})}}return t.InterruptibleTransaction=a,t.default=h,Object.defineProperty(t,"__esModule",{value:!0}),t}({});Object.defineProperty(window.__TAURI__,"sqlite",{value:__TAURI_PLUGIN_SQLITE__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_SQLITE__=function(t){"use strict";async function e(t,e={},a){return window.__TAURI_INTERNALS__.invoke(t,e,a)}"function"==typeof SuppressedError&&SuppressedError;class a{constructor(t,e){this._dbPath=t,this._transactionId=e}async read(t,a){return await e("plugin:sqlite|transaction_read",{token:{dbPath:this._dbPath,transactionId:this._transactionId},query:t,values:a??[]})}async continueWith(t){const n=await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Continue",statements:t.map(([t,e])=>({query:t,values:e??[]}))}});return new a(n.dbPath,n.transactionId)}async commit(){await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Commit"}})}async rollback(){await e("plugin:sqlite|transaction_continue",{token:{dbPath:this._dbPath,transactionId:this._transactionId},action:{type:"Rollback"}})}}class n{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|fetch_all",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null})}}class s{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|fetch_one",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null})}}class i{constructor(t,e,a,n=[]){this._db=t,this._query=e,this._bindValues=a,this._attached=n}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){const[t,a]=await e("plugin:sqlite|execute",{db:this._db.path,query:this._query,values:this._bindValues,attached:this._attached.length>0?this._attached:null});return{lastInsertId:a,rowsAffected:t}}}class h{constructor(t,e,a=[]){this._db=t,this._initialStatements=e,this._attached=a}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){const t=await e("plugin:sqlite|begin_interruptible_transaction",{db:this._db.path,initialStatements:this._initialStatements.map(([t,e])=>({query:t,values:e??[]})),attached:this._attached.length>0?this._attached:null});return new a(t.dbPath,t.transactionId)}}class r{constructor(t,e,a=[]){this._db=t,this._statements=e,this._attached=a}attach(t){return this._attached=t,this}then(t,e){return this._execute().then(t,e)}async _execute(){return await e("plugin:sqlite|execute_transaction",{db:this._db.path,statements:this._statements.map(([t,e])=>({query:t,values:e??[]})),attached:this._attached.length>0?this._attached:null})}}class c{constructor(t){this.path=t}static async load(t,a){const n=await e("plugin:sqlite|load",{db:t,customConfig:a});return new c(n)}static get(t){return new c(t)}static async close_all(){await e("plugin:sqlite|close_all")}execute(t,e){return new i(this,t,e??[])}executeTransaction(t){return new r(this,t)}fetchAll(t,e){return new n(this,t,e??[])}fetchOne(t,e){return new s(this,t,e??[])}async close(){return await e("plugin:sqlite|close",{db:this.path})}async remove(){return await e("plugin:sqlite|remove",{db:this.path})}beginInterruptibleTransaction(t){return new h(this,t)}async getMigrationEvents(){return await e("plugin:sqlite|get_migration_events",{db:this.path})}}return t.InterruptibleTransaction=a,t.default=c,Object.defineProperty(t,"__esModule",{value:!0}),t}({});Object.defineProperty(window.__TAURI__,"sqlite",{value:__TAURI_PLUGIN_SQLITE__})} diff --git a/build.rs b/build.rs index 8784faf..ab74261 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ fn main() { "load", "execute", "execute_transaction", - "execute_interruptible_transaction", + "begin_interruptible_transaction", "transaction_continue", "transaction_read", "fetch_all", @@ -11,6 +11,7 @@ fn main() { "close", "close_all", "remove", + "get_migration_events", ]) .build(); } diff --git a/crates/sqlx-sqlite-observer/Cargo.toml b/crates/sqlx-sqlite-observer/Cargo.toml index b0b30d0..36742b8 100644 --- a/crates/sqlx-sqlite-observer/Cargo.toml +++ b/crates/sqlx-sqlite-observer/Cargo.toml @@ -13,11 +13,11 @@ keywords = ["sqlite", "sqlx", "reactive", "observer", "database"] categories = ["database", "asynchronous"] [features] -conn-mgr = ["dep:sqlx-sqlite-conn-mgr"] # Bundle SQLite by default - preupdate hooks require SQLITE_ENABLE_PREUPDATE_HOOK # which most system SQLite libraries don't have enabled. default = ["bundled"] bundled = ["libsqlite3-sys/bundled"] +conn-mgr = ["dep:sqlx-sqlite-conn-mgr"] [dependencies] tokio = { version = "1.49.0", features = ["sync"] } @@ -29,7 +29,7 @@ regex = "1.12.3" sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"], default-features = false } # Required for preupdate_hook - SQLite must be compiled with SQLITE_ENABLE_PREUPDATE_HOOK libsqlite3-sys = { version = "0.30.1", features = ["preupdate_hook"] } -sqlx-sqlite-conn-mgr = { version = "0.8.6", optional = true } +sqlx-sqlite-conn-mgr = { path = "../sqlx-sqlite-conn-mgr", version = "0.8.6", optional = true } [dev-dependencies] tokio = { version = "1.49.0", features = ["full", "macros"] } diff --git a/crates/sqlx-sqlite-observer/README.md b/crates/sqlx-sqlite-observer/README.md index a751792..a367047 100644 --- a/crates/sqlx-sqlite-observer/README.md +++ b/crates/sqlx-sqlite-observer/README.md @@ -62,19 +62,19 @@ The library uses SQLite's native hooks for transaction-safe change tracking: │ (captures data) │ │ (Vec) │ │ │ └─────────────────┘ └────────┬────────┘ └─────────────────┘ │ ▲ - ┌────────────┼────────────┐ │ - │ │ │ │ - ┌─────▼────┐ ┌────▼─────┐ │ │ - │ COMMIT │ │ ROLLBACK │ │ │ - └─────┬────┘ └────┬─────┘ │ │ - │ │ │ │ - ▼ ▼ │ │ - on_commit() on_rollback() │ │ - │ │ │ │ - │ buffer.clear() │ │ - │ (discard) │ │ - │ │ │ - └─────────────────────────┴──────────┘ + ┌────────────| │ + │ │ │ + ┌─────▼────┐ ┌────▼─────┐ │ + │ COMMIT │ │ ROLLBACK │ │ + └─────┬────┘ └────┬─────┘ │ + │ │ │ + ▼ ▼ │ + on_commit() on_rollback() │ + │ │ │ + │ buffer.clear() │ + │ (discard) │ + │ │ + └────────────────────────────────────┘ change_tx.send() (publish) ``` @@ -283,6 +283,7 @@ async fn main() -> Result<(), Box> { let config = ObserverConfig::new() .with_tables(["users"]) .with_capture_values(false); + let observer = SqliteObserver::new( SqlitePool::connect("sqlite:mydb.db").await?, config, diff --git a/crates/sqlx-sqlite-toolkit/Cargo.toml b/crates/sqlx-sqlite-toolkit/Cargo.toml new file mode 100644 index 0000000..3593d66 --- /dev/null +++ b/crates/sqlx-sqlite-toolkit/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sqlx-sqlite-toolkit" +# Sync major.minor with major.minor of SQLx crate +version = "0.8.6" +license = "MIT" +edition = "2024" +rust-version = "1.89" + +[features] +default = [] +observer = ["dep:sqlx-sqlite-observer"] + +[dependencies] +sqlx-sqlite-conn-mgr = { path = "../sqlx-sqlite-conn-mgr" } +sqlx-sqlite-observer = { path = "../sqlx-sqlite-observer", features = ["conn-mgr"], optional = true } +sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +indexmap = { version = "2.12", features = ["serde"] } +base64 = "0.22" +time = "0.3" +uuid = { version = "1.11", features = ["v4"] } +tokio = { version = "1.48.0", features = ["sync", "rt"] } +tracing = { version = "0.1", default-features = false, features = ["std", "release_max_level_off"] } + +[dev-dependencies] +tempfile = "3.23.0" +tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] } diff --git a/crates/sqlx-sqlite-toolkit/README.md b/crates/sqlx-sqlite-toolkit/README.md new file mode 100644 index 0000000..4fd9673 --- /dev/null +++ b/crates/sqlx-sqlite-toolkit/README.md @@ -0,0 +1,228 @@ +# SQLx SQLite Toolkit + +High-level SQLite API providing builder-pattern queries, transaction management, +and JSON type decoding. Built on top of +[`sqlx-sqlite-conn-mgr`](../sqlx-sqlite-conn-mgr) and optionally integrates +[`sqlx-sqlite-observer`](../sqlx-sqlite-observer) for reactive change +notifications. + +Not dependent on Tauri — usable in any Rust project. + +## Features + + * **`DatabaseWrapper`**: Main entry point wrapping a connection-managed database + * **Builder-pattern queries**: `execute()`, `fetch_all()`, `fetch_one()` with + optional `.attach()` for cross-database operations + * **Transactions**: Atomic `execute_transaction()` and interruptible transactions + with mid-transaction reads + * **JSON type decoding**: Automatic SQLite-to-JSON value conversion + (INTEGER, REAL, TEXT, NULL, BLOB as base64) + * **Transaction state tracking**: `ActiveInterruptibleTransactions` and + `ActiveRegularTransactions` for managing in-flight transactions + * **Observer integration** (optional `observer` feature): Route writes through + `sqlx-sqlite-observer` for change notifications + +## Installation + +```toml +[dependencies] +sqlx-sqlite-toolkit = { version = "0.8" } + +# With observer support +sqlx-sqlite-toolkit = { version = "0.8", features = ["observer"] } +``` + +## Usage + +### Connect + +```rust +use sqlx_sqlite_toolkit::DatabaseWrapper; +use std::path::Path; + +let db = DatabaseWrapper::connect(Path::new("mydb.db"), None).await?; + +// With custom configuration +use sqlx_sqlite_toolkit::SqliteDatabaseConfig; +use std::time::Duration; + +let config = SqliteDatabaseConfig { + max_read_connections: 10, + idle_timeout: Duration::from_secs(60), +}; +let db = DatabaseWrapper::connect(Path::new("mydb.db"), Some(config)).await?; +``` + +### Write Operations + +```rust +use serde_json::json; + +let result = db.execute( + "INSERT INTO users (name, email) VALUES (?, ?)".into(), + vec![json!("Alice"), json!("alice@example.com")] +).await?; + +println!("Inserted row {}, affected {}", result.last_insert_id, result.rows_affected); +``` + +### Read Operations + +```rust +use serde_json::json; + +// Multiple rows — returns Vec> +let users = db.fetch_all( + "SELECT * FROM users WHERE active = ?".into(), + vec![json!(true)] +).await?; + +// Single row — returns Option> +let user = db.fetch_one( + "SELECT * FROM users WHERE id = ?".into(), + vec![json!(42)] +).await?; +``` + +### Transactions + +Atomic execution of multiple statements: + +```rust +use serde_json::json; + +let results = db.execute_transaction(vec![ + ("UPDATE accounts SET balance = balance - ? WHERE id = ?", vec![json!(100), json!(1)]), + ("UPDATE accounts SET balance = balance + ? WHERE id = ?", vec![json!(100), json!(2)]), +]).await?; +// Commits on success, rolls back on any failure +``` + +### Interruptible Transactions + +For transactions that need to read data mid-transaction: + +```rust +use serde_json::json; + +let mut tx = db.begin_interruptible_transaction() + .execute(vec![ + ("INSERT INTO orders (user_id, total) VALUES (?, ?)", vec![json!(123), json!(0)]), + ]) + .await?; + +// Read uncommitted data +let rows = tx.read( + "SELECT id FROM orders WHERE user_id = ? ORDER BY id DESC LIMIT 1".into(), + vec![json!(123)] +).await?; +let order_id = rows[0].get("id").unwrap().as_i64().unwrap(); + +// Continue with more statements +tx.continue_with(vec![ + ("INSERT INTO order_items (order_id, product_id) VALUES (?, ?)", vec![json!(order_id), json!(456)]), +]).await?; + +tx.commit().await?; +// Or: tx.rollback().await?; +``` + +### Cross-Database Queries + +Attach other databases using the builder pattern: + +```rust +use sqlx_sqlite_toolkit::{DatabaseWrapper, AttachedSpec, AttachedMode}; +use serde_json::json; +use std::sync::Arc; + +let main_db = DatabaseWrapper::connect("main.db".as_ref(), None).await?; +let stats_db = DatabaseWrapper::connect("stats.db".as_ref(), None).await?; + +let results = main_db.execute_transaction(vec![ + ("INSERT INTO orders (user_id) VALUES (?)", vec![json!(1)]), + ("UPDATE stats.counters SET n = n + 1", vec![]), +]) +.attach(vec![AttachedSpec { + database: Arc::clone(stats_db.inner()), + schema_name: "stats".to_string(), + mode: AttachedMode::ReadWrite, +}]) +.await?; +``` + +### Transaction State Management + +Track active transactions across your application: + +```rust +use sqlx_sqlite_toolkit::{ + ActiveInterruptibleTransactions, ActiveRegularTransactions, + cleanup_all_transactions, +}; + +let interruptible = ActiveInterruptibleTransactions::default(); +let regular = ActiveRegularTransactions::default(); + +// Insert/remove transactions as they start/finish +// ... + +// On application exit, abort all in-flight transactions +cleanup_all_transactions(&interruptible, ®ular).await; +``` + +## API Reference + +### `DatabaseWrapper` + +| Method | Description | +| ------ | ----------- | +| `connect(path, config?)` | Connect to database, returns `DatabaseWrapper` | +| `execute(query, values)` | Execute write query, returns `WriteQueryResult` | +| `execute_transaction(stmts)` | Execute atomically (builder, supports `.attach()`) | +| `begin_interruptible_transaction()` | Begin interruptible transaction (builder) | +| `fetch_all(query, values)` | Fetch all rows as JSON maps | +| `fetch_one(query, values)` | Fetch single row or `None` | +| `acquire_writer()` | Acquire exclusive `WriterGuard` | +| `run_migrations(migrator)` | Run pending migrations | +| `close()` | Close connection | +| `remove()` | Close and delete database file(s) | + +### `ActiveInterruptibleTransaction` + +| Method | Description | +| ------ | ----------- | +| `read(query, values)` | Read within transaction (sees uncommitted data) | +| `continue_with(statements)` | Execute additional statements | +| `commit()` | Commit and release writer | +| `rollback()` | Rollback and release writer | + +### Error Codes + +All errors provide an `error_code()` method returning a machine-readable string: + +| Code | Description | +| ---- | ----------- | +| `SQLITE_*` | SQLite-level error (constraint, etc.) | +| `SQLX_ERROR` | SQLx error without SQLite code | +| `CONNECTION_ERROR` | Connection manager error | +| `UNSUPPORTED_DATATYPE` | Unmappable SQLite type | +| `MULTIPLE_ROWS_RETURNED` | `fetch_one` got multiple rows | +| `TRANSACTION_ROLLBACK_FAILED` | Rollback failed after error | +| `TRANSACTION_ALREADY_FINALIZED` | Double commit/rollback | +| `TRANSACTION_ALREADY_ACTIVE` | Duplicate interruptible transaction | +| `NO_ACTIVE_TRANSACTION` | Remove from empty state | +| `INVALID_TRANSACTION_TOKEN` | Wrong transaction ID | +| `IO_ERROR` | File system error | + +## Development + +```bash +cargo build # Build +cargo test -p sqlx-sqlite-toolkit # Test +cargo lint-clippy && cargo lint-fmt # Lint +``` + +## License + +MIT diff --git a/src/builders.rs b/crates/sqlx-sqlite-toolkit/src/builders.rs similarity index 94% rename from src/builders.rs rename to crates/sqlx-sqlite-toolkit/src/builders.rs index 9779d03..1d5e684 100644 --- a/src/builders.rs +++ b/crates/sqlx-sqlite-toolkit/src/builders.rs @@ -9,7 +9,7 @@ use serde_json::Value as JsonValue; use sqlx_sqlite_conn_mgr::AttachedSpec; use crate::Error; -use crate::wrapper::{WriteQueryResult, bind_value}; +use crate::wrapper::{DatabaseWrapper, WriteQueryResult, bind_value}; /// Builder for SELECT queries returning multiple rows pub struct FetchAllBuilder { @@ -155,18 +155,14 @@ impl IntoFuture for FetchOneBuilder { /// Builder for write queries (INSERT/UPDATE/DELETE) pub struct ExecuteBuilder { - db: Arc, + db: DatabaseWrapper, query: String, values: Vec, attached: Vec, } impl ExecuteBuilder { - pub(crate) fn new( - db: Arc, - query: String, - values: Vec, - ) -> Self { + pub(crate) fn new(db: DatabaseWrapper, query: String, values: Vec) -> Self { Self { db, query, @@ -184,7 +180,7 @@ impl ExecuteBuilder { /// Execute the write operation pub async fn execute(self) -> Result { if self.attached.is_empty() { - // No attached databases - use regular writer + // No attached databases - use wrapper's writer (routes through observer when in use) let mut writer = self.db.acquire_writer().await?; let mut q = sqlx::query(&self.query); for value in self.values { @@ -198,7 +194,8 @@ impl ExecuteBuilder { } else { // With attached database(s) - acquire writer with attached database(s) let mut conn = - sqlx_sqlite_conn_mgr::acquire_writer_with_attached(&self.db, self.attached).await?; + sqlx_sqlite_conn_mgr::acquire_writer_with_attached(self.db.inner(), self.attached) + .await?; let mut q = sqlx::query(&self.query); for value in self.values { @@ -227,7 +224,7 @@ impl IntoFuture for ExecuteBuilder { } /// Helper to decode SQLite rows to JSON -fn decode_rows( +pub(crate) fn decode_rows( rows: Vec, ) -> Result>, Error> { use sqlx::{Column, Row}; diff --git a/src/decode.rs b/crates/sqlx-sqlite-toolkit/src/decode.rs similarity index 100% rename from src/decode.rs rename to crates/sqlx-sqlite-toolkit/src/decode.rs diff --git a/crates/sqlx-sqlite-toolkit/src/error.rs b/crates/sqlx-sqlite-toolkit/src/error.rs new file mode 100644 index 0000000..b3a784a --- /dev/null +++ b/crates/sqlx-sqlite-toolkit/src/error.rs @@ -0,0 +1,167 @@ +/// Result type alias for toolkit operations. +pub type Result = std::result::Result; + +/// Error types for SQLite toolkit operations. +/// +/// These are pure database-operation errors with no Tauri dependencies. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error from SQLx operations. + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + + /// Error from the connection manager. + #[error(transparent)] + ConnectionManager(#[from] sqlx_sqlite_conn_mgr::Error), + + /// SQLite type that cannot be mapped to JSON. + #[error("unsupported datatype: {0}")] + UnsupportedDatatype(String), + + /// Multiple rows returned from fetchOne query. + #[error("fetchOne() query returned {0} rows, expected 0 or 1")] + MultipleRowsReturned(usize), + + /// Transaction failed and rollback also failed. + #[error("transaction failed: {transaction_error}; rollback also failed: {rollback_error}")] + TransactionRollbackFailed { + transaction_error: String, + rollback_error: String, + }, + + /// Transaction has already been committed or rolled back. + #[error("transaction has already been finalized (committed or rolled back)")] + TransactionAlreadyFinalized, + + /// Transaction already active for this database. + #[error("transaction already active for database: {0}")] + TransactionAlreadyActive(String), + + /// No active transaction for this database. + #[error("no active transaction for database: {0}")] + NoActiveTransaction(String), + + /// Invalid transaction token provided. + #[error("invalid transaction token")] + InvalidTransactionToken, + + /// Error from the observer (change notifications). + #[cfg(feature = "observer")] + #[error(transparent)] + Observer(#[from] sqlx_sqlite_observer::Error), + + /// I/O error when accessing database files. + #[error("io error: {0}")] + Io(#[from] std::io::Error), + + /// Generic error for operations that don't fit other categories. + #[error("{0}")] + Other(String), +} + +impl Error { + /// Extract a structured error code from the error type. + /// + /// This provides machine-readable error codes for error handling. + pub fn error_code(&self) -> String { + match self { + Error::Sqlx(e) => { + if let Some(code) = e.as_database_error().and_then(|db_err| db_err.code()) { + return format!("SQLITE_{}", code); + } + "SQLX_ERROR".to_string() + } + Error::ConnectionManager(_) => "CONNECTION_ERROR".to_string(), + Error::UnsupportedDatatype(_) => "UNSUPPORTED_DATATYPE".to_string(), + Error::MultipleRowsReturned(_) => "MULTIPLE_ROWS_RETURNED".to_string(), + Error::TransactionRollbackFailed { .. } => "TRANSACTION_ROLLBACK_FAILED".to_string(), + Error::TransactionAlreadyFinalized => "TRANSACTION_ALREADY_FINALIZED".to_string(), + Error::TransactionAlreadyActive(_) => "TRANSACTION_ALREADY_ACTIVE".to_string(), + Error::NoActiveTransaction(_) => "NO_ACTIVE_TRANSACTION".to_string(), + Error::InvalidTransactionToken => "INVALID_TRANSACTION_TOKEN".to_string(), + #[cfg(feature = "observer")] + Error::Observer(_) => "OBSERVER_ERROR".to_string(), + Error::Io(_) => "IO_ERROR".to_string(), + Error::Other(_) => "ERROR".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_code_unsupported_datatype() { + let err = Error::UnsupportedDatatype("WEIRD".into()); + assert_eq!(err.error_code(), "UNSUPPORTED_DATATYPE"); + } + + #[test] + fn test_error_code_multiple_rows_returned() { + let err = Error::MultipleRowsReturned(5); + assert_eq!(err.error_code(), "MULTIPLE_ROWS_RETURNED"); + assert!(err.to_string().contains("5 rows")); + } + + #[test] + fn test_error_code_transaction_rollback_failed() { + let err = Error::TransactionRollbackFailed { + transaction_error: "constraint".into(), + rollback_error: "busy".into(), + }; + assert_eq!(err.error_code(), "TRANSACTION_ROLLBACK_FAILED"); + assert!(err.to_string().contains("constraint")); + assert!(err.to_string().contains("busy")); + } + + #[test] + fn test_error_code_transaction_already_finalized() { + assert_eq!( + Error::TransactionAlreadyFinalized.error_code(), + "TRANSACTION_ALREADY_FINALIZED" + ); + } + + #[test] + fn test_error_code_transaction_already_active() { + let err = Error::TransactionAlreadyActive("main.db".into()); + assert_eq!(err.error_code(), "TRANSACTION_ALREADY_ACTIVE"); + assert!(err.to_string().contains("main.db")); + } + + #[test] + fn test_error_code_no_active_transaction() { + let err = Error::NoActiveTransaction("test.db".into()); + assert_eq!(err.error_code(), "NO_ACTIVE_TRANSACTION"); + assert!(err.to_string().contains("test.db")); + } + + #[test] + fn test_error_code_invalid_transaction_token() { + assert_eq!( + Error::InvalidTransactionToken.error_code(), + "INVALID_TRANSACTION_TOKEN" + ); + } + + #[test] + fn test_error_code_io() { + let err = Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing")); + assert_eq!(err.error_code(), "IO_ERROR"); + } + + #[test] + fn test_error_code_other() { + let err = Error::Other("something went wrong".into()); + assert_eq!(err.error_code(), "ERROR"); + assert_eq!(err.to_string(), "something went wrong"); + } + + #[test] + fn test_error_code_sqlx_non_database() { + // RowNotFound is not a database error, so no SQLite code + let err = Error::Sqlx(sqlx::Error::RowNotFound); + assert_eq!(err.error_code(), "SQLX_ERROR"); + } +} diff --git a/crates/sqlx-sqlite-toolkit/src/lib.rs b/crates/sqlx-sqlite-toolkit/src/lib.rs new file mode 100644 index 0000000..41ddcc3 --- /dev/null +++ b/crates/sqlx-sqlite-toolkit/src/lib.rs @@ -0,0 +1,57 @@ +//! High-level SQLite toolkit providing builders, transactions, and type decoding. +//! +//! This crate sits between the low-level connection manager (`sqlx-sqlite-conn-mgr`) +//! and application-level code (e.g., a Tauri plugin). It provides: +//! +//! - [`DatabaseWrapper`] — main entry point wrapping a connection-managed database +//! - Builder-pattern APIs for queries ([`ExecuteBuilder`], [`FetchAllBuilder`], [`FetchOneBuilder`]) +//! - Transaction support ([`TransactionExecutionBuilder`], [`InterruptibleTransactionBuilder`]) +//! - JSON type decoding for SQLite values +//! +//! # Example +//! +//! ```no_run +//! use sqlx_sqlite_toolkit::DatabaseWrapper; +//! use serde_json::json; +//! +//! # async fn example() -> Result<(), Box> { +//! let db = DatabaseWrapper::connect(std::path::Path::new("mydb.db"), None).await?; +//! +//! // Write +//! db.execute("INSERT INTO users (name) VALUES (?)".into(), vec![json!("Alice")]).await?; +//! +//! // Read +//! let rows = db.fetch_all("SELECT * FROM users".into(), vec![]).await?; +//! +//! // Transaction +//! let results = db.execute_transaction(vec![ +//! ("INSERT INTO users (name) VALUES (?)", vec![json!("Bob")]), +//! ("INSERT INTO users (name) VALUES (?)", vec![json!("Charlie")]), +//! ]).await?; +//! +//! db.close().await?; +//! # Ok(()) +//! # } +//! ``` + +pub mod builders; +pub mod decode; +pub mod error; +pub mod transactions; +pub mod wrapper; + +pub use builders::{ExecuteBuilder, FetchAllBuilder, FetchOneBuilder}; +pub use error::{Error, Result}; +pub use transactions::{ + ActiveInterruptibleTransaction, ActiveInterruptibleTransactions, ActiveRegularTransactions, + Statement, TransactionWriter, cleanup_all_transactions, +}; +pub use wrapper::{ + DatabaseWrapper, InterruptibleTransaction, InterruptibleTransactionBuilder, + TransactionExecutionBuilder, WriteQueryResult, WriterGuard, bind_value, +}; + +// Re-export commonly used types from dependencies +pub use sqlx_sqlite_conn_mgr::{ + AttachedMode, AttachedSpec, Migrator, SqliteDatabase, SqliteDatabaseConfig, +}; diff --git a/src/transactions.rs b/crates/sqlx-sqlite-toolkit/src/transactions.rs similarity index 81% rename from src/transactions.rs rename to crates/sqlx-sqlite-toolkit/src/transactions.rs index c43fe26..438eb92 100644 --- a/src/transactions.rs +++ b/crates/sqlx-sqlite-toolkit/src/transactions.rs @@ -8,61 +8,72 @@ use serde::Deserialize; use serde_json::Value as JsonValue; use sqlx::{Column, Row}; use sqlx_sqlite_conn_mgr::{AttachedWriteGuard, WriteGuard}; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use tokio::task::AbortHandle; use tracing::debug; +#[cfg(feature = "observer")] +use sqlx_sqlite_observer::ObservableWriteGuard; + +use crate::wrapper::WriterGuard; use crate::{Error, Result, WriteQueryResult}; -/// Wrapper around WriteGuard or AttachedWriteGuard to unify transaction handling +/// Wrapper around WriteGuard, ObservableWriteGuard, or AttachedWriteGuard +/// to unify transaction handling. pub enum TransactionWriter { Regular(WriteGuard), Attached(AttachedWriteGuard), + #[cfg(feature = "observer")] + Observable(ObservableWriteGuard), } impl TransactionWriter { /// Execute a query on either writer type - pub(crate) async fn execute_query<'a>( + pub async fn execute_query<'a>( &mut self, query: sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>>, ) -> Result { match self { Self::Regular(w) => query.execute(&mut **w).await.map_err(Into::into), Self::Attached(w) => query.execute(&mut **w).await.map_err(Into::into), + #[cfg(feature = "observer")] + Self::Observable(w) => query.execute(&mut **w).await.map_err(Into::into), } } /// Fetch all rows from either writer type - pub(crate) async fn fetch_all<'a>( + pub async fn fetch_all<'a>( &mut self, query: sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>>, ) -> Result> { match self { Self::Regular(w) => query.fetch_all(&mut **w).await.map_err(Into::into), Self::Attached(w) => query.fetch_all(&mut **w).await.map_err(Into::into), + #[cfg(feature = "observer")] + Self::Observable(w) => query.fetch_all(&mut **w).await.map_err(Into::into), } } /// Begin an immediate transaction - pub(crate) async fn begin_immediate(&mut self) -> Result<()> { + pub async fn begin_immediate(&mut self) -> Result<()> { self.execute_query(sqlx::query("BEGIN IMMEDIATE")).await?; Ok(()) } /// Commit the current transaction - pub(crate) async fn commit(&mut self) -> Result<()> { + pub async fn commit(&mut self) -> Result<()> { self.execute_query(sqlx::query("COMMIT")).await?; Ok(()) } /// Rollback the current transaction - pub(crate) async fn rollback(&mut self) -> Result<()> { + pub async fn rollback(&mut self) -> Result<()> { self.execute_query(sqlx::query("ROLLBACK")).await?; Ok(()) } /// Detach all attached databases if this is an attached writer - pub(crate) async fn detach_if_attached(self) -> Result<()> { + pub async fn detach_if_attached(self) -> Result<()> { if let Self::Attached(w) = self { w.detach_all().await?; } @@ -70,6 +81,16 @@ impl TransactionWriter { } } +impl From for TransactionWriter { + fn from(guard: WriterGuard) -> Self { + match guard { + WriterGuard::Regular(w) => TransactionWriter::Regular(w), + #[cfg(feature = "observer")] + WriterGuard::Observable(w) => TransactionWriter::Observable(w), + } + } +} + /// Active transaction state holding the writer and metadata #[must_use = "if unused, the transaction is immediately rolled back"] pub struct ActiveInterruptibleTransaction { @@ -106,13 +127,6 @@ impl ActiveInterruptibleTransaction { &self.transaction_id } - pub fn validate_token(&self, token_id: &str) -> Result<()> { - if self.transaction_id != token_id { - return Err(Error::InvalidTransactionToken); - } - Ok(()) - } - /// Execute a read query within this transaction and return decoded results pub async fn read( &mut self, @@ -227,18 +241,23 @@ impl Drop for ActiveInterruptibleTransaction { } } -/// Global state tracking all active interruptible transactions +/// Global state tracking all active interruptible transactions. +/// +/// Enforces one interruptible transaction per database path. +/// Uses `Mutex` rather than `RwLock` because all operations require write access, +/// and `Mutex` only requires `T: Send` (not `T: Sync`) — avoiding an +/// `unsafe impl Sync` that would otherwise be needed due to non-`Sync` inner +/// types (`PoolConnection`, raw pointers in observer guards). #[derive(Clone, Default)] pub struct ActiveInterruptibleTransactions( - Arc>>, + Arc>>, ); impl ActiveInterruptibleTransactions { pub async fn insert(&self, db_path: String, tx: ActiveInterruptibleTransaction) -> Result<()> { use std::collections::hash_map::Entry; - let mut txs = self.0.write().await; + let mut txs = self.0.lock().await; - // Ensure only one transaction per database using Entry API match txs.entry(db_path.clone()) { Entry::Vacant(e) => { e.insert(tx); @@ -249,7 +268,7 @@ impl ActiveInterruptibleTransactions { } pub async fn abort_all(&self) { - let mut txs = self.0.write().await; + let mut txs = self.0.lock().await; debug!("Aborting {} active interruptible transaction(s)", txs.len()); for db_path in txs.keys() { @@ -270,22 +289,25 @@ impl ActiveInterruptibleTransactions { db_path: &str, token_id: &str, ) -> Result { - let mut txs = self.0.write().await; + let mut txs = self.0.lock().await; // Validate token before removal let tx = txs .get(db_path) .ok_or_else(|| Error::NoActiveTransaction(db_path.to_string()))?; - tx.validate_token(token_id)?; + if tx.transaction_id() != token_id { + return Err(Error::InvalidTransactionToken); + } // Safe unwrap: we just confirmed the key exists above Ok(txs.remove(db_path).unwrap()) } } -/// Tracking for regular (non-pausable) transactions that are in-flight -/// This allows us to abort them on app exit +/// Tracking for regular (non-pausable) transactions that are in-flight. +/// +/// Holds abort handles so transactions can be cancelled on app exit. #[derive(Clone, Default)] pub struct ActiveRegularTransactions(Arc>>); @@ -309,24 +331,19 @@ impl ActiveRegularTransactions { abort_handle.abort(); } - // Clear all tracked transactions to prevent memory leak txs.clear(); } } -/// Cleanup all transactions on app exit +/// Cleanup all transactions on app exit. pub async fn cleanup_all_transactions( interruptible: &ActiveInterruptibleTransactions, regular: &ActiveRegularTransactions, ) { debug!("Cleaning up all active transactions"); - // Abort all transaction tasks interruptible.abort_all().await; regular.abort_all().await; - // Interruptible transactions will auto-rollback when dropped - // Regular transactions will also auto-rollback when aborted task cleans up - debug!("Transaction cleanup initiated"); } diff --git a/src/wrapper.rs b/crates/sqlx-sqlite-toolkit/src/wrapper.rs similarity index 62% rename from src/wrapper.rs rename to crates/sqlx-sqlite-toolkit/src/wrapper.rs index ab5cea7..14a1e67 100644 --- a/src/wrapper.rs +++ b/crates/sqlx-sqlite-toolkit/src/wrapper.rs @@ -1,11 +1,13 @@ -use std::fs::create_dir_all; -use std::path::PathBuf; +use std::ops::{Deref, DerefMut}; use std::sync::Arc; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; -use sqlx_sqlite_conn_mgr::{SqliteDatabase, SqliteDatabaseConfig}; -use tauri::{AppHandle, Manager, Runtime}; +use sqlx::sqlite::SqliteConnection; +use sqlx_sqlite_conn_mgr::{SqliteDatabase, SqliteDatabaseConfig, WriteGuard}; + +#[cfg(feature = "observer")] +use sqlx_sqlite_observer::{ObservableSqliteDatabase, ObservableWriteGuard, ObserverConfig}; use crate::Error; @@ -21,10 +23,52 @@ pub struct WriteQueryResult { pub last_insert_id: i64, } -/// Wrapper around SqliteDatabase that adapts it for the plugin interface +/// Unified writer guard that routes through observer when enabled. +/// +/// Derefs to `SqliteConnection` so it can be used with `sqlx::query().execute()`. +pub enum WriterGuard { + /// Regular writer from the connection manager. + Regular(WriteGuard), + /// Observable writer that tracks changes via SQLite hooks. + #[cfg(feature = "observer")] + Observable(ObservableWriteGuard), +} + +impl Deref for WriterGuard { + type Target = SqliteConnection; + + fn deref(&self) -> &Self::Target { + match self { + WriterGuard::Regular(w) => w, + #[cfg(feature = "observer")] + WriterGuard::Observable(w) => w, + } + } +} + +impl DerefMut for WriterGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + WriterGuard::Regular(w) => &mut *w, + #[cfg(feature = "observer")] + WriterGuard::Observable(w) => &mut *w, + } + } +} + +/// Wrapper around SqliteDatabase that provides a high-level API for database operations. +/// +/// This struct is the main entry point for interacting with SQLite databases through +/// the toolkit. It wraps the connection manager's `SqliteDatabase` and provides +/// builder-pattern APIs for queries, transactions, and write operations. +/// +/// When the `observer` feature is enabled, the wrapper can also manage an +/// `ObservableSqliteDatabase` for change notification support. #[derive(Clone)] pub struct DatabaseWrapper { inner: Arc, + #[cfg(feature = "observer")] + observer: Option, } impl DatabaseWrapper { @@ -32,24 +76,6 @@ impl DatabaseWrapper { /// /// This is useful when you need to create `AttachedSpec` instances for cross-database /// operations with interruptible transactions. - /// - /// # Example - /// - /// ```no_run - /// # use tauri_plugin_sqlite::{DatabaseWrapper, AttachedSpec, AttachedMode}; - /// # use std::sync::Arc; - /// # async fn example() -> Result<(), Box> { - /// # let db1: DatabaseWrapper = todo!(); - /// # let db2: DatabaseWrapper = todo!(); - /// // Create an attached spec using the inner database reference - /// let spec = AttachedSpec { - /// database: Arc::clone(db2.inner()), - /// schema_name: "other".to_string(), - /// mode: AttachedMode::ReadOnly, - /// }; - /// # Ok(()) - /// # } - /// ``` pub fn inner(&self) -> &Arc { &self.inner } @@ -59,39 +85,48 @@ impl DatabaseWrapper { &self.inner } - /// Acquire writer connection (for pausable transactions) - pub async fn acquire_writer(&self) -> Result { + /// Acquire a writer guard. + /// + /// When observation is enabled, returns an observable writer that tracks + /// changes via SQLite hooks. Otherwise, returns a regular writer. + pub async fn acquire_writer(&self) -> Result { + #[cfg(feature = "observer")] + if let Some(ref observable) = self.observer { + let writer = observable.acquire_writer().await.map_err(Error::Observer)?; + return Ok(WriterGuard::Observable(writer)); + } + + Ok(WriterGuard::Regular(self.inner.acquire_writer().await?)) + } + + /// Acquire a regular (non-observable) writer connection. + /// + /// This always bypasses the observer, even when observation is enabled. + /// Useful when you need a writer for operations that should not trigger + /// change notifications (e.g., internal bookkeeping). + pub async fn acquire_regular_writer(&self) -> Result { Ok(self.inner.acquire_writer().await?) } - /// Begin an interruptible transaction that can be paused and resumed + /// Begin an interruptible transaction that can be paused and resumed. /// /// Returns a builder that allows attaching databases before executing the transaction. + /// Unlike `execute_transaction()`, this allows reading uncommitted data mid-transaction. /// - /// # Example + /// # Examples /// /// ```no_run - /// # use tauri_plugin_sqlite::{DatabaseWrapper, Statement}; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// // Start transaction with initial statements - /// let mut tx = db - /// .begin_interruptible_transaction() - /// .execute(vec![ - /// ("DELETE FROM cache WHERE expired = 1", vec![]) - /// ]) - /// .await?; - /// - /// // Continue with more work - /// let results = tx.continue_with(vec![ - /// Statement { - /// query: "INSERT INTO items (name) VALUES (?)".to_string(), - /// values: vec![json!("item1")], - /// } - /// ]).await?; - /// - /// // Commit when done + /// # async fn example(db: &sqlx_sqlite_toolkit::DatabaseWrapper) -> Result<(), sqlx_sqlite_toolkit::Error> { + /// use serde_json::json; + /// + /// let mut tx = db.begin_interruptible_transaction() + /// .execute(vec![ + /// ("INSERT INTO users (name) VALUES (?)", vec![json!("Alice")]), + /// ]).await?; + /// + /// // Read uncommitted data within the transaction + /// let rows = tx.read("SELECT count(*) as n FROM users".into(), vec![]).await?; + /// /// tx.commit().await?; /// # Ok(()) /// # } @@ -100,66 +135,63 @@ impl DatabaseWrapper { InterruptibleTransactionBuilder::new(self.clone()) } - /// Connect to a SQLite database via the connection manager - pub async fn connect( - path: &str, - app: &AppHandle, - custom_config: Option, - ) -> Result { - // Resolve path relative to app_config_dir - let abs_path = resolve_database_path(path, app)?; - - Self::connect_with_path(&abs_path, custom_config).await - } - /// Connect to a SQLite database with an absolute path. /// - /// This is the core connection method used by `connect()`. It's also - /// used by the migration task during plugin setup. + /// This is the core connection method. It connects to the database at the given + /// absolute path with optional configuration. /// /// Note: `SqliteDatabase::connect()` caches instances in a global registry. /// Multiple calls with the same path return the same underlying database, /// so this wrapper is lightweight - the actual connection pools are shared. - pub async fn connect_with_path( + /// + /// # Examples + /// + /// ```no_run + /// # async fn example() -> Result<(), sqlx_sqlite_toolkit::Error> { + /// use sqlx_sqlite_toolkit::DatabaseWrapper; + /// use std::path::Path; + /// + /// let db = DatabaseWrapper::connect(Path::new("/tmp/my.db"), None).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn connect( abs_path: &std::path::Path, custom_config: Option, ) -> Result { - // Use connection manager to connect with optional custom config let db = SqliteDatabase::connect(abs_path, custom_config).await?; - Ok(Self { inner: db }) + Ok(Self { + inner: db, + #[cfg(feature = "observer")] + observer: None, + }) } - /// Create a builder for write queries (INSERT/UPDATE/DELETE) + /// Create a builder for write queries (INSERT/UPDATE/DELETE). /// /// Returns a builder that can optionally attach databases before executing. /// - /// # Example + /// # Examples /// /// ```no_run - /// # use tauri_plugin_sqlite::DatabaseWrapper; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// # let sql = "INSERT INTO users (name) VALUES (?)"; - /// # let params = vec![json!("Alice")]; - /// // Without attached databases - /// db.execute(sql.to_string(), params.clone()).await?; - /// - /// // With attached database(s) - /// # let spec1 = todo!(); - /// # let spec2 = todo!(); - /// db.execute(sql.to_string(), params) - /// .attach(vec![spec1, spec2]) - /// .await?; + /// # async fn example(db: &sqlx_sqlite_toolkit::DatabaseWrapper) -> Result<(), sqlx_sqlite_toolkit::Error> { + /// use serde_json::json; + /// + /// let result = db.execute( + /// "INSERT INTO users (name, age) VALUES (?, ?)".into(), + /// vec![json!("Alice"), json!(30)], + /// ).execute().await?; + /// + /// println!("Inserted row {}", result.last_insert_id); /// # Ok(()) /// # } /// ``` pub fn execute(&self, query: String, values: Vec) -> crate::builders::ExecuteBuilder { - crate::builders::ExecuteBuilder::new(Arc::clone(&self.inner), query, values) + crate::builders::ExecuteBuilder::new(self.clone(), query, values) } - /// Execute multiple statements atomically within a transaction + /// Execute multiple statements atomically within a transaction. /// /// Returns a builder that allows attaching databases before executing the transaction. /// All statements either succeed together or fail together. @@ -167,22 +199,18 @@ impl DatabaseWrapper { /// Use this when you have a batch of writes and don't need to read data mid-transaction. /// For transactions requiring reads of uncommitted data, use `begin_interruptible_transaction()`. /// - /// # Example + /// # Examples /// /// ```no_run - /// # use tauri_plugin_sqlite::DatabaseWrapper; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// // Execute multiple inserts atomically - /// let results = db - /// .execute_transaction(vec![ - /// ("INSERT INTO users (name) VALUES (?)", vec![json!("Alice")]), - /// ("INSERT INTO audit_log (action) VALUES (?)", vec![json!("user_created")]) - /// ]) - /// .await?; - /// - /// println!("User ID: {}", results[0].last_insert_id); + /// # async fn example(db: &sqlx_sqlite_toolkit::DatabaseWrapper) -> Result<(), sqlx_sqlite_toolkit::Error> { + /// use serde_json::json; + /// + /// let results = db.execute_transaction(vec![ + /// ("INSERT INTO users (name) VALUES (?)", vec![json!("Alice")]), + /// ("INSERT INTO users (name) VALUES (?)", vec![json!("Bob")]), + /// ]).execute().await?; + /// + /// println!("Inserted {} rows total", results.len()); /// # Ok(()) /// # } /// ``` @@ -193,28 +221,22 @@ impl DatabaseWrapper { TransactionExecutionBuilder::new(self.clone(), statements) } - /// Create a builder for SELECT queries returning multiple rows + /// Create a builder for SELECT queries returning multiple rows. /// /// Returns a builder that can optionally attach databases before executing. /// - /// # Example + /// # Examples /// /// ```no_run - /// # use tauri_plugin_sqlite::DatabaseWrapper; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// # let sql = "SELECT * FROM users"; - /// # let params = vec![]; - /// // Without attached databases - /// db.fetch_all(sql.to_string(), params.clone()).await?; - /// - /// // With attached database(s) - /// # let spec1 = todo!(); - /// # let spec2 = todo!(); - /// db.fetch_all(sql.to_string(), params) - /// .attach(vec![spec1, spec2]) - /// .await?; + /// # async fn example(db: &sqlx_sqlite_toolkit::DatabaseWrapper) -> Result<(), sqlx_sqlite_toolkit::Error> { + /// let rows = db.fetch_all( + /// "SELECT name, age FROM users WHERE age > ?".into(), + /// vec![serde_json::json!(21)], + /// ).execute().await?; + /// + /// for row in &rows { + /// println!("{}: {}", row["name"], row["age"]); + /// } /// # Ok(()) /// # } /// ``` @@ -226,30 +248,24 @@ impl DatabaseWrapper { crate::builders::FetchAllBuilder::new(Arc::clone(&self.inner), query, values) } - /// Create a builder for SELECT queries returning zero or one row + /// Create a builder for SELECT queries returning zero or one row. /// /// Returns a builder that can optionally attach databases before executing. - /// /// Returns an error if the query returns more than one row. /// - /// # Example + /// # Examples /// /// ```no_run - /// # use tauri_plugin_sqlite::DatabaseWrapper; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// # let sql = "SELECT * FROM users WHERE id = ?"; - /// # let params = vec![json!(1)]; - /// // Without attached databases - /// db.fetch_one(sql.to_string(), params.clone()).await?; - /// - /// // With attached database(s) - /// # let spec1 = todo!(); - /// # let spec2 = todo!(); - /// db.fetch_one(sql.to_string(), params) - /// .attach(vec![spec1, spec2]) - /// .await?; + /// # async fn example(db: &sqlx_sqlite_toolkit::DatabaseWrapper) -> Result<(), sqlx_sqlite_toolkit::Error> { + /// let user = db.fetch_one( + /// "SELECT name FROM users WHERE id = ?".into(), + /// vec![serde_json::json!(1)], + /// ).execute().await?; + /// + /// match user { + /// Some(row) => println!("Found: {}", row["name"]), + /// None => println!("Not found"), + /// } /// # Ok(()) /// # } /// ``` @@ -273,19 +289,64 @@ impl DatabaseWrapper { Ok(()) } - /// Close the database connection + /// Close the database connection. + /// + /// Checkpoints the WAL and closes all connection pools. pub async fn close(self) -> Result<(), Error> { - // Close via Arc (handles both owned and shared cases) self.inner.close().await?; Ok(()) } - /// Close the database connection and remove all database files + /// Close the database connection and remove all database files. + /// + /// Removes the main database file, WAL, and SHM files. pub async fn remove(self) -> Result<(), Error> { - // Remove via Arc (handles both owned and shared cases) self.inner.remove().await?; Ok(()) } + + /// Enable observation on this database for the specified tables. + /// + /// After calling this, write operations will be tracked and subscribers + /// can receive change notifications. + /// + /// Requires the `observer` feature. + #[cfg(feature = "observer")] + pub fn enable_observation(&mut self, config: ObserverConfig) { + self.observer = Some(ObservableSqliteDatabase::new( + Arc::clone(&self.inner), + config, + )); + } + + /// Disable observation on this database. + /// + /// Drops the observable wrapper and stops tracking changes. + /// Existing subscribers will stop receiving notifications. + /// + /// Requires the `observer` feature. + #[cfg(feature = "observer")] + pub fn disable_observation(&mut self) { + self.observer = None; + } + + /// Get a reference to the observable database, if observation is enabled. + /// + /// Returns `None` if observation has not been enabled via `enable_observation()`. + /// + /// Requires the `observer` feature. + #[cfg(feature = "observer")] + pub fn observable(&self) -> Option<&ObservableSqliteDatabase> { + self.observer.as_ref() + } + + /// Returns true if observation is currently enabled on this database. + /// + /// Requires the `observer` feature. + #[cfg(feature = "observer")] + pub fn is_observing(&self) -> bool { + self.observer.is_some() + } } /// Builder for interruptible transactions with optional attached databases @@ -303,32 +364,6 @@ impl InterruptibleTransactionBuilder { } /// Attach databases for cross-database operations - /// - /// # Example - /// - /// ```no_run - /// # use tauri_plugin_sqlite::DatabaseWrapper; - /// # use sqlx_sqlite_conn_mgr::{AttachedSpec, AttachedMode}; - /// # use serde_json::json; - /// # async fn example() -> Result<(), Box> { - /// # let db: DatabaseWrapper = todo!(); - /// # let archive_db: std::sync::Arc = todo!(); - /// let mut tx = db - /// .begin_interruptible_transaction() - /// .attach(vec![AttachedSpec { - /// database: archive_db, - /// schema_name: "archive".to_string(), - /// mode: AttachedMode::ReadOnly, - /// }]) - /// .execute(vec![ - /// ("INSERT INTO items SELECT * FROM archive.items", vec![]) - /// ]) - /// .await?; - /// - /// tx.commit().await?; - /// # Ok(()) - /// # } - /// ``` pub fn attach(mut self, specs: Vec) -> Self { self.attached = specs; self @@ -345,7 +380,8 @@ impl InterruptibleTransactionBuilder { // Acquire appropriate writer based on whether databases are attached let mut writer = if self.attached.is_empty() { - TransactionWriter::Regular(self.db.acquire_writer().await?) + let guard = self.db.acquire_writer().await?; + TransactionWriter::from(guard) } else { let guard = sqlx_sqlite_conn_mgr::acquire_writer_with_attached(self.db.inner(), self.attached) @@ -449,7 +485,8 @@ impl TransactionExecutionBuilder { // Acquire appropriate writer based on whether databases are attached let mut writer = if self.attached.is_empty() { - TransactionWriter::Regular(self.db.acquire_writer().await?) + let guard = self.db.acquire_writer().await?; + TransactionWriter::from(guard) } else { let guard = sqlx_sqlite_conn_mgr::acquire_writer_with_attached(self.db.inner(), self.attached) @@ -506,7 +543,7 @@ impl std::future::IntoFuture for TransactionExecutionBuilder { } /// Helper function to bind a JSON value to a SQLx query -pub(crate) fn bind_value<'a>( +pub fn bind_value<'a>( query: sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>>, value: JsonValue, ) -> sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>> { @@ -534,19 +571,3 @@ pub(crate) fn bind_value<'a>( query.bind(value) } } - -/// Resolve database file path relative to app config directory. -/// -/// Paths are joined to `app_config_dir()` (e.g., `Library/Application Support/${bundleIdentifier}` on iOS). -/// Special paths like `:memory:` are passed through unchanged. -fn resolve_database_path(path: &str, app: &AppHandle) -> Result { - let app_path = app - .path() - .app_config_dir() - .expect("No App config path was found!"); - - create_dir_all(&app_path).expect("Couldn't create app config dir"); - - // Join the relative path to the app config directory - Ok(app_path.join(path)) -} diff --git a/tests/interruptible_transaction_tests.rs b/crates/sqlx-sqlite-toolkit/tests/interruptible_transaction_tests.rs similarity index 98% rename from tests/interruptible_transaction_tests.rs rename to crates/sqlx-sqlite-toolkit/tests/interruptible_transaction_tests.rs index c54ed99..f705902 100644 --- a/tests/interruptible_transaction_tests.rs +++ b/crates/sqlx-sqlite-toolkit/tests/interruptible_transaction_tests.rs @@ -1,11 +1,11 @@ use serde_json::json; -use tauri_plugin_sqlite::{DatabaseWrapper, Statement}; +use sqlx_sqlite_toolkit::{DatabaseWrapper, Statement}; use tempfile::TempDir; async fn create_test_db(name: &str) -> (DatabaseWrapper, TempDir) { let temp_dir = TempDir::new().expect("Failed to create temp directory"); let db_path = temp_dir.path().join(name); - let wrapper = DatabaseWrapper::connect_with_path(&db_path, None) + let wrapper = DatabaseWrapper::connect(&db_path, None) .await .expect("Failed to connect to test database"); diff --git a/crates/sqlx-sqlite-toolkit/tests/transaction_state_tests.rs b/crates/sqlx-sqlite-toolkit/tests/transaction_state_tests.rs new file mode 100644 index 0000000..ba5ba00 --- /dev/null +++ b/crates/sqlx-sqlite-toolkit/tests/transaction_state_tests.rs @@ -0,0 +1,294 @@ +//! Tests for transaction state management types. + +use serde_json::json; +use sqlx_sqlite_toolkit::{ + ActiveInterruptibleTransactions, ActiveRegularTransactions, DatabaseWrapper, Error, + cleanup_all_transactions, +}; +use tempfile::TempDir; + +/// Helper to extract Err from Result +/// since ActiveInterruptibleTransaction doesn't implement Debug. +fn expect_err( + result: std::result::Result, +) -> Error { + match result { + Err(e) => e, + Ok(_) => panic!("expected Err, got Ok"), + } +} + +async fn create_test_db(name: &str) -> (DatabaseWrapper, TempDir) { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let db_path = temp_dir.path().join(name); + let wrapper = DatabaseWrapper::connect(&db_path, None) + .await + .expect("Failed to connect to test database"); + + (wrapper, temp_dir) +} + +/// Helper to create a real ActiveInterruptibleTransaction by starting +/// an actual database transaction (the type requires a real writer). +async fn begin_transaction( + db: &DatabaseWrapper, + db_path: &str, +) -> sqlx_sqlite_toolkit::ActiveInterruptibleTransaction { + use sqlx_sqlite_toolkit::TransactionWriter; + + let guard = db.acquire_writer().await.unwrap(); + let mut writer = TransactionWriter::from(guard); + writer.begin_immediate().await.unwrap(); + + sqlx_sqlite_toolkit::ActiveInterruptibleTransaction::new( + db_path.to_string(), + uuid::Uuid::new_v4().to_string(), + writer, + ) +} + +// ============================================================================ +// ActiveInterruptibleTransactions tests +// ============================================================================ + +#[tokio::test] +async fn test_insert_and_remove() { + let (db, _temp) = create_test_db("test.db").await; + + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + + let state = ActiveInterruptibleTransactions::default(); + let tx = begin_transaction(&db, "test.db").await; + let tx_id = tx.transaction_id().to_string(); + + state.insert("test.db".into(), tx).await.unwrap(); + + let removed = state.remove("test.db", &tx_id).await.unwrap(); + assert_eq!(removed.db_path(), "test.db"); + assert_eq!(removed.transaction_id(), tx_id); +} + +#[tokio::test] +async fn test_insert_duplicate_rejected() { + // Use two separate databases so both can acquire writers independently, + // but insert them under the same key to test duplicate rejection. + let (db1, _temp1) = create_test_db("dup1.db").await; + let (db2, _temp2) = create_test_db("dup2.db").await; + + for db in [&db1, &db2] { + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + } + + let state = ActiveInterruptibleTransactions::default(); + + let tx1 = begin_transaction(&db1, "shared-key").await; + state.insert("shared-key".into(), tx1).await.unwrap(); + + // Second insert for same key should fail + let tx2 = begin_transaction(&db2, "shared-key").await; + let err = state.insert("shared-key".into(), tx2).await.unwrap_err(); + assert_eq!(err.error_code(), "TRANSACTION_ALREADY_ACTIVE"); + assert!(err.to_string().contains("shared-key")); +} + +#[tokio::test] +async fn test_remove_nonexistent_db() { + let state = ActiveInterruptibleTransactions::default(); + + let err = expect_err(state.remove("nonexistent.db", "some-token").await); + assert_eq!(err.error_code(), "NO_ACTIVE_TRANSACTION"); + assert!(err.to_string().contains("nonexistent.db")); +} + +#[tokio::test] +async fn test_remove_wrong_token() { + let (db, _temp) = create_test_db("token.db").await; + + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + + let state = ActiveInterruptibleTransactions::default(); + let tx = begin_transaction(&db, "token.db").await; + + state.insert("token.db".into(), tx).await.unwrap(); + + let err = expect_err(state.remove("token.db", "wrong-token-id").await); + assert_eq!(err.error_code(), "INVALID_TRANSACTION_TOKEN"); +} + +#[tokio::test] +async fn test_abort_all_clears_transactions() { + let (db, _temp) = create_test_db("abort.db").await; + + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + + let state = ActiveInterruptibleTransactions::default(); + let tx = begin_transaction(&db, "abort.db").await; + let tx_id = tx.transaction_id().to_string(); + + state.insert("abort.db".into(), tx).await.unwrap(); + state.abort_all().await; + + // After abort_all, remove should fail (transaction was cleared) + let err = expect_err(state.remove("abort.db", &tx_id).await); + assert_eq!(err.error_code(), "NO_ACTIVE_TRANSACTION"); +} + +#[tokio::test] +async fn test_abort_all_auto_rollbacks_uncommitted_writes() { + let (db, _temp) = create_test_db("rollback.db").await; + + db.execute( + "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)".into(), + vec![], + ) + .await + .unwrap(); + + let state = ActiveInterruptibleTransactions::default(); + let mut tx = begin_transaction(&db, "rollback.db").await; + + // Write inside the transaction + tx.continue_with(vec![( + "INSERT INTO t (val) VALUES (?)", + vec![json!("uncommitted")], + )]) + .await + .unwrap(); + + // Store and abort (should auto-rollback on drop) + state.insert("rollback.db".into(), tx).await.unwrap(); + state.abort_all().await; + + // The uncommitted write should not be visible + let rows = db + .fetch_all("SELECT * FROM t".into(), vec![]) + .await + .unwrap(); + + assert!( + rows.is_empty(), + "Aborted transaction writes should be rolled back" + ); +} + +#[tokio::test] +async fn test_insert_after_abort_all_succeeds() { + // Use two separate databases to avoid writer contention during abort/reacquire. + let (db1, _temp1) = create_test_db("reuse1.db").await; + let (db2, _temp2) = create_test_db("reuse2.db").await; + + for db in [&db1, &db2] { + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + } + + let state = ActiveInterruptibleTransactions::default(); + + let tx = begin_transaction(&db1, "reuse-key").await; + state.insert("reuse-key".into(), tx).await.unwrap(); + state.abort_all().await; + + // Should be able to insert again after abort + let tx2 = begin_transaction(&db2, "reuse-key").await; + state.insert("reuse-key".into(), tx2).await.unwrap(); +} + +// ============================================================================ +// ActiveRegularTransactions tests +// ============================================================================ + +#[tokio::test] +async fn test_regular_insert_and_remove() { + let state = ActiveRegularTransactions::default(); + + let handle = tokio::spawn(async { /* no-op */ }); + state.insert("tx-1".into(), handle.abort_handle()).await; + + // Remove should succeed (no panic, no error) + state.remove("tx-1").await; + + // Removing again is a no-op + state.remove("tx-1").await; +} + +#[tokio::test] +async fn test_regular_abort_all_cancels_tasks() { + let state = ActiveRegularTransactions::default(); + + // Spawn a long-running task + let handle = tokio::spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + }); + let abort_handle = handle.abort_handle(); + + state.insert("long-task".into(), abort_handle).await; + state.abort_all().await; + + // The task should have been aborted + let result = handle.await; + assert!(result.unwrap_err().is_cancelled()); +} + +#[tokio::test] +async fn test_regular_abort_all_clears_state() { + let state = ActiveRegularTransactions::default(); + + let h1 = tokio::spawn(async {}); + let h2 = tokio::spawn(async {}); + + state.insert("a".into(), h1.abort_handle()).await; + state.insert("b".into(), h2.abort_handle()).await; + + state.abort_all().await; + + // State should be empty — inserting new keys should work + let h3 = tokio::spawn(async {}); + state.insert("a".into(), h3.abort_handle()).await; +} + +// ============================================================================ +// cleanup_all_transactions tests +// ============================================================================ + +#[tokio::test] +async fn test_cleanup_all_transactions() { + let (db, _temp) = create_test_db("cleanup.db").await; + + db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)".into(), vec![]) + .await + .unwrap(); + + let interruptible = ActiveInterruptibleTransactions::default(); + let regular = ActiveRegularTransactions::default(); + + // Add an interruptible transaction + let tx = begin_transaction(&db, "cleanup.db").await; + interruptible.insert("cleanup.db".into(), tx).await.unwrap(); + + // Add a regular transaction + let handle = tokio::spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + }); + regular + .insert("regular-1".into(), handle.abort_handle()) + .await; + + // Cleanup should clear both + cleanup_all_transactions(&interruptible, ®ular).await; + + // Interruptible should be empty + let err = expect_err(interruptible.remove("cleanup.db", "any").await); + assert_eq!(err.error_code(), "NO_ACTIVE_TRANSACTION"); + + // Regular task should be cancelled + assert!(handle.await.unwrap_err().is_cancelled()); +} diff --git a/tests/wrapper_tests.rs b/crates/sqlx-sqlite-toolkit/tests/wrapper_tests.rs similarity index 98% rename from tests/wrapper_tests.rs rename to crates/sqlx-sqlite-toolkit/tests/wrapper_tests.rs index 8ca76af..4a0bb7d 100644 --- a/tests/wrapper_tests.rs +++ b/crates/sqlx-sqlite-toolkit/tests/wrapper_tests.rs @@ -1,11 +1,11 @@ use serde_json::{Value as JsonValue, json}; -use tauri_plugin_sqlite::DatabaseWrapper; +use sqlx_sqlite_toolkit::DatabaseWrapper; use tempfile::TempDir; async fn create_test_db() -> (DatabaseWrapper, TempDir) { let temp_dir = TempDir::new().expect("Failed to create temp directory"); let db_path = temp_dir.path().join("test.db"); - let wrapper = DatabaseWrapper::connect_with_path(&db_path, None) + let wrapper = DatabaseWrapper::connect(&db_path, None) .await .expect("Failed to connect to test database"); diff --git a/permissions/autogenerated/commands/begin_interruptible_transaction.toml b/permissions/autogenerated/commands/begin_interruptible_transaction.toml new file mode 100644 index 0000000..8a42f87 --- /dev/null +++ b/permissions/autogenerated/commands/begin_interruptible_transaction.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-begin-interruptible-transaction" +description = "Enables the begin_interruptible_transaction command without any pre-configured scope." +commands.allow = ["begin_interruptible_transaction"] + +[[permission]] +identifier = "deny-begin-interruptible-transaction" +description = "Denies the begin_interruptible_transaction command without any pre-configured scope." +commands.deny = ["begin_interruptible_transaction"] diff --git a/permissions/autogenerated/commands/execute_interruptible_transaction.toml b/permissions/autogenerated/commands/execute_interruptible_transaction.toml deleted file mode 100644 index 76be4cd..0000000 --- a/permissions/autogenerated/commands/execute_interruptible_transaction.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-execute-interruptible-transaction" -description = "Enables the execute_interruptible_transaction command without any pre-configured scope." -commands.allow = ["execute_interruptible_transaction"] - -[[permission]] -identifier = "deny-execute-interruptible-transaction" -description = "Denies the execute_interruptible_transaction command without any pre-configured scope." -commands.deny = ["execute_interruptible_transaction"] diff --git a/permissions/autogenerated/commands/get_migration_events.toml b/permissions/autogenerated/commands/get_migration_events.toml new file mode 100644 index 0000000..87572ca --- /dev/null +++ b/permissions/autogenerated/commands/get_migration_events.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-migration-events" +description = "Enables the get_migration_events command without any pre-configured scope." +commands.allow = ["get_migration_events"] + +[[permission]] +identifier = "deny-get-migration-events" +description = "Denies the get_migration_events command without any pre-configured scope." +commands.deny = ["get_migration_events"] diff --git a/permissions/autogenerated/reference.md b/permissions/autogenerated/reference.md index 86dbe97..6707e3e 100644 --- a/permissions/autogenerated/reference.md +++ b/permissions/autogenerated/reference.md @@ -7,7 +7,7 @@ Default permissions for the sqlite plugin - allows all database operations - `allow-load` - `allow-execute` - `allow-execute-transaction` -- `allow-execute-interruptible-transaction` +- `allow-begin-interruptible-transaction` - `allow-transaction-continue` - `allow-transaction-read` - `allow-fetch-all` @@ -28,12 +28,12 @@ Default permissions for the sqlite plugin - allows all database operations -`sqlite:allow-close` +`sqlite:allow-begin-interruptible-transaction` -Enables the close command without any pre-configured scope. +Enables the begin_interruptible_transaction command without any pre-configured scope. @@ -41,12 +41,12 @@ Enables the close command without any pre-configured scope. -`sqlite:deny-close` +`sqlite:deny-begin-interruptible-transaction` -Denies the close command without any pre-configured scope. +Denies the begin_interruptible_transaction command without any pre-configured scope. @@ -54,12 +54,12 @@ Denies the close command without any pre-configured scope. -`sqlite:allow-close-all` +`sqlite:allow-close` -Enables the close_all command without any pre-configured scope. +Enables the close command without any pre-configured scope. @@ -67,12 +67,12 @@ Enables the close_all command without any pre-configured scope. -`sqlite:deny-close-all` +`sqlite:deny-close` -Denies the close_all command without any pre-configured scope. +Denies the close command without any pre-configured scope. @@ -80,12 +80,12 @@ Denies the close_all command without any pre-configured scope. -`sqlite:allow-execute` +`sqlite:allow-close-all` -Enables the execute command without any pre-configured scope. +Enables the close_all command without any pre-configured scope. @@ -93,12 +93,12 @@ Enables the execute command without any pre-configured scope. -`sqlite:deny-execute` +`sqlite:deny-close-all` -Denies the execute command without any pre-configured scope. +Denies the close_all command without any pre-configured scope. @@ -106,12 +106,12 @@ Denies the execute command without any pre-configured scope. -`sqlite:allow-execute-interruptible-transaction` +`sqlite:allow-execute` -Enables the execute_interruptible_transaction command without any pre-configured scope. +Enables the execute command without any pre-configured scope. @@ -119,12 +119,12 @@ Enables the execute_interruptible_transaction command without any pre-configured -`sqlite:deny-execute-interruptible-transaction` +`sqlite:deny-execute` -Denies the execute_interruptible_transaction command without any pre-configured scope. +Denies the execute command without any pre-configured scope. @@ -210,6 +210,32 @@ Denies the fetch_one command without any pre-configured scope. +`sqlite:allow-get-migration-events` + + + + +Enables the get_migration_events command without any pre-configured scope. + + + + + + + +`sqlite:deny-get-migration-events` + + + + +Denies the get_migration_events command without any pre-configured scope. + + + + + + + `sqlite:allow-hello` diff --git a/permissions/default.toml b/permissions/default.toml index 07cae4c..632f062 100644 --- a/permissions/default.toml +++ b/permissions/default.toml @@ -10,7 +10,7 @@ permissions = [ "allow-load", "allow-execute", "allow-execute-transaction", - "allow-execute-interruptible-transaction", + "allow-begin-interruptible-transaction", "allow-transaction-continue", "allow-transaction-read", "allow-fetch-all", diff --git a/permissions/schemas/schema.json b/permissions/schemas/schema.json index 5bc26dd..c2d8d55 100644 --- a/permissions/schemas/schema.json +++ b/permissions/schemas/schema.json @@ -294,6 +294,18 @@ "PermissionKind": { "type": "string", "oneOf": [ + { + "description": "Enables the begin_interruptible_transaction command without any pre-configured scope.", + "type": "string", + "const": "allow-begin-interruptible-transaction", + "markdownDescription": "Enables the begin_interruptible_transaction command without any pre-configured scope." + }, + { + "description": "Denies the begin_interruptible_transaction command without any pre-configured scope.", + "type": "string", + "const": "deny-begin-interruptible-transaction", + "markdownDescription": "Denies the begin_interruptible_transaction command without any pre-configured scope." + }, { "description": "Enables the close command without any pre-configured scope.", "type": "string", @@ -330,18 +342,6 @@ "const": "deny-execute", "markdownDescription": "Denies the execute command without any pre-configured scope." }, - { - "description": "Enables the execute_interruptible_transaction command without any pre-configured scope.", - "type": "string", - "const": "allow-execute-interruptible-transaction", - "markdownDescription": "Enables the execute_interruptible_transaction command without any pre-configured scope." - }, - { - "description": "Denies the execute_interruptible_transaction command without any pre-configured scope.", - "type": "string", - "const": "deny-execute-interruptible-transaction", - "markdownDescription": "Denies the execute_interruptible_transaction command without any pre-configured scope." - }, { "description": "Enables the execute_transaction command without any pre-configured scope.", "type": "string", @@ -378,6 +378,18 @@ "const": "deny-fetch-one", "markdownDescription": "Denies the fetch_one command without any pre-configured scope." }, + { + "description": "Enables the get_migration_events command without any pre-configured scope.", + "type": "string", + "const": "allow-get-migration-events", + "markdownDescription": "Enables the get_migration_events command without any pre-configured scope." + }, + { + "description": "Denies the get_migration_events command without any pre-configured scope.", + "type": "string", + "const": "deny-get-migration-events", + "markdownDescription": "Denies the get_migration_events command without any pre-configured scope." + }, { "description": "Enables the hello command without any pre-configured scope.", "type": "string", @@ -439,10 +451,10 @@ "markdownDescription": "Denies the transaction_read command without any pre-configured scope." }, { - "description": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-execute-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`", + "description": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-execute-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`" + "markdownDescription": "Default permissions for the sqlite plugin - allows all database operations\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-execute`\n- `allow-execute-transaction`\n- `allow-begin-interruptible-transaction`\n- `allow-transaction-continue`\n- `allow-transaction-read`\n- `allow-fetch-all`\n- `allow-fetch-one`\n- `allow-close`\n- `allow-close-all`\n- `allow-remove`" } ] } diff --git a/src/commands.rs b/src/commands.rs index 8dfee07..e4a85b4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,18 +7,15 @@ use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use sqlx_sqlite_conn_mgr::SqliteDatabaseConfig; +use sqlx_sqlite_toolkit::{ + ActiveInterruptibleTransaction, ActiveInterruptibleTransactions, ActiveRegularTransactions, + DatabaseWrapper, Statement, TransactionWriter, WriteQueryResult, +}; use std::sync::Arc; use tauri::{AppHandle, Runtime, State}; use uuid::Uuid; -use crate::{ - DbInstances, Error, MigrationEvent, MigrationStates, MigrationStatus, Result, WriteQueryResult, - transactions::{ - ActiveInterruptibleTransaction, ActiveInterruptibleTransactions, ActiveRegularTransactions, - Statement, TransactionWriter, - }, - wrapper::DatabaseWrapper, -}; +use crate::{DbInstances, Error, MigrationEvent, MigrationStates, MigrationStatus, Result}; /// Token representing an active interruptible transaction #[derive(Debug, Clone, Serialize, Deserialize)] @@ -128,7 +125,7 @@ pub async fn load( } Entry::Vacant(entry) => { // We won the race, create and insert the wrapper - let wrapper = DatabaseWrapper::connect(&db, &app, custom_config).await?; + let wrapper = crate::resolve::connect(&db, &app, custom_config).await?; entry.insert(wrapper); Ok(db) } @@ -264,7 +261,7 @@ pub async fn execute_transaction( // Wait for transaction to complete match handle.await { - Ok(result) => result, + Ok(result) => Ok(result?), Err(e) => { // Task panicked or was aborted - ensure cleanup regular_txs.remove(&tx_key).await; @@ -357,10 +354,10 @@ pub async fn close_all(db_instances: State<'_, DbInstances>) -> Result<()> { let wrappers: Vec = instances.drain().map(|(_, v)| v).collect(); // Close each connection, continuing on errors to ensure all get closed - let mut last_error = None; + let mut last_error: Option = None; for wrapper in wrappers { if let Err(e) = wrapper.close().await { - last_error = Some(e); + last_error = Some(e.into()); } } @@ -435,35 +432,20 @@ pub async fn begin_interruptible_transaction( .await?; TransactionWriter::Attached(guard) } else { - TransactionWriter::Regular(wrapper.acquire_writer().await?) + TransactionWriter::from(wrapper.acquire_writer().await?) }; // Begin transaction - match &mut writer { - TransactionWriter::Regular(w) => { - sqlx::query("BEGIN IMMEDIATE").execute(&mut **w).await?; - } - TransactionWriter::Attached(w) => { - sqlx::query("BEGIN IMMEDIATE").execute(&mut **w).await?; - } - } + writer.begin_immediate().await?; // Execute initial statements - for statement in initial_statements { - let mut q = sqlx::query(&statement.query); - for value in statement.values { - q = crate::wrapper::bind_value(q, value); - } - match &mut writer { - TransactionWriter::Regular(w) => q.execute(&mut **w).await?, - TransactionWriter::Attached(w) => q.execute(&mut **w).await?, - }; - } + let mut active_tx = + ActiveInterruptibleTransaction::new(db.clone(), transaction_id.clone(), writer); - // Store transaction state - let tx = ActiveInterruptibleTransaction::new(db.clone(), transaction_id.clone(), writer); + active_tx.continue_with(initial_statements).await?; - active_txs.insert(db.clone(), tx).await?; + // Store transaction state + active_txs.insert(db.clone(), active_tx).await?; Ok(TransactionToken { db_path: db, @@ -495,14 +477,14 @@ pub async fn transaction_continue( Ok(()) => Ok(Some(token)), Err(e) => { // Transaction lost but will auto-rollback via Drop - Err(e) + Err(e.into()) } } } Err(e) => { // Execution failed, explicitly rollback before returning error let _ = tx.rollback().await; - Err(e) + Err(e.into()) } } } @@ -553,14 +535,14 @@ pub async fn transaction_read( Ok(()) => Ok(results), Err(e) => { // Transaction lost but will auto-rollback via Drop - Err(e) + Err(e.into()) } } } Err(e) => { // Read failed, explicitly rollback before returning error let _ = tx.rollback().await; - Err(e) + Err(e.into()) } } } diff --git a/src/error.rs b/src/error.rs index 3468a6f..891ec99 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,15 +11,13 @@ struct ErrorResponse { } /// Error types for the SQLite plugin. +/// +/// Plugin-specific errors wrap toolkit errors with additional state-management variants. #[derive(Debug, thiserror::Error)] pub enum Error { - /// Error from SQLx operations. + /// Error from the SQLite toolkit (database operations). #[error(transparent)] - Sqlx(#[from] sqlx::Error), - - /// Error from the connection manager. - #[error(transparent)] - ConnectionManager(#[from] sqlx_sqlite_conn_mgr::Error), + Toolkit(#[from] sqlx_sqlite_toolkit::Error), /// Error from database migrations. #[error(transparent)] @@ -33,71 +31,39 @@ pub enum Error { #[error("database {0} not loaded")] DatabaseNotLoaded(String), - /// SQLite type that cannot be mapped to JSON. - #[error("unsupported datatype: {0}")] - UnsupportedDatatype(String), - - /// I/O error when accessing database files. - #[error("io error: {0}")] - Io(#[from] std::io::Error), - - /// Multiple rows returned from fetchOne query. - #[error("fetchOne() query returned {0} rows, expected 0 or 1")] - MultipleRowsReturned(usize), - - /// Transaction failed and rollback also failed. - #[error("transaction failed: {transaction_error}; rollback also failed: {rollback_error}")] - TransactionRollbackFailed { - transaction_error: String, - rollback_error: String, - }, - - /// Transaction already active for this database. - #[error("transaction already active for database: {0}")] - TransactionAlreadyActive(String), - - /// No active transaction for this database. - #[error("no active transaction for database: {0}")] - NoActiveTransaction(String), - - /// Invalid transaction token provided. - #[error("invalid transaction token")] - InvalidTransactionToken, - - /// Transaction has already been committed or rolled back. - #[error("transaction has already been finalized (committed or rolled back)")] - TransactionAlreadyFinalized, - /// Generic error for operations that don't fit other categories. #[error("{0}")] Other(String), } +impl From for Error { + fn from(e: sqlx_sqlite_conn_mgr::Error) -> Self { + Error::Toolkit(sqlx_sqlite_toolkit::Error::from(e)) + } +} + +impl From for Error { + fn from(e: sqlx::Error) -> Self { + Error::Toolkit(sqlx_sqlite_toolkit::Error::from(e)) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Toolkit(sqlx_sqlite_toolkit::Error::from(e)) + } +} + impl Error { /// Extract a structured error code from the error type. /// /// This provides machine-readable error codes for frontend error handling. fn error_code(&self) -> String { match self { - Error::Sqlx(e) => { - // Extract SQLite error codes from sqlx errors - if let Some(code) = e.as_database_error().and_then(|db_err| db_err.code()) { - return format!("SQLITE_{}", code); - } - "SQLX_ERROR".to_string() - } - Error::ConnectionManager(_) => "CONNECTION_ERROR".to_string(), + Error::Toolkit(e) => e.error_code(), Error::Migration(_) => "MIGRATION_ERROR".to_string(), Error::InvalidPath(_) => "INVALID_PATH".to_string(), Error::DatabaseNotLoaded(_) => "DATABASE_NOT_LOADED".to_string(), - Error::UnsupportedDatatype(_) => "UNSUPPORTED_DATATYPE".to_string(), - Error::Io(_) => "IO_ERROR".to_string(), - Error::MultipleRowsReturned(_) => "MULTIPLE_ROWS_RETURNED".to_string(), - Error::TransactionRollbackFailed { .. } => "TRANSACTION_ROLLBACK_FAILED".to_string(), - Error::TransactionAlreadyActive(_) => "TRANSACTION_ALREADY_ACTIVE".to_string(), - Error::NoActiveTransaction(_) => "NO_ACTIVE_TRANSACTION".to_string(), - Error::InvalidTransactionToken => "INVALID_TRANSACTION_TOKEN".to_string(), - Error::TransactionAlreadyFinalized => "TRANSACTION_ALREADY_FINALIZED".to_string(), Error::Other(_) => "ERROR".to_string(), } } @@ -134,13 +100,15 @@ mod tests { #[test] fn test_error_code_unsupported_datatype() { - let err = Error::UnsupportedDatatype("WEIRD_TYPE".into()); + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::UnsupportedDatatype( + "WEIRD_TYPE".into(), + )); assert_eq!(err.error_code(), "UNSUPPORTED_DATATYPE"); } #[test] fn test_error_code_multiple_rows() { - let err = Error::MultipleRowsReturned(5); + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::MultipleRowsReturned(5)); assert_eq!(err.error_code(), "MULTIPLE_ROWS_RETURNED"); } @@ -176,7 +144,7 @@ mod tests { #[test] fn test_error_serialization_multiple_rows() { - let err = Error::MultipleRowsReturned(3); + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::MultipleRowsReturned(3)); let json = serde_json::to_value(&err).unwrap(); assert_eq!(json["code"], "MULTIPLE_ROWS_RETURNED"); @@ -188,7 +156,7 @@ mod tests { #[test] fn test_error_message_format() { // Verify error messages are descriptive - let err = Error::MultipleRowsReturned(5); + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::MultipleRowsReturned(5)); let message = err.to_string(); assert!(message.contains("fetchOne()")); assert!(message.contains("5 rows")); @@ -197,19 +165,19 @@ mod tests { #[test] fn test_error_code_transaction_rollback_failed() { - let err = Error::TransactionRollbackFailed { + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::TransactionRollbackFailed { transaction_error: "constraint violation".to_string(), rollback_error: "connection lost".to_string(), - }; + }); assert_eq!(err.error_code(), "TRANSACTION_ROLLBACK_FAILED"); } #[test] fn test_error_serialization_transaction_rollback_failed() { - let err = Error::TransactionRollbackFailed { + let err = Error::Toolkit(sqlx_sqlite_toolkit::Error::TransactionRollbackFailed { transaction_error: "constraint violation".to_string(), rollback_error: "connection lost".to_string(), - }; + }); let json = serde_json::to_value(&err).unwrap(); assert_eq!(json["code"], "TRANSACTION_ROLLBACK_FAILED"); diff --git a/src/lib.rs b/src/lib.rs index f6ec059..7f32dfa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,20 +7,17 @@ use tauri::{Emitter, Manager, RunEvent, Runtime, plugin::Builder as PluginBuilde use tokio::sync::{Notify, RwLock}; use tracing::{debug, error, info, trace, warn}; -mod builders; mod commands; -mod decode; mod error; -mod transactions; -mod wrapper; +mod resolve; pub use error::{Error, Result}; pub use sqlx_sqlite_conn_mgr::{ AttachedMode, AttachedSpec, Migrator as SqliteMigrator, SqliteDatabaseConfig, }; -pub use transactions::{ActiveInterruptibleTransactions, ActiveRegularTransactions, Statement}; -pub use wrapper::{ - DatabaseWrapper, InterruptibleTransaction, InterruptibleTransactionBuilder, +pub use sqlx_sqlite_toolkit::{ + ActiveInterruptibleTransactions, ActiveRegularTransactions, DatabaseWrapper, + InterruptibleTransaction, InterruptibleTransactionBuilder, Statement, TransactionExecutionBuilder, WriteQueryResult, }; @@ -253,7 +250,7 @@ impl Builder { handle.block_on(async { // First, abort all active transactions debug!("Aborting active transactions"); - transactions::cleanup_all_transactions(&interruptible_txs_clone, ®ular_txs_clone).await; + sqlx_sqlite_toolkit::cleanup_all_transactions(&interruptible_txs_clone, ®ular_txs_clone).await; // Then close databases let mut guard = instances_clone.0.write().await; @@ -383,7 +380,7 @@ async fn run_migrations_for_database( }; // Connect to database - let db = match DatabaseWrapper::connect_with_path(&abs_path, None).await { + let db = match DatabaseWrapper::connect(&abs_path, None).await { Ok(wrapper) => wrapper, Err(e) => { let error_msg = e.to_string(); @@ -469,7 +466,7 @@ fn resolve_migration_path( .app_config_dir() .map_err(|_| Error::InvalidPath("No app config path found".to_string()))?; - std::fs::create_dir_all(&app_path).map_err(Error::Io)?; + std::fs::create_dir_all(&app_path)?; Ok(app_path.join(path)) } diff --git a/src/resolve.rs b/src/resolve.rs new file mode 100644 index 0000000..bc67a12 --- /dev/null +++ b/src/resolve.rs @@ -0,0 +1,38 @@ +use std::fs::create_dir_all; +use std::path::PathBuf; + +use sqlx_sqlite_conn_mgr::SqliteDatabaseConfig; +use sqlx_sqlite_toolkit::DatabaseWrapper; +use tauri::{AppHandle, Manager, Runtime}; + +use crate::Error; + +/// Connect to a SQLite database via the connection manager, resolving +/// the path relative to the app config directory. +/// +/// This is the Tauri-specific connection method that resolves relative paths +/// before delegating to the toolkit's `DatabaseWrapper::connect()`. +pub async fn connect( + path: &str, + app: &AppHandle, + custom_config: Option, +) -> Result { + let abs_path = resolve_database_path(path, app)?; + Ok(DatabaseWrapper::connect(&abs_path, custom_config).await?) +} + +/// Resolve database file path relative to app config directory. +/// +/// Paths are joined to `app_config_dir()` (e.g., `Library/Application Support/${bundleIdentifier}` on iOS). +/// Special paths like `:memory:` are passed through unchanged. +pub fn resolve_database_path(path: &str, app: &AppHandle) -> Result { + let app_path = app + .path() + .app_config_dir() + .map_err(|_| Error::InvalidPath("No app config path found".to_string()))?; + + create_dir_all(&app_path)?; + + // Join the relative path to the app config directory + Ok(app_path.join(path)) +}