diff --git a/Cargo.lock b/Cargo.lock index 98292857..3733f13e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.12.0", + "bitflags", "bytes", "futures-core", "futures-sink", @@ -31,7 +31,7 @@ dependencies = [ "actix-tls", "actix-utils", "base64 0.22.1", - "bitflags 2.12.0", + "bitflags", "brotli 8.0.3", "bytes", "bytestring", @@ -176,7 +176,7 @@ dependencies = [ "pin-project-lite", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tracing", "webpki-roots 0.26.11", @@ -275,19 +275,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -318,31 +305,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.12.0", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 2.0.18", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -503,7 +465,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.4", + "rustix", "slab", "windows-sys 0.61.2", ] @@ -587,6 +549,19 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atoi" version = "2.0.0" @@ -635,35 +610,13 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.9.4", - "rustls", + "rustls 0.23.40", "serde", "serde_json", "serde_urlencoded", "tokio", ] -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -694,6 +647,17 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bigdecimal" version = "0.4.10" @@ -709,12 +673,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.12.0" @@ -751,15 +709,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] - [[package]] name = "blocking" version = "1.6.2" @@ -824,6 +773,15 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "btoi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" +dependencies = [ + "num-traits", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -851,20 +809,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.12.0", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - [[package]] name = "cc" version = "1.2.63" @@ -883,12 +827,6 @@ 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 = "chacha20" version = "0.10.0" @@ -954,15 +892,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cmov" version = "0.5.4" @@ -975,16 +904,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1014,6 +933,12 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "connection-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3" + [[package]] name = "const-oid" version = "0.9.6" @@ -1107,30 +1032,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1150,27 +1051,21 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.4.0" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "crc-catalog", + "cfg-if", ] [[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[package]] -name = "crc32fast" -version = "1.5.0" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "cfg-if", + "crossbeam-utils", ] [[package]] @@ -1259,12 +1154,6 @@ dependencies = [ "cmov", ] -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1373,6 +1262,26 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "deadpool" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + [[package]] name = "der" version = "0.7.10" @@ -1499,33 +1408,6 @@ dependencies = [ "ctutils", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "displaydoc" version = "0.2.6" @@ -1537,15 +1419,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - [[package]] name = "dlv-list" version = "0.5.2" @@ -1561,18 +1434,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1635,7 +1496,7 @@ dependencies = [ "ff", "generic-array", "group", - "hkdf 0.12.4", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -1653,6 +1514,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1701,6 +1582,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" @@ -1739,17 +1638,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1768,33 +1656,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1804,12 +1665,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.32" @@ -1852,17 +1707,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.32" @@ -1900,7 +1744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls", + "rustls 0.23.40", "rustls-pki-types", ] @@ -2066,12 +1910,26 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" -version = "0.11.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ "hashbrown 0.16.1", ] @@ -2103,15 +1961,6 @@ dependencies = [ "hmac 0.12.1", ] -[[package]] -name = "hkdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" -dependencies = [ - "hmac 0.13.0", -] - [[package]] name = "hmac" version = "0.12.1" @@ -2423,64 +2272,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -2514,6 +2305,15 @@ dependencies = [ "serde", ] +[[package]] +name = "keyed_priority_queue" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" +dependencies = [ + "indexmap 2.14.0", +] + [[package]] name = "lambda-web" version = "0.2.1" @@ -2614,16 +2414,6 @@ dependencies = [ "rle-decode-fast", ] -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "libm" version = "0.2.16" @@ -2636,29 +2426,20 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.12.0", "libc", - "plain", - "redox_syscall 0.8.0", ] [[package]] name = "libsqlite3-sys" -version = "0.38.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76001fb4daed01e5f2b518aac0b4dc592e7c734da63dbffcf0c64fa612a8d0c" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2703,6 +2484,15 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "markdown" version = "1.0.0" @@ -2789,33 +2579,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" [[package]] -name = "ndk" -version = "0.9.0" +name = "mysql-common-derive" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" dependencies = [ - "bitflags 2.12.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", + "darling 0.20.11", + "heck", + "num-bigint", + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", + "termcolor", + "thiserror 2.0.18", ] [[package]] -name = "ndk-context" -version = "0.1.1" +name = "mysql_async" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +checksum = "3519e91b0d254ac1ffa495bc42053286cb2172ad7241d5b3b1b9f8a891f21ee2" +dependencies = [ + "bytes", + "crossbeam-queue", + "crossbeam-utils", + "flate2", + "futures-core", + "futures-sink", + "futures-util", + "keyed_priority_queue", + "lru", + "mysql_common", + "percent-encoding", + "rand 0.10.1", + "rustls 0.23.40", + "serde", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "twox-hash", + "url", + "webpki-roots 1.0.7", +] [[package]] -name = "ndk-sys" -version = "0.6.0+11769913" +name = "mysql_common" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +checksum = "4b42ced54aa8ac97226486337973f9bc3956e24f03a23e88a6e18f640959d6e2" dependencies = [ - "jni-sys 0.3.1", + "base64 0.22.1", + "bigdecimal 0.4.10", + "bitflags", + "btoi", + "byteorder", + "bytes", + "chrono", + "crc32fast", + "flate2", + "getrandom 0.3.4", + "mysql-common-derive", + "num-bigint", + "num-traits", + "regex", + "saturating", + "serde", + "serde_json", + "sha1 0.10.6", + "sha2 0.10.9", + "thiserror 2.0.18", + "uuid", ] [[package]] @@ -2924,25 +2761,13 @@ dependencies = [ ] [[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", + "hermit-abi", + "libc", ] [[package]] @@ -2964,171 +2789,13 @@ dependencies = [ "url", ] -[[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", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.12.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-foundation", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.12.0", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.12.0", - "block2", - "dispatch", - "libc", - "objc2", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", + "bitflags", ] [[package]] @@ -3140,51 +2807,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.12.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - [[package]] name = "odbc-api" version = "28.0.0" @@ -3196,7 +2818,6 @@ dependencies = [ "odbc-sys", "thiserror 2.0.18", "widestring", - "winit", ] [[package]] @@ -3260,6 +2881,12 @@ dependencies = [ "url", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3341,22 +2968,6 @@ dependencies = [ "tokio-stream", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", -] - [[package]] name = "ordered-float" version = "2.10.1" @@ -3424,7 +3035,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -3520,6 +3131,25 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -3584,12 +3214,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "polling" version = "3.11.0" @@ -3600,7 +3224,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -3610,6 +3234,39 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "postgres-protocol" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac 0.13.0", + "md-5", + "memchr", + "rand 0.10.1", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator 0.2.0", + "postgres-protocol", + "serde_core", + "serde_json", + "uuid", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -3634,6 +3291,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "prettyplease" version = "0.2.37" @@ -3662,6 +3325,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -3791,61 +3476,26 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rcgen" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ - "aws-lc-rs", "pem", + "ring", "rustls-pki-types", "time", "yasna", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.12.0", -] - -[[package]] -name = "redox_syscall" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" -dependencies = [ - "bitflags 2.12.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.17", - "libredox", - "thiserror 2.0.18", + "bitflags", ] [[package]] @@ -3939,7 +3589,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" dependencies = [ - "bitflags 2.12.0", + "bitflags", "once_cell", "serde", "serde_derive", @@ -3967,6 +3617,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "chrono", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink 0.10.0", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -3997,28 +3662,27 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.12.0", + "bitflags", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "rustls" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "bitflags 2.12.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", ] [[package]] @@ -4027,11 +3691,11 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -4045,7 +3709,6 @@ dependencies = [ "async-io", "async-trait", "async-web-client", - "aws-lc-rs", "base64 0.22.1", "blocking", "chrono", @@ -4055,6 +3718,7 @@ dependencies = [ "log", "pem", "rcgen", + "ring", "serde", "serde_json", "thiserror 2.0.18", @@ -4062,16 +3726,37 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -4083,13 +3768,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4108,13 +3802,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "same-file" -version = "1.0.6" +name = "saturating" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" [[package]] name = "schannel" @@ -4155,6 +3846,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -4169,13 +3870,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.12.0", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4423,20 +4137,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" +name = "siphasher" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -4450,15 +4154,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - [[package]] name = "socket2" version = "0.5.10" @@ -4484,9 +4179,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] name = "spki" @@ -4514,11 +4206,13 @@ dependencies = [ "async-trait", "awc", "base64 0.22.1", - "bigdecimal", + "bigdecimal 0.4.10", + "bytes", "chrono", "clap", "config", "csv-async", + "deadpool", "dotenvy", "encoding_rs", "futures-util", @@ -4530,7 +4224,8 @@ dependencies = [ "log", "markdown", "mime_guess", - "odbc-sys", + "mysql_async", + "odbc-api", "openidconnect", "opentelemetry", "opentelemetry-http", @@ -4540,15 +4235,17 @@ dependencies = [ "percent-encoding", "rand 0.10.1", "regex", - "rustls", + "rustls 0.23.40", "rustls-acme", - "rustls-native-certs", + "rustls-native-certs 0.8.3", "serde", "serde_json", "sha2 0.11.0", "sqlparser", - "sqlx-oldapi", + "tiberius", "tokio", + "tokio-postgres", + "tokio-rusqlite", "tokio-stream", "tokio-util", "tracing", @@ -4556,6 +4253,7 @@ dependencies = [ "tracing-log", "tracing-opentelemetry", "tracing-subscriber", + "uuid", ] [[package]] @@ -4579,110 +4277,6 @@ dependencies = [ "syn", ] -[[package]] -name = "sqlx-core-oldapi" -version = "0.6.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e33eb18d1e750df8aef99361ee02e170562dff119108680bc9035e796cd2a84" -dependencies = [ - "ahash", - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.12.0", - "byteorder", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "dirs", - "dotenvy", - "either", - "encoding_rs", - "event-listener", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "hashlink", - "hex", - "hkdf 0.13.0", - "hmac 0.13.0", - "indexmap 2.14.0", - "itoa", - "libc", - "libsqlite3-sys", - "log", - "md-5", - "memchr", - "num-bigint", - "odbc-api", - "once_cell", - "percent-encoding", - "rand 0.10.1", - "regex", - "rsa", - "rustls", - "serde", - "serde_json", - "sha1 0.10.6", - "sha1 0.11.0", - "sha2 0.11.0", - "smallvec", - "sqlx-rt-oldapi", - "stringprep", - "thiserror 2.0.18", - "tokio-stream", - "tokio-util", - "url", - "uuid", - "webpki-roots 1.0.7", - "whoami", -] - -[[package]] -name = "sqlx-macros-oldapi" -version = "0.6.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adbf4ebc08c19991fa51993f471e572930c4dec146d3dc915a8e54db91d624c6" -dependencies = [ - "dotenvy", - "either", - "heck", - "once_cell", - "proc-macro2", - "quote", - "serde_json", - "sha2 0.11.0", - "sqlx-core-oldapi", - "sqlx-rt-oldapi", - "syn", - "url", -] - -[[package]] -name = "sqlx-oldapi" -version = "0.6.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ba4d352504ee1a0a76eb052879d68ba63536c738da5062026ac0d3dc724c7" -dependencies = [ - "sqlx-core-oldapi", - "sqlx-macros-oldapi", -] - -[[package]] -name = "sqlx-rt-oldapi" -version = "0.6.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8d629fed8792460ff39bb58cb154bfe181893ab9a51d0a3634950b35672a57" -dependencies = [ - "once_cell", - "tokio", - "tokio-rustls", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4743,10 +4337,19 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4796,6 +4399,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiberius" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d" +dependencies = [ + "async-trait", + "asynchronous-codec", + "bigdecimal 0.3.1", + "byteorder", + "bytes", + "chrono", + "connection-string", + "encoding_rs", + "enumflags2", + "futures-util", + "num-traits", + "once_cell", + "pin-project-lite", + "pretty-hex", + "rustls-native-certs 0.6.3", + "rustls-pemfile", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "time" version = "0.3.47" @@ -4889,13 +4522,60 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-postgres" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.1", + "socket2 0.6.4", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rusqlite" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302563ae4a2127f3d2c105f4f2f0bd7cae3609371755600ebc148e0ccd8510d6" +dependencies = [ + "crossbeam-channel", + "rusqlite", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.40", "tokio", ] @@ -5089,6 +4769,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typeid" version = "1.0.3" @@ -5227,16 +4913,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -5301,16 +4977,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.122" @@ -5371,7 +5037,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.12.0", + "bitflags", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -5511,15 +5177,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5593,46 +5250,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winit" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" -dependencies = [ - "android-activity", - "atomic-waker", - "bitflags 2.12.0", - "block2", - "calloop", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "orbclient", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "xkbcommon-dl", -] - [[package]] name = "winnow" version = "1.0.3" @@ -5706,7 +5323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.12.0", + "bitflags", "indexmap 2.14.0", "log", "serde", @@ -5759,25 +5376,6 @@ dependencies = [ "time", ] -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.12.0", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - [[package]] name = "yaml-rust2" version = "0.11.0" @@ -5786,7 +5384,7 @@ checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" dependencies = [ "arraydeque", "encoding_rs", - "hashlink", + "hashlink 0.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 67740f70..a5dcd060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,20 +18,13 @@ panic = "abort" codegen-units = 2 [dependencies] -sqlx = { package = "sqlx-oldapi", version = "0.6.56", default-features = false, features = [ - "any", - "runtime-tokio-rustls", - "migrate", - "sqlite", - "postgres", - "mysql", - "mssql", - "odbc", - "chrono", - "bigdecimal", - "json", - "uuid", -] } +tokio-postgres = { version = "0.7.17", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } +mysql_async = { version = "0.37.0", default-features = false, features = ["default-rustls-ring", "chrono", "bigdecimal"] } +tiberius = { version = "0.12.3", default-features = false, features = ["tds73", "tokio", "tokio-util", "rustls", "chrono", "bigdecimal"] } +tokio-rusqlite = { version = "0.7.0", features = ["bundled", "functions", "load_extension", "collation", "chrono", "blob"] } +odbc-api = { version = "28.0.0", default-features = false, features = ["odbc_version_3_80"] } +deadpool = { version = "0.13.0", features = ["rt_tokio_1"] } +uuid = "1" chrono = "0.4.23" actix-web = { version = "4", features = ["rustls-0_23", "cookies"] } percent-encoding = "2.2.0" @@ -65,19 +58,19 @@ actix-web-httpauth = "0.8.0" rand = "0.10.0" actix-multipart = "0.7.2" base64 = "0.22" +bytes = "1" hmac = "0.13" sha2 = "0.11" -rustls-acme = "0.15" +rustls-acme = { version = "0.15", default-features = false, features = ["ring", "tls12", "webpki-roots"] } dotenvy = "0.15.7" csv-async = { version = "1.2.6", features = ["tokio"] } -rustls = { version = "0.23" } # keep in sync with actix-web, awc, rustls-acme, and sqlx +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } # keep in sync with actix-web, awc, and rustls-acme rustls-native-certs = "0.8.1" awc = { version = "3", features = ["rustls-0_23-webpki-roots"] } clap = { version = "4.5.17", features = ["derive"] } -tokio-util = "0.7.12" +tokio-util = { version = "0.7.12", features = ["compat"] } openidconnect = { version = "4.0.0", default-features = false, features = ["accept-rfc3339-timestamps"] } encoding_rs = "0.8.35" -odbc-sys = { version = "0", optional = true } regex = "1" # OpenTelemetry / tracing @@ -95,7 +88,7 @@ opentelemetry-semantic-conventions = { version = "0.32", features = ["semconv_ex [features] default = [] -odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"] +odbc-static = ["odbc-api/vendored-unix-odbc"] lambda-web = ["dep:lambda-web", "odbc-static"] [dev-dependencies] @@ -104,7 +97,7 @@ tokio = { version = "1", features = ["rt", "time", "test-util"] } [build-dependencies] awc = { version = "3", features = ["rustls-0_23-webpki-roots"] } -rustls = "0.23" +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } actix-rt = "2.8" libflate = "2" futures-util = "0.3.21" diff --git a/build.rs b/build.rs index 6f9f4a96..804cdb7f 100644 --- a/build.rs +++ b/build.rs @@ -12,7 +12,7 @@ use std::time::Duration; #[actix_rt::main] async fn main() { - rustls::crypto::aws_lc_rs::default_provider() + rustls::crypto::ring::default_provider() .install_default() .unwrap(); diff --git a/src/filesystem.rs b/src/filesystem.rs index fd33b480..5fe82a72 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,12 +1,10 @@ use crate::webserver::ErrorWithStatus; use crate::webserver::database::SupportedDatabase; +use crate::webserver::database::{DbParam, driver::DbValue}; use crate::webserver::{Database, StatusCodeResultExt, make_placeholder}; use crate::{AppState, TEMPLATES_DIR}; use anyhow::Context; use chrono::{DateTime, Utc}; -use sqlx::any::{AnyStatement, AnyTypeInfo}; -use sqlx::postgres::types::PgTimeTz; -use sqlx::{Executor, Postgres, Statement, Type}; use std::fmt::Write; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; @@ -241,9 +239,9 @@ async fn file_modified_since_local(path: &Path, since: DateTime) -> tokio:: } pub struct DbFsQueries { - was_modified: AnyStatement<'static>, - read_file: AnyStatement<'static>, - exists: AnyStatement<'static>, + was_modified: String, + read_file: String, + exists: String, } impl DbFsQueries { @@ -269,52 +267,48 @@ impl DbFsQueries { log::debug!("Initializing database filesystem queries"); Self::check_table_available(db).await?; Ok(Self { - was_modified: Self::make_was_modified_query(db).await?, - read_file: Self::make_read_file_query(db).await?, - exists: Self::make_exists_query(db).await?, + was_modified: Self::make_was_modified_query(db), + read_file: Self::make_read_file_query(db), + exists: Self::make_exists_query(db), }) } async fn check_table_available(db: &Database) -> anyhow::Result<()> { db.connection - .execute("SELECT 1 FROM sqlpage_files WHERE 1 = 0") + .acquire() + .await + .context("Unable to acquire database connection")? + .execute_command("SELECT 1 FROM sqlpage_files WHERE 1 = 0", &[]) .await - .map(|_| ()) .context("Unable to access sqlpage_files")?; Ok(()) } - async fn make_was_modified_query(db: &Database) -> anyhow::Result> { + fn make_was_modified_query(db: &Database) -> String { let was_modified_query = format!( "SELECT 1 from sqlpage_files WHERE last_modified >= {} AND path = {}", make_placeholder(db.info.kind, 1), make_placeholder(db.info.kind, 2) ); - let param_types: &[AnyTypeInfo; 2] = &[ - PgTimeTz::type_info().into(), - >::type_info().into(), - ]; log::debug!("Preparing the database filesystem was_modified_query: {was_modified_query}"); - db.prepare_with(&was_modified_query, param_types).await + was_modified_query } - async fn make_read_file_query(db: &Database) -> anyhow::Result> { + fn make_read_file_query(db: &Database) -> String { let read_file_query = format!( "SELECT contents from sqlpage_files WHERE path = {}", make_placeholder(db.info.kind, 1), ); - let param_types: &[AnyTypeInfo; 1] = &[>::type_info().into()]; log::debug!("Preparing the database filesystem read_file_query: {read_file_query}"); - db.prepare_with(&read_file_query, param_types).await + read_file_query } - async fn make_exists_query(db: &Database) -> anyhow::Result> { + fn make_exists_query(db: &Database) -> String { let exists_query = format!( "SELECT 1 from sqlpage_files WHERE path = {}", make_placeholder(db.info.kind, 1), ); - let param_types: &[AnyTypeInfo; 1] = &[>::type_info().into()]; - db.prepare_with(&exists_query, param_types).await + exists_query } async fn file_modified_since_in_db( @@ -323,22 +317,22 @@ impl DbFsQueries { path: &Path, since: DateTime, ) -> anyhow::Result { - let query = self - .was_modified - .query_as::<(i32,)>() - .bind(since) - .bind(path.display().to_string()); + let params = [ + DbParam::Timestamp(since), + DbParam::Text(path.display().to_string()), + ]; log::trace!( "Checking if file {} was modified since {} by executing query: \n\ {}\n\ with parameters: {:?}", path.display(), since, - self.was_modified.sql(), + self.was_modified, (since, path) ); - let was_modified_i32 = query - .fetch_optional(&app_state.db.connection) + let mut conn = app_state.db.connection.acquire().await?; + let was_modified_i32 = conn + .fetch_optional(&self.was_modified, ¶ms) .await .with_context(|| { format!( @@ -350,44 +344,47 @@ impl DbFsQueries { "DB File {} was modified result: {was_modified_i32:?}", path.display() ); - Ok(was_modified_i32 == Some((1,))) + Ok(was_modified_i32.is_some()) } async fn read_file(&self, app_state: &AppState, path: &Path) -> anyhow::Result> { log::debug!("Reading file {} from the database", path.display()); - self.read_file - .query_as::<(Vec,)>() - .bind(path.display().to_string()) - .fetch_optional(&app_state.db.connection) - .await - .map_err(anyhow::Error::from) - .and_then(|modified| { - if let Some((modified,)) = modified { - Ok(modified) - } else { - Err(ErrorWithStatus { - status: actix_web::http::StatusCode::NOT_FOUND, - } - .into()) + let mut conn = app_state.db.connection.acquire().await?; + conn.fetch_optional( + &self.read_file, + &[DbParam::Text(path.display().to_string())], + ) + .await + .map_err(anyhow::Error::from) + .and_then(|row| { + if let Some(row) = row { + match row.values.first() { + Some(DbValue::Bytes(bytes)) => Ok(bytes.clone()), + Some(DbValue::Text(text)) => Ok(text.as_bytes().to_vec()), + _ => Ok(Vec::new()), + } + } else { + Err(ErrorWithStatus { + status: actix_web::http::StatusCode::NOT_FOUND, } - }) - .with_context(|| format!("Unable to read {} from the database", path.display())) + .into()) + } + }) + .with_context(|| format!("Unable to read {} from the database", path.display())) } async fn file_exists(&self, app_state: &AppState, path: &Path) -> anyhow::Result { - let query = self - .exists - .query_as::<(i32,)>() - .bind(path.display().to_string()); + let params = [DbParam::Text(path.display().to_string())]; log::trace!( "Checking if file {} exists by executing query: \n\ {}\n\ with parameters: {:?}", path.display(), - self.exists.sql(), + self.exists, (path,) ); - let result = query.fetch_optional(&app_state.db.connection).await; + let mut conn = app_state.db.connection.acquire().await?; + let result = conn.fetch_optional(&self.exists, ¶ms).await; log::debug!("DB File exists result: {result:?}"); result.map(|result| result.is_some()).with_context(|| { format!( @@ -401,7 +398,6 @@ impl DbFsQueries { #[actix_web::test] async fn test_sql_file_read_utf8() -> anyhow::Result<()> { use crate::app_config; - use sqlx::Executor; let config = app_config::tests::test_config(); let state = AppState::init(&config).await?; @@ -416,10 +412,11 @@ async fn test_sql_file_read_utf8() -> anyhow::Result<()> { let create_table_sql = DbFsQueries::get_create_table_sql(state.db.info.database_type); let db = &state.db; - let conn = &db.connection; - conn.execute("DROP TABLE IF EXISTS sqlpage_files").await?; + let mut conn = db.connection.acquire().await?; + conn.execute_command("DROP TABLE IF EXISTS sqlpage_files", &[]) + .await?; log::debug!("Creating table sqlpage_files: {create_table_sql}"); - conn.execute(create_table_sql).await?; + conn.execute_command(create_table_sql, &[]).await?; let dbms = db.info.kind; let insert_sql = format!( @@ -427,11 +424,14 @@ async fn test_sql_file_read_utf8() -> anyhow::Result<()> { make_placeholder(dbms, 1), make_placeholder(dbms, 2) ); - sqlx::query(&insert_sql) - .bind("unit test file.txt") - .bind("Héllö world! 😀".as_bytes()) - .execute(conn) - .await?; + conn.execute_command( + &insert_sql, + &[ + DbParam::Text("unit test file.txt".into()), + DbParam::Bytes("Héllö world! 😀".as_bytes().to_vec()), + ], + ) + .await?; let fs = FileSystem::init("/", db).await; let actual = fs diff --git a/src/lib.rs b/src/lib.rs index d0c13b3f..7600f74b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ //! When processing a request, `SQLPage`: //! //! 1. Parses the SQL using sqlparser-rs. Once a SQL file is parsed, it is cached for later reuse. -//! 2. Executes queries through sqlx. +//! 2. Executes queries through native database drivers. //! 3. Finds the requested component's handlebars template in the database or in the filesystem. //! 4. Maps results to the component template, using handlebars-rs. //! 5. Streams rendered HTML to the client. diff --git a/src/telemetry_metrics.rs b/src/telemetry_metrics.rs index 89974438..7748aaac 100644 --- a/src/telemetry_metrics.rs +++ b/src/telemetry_metrics.rs @@ -1,8 +1,8 @@ +use crate::webserver::database::DbPool; use opentelemetry::global; use opentelemetry::metrics::{Histogram, ObservableGauge}; use opentelemetry_semantic_conventions::attribute as otel; use opentelemetry_semantic_conventions::metric as otel_metric; -use sqlx::AnyPool; pub struct TelemetryMetrics { pub http_request_duration: Histogram, @@ -41,7 +41,7 @@ impl Default for TelemetryMetrics { impl TelemetryMetrics { #[must_use] - pub fn new(pool: &AnyPool, db_system_name: &'static str) -> Self { + pub fn new(pool: &DbPool, db_system_name: &'static str) -> Self { let meter = global::meter("sqlpage"); let http_request_duration = meter .f64_histogram(otel_metric::HTTP_SERVER_REQUEST_DURATION) @@ -60,9 +60,8 @@ impl TelemetryMetrics { .with_description("Number of connections in the database pool.") .with_callback(move |observer| { let size = pool_ref.size(); - let idle_u32 = u32::try_from(pool_ref.num_idle()).unwrap_or(u32::MAX); - let used = i64::from(size.saturating_sub(idle_u32)); - let idle = i64::from(idle_u32); + let idle = i64::from(pool_ref.num_idle()); + let used = i64::from(size); observer.observe( used, &[ diff --git a/src/webserver/database/connect.rs b/src/webserver/database/connect.rs index 3433e81e..92093551 100644 --- a/src/webserver/database/connect.rs +++ b/src/webserver/database/connect.rs @@ -1,46 +1,45 @@ -use std::{mem::take, time::Duration}; +use std::time::Duration; use super::Database; use crate::{ ON_CONNECT_FILE, ON_RESET_FILE, app_config::AppConfig, - webserver::database::{DbInfo, SupportedDatabase}, + webserver::database::{DbInfo, DbKind, DbPool, SupportedDatabase}, }; use anyhow::Context; -use futures_util::future::BoxFuture; -use sqlx::odbc::OdbcConnectOptions; -use sqlx::{ - ConnectOptions, Connection, Executor, - any::{Any, AnyConnectOptions, AnyConnection, AnyKind}, - pool::PoolOptions, - sqlite::{Function, SqliteConnectOptions, SqliteFunctionCtx}, -}; impl Database { pub async fn init(config: &AppConfig) -> anyhow::Result { let database_url = &config.database_url; - let mut connect_options: AnyConnectOptions = database_url - .parse() - .with_context(|| format!("\"{database_url}\" is not a valid database URL. Please change the \"database_url\" option in the configuration file."))?; - if let Some(password) = &config.database_password { - set_database_password(&mut connect_options, password); + let db_kind = kind_from_database_url(database_url).with_context(|| { + format!( + "\"{database_url}\" is not a valid database URL. Please change the \"database_url\" option in the configuration file." + ) + })?; + if config.database_password.is_some() && matches!(db_kind, DbKind::Sqlite | DbKind::Odbc) { + log::warn!( + "Setting a separate database password is not supported for {db_kind:?}; include credentials in the database URL or connection string" + ); } - connect_options.log_statements(log::LevelFilter::Trace); - connect_options.log_slow_statements( - log::LevelFilter::Warn, - std::time::Duration::from_millis(250), - ); - log::debug!( - "Connecting to a {:?} database on {}", - connect_options.kind(), - database_url + log::debug!("Connecting to a {db_kind:?} database on {database_url}"); + let on_connect_sql = read_connection_handler(config, ON_CONNECT_FILE); + let on_reset_sql = read_connection_handler(config, ON_RESET_FILE); + if on_reset_sql.is_some() { + log::warn!( + "{ON_RESET_FILE} is currently ignored by the native driver pool because connections are not reused yet" + ); + } + + let pool = DbPool::new( + config, + db_kind, + default_max_connections(config, db_kind), + on_connect_sql, ); - set_custom_connect_options(&mut connect_options, config); - log::debug!("Connecting to database: {database_url}"); - let mut retries = config.database_connection_retries; - let mut conn: AnyConnection = loop { - match AnyConnection::connect_with(&connect_options).await { + let mut retries = config.database_connection_retries; + let mut conn = loop { + match pool.acquire().await { Ok(c) => break c, Err(e) => { if retries == 0 { @@ -53,17 +52,11 @@ impl Database { } } }; - let dbms_name: String = conn.dbms_name().await?; + let dbms_name = conn.dbms_name().await?; let database_type = SupportedDatabase::from_dbms_name(&dbms_name); drop(conn); - let db_kind = connect_options.kind(); - let pool = Self::create_pool_options(config, db_kind) - .connect_with(connect_options) - .await - .with_context(|| format!("Unable to open connection pool to {database_url}"))?; - - log::debug!("Initialized {dbms_name:?} database pool: {pool:#?}"); + log::debug!("Initialized {dbms_name:?} database pool"); Ok(Database { connection: pool, info: DbInfo { @@ -73,203 +66,63 @@ impl Database { }, }) } - - fn create_pool_options(config: &AppConfig, kind: AnyKind) -> PoolOptions { - let mut pool_options = PoolOptions::new() - .max_connections(if let Some(max) = config.max_database_pool_connections { - max - } else { - // Different databases have a different number of max concurrent connections allowed by default - match kind { - AnyKind::Postgres | AnyKind::Odbc => 50, // Default to PostgreSQL-like limits for Generic - AnyKind::MySql => 75, - AnyKind::Sqlite => { - if config.database_url.contains(":memory:") { - 128 - } else { - 16 - } - } - AnyKind::Mssql => 100, - } - }) - .idle_timeout(config.database_connection_idle_timeout) - .max_lifetime(config.database_connection_max_lifetime) - .acquire_timeout(Duration::from_secs_f64( - config.database_connection_acquire_timeout_seconds, - )); - pool_options = add_on_return_to_pool(config, pool_options); - pool_options = add_on_connection_handler(config, pool_options); - pool_options - } } -fn add_on_return_to_pool(config: &AppConfig, pool_options: PoolOptions) -> PoolOptions { - let on_disconnect_file = config.configuration_directory.join(ON_RESET_FILE); - let sql = if on_disconnect_file.exists() { - log::info!( - "Creating a custom SQL connection cleanup handler from {}", - on_disconnect_file.display() - ); - match std::fs::read_to_string(&on_disconnect_file) { - Ok(sql) => { - log::trace!("The custom SQL connection cleanup handler is:\n{sql}"); - Some(std::sync::Arc::new(sql)) - } - Err(e) => { - log::error!( - "Unable to read the file {}: {}", - on_disconnect_file.display(), - e - ); - None - } - } +fn kind_from_database_url(url: &str) -> anyhow::Result { + if url.starts_with("sqlite:") { + Ok(DbKind::Sqlite) + } else if url.starts_with("postgres:") || url.starts_with("postgresql:") { + Ok(DbKind::Postgres) + } else if url.starts_with("mysql:") || url.starts_with("mariadb:") { + Ok(DbKind::MySql) + } else if url.starts_with("mssql:") || url.starts_with("sqlserver:") { + Ok(DbKind::Mssql) + } else if url.starts_with("odbc:") { + Ok(DbKind::Odbc) } else { - log::debug!( - "Not creating a custom SQL connection cleanup handler because {} does not exist", - on_disconnect_file.display() - ); - None - }; + anyhow::bail!("unsupported database URL scheme") + } +} - pool_options.after_release(move |conn, meta| { - let sql = sql.clone(); - Box::pin(async move { - if let Some(sql) = sql { - on_return_to_pool(conn, meta, sql).await +fn default_max_connections(config: &AppConfig, kind: DbKind) -> u32 { + if let Some(max) = config.max_database_pool_connections { + return max; + } + match kind { + DbKind::Postgres | DbKind::Odbc => 50, + DbKind::MySql => 75, + DbKind::Sqlite => { + if config.database_url.contains(":memory:") { + 128 } else { - Ok(true) + 16 } - }) - }) -} - -fn on_return_to_pool( - conn: &mut sqlx::AnyConnection, - meta: sqlx::pool::PoolConnectionMetadata, - sql: std::sync::Arc, -) -> BoxFuture<'_, Result> { - use sqlx::Row; - Box::pin(async move { - log::trace!("Running the custom SQL connection cleanup handler. {meta:?}"); - let query_result = conn.fetch_optional(sql.as_str()).await?; - if let Some(query_result) = query_result { - let is_healthy = query_result.try_get::(0); - log::debug!("Is the connection healthy? {is_healthy:?}"); - is_healthy - } else { - log::debug!("No result from the custom SQL connection cleanup handler"); - Ok(true) } - }) + DbKind::Mssql => 100, + } } -fn add_on_connection_handler( - config: &AppConfig, - pool_options: PoolOptions, -) -> PoolOptions { - let on_connect_file = config.configuration_directory.join(ON_CONNECT_FILE); - let on_connect_file_display = on_connect_file.display().to_string(); - let sql = if on_connect_file.exists() { - log::info!( - "Creating a custom SQL database connection handler from {}", - on_connect_file.display() - ); - match std::fs::read_to_string(&on_connect_file) { - Ok(sql) => { - log::trace!("The custom SQL database connection handler is:\n{sql}"); - Some(std::sync::Arc::new(sql)) - } - Err(e) => { - log::error!( - "Unable to read the file {}: {}", - on_connect_file.display(), - e - ); - None - } - } - } else { +fn read_connection_handler(config: &AppConfig, file_name: &str) -> Option { + let file = config.configuration_directory.join(file_name); + if !file.exists() { log::debug!( - "Not creating a custom SQL database connection handler because {} does not exist", - on_connect_file.display() + "Not creating a custom SQL connection handler because {} does not exist", + file.display() ); - None - }; - - pool_options.after_connect(move |conn, _| { - let sql = sql.clone(); - let on_connect_file_display = on_connect_file_display.clone(); - Box::pin(async move { - if let Some(sql) = sql { - log::debug!("Running {on_connect_file_display} on new connection"); - let r = conn.execute(sql.as_str()).await?; - log::debug!("Finished running connection handler on new connection: {r:?}"); - } - Ok(()) - }) - }) -} - -fn set_custom_connect_options(options: &mut AnyConnectOptions, config: &AppConfig) { - if let Some(sqlite_options) = options.as_sqlite_mut() { - set_custom_connect_options_sqlite(sqlite_options, config); - } - - if let Some(odbc_options) = options.as_odbc_mut() { - set_custom_connect_options_odbc(odbc_options, config); + return None; } -} - -fn set_custom_connect_options_sqlite( - sqlite_options: &mut SqliteConnectOptions, - config: &AppConfig, -) { - for extension_name in &config.sqlite_extensions { - log::info!("Loading SQLite extension: {extension_name}"); - *sqlite_options = std::mem::take(sqlite_options).extension(extension_name.clone()); - } - *sqlite_options = std::mem::take(sqlite_options) - .collation("NOCASE", |a, b| a.to_lowercase().cmp(&b.to_lowercase())) - .function(make_sqlite_fun("upper", str::to_uppercase)) - .function(make_sqlite_fun("lower", str::to_lowercase)); -} - -fn make_sqlite_fun(name: &str, f: fn(&str) -> String) -> Function { - Function::new(name, move |ctx: &SqliteFunctionCtx| { - let arg = ctx.try_get_arg::>(0); - match arg { - Ok(Some(s)) => ctx.set_result(f(s)), - Ok(None) => ctx.set_result(None::), - Err(e) => ctx.set_error(&e.to_string()), + log::info!( + "Creating a custom SQL connection handler from {}", + file.display() + ); + match std::fs::read_to_string(&file) { + Ok(sql) => { + log::trace!("The custom SQL connection handler is:\n{sql}"); + Some(sql) + } + Err(e) => { + log::error!("Unable to read the file {}: {}", file.display(), e); + None } - }) -} - -fn set_custom_connect_options_odbc(odbc_options: &mut OdbcConnectOptions, config: &AppConfig) { - // Allow fetching very large text fields when using ODBC by removing the max column size limit - let batch_size = config.max_pending_rows.clamp(1, 1024); - odbc_options.batch_size(batch_size); - log::trace!("ODBC batch size set to {batch_size}"); - // Disables ODBC batching, but avoids truncation of large text fields - odbc_options.max_column_size(None); -} - -fn set_database_password(options: &mut AnyConnectOptions, password: &str) { - if let Some(opts) = options.as_postgres_mut() { - *opts = take(opts).password(password); - } else if let Some(opts) = options.as_mysql_mut() { - *opts = take(opts).password(password); - } else if let Some(opts) = options.as_mssql_mut() { - *opts = take(opts).password(password); - } else if let Some(_opts) = options.as_odbc_mut() { - log::warn!( - "Setting a password for an ODBC connection is not supported via separate config; include credentials in the DSN or connection string" - ); - } else if let Some(_opts) = options.as_sqlite_mut() { - log::warn!("Setting a password for a SQLite database is not supported"); - } else { - unreachable!("Unsupported database type"); } } diff --git a/src/webserver/database/csv_import.rs b/src/webserver/database/csv_import.rs index 94ac13df..66d047a4 100644 --- a/src/webserver/database/csv_import.rs +++ b/src/webserver/database/csv_import.rs @@ -5,15 +5,11 @@ use futures_util::StreamExt; use sqlparser::ast::{ CopyLegacyCsvOption, CopyLegacyOption, CopyOption, CopySource, CopyTarget, Statement, }; -use sqlx::{ - AnyConnection, Arguments, Executor, PgConnection, - any::{AnyArguments, AnyConnectionKind, AnyKind}, -}; use tokio::io::AsyncRead; use crate::webserver::http_request_info::RequestInfo; -use super::make_placeholder; +use super::{DbConnection, DbKind, DbParam, make_placeholder}; #[derive(Debug, PartialEq)] pub(super) struct CsvImport { @@ -142,7 +138,7 @@ pub(super) fn extract_csv_copy_statement(stmt: &mut Statement) -> Option anyhow::Result<()> { @@ -167,14 +163,7 @@ pub(super) async fn run_csv_import( ) })?; let buffered = tokio::io::BufReader::new(file); - // private_get_mut is not supposed to be used outside of sqlx, but it is the only way to - // access the underlying connection - match db.private_get_mut() { - AnyConnectionKind::Postgres(pg_connection) => { - run_csv_import_postgres(pg_connection, csv_import, buffered).await - } - _ => run_csv_import_insert(db, csv_import, buffered).await, - } + run_csv_import_insert(db, csv_import, buffered).await .with_context(|| { let table_name = &csv_import.table_name; format!( @@ -187,35 +176,8 @@ pub(super) async fn run_csv_import( /// This function does not parse the CSV file, it only sends it to postgres. /// This is the fastest way to import a CSV file into postgres -async fn run_csv_import_postgres( - db: &mut PgConnection, - csv_import: &CsvImport, - file: impl AsyncRead + Unpin + Send, -) -> anyhow::Result<()> { - log::debug!("Running CSV import with postgres"); - let mut copy_transact = db - .copy_in_raw(csv_import.query.as_str()) - .await - .with_context(|| "The postgres COPY FROM STDIN command failed.")?; - log::debug!("Copy transaction created"); - match copy_transact.read_from(file).await { - Ok(_) => { - log::debug!("Copy transaction finished successfully"); - copy_transact.finish().await?; - Ok(()) - } - Err(e) => { - log::debug!("Copy transaction failed with error: {e}"); - copy_transact - .abort("The COPY FROM STDIN command failed.") - .await?; - Err(e.into()) - } - } -} - async fn run_csv_import_insert( - db: &mut AnyConnection, + db: &mut DbConnection, csv_import: &CsvImport, file: impl AsyncRead + Unpin + Send, ) -> anyhow::Result<()> { @@ -256,7 +218,7 @@ async fn compute_column_indices( Ok(col_idxs) } -fn create_insert_stmt(db_kind: AnyKind, csv_import: &CsvImport) -> String { +fn create_insert_stmt(db_kind: DbKind, csv_import: &CsvImport) -> String { let columns = csv_import.columns.join(", "); let placeholders = csv_import .columns @@ -276,20 +238,20 @@ fn create_insert_stmt(db_kind: AnyKind, csv_import: &CsvImport) -> String { async fn process_csv_record( record: csv_async::StringRecord, - db: &mut AnyConnection, + db: &mut DbConnection, insert_stmt: &str, csv_import: &CsvImport, column_indices: &[usize], ) -> anyhow::Result<()> { - let mut arguments = AnyArguments::default(); + let mut arguments = Vec::with_capacity(column_indices.len()); let null_str = csv_import.null_str.as_deref().unwrap_or_default(); for (&i, column) in column_indices.iter().zip(csv_import.columns.iter()) { let value = record.get(i).unwrap_or_default(); let value = if value == null_str { None } else { Some(value) }; log::trace!("CSV value: {column}={value:?}"); - arguments.add(value); + arguments.push(value.map_or(DbParam::Null, |v| DbParam::Text(v.to_string()))); } - db.execute((insert_stmt, Some(arguments))).await?; + db.execute_command(insert_stmt, &arguments).await?; Ok(()) } @@ -328,7 +290,7 @@ fn test_make_statement() { escape: None, uploaded_file: "my_file.csv".into(), }; - let insert_stmt = create_insert_stmt(AnyKind::Postgres, &csv_import); + let insert_stmt = create_insert_stmt(DbKind::Postgres, &csv_import); assert_eq!( insert_stmt, "INSERT INTO my_table (col1, col2) VALUES ($1, $2)" @@ -337,7 +299,7 @@ fn test_make_statement() { #[actix_web::test] async fn test_end_to_end() { - use sqlx::ConnectOptions; + use crate::app_config::tests::test_config; let mut copy_stmt = sqlparser::parser::Parser::parse_sql( &sqlparser::dialect::GenericDialect {}, @@ -362,13 +324,11 @@ async fn test_end_to_end() { uploaded_file: "my_file.csv".into(), } ); - let mut conn = "sqlite::memory:" - .parse::() - .unwrap() - .connect() + let db = crate::webserver::Database::init(&test_config()) .await .unwrap(); - conn.execute("CREATE TABLE my_table (col1 TEXT, col2 TEXT)") + let mut conn = db.connection.acquire().await.unwrap(); + conn.execute_command("CREATE TABLE my_table (col1 TEXT, col2 TEXT)", &[]) .await .unwrap(); let csv = "col2;col1\na;b\nc;d"; // order is different from the table @@ -376,10 +336,23 @@ async fn test_end_to_end() { run_csv_import_insert(&mut conn, &csv_import, file) .await .unwrap(); - let rows: Vec<(String, String)> = sqlx::query_as("SELECT * FROM my_table") - .fetch_all(&mut conn) - .await - .unwrap(); + let rows = conn.execute("SELECT * FROM my_table", &[]).await.unwrap(); + let rows: Vec<(String, String)> = rows + .into_iter() + .filter_map(|item| match item { + super::driver::DbStatementResult::Row(row) => Some(( + match &row.values[0] { + super::driver::DbValue::Text(s) => s.clone(), + other => format!("{other:?}"), + }, + match &row.values[1] { + super::driver::DbValue::Text(s) => s.clone(), + other => format!("{other:?}"), + }, + )), + super::driver::DbStatementResult::Finished => None, + }) + .collect(); assert_eq!( rows, vec![("b".into(), "a".into()), ("d".into(), "c".into())] diff --git a/src/webserver/database/driver.rs b/src/webserver/database/driver.rs new file mode 100644 index 00000000..258c9f08 --- /dev/null +++ b/src/webserver/database/driver.rs @@ -0,0 +1,1303 @@ +use std::borrow::Cow; +use std::fmt; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Mutex; +use std::sync::{ + Arc, + atomic::{AtomicU32, Ordering}, +}; +use std::time::Duration; + +use anyhow::Context; +use chrono::{Datelike, Timelike}; +use futures_util::stream::Stream; +use mysql_async::prelude::Queryable; +use odbc_api::parameter::{InputParameter, VarCharBox, WithDataType}; +use odbc_api::{ConnectionOptions, Cursor, IntoParameter, ResultSetMetadata}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; +use tokio_rusqlite::rusqlite; +use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt}; + +use crate::app_config::AppConfig; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DbKind { + Sqlite, + Postgres, + MySql, + Mssql, + Odbc, +} + +#[derive(Debug, Clone)] +pub enum DbParam { + Null, + Bool(bool), + Integer(i64), + Text(String), + Bytes(Vec), + Timestamp(chrono::DateTime), +} + +impl From> for DbParam { + fn from(value: Option) -> Self { + value.map_or(Self::Null, Self::Text) + } +} + +#[derive(Debug, Clone)] +pub enum DbValue { + Null, + Bool(bool), + Integer(i64), + Real(f64), + Text(String), + Bytes(Vec), +} + +#[derive(Debug, Clone)] +pub struct DbColumn { + pub name: String, + pub type_name: Option, +} + +#[derive(Debug, Clone)] +pub struct DbRow { + pub columns: Vec, + pub values: Vec, + pub kind: DbKind, +} + +#[derive(Debug, Clone)] +pub enum DbStatementResult { + Finished, + Row(DbRow), +} + +#[derive(Debug)] +pub enum DbError { + PoolTimedOut, + UnsupportedBackend(DbKind), + Database { + message: String, + offset: Option, + }, +} + +impl fmt::Display for DbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PoolTimedOut => write!(f, "database connection pool timed out"), + Self::UnsupportedBackend(kind) => { + write!(f, "database backend {kind:?} is not implemented yet") + } + Self::Database { message, .. } => f.write_str(message), + } + } +} + +impl std::error::Error for DbError {} + +impl From for DbError { + fn from(error: rusqlite::Error) -> Self { + Self::Database { + message: error.to_string(), + offset: None, + } + } +} + +impl From for DbError { + fn from(error: tokio_rusqlite::Error) -> Self { + Self::Database { + message: error.to_string(), + offset: None, + } + } +} + +impl From for DbError { + fn from(error: tokio_postgres::Error) -> Self { + db_error(error) + } +} + +impl From for DbError { + fn from(error: mysql_async::Error) -> Self { + db_error(error) + } +} + +impl From for DbError { + fn from(error: tiberius::error::Error) -> Self { + db_error(error) + } +} + +fn db_error(error: impl std::error::Error) -> DbError { + DbError::Database { + message: error.to_string(), + offset: None, + } +} + +#[derive(Clone)] +pub struct DbPool { + inner: Arc, +} + +struct DbPoolInner { + url: String, + kind: DbKind, + max_size: u32, + acquire_timeout: Duration, + semaphore: Arc, + active: AtomicU32, + on_connect_sql: Option>, + sqlite_extensions: Vec, + idle_sqlite: Mutex>, +} + +pub struct DbConnection { + inner: NativeConnection, + pool: Arc, + _permit: OwnedSemaphorePermit, +} + +enum NativeConnection { + Sqlite(tokio_rusqlite::Connection), + Postgres(tokio_postgres::Client), + MySql(mysql_async::Conn), + Mssql(Box>>), + Odbc(odbc_api::Connection<'static>), + Closed, +} + +impl Drop for DbConnection { + fn drop(&mut self) { + let inner = std::mem::replace(&mut self.inner, NativeConnection::Closed); + if let NativeConnection::Sqlite(conn) = inner + && let Ok(mut idle) = self.pool.idle_sqlite.lock() + && idle.is_none() + { + *idle = Some(conn); + } + self.pool.active.fetch_sub(1, Ordering::Relaxed); + } +} + +impl DbPool { + pub fn new( + config: &AppConfig, + kind: DbKind, + max_size: u32, + on_connect_sql: Option, + ) -> Self { + let url = if kind == DbKind::Sqlite { + normalize_sqlite_url(&config.database_url) + } else { + config.database_url.clone() + }; + Self { + inner: Arc::new(DbPoolInner { + url, + kind, + max_size, + acquire_timeout: Duration::from_secs_f64( + config.database_connection_acquire_timeout_seconds, + ), + semaphore: Arc::new(Semaphore::new( + usize::try_from(max_size).unwrap_or(usize::MAX), + )), + active: AtomicU32::new(0), + on_connect_sql: on_connect_sql.map(Arc::new), + sqlite_extensions: config.sqlite_extensions.clone(), + idle_sqlite: Mutex::new(None), + }), + } + } + + #[must_use] + pub fn kind(&self) -> DbKind { + self.inner.kind + } + + #[must_use] + pub fn size(&self) -> u32 { + self.inner.active.load(Ordering::Relaxed) + } + + #[must_use] + pub fn num_idle(&self) -> u32 { + 0 + } + + #[must_use] + pub fn max_size(&self) -> u32 { + self.inner.max_size + } + + pub fn close(&self) {} + + pub async fn acquire(&self) -> Result { + let permit = tokio::time::timeout( + self.inner.acquire_timeout, + self.inner.semaphore.clone().acquire_owned(), + ) + .await + .map_err(|_| DbError::PoolTimedOut)? + .map_err(|_| DbError::PoolTimedOut)?; + self.inner.active.fetch_add(1, Ordering::Relaxed); + if self.inner.kind == DbKind::Sqlite + && let Ok(mut idle) = self.inner.idle_sqlite.lock() + && let Some(conn) = idle.take() + { + return Ok(DbConnection { + inner: NativeConnection::Sqlite(conn), + pool: self.inner.clone(), + _permit: permit, + }); + } + match self.inner.connect().await { + Ok(inner) => Ok(DbConnection { + inner, + pool: self.inner.clone(), + _permit: permit, + }), + Err(e) => { + self.inner.active.fetch_sub(1, Ordering::Relaxed); + Err(e) + } + } + } +} + +impl DbPoolInner { + async fn connect(&self) -> Result { + let mut conn = match self.kind { + DbKind::Sqlite => NativeConnection::Sqlite(open_sqlite(&self.url).await?), + DbKind::Postgres => NativeConnection::Postgres(open_postgres(&self.url).await?), + DbKind::MySql => NativeConnection::MySql(open_mysql(&self.url).await?), + DbKind::Mssql => NativeConnection::Mssql(Box::new(open_mssql(&self.url).await?)), + DbKind::Odbc => NativeConnection::Odbc(open_odbc(&self.url)?), + }; + conn.configure(self).await?; + if let Some(sql) = &self.on_connect_sql { + conn.execute(sql, &[]).await?; + } + Ok(conn) + } +} + +impl DbConnection { + #[must_use] + pub fn kind(&self) -> DbKind { + self.pool.kind + } + + pub async fn dbms_name(&mut self) -> Result { + match &mut self.inner { + NativeConnection::Sqlite(_) => Ok("SQLite".to_string()), + NativeConnection::Postgres(client) => { + let row = client.query_one("SELECT version()", &[]).await?; + let version: String = row.try_get(0)?; + if version.starts_with("PostgreSQL") { + Ok("PostgreSQL".to_string()) + } else { + Ok(version) + } + } + NativeConnection::MySql(_) => Ok("MySQL".to_string()), + NativeConnection::Mssql(_) => Ok("Microsoft SQL Server".to_string()), + NativeConnection::Odbc(conn) => { + conn.database_management_system_name().map_err(db_error) + } + NativeConnection::Closed => Err(DbError::Database { + message: "database connection is closed".to_string(), + offset: None, + }), + } + } + + pub async fn execute( + &mut self, + sql: &str, + params: &[DbParam], + ) -> Result, DbError> { + self.inner.execute(sql, params).await + } + + pub fn execute_stream<'a>( + &'a mut self, + sql: &'a str, + params: &'a [DbParam], + ) -> Pin> + 'a>> { + self.inner.execute_stream(sql, params) + } + + pub async fn execute_command(&mut self, sql: &str, params: &[DbParam]) -> Result<(), DbError> { + let _ = self.execute(sql, params).await?; + Ok(()) + } + + pub async fn execute_batch(&mut self, sql: &str) -> Result<(), DbError> { + self.inner.execute_batch(sql).await + } + + pub async fn fetch_optional( + &mut self, + sql: &str, + params: &[DbParam], + ) -> Result, DbError> { + let results = self.execute(sql, params).await?; + Ok(results.into_iter().find_map(|item| match item { + DbStatementResult::Row(row) => Some(row), + DbStatementResult::Finished => None, + })) + } +} + +impl NativeConnection { + async fn configure(&mut self, pool: &DbPoolInner) -> Result<(), DbError> { + match self { + Self::Sqlite(conn) => configure_sqlite(conn, &pool.sqlite_extensions).await, + Self::Postgres(_) | Self::MySql(_) | Self::Mssql(_) | Self::Odbc(_) | Self::Closed => { + Ok(()) + } + } + } + + async fn execute( + &mut self, + sql: &str, + params: &[DbParam], + ) -> Result, DbError> { + match self { + Self::Sqlite(conn) => execute_sqlite(conn, sql, params).await, + Self::Postgres(client) => execute_postgres(client, sql, params).await, + Self::MySql(conn) => execute_mysql(conn, sql, params).await, + Self::Mssql(client) => execute_mssql(client, sql, params).await, + Self::Odbc(conn) => execute_odbc(conn, sql, params), + Self::Closed => Err(DbError::Database { + message: "database connection is closed".to_string(), + offset: None, + }), + } + } + + fn execute_stream<'a>( + &'a mut self, + sql: &'a str, + params: &'a [DbParam], + ) -> Pin> + 'a>> { + match self { + Self::Sqlite(conn) => stream_sqlite(conn, sql, params), + _ => Box::pin(async_stream::try_stream! { + for item in self.execute(sql, params).await? { + yield item; + } + }), + } + } + + async fn execute_batch(&mut self, sql: &str) -> Result<(), DbError> { + match self { + Self::Sqlite(conn) => execute_sqlite_batch(conn, sql).await, + Self::Postgres(client) => client.batch_execute(sql).await.map_err(Into::into), + Self::MySql(conn) => { + conn.query_drop(sql).await?; + Ok(()) + } + Self::Mssql(client) => { + client.simple_query(sql).await?.into_results().await?; + Ok(()) + } + Self::Odbc(conn) => { + let _ = conn.execute(sql, (), None).map_err(db_error)?; + Ok(()) + } + Self::Closed => Err(DbError::Database { + message: "database connection is closed".to_string(), + offset: None, + }), + } + } +} + +async fn open_sqlite(url: &str) -> Result { + let sqlite_path = sqlite_path_from_url(url).map_err(|e| DbError::Database { + message: e.to_string(), + offset: None, + })?; + let flags = rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE + | rusqlite::OpenFlags::SQLITE_OPEN_CREATE + | rusqlite::OpenFlags::SQLITE_OPEN_URI; + if sqlite_path == ":memory:" { + tokio_rusqlite::Connection::open_in_memory_with_flags(flags) + .await + .map_err(Into::into) + } else { + tokio_rusqlite::Connection::open_with_flags(sqlite_path, flags) + .await + .map_err(Into::into) + } +} + +async fn open_postgres(url: &str) -> Result { + let (client, connection) = tokio_postgres::connect(url, tokio_postgres::NoTls).await?; + tokio::spawn(async move { + if let Err(error) = connection.await { + log::debug!("PostgreSQL connection task finished with error: {error}"); + } + }); + Ok(client) +} + +async fn open_mysql(url: &str) -> Result { + mysql_async::Conn::from_url(url).await.map_err(Into::into) +} + +async fn open_mssql(url: &str) -> Result>, DbError> { + let mut config = mssql_config_from_url(url)?; + config.trust_cert(); + let tcp = tokio::net::TcpStream::connect(config.get_addr()) + .await + .map_err(db_error)?; + tcp.set_nodelay(true).map_err(db_error)?; + tiberius::Client::connect(config, tcp.compat_write()) + .await + .map_err(Into::into) +} + +fn open_odbc(url: &str) -> Result, DbError> { + let conn_str = odbc_connection_string(url); + odbc_api::environment() + .map_err(db_error)? + .connect_with_connection_string(&conn_str, ConnectionOptions::default()) + .map_err(db_error) +} + +fn odbc_connection_string(url: &str) -> String { + let trimmed = url.trim().strip_prefix("odbc:").unwrap_or(url.trim()); + if trimmed.contains('=') { + trimmed.to_string() + } else { + format!("DSN={trimmed}") + } +} + +fn mssql_config_from_url(url: &str) -> Result { + if url.starts_with("jdbc:") { + return tiberius::Config::from_jdbc_string(url).map_err(Into::into); + } + if url.contains('=') { + return tiberius::Config::from_ado_string(url).map_err(Into::into); + } + + let without_scheme = url + .strip_prefix("mssql://") + .or_else(|| url.strip_prefix("sqlserver://")) + .ok_or_else(|| DbError::Database { + message: format!("not a SQL Server URL: {url}"), + offset: None, + })?; + let (authority, database) = without_scheme + .split_once('/') + .map_or((without_scheme, ""), |(authority, rest)| { + (authority, rest.split(['?', '#']).next().unwrap_or("")) + }); + let (credentials, host_port) = authority + .rsplit_once('@') + .map_or(("", authority), |(credentials, host_port)| { + (credentials, host_port) + }); + let (user, password) = credentials.split_once(':').unwrap_or((credentials, "")); + let (host, port) = parse_host_port(host_port); + + let mut config = tiberius::Config::new(); + config.host(decode_url_part(host)?); + if let Some(port) = port { + config.port(port); + } + if !database.is_empty() { + config.database(decode_url_part(database)?); + } + if !user.is_empty() { + config.authentication(tiberius::AuthMethod::sql_server( + decode_url_part(user)?, + decode_url_part(password)?, + )); + } + Ok(config) +} + +fn parse_host_port(host_port: &str) -> (&str, Option) { + let Some((host, port)) = host_port.rsplit_once(':') else { + return (host_port, None); + }; + match port.parse::() { + Ok(port) => (host, Some(port)), + Err(_) => (host_port, None), + } +} + +fn decode_url_part(value: &str) -> Result { + percent_encoding::percent_decode_str(value) + .decode_utf8() + .map(std::borrow::Cow::into_owned) + .map_err(db_error) +} + +fn sqlite_path_from_url(url: &str) -> anyhow::Result { + let Some(rest) = url.strip_prefix("sqlite:") else { + anyhow::bail!("not a sqlite URL: {url}"); + }; + let rest = rest.strip_prefix("//").unwrap_or(rest); + let decoded = percent_encoding::percent_decode_str(rest) + .decode_utf8() + .with_context(|| format!("invalid percent encoding in sqlite URL {url:?}"))?; + let decoded = decoded.into_owned(); + if decoded.contains('?') && !decoded.starts_with("file:") { + Ok(format!("file:{decoded}")) + } else { + Ok(decoded) + } +} + +fn normalize_sqlite_url(url: &str) -> String { + static MEMORY_DB_ID: AtomicU32 = AtomicU32::new(0); + let Ok(path) = sqlite_path_from_url(url) else { + return url.to_string(); + }; + if path == ":memory:" || path.starts_with("file::memory:") { + let id = MEMORY_DB_ID.fetch_add(1, Ordering::Relaxed); + format!("sqlite://file:sqlpage_memory_{id}?mode=memory&cache=shared") + } else { + url.to_string() + } +} + +async fn configure_sqlite( + conn: &tokio_rusqlite::Connection, + extensions: &[String], +) -> Result<(), DbError> { + let extensions = extensions.to_vec(); + conn.call(move |conn| { + conn.create_collation("NOCASE", |a, b| a.to_lowercase().cmp(&b.to_lowercase()))?; + conn.create_scalar_function( + "upper", + 1, + rusqlite::functions::FunctionFlags::SQLITE_UTF8 + | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, + |ctx| { + let arg = ctx.get::>(0)?; + Ok(arg.map(|s| s.to_uppercase())) + }, + )?; + conn.create_scalar_function( + "lower", + 1, + rusqlite::functions::FunctionFlags::SQLITE_UTF8 + | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, + |ctx| { + let arg = ctx.get::>(0)?; + Ok(arg.map(|s| s.to_lowercase())) + }, + )?; + if !extensions.is_empty() { + unsafe { conn.load_extension_enable()? }; + for extension in extensions { + unsafe { conn.load_extension(PathBuf::from(extension), None::<&str>)? }; + } + conn.load_extension_disable()?; + } + Ok::<_, rusqlite::Error>(()) + }) + .await + .map_err(Into::into) +} + +async fn execute_sqlite( + conn: &tokio_rusqlite::Connection, + sql: &str, + params: &[DbParam], +) -> Result, DbError> { + let sql = sql.to_string(); + let params = params.to_vec(); + conn.call(move |conn| { + let mut stmt = conn.prepare(&sql)?; + let values = params + .into_iter() + .map(sqlite_value_from_param) + .collect::>(); + let column_count = stmt.column_count(); + if column_count == 0 { + if values.is_empty() { + drop(stmt); + conn.execute_batch(&sql)?; + } else { + stmt.execute(rusqlite::params_from_iter(values))?; + } + return Ok(vec![DbStatementResult::Finished]); + } + let columns = (0..column_count) + .map(|idx| DbColumn { + name: stmt.column_name(idx).unwrap_or("").to_string(), + type_name: None, + }) + .collect::>(); + let mut rows = stmt.query(rusqlite::params_from_iter(values))?; + let mut result = Vec::new(); + while let Some(row) = rows.next()? { + let mut values = Vec::with_capacity(column_count); + for idx in 0..column_count { + values.push(sqlite_value(row.get_ref(idx)?)); + } + result.push(DbStatementResult::Row(DbRow { + columns: columns.clone(), + values, + kind: DbKind::Sqlite, + })); + } + Ok(result) + }) + .await + .map_err(Into::into) +} + +fn stream_sqlite<'a>( + conn: &'a tokio_rusqlite::Connection, + sql: &str, + params: &[DbParam], +) -> Pin> + 'a>> { + let sql = sql.to_string(); + let params = params.to_vec(); + Box::pin(async_stream::try_stream! { + let (tx, mut rx) = tokio::sync::mpsc::channel(32); + let call = conn.call(move |conn| { + let mut stmt = conn.prepare(&sql).map_err(DbError::from)?; + let values = params + .into_iter() + .map(sqlite_value_from_param) + .collect::>(); + let column_count = stmt.column_count(); + if column_count == 0 { + if values.is_empty() { + drop(stmt); + conn.execute_batch(&sql).map_err(DbError::from)?; + } else { + stmt.execute(rusqlite::params_from_iter(values)) + .map_err(DbError::from)?; + } + let _ = tx.blocking_send(DbStatementResult::Finished); + return Ok(()); + } + let columns = (0..column_count) + .map(|idx| DbColumn { + name: stmt.column_name(idx).unwrap_or("").to_string(), + type_name: None, + }) + .collect::>(); + let mut rows = stmt + .query(rusqlite::params_from_iter(values)) + .map_err(DbError::from)?; + while let Some(row) = rows.next().map_err(DbError::from)? { + let mut values = Vec::with_capacity(column_count); + for idx in 0..column_count { + values.push(sqlite_value(row.get_ref(idx).map_err(DbError::from)?)); + } + if tx + .blocking_send(DbStatementResult::Row(DbRow { + columns: columns.clone(), + values, + kind: DbKind::Sqlite, + })) + .is_err() + { + return Ok(()); + } + } + Ok(()) + }); + tokio::pin!(call); + let mut call_finished = false; + let mut call_result = None; + loop { + tokio::select! { + result = &mut call, if !call_finished => { + call_finished = true; + call_result = Some(sqlite_call_result(result)); + } + maybe_item = rx.recv() => { + match maybe_item { + Some(item) => yield item, + None => break, + } + } + } + } + if !call_finished { + call_result = Some(sqlite_call_result(call.await)); + } + if let Some(result) = call_result { + result?; + } + }) +} + +fn sqlite_call_result(result: Result<(), tokio_rusqlite::Error>) -> Result<(), DbError> { + match result { + Ok(()) => Ok(()), + Err(tokio_rusqlite::Error::Error(error)) => Err(error), + Err(error) => Err(DbError::Database { + message: error.to_string(), + offset: None, + }), + } +} + +async fn execute_sqlite_batch(conn: &tokio_rusqlite::Connection, sql: &str) -> Result<(), DbError> { + let sql = sql.to_string(); + conn.call(move |conn| conn.execute_batch(&sql)) + .await + .map_err(Into::into) +} + +async fn execute_postgres( + client: &tokio_postgres::Client, + sql: &str, + params: &[DbParam], +) -> Result, DbError> { + let params = params.iter().map(PgParam::from).collect::>(); + let param_refs = params + .iter() + .map(|param| param as &(dyn tokio_postgres::types::ToSql + Sync)) + .collect::>(); + let rows = client.query(sql, ¶m_refs).await?; + if rows.is_empty() { + return Ok(vec![DbStatementResult::Finished]); + } + Ok(rows + .into_iter() + .map(|row| DbStatementResult::Row(postgres_row(&row))) + .collect()) +} + +#[derive(Debug)] +enum PgParam { + Null(Option), + Bool(bool), + Integer(i64), + Text(String), + Bytes(Vec), + Timestamp(chrono::DateTime), +} + +impl From<&DbParam> for PgParam { + fn from(value: &DbParam) -> Self { + match value { + DbParam::Null => Self::Null(None), + DbParam::Bool(value) => Self::Bool(*value), + DbParam::Integer(value) => Self::Integer(*value), + DbParam::Text(value) => Self::Text(value.clone()), + DbParam::Bytes(value) => Self::Bytes(value.clone()), + DbParam::Timestamp(value) => Self::Timestamp(*value), + } + } +} + +impl tokio_postgres::types::ToSql for PgParam { + fn to_sql( + &self, + ty: &tokio_postgres::types::Type, + out: &mut bytes::BytesMut, + ) -> Result> + where + Self: Sized, + { + match self { + Self::Null(value) => value.to_sql(ty, out), + Self::Bool(value) => value.to_sql(ty, out), + Self::Integer(value) => value.to_sql(ty, out), + Self::Text(value) => value.to_sql(ty, out), + Self::Bytes(value) => value.to_sql(ty, out), + Self::Timestamp(value) => value.to_sql(ty, out), + } + } + + fn accepts(_ty: &tokio_postgres::types::Type) -> bool + where + Self: Sized, + { + true + } + + tokio_postgres::types::to_sql_checked!(); +} + +fn postgres_row(row: &tokio_postgres::Row) -> DbRow { + let columns = row + .columns() + .iter() + .map(|column| DbColumn { + name: column.name().to_string(), + type_name: Some(column.type_().name().to_string()), + }) + .collect::>(); + let values = row + .columns() + .iter() + .enumerate() + .map(|(idx, column)| postgres_value(row, idx, column.type_())) + .collect::>(); + DbRow { + columns, + values, + kind: DbKind::Postgres, + } +} + +fn postgres_value( + row: &tokio_postgres::Row, + idx: usize, + ty: &tokio_postgres::types::Type, +) -> DbValue { + use tokio_postgres::types::Type; + if row.try_get::<_, Option>(idx).ok() == Some(None) { + return DbValue::Null; + } + match *ty { + Type::BOOL => row + .try_get::<_, bool>(idx) + .map_or(DbValue::Null, DbValue::Bool), + Type::INT2 => row + .try_get::<_, i16>(idx) + .map_or(DbValue::Null, |value| DbValue::Integer(i64::from(value))), + Type::INT4 => row + .try_get::<_, i32>(idx) + .map_or(DbValue::Null, |value| DbValue::Integer(i64::from(value))), + Type::INT8 => row + .try_get::<_, i64>(idx) + .map_or(DbValue::Null, DbValue::Integer), + Type::FLOAT4 => row + .try_get::<_, f32>(idx) + .map_or(DbValue::Null, |value| DbValue::Real(f64::from(value))), + Type::FLOAT8 => row + .try_get::<_, f64>(idx) + .map_or(DbValue::Null, DbValue::Real), + Type::BYTEA => row + .try_get::<_, Vec>(idx) + .map_or(DbValue::Null, DbValue::Bytes), + Type::TIMESTAMPTZ => row + .try_get::<_, chrono::DateTime>(idx) + .map_or(DbValue::Null, |value| DbValue::Text(value.to_rfc3339())), + Type::TIMESTAMP => row + .try_get::<_, chrono::NaiveDateTime>(idx) + .map_or(DbValue::Null, |value| DbValue::Text(value.to_string())), + Type::DATE => row + .try_get::<_, chrono::NaiveDate>(idx) + .map_or(DbValue::Null, |value| DbValue::Text(value.to_string())), + Type::JSON | Type::JSONB => row + .try_get::<_, serde_json::Value>(idx) + .map_or(DbValue::Null, |value| DbValue::Text(value.to_string())), + Type::UUID => row + .try_get::<_, uuid::Uuid>(idx) + .map_or(DbValue::Null, |value| DbValue::Text(value.to_string())), + _ => row + .try_get::<_, String>(idx) + .map_or(DbValue::Null, DbValue::Text), + } +} + +async fn execute_mysql( + conn: &mut mysql_async::Conn, + sql: &str, + params: &[DbParam], +) -> Result, DbError> { + let values = params + .iter() + .map(mysql_value_from_param) + .collect::>(); + if values.is_empty() { + collect_mysql_results(conn.query_iter(sql).await?).await + } else { + collect_mysql_results( + conn.exec_iter(sql, mysql_async::Params::Positional(values)) + .await?, + ) + .await + } +} + +async fn collect_mysql_results

( + mut query_result: mysql_async::QueryResult<'_, 'static, P>, +) -> Result, DbError> +where + P: mysql_async::prelude::Protocol, +{ + let mut result = Vec::new(); + loop { + let columns = query_result + .columns_ref() + .iter() + .map(|column| DbColumn { + name: column.name_str().into_owned(), + type_name: Some(format!("{:?}", column.column_type())), + }) + .collect::>(); + while let Some(row) = query_result.next().await? { + let values = row + .unwrap_raw() + .into_iter() + .map(mysql_value) + .collect::>(); + result.push(DbStatementResult::Row(DbRow { + columns: columns.clone(), + values, + kind: DbKind::MySql, + })); + } + if query_result.is_empty() { + break; + } + } + query_result.drop_result().await?; + if result.is_empty() { + result.push(DbStatementResult::Finished); + } + Ok(result) +} + +fn mysql_value_from_param(param: &DbParam) -> mysql_async::Value { + match param { + DbParam::Null => mysql_async::Value::NULL, + DbParam::Bool(value) => mysql_async::Value::Int(i64::from(*value)), + DbParam::Integer(value) => mysql_async::Value::Int(*value), + DbParam::Text(value) => mysql_async::Value::Bytes(value.clone().into_bytes()), + DbParam::Bytes(value) => mysql_async::Value::Bytes(value.clone()), + DbParam::Timestamp(value) => { + let value = value.naive_utc(); + mysql_async::Value::Date( + u16::try_from(value.year()).unwrap_or_default(), + u8::try_from(value.month()).unwrap_or_default(), + u8::try_from(value.day()).unwrap_or_default(), + u8::try_from(value.hour()).unwrap_or_default(), + u8::try_from(value.minute()).unwrap_or_default(), + u8::try_from(value.second()).unwrap_or_default(), + value.nanosecond() / 1000, + ) + } + } +} + +fn mysql_value(value: Option) -> DbValue { + use mysql_async::Value; + match value { + None | Some(Value::NULL) => DbValue::Null, + Some(Value::Int(value)) => DbValue::Integer(value), + Some(Value::UInt(value)) => { + i64::try_from(value).map_or_else(|_| DbValue::Text(value.to_string()), DbValue::Integer) + } + Some(Value::Float(value)) => DbValue::Real(f64::from(value)), + Some(Value::Double(value)) => DbValue::Real(value), + Some(Value::Bytes(bytes)) => String::from_utf8(bytes) + .map_or_else(|err| DbValue::Bytes(err.into_bytes()), DbValue::Text), + Some(Value::Date(year, month, day, hour, minute, second, micros)) => DbValue::Text( + format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{micros:06}"), + ), + Some(Value::Time(negative, days, hours, minutes, seconds, micros)) => { + DbValue::Text(format!( + "{}{:03}:{minutes:02}:{seconds:02}.{micros:06}", + if negative { "-" } else { "" }, + days * 24 + u32::from(hours) + )) + } + } +} + +async fn execute_mssql( + client: &mut tiberius::Client>, + sql: &str, + params: &[DbParam], +) -> Result, DbError> { + let params = params.iter().map(MssqlParam::from).collect::>(); + let param_refs = params + .iter() + .map(|param| param as &dyn tiberius::ToSql) + .collect::>(); + let rows = client.query(sql, ¶m_refs).await?.into_results().await?; + let mut result = Vec::new(); + for set in rows { + for row in set { + result.push(DbStatementResult::Row(mssql_row(row))); + } + } + if result.is_empty() { + result.push(DbStatementResult::Finished); + } + Ok(result) +} + +enum MssqlParam { + Text(Option), + Bool(bool), + Integer(i64), + Bytes(Vec), + Timestamp(chrono::NaiveDateTime), +} + +impl From<&DbParam> for MssqlParam { + fn from(value: &DbParam) -> Self { + match value { + DbParam::Null => Self::Text(None), + DbParam::Bool(value) => Self::Bool(*value), + DbParam::Integer(value) => Self::Integer(*value), + DbParam::Text(value) => Self::Text(Some(value.clone())), + DbParam::Bytes(value) => Self::Bytes(value.clone()), + DbParam::Timestamp(value) => Self::Timestamp(value.naive_utc()), + } + } +} + +impl tiberius::ToSql for MssqlParam { + fn to_sql(&self) -> tiberius::ColumnData<'_> { + match self { + Self::Text(value) => tiberius::ColumnData::String(value.as_ref().map(Cow::from)), + Self::Bool(value) => tiberius::ColumnData::Bit(Some(*value)), + Self::Integer(value) => tiberius::ColumnData::I64(Some(*value)), + Self::Bytes(value) => tiberius::ColumnData::Binary(Some(Cow::from(value))), + Self::Timestamp(value) => value.to_sql(), + } + } +} + +fn mssql_row(row: tiberius::Row) -> DbRow { + let columns = row + .columns() + .iter() + .map(|column| DbColumn { + name: column.name().to_string(), + type_name: Some(format!("{:?}", column.column_type())), + }) + .collect::>(); + let values = row.into_iter().map(mssql_value).collect::>(); + DbRow { + columns, + values, + kind: DbKind::Mssql, + } +} + +fn mssql_value(value: tiberius::ColumnData<'static>) -> DbValue { + use tiberius::ColumnData; + match value { + ColumnData::U8(value) => { + value.map_or(DbValue::Null, |value| DbValue::Integer(i64::from(value))) + } + ColumnData::I16(value) => { + value.map_or(DbValue::Null, |value| DbValue::Integer(i64::from(value))) + } + ColumnData::I32(value) => { + value.map_or(DbValue::Null, |value| DbValue::Integer(i64::from(value))) + } + ColumnData::I64(value) => value.map_or(DbValue::Null, DbValue::Integer), + ColumnData::F32(value) => { + value.map_or(DbValue::Null, |value| DbValue::Real(f64::from(value))) + } + ColumnData::F64(value) => value.map_or(DbValue::Null, DbValue::Real), + ColumnData::Bit(value) => value.map_or(DbValue::Null, DbValue::Bool), + ColumnData::String(value) => { + value.map_or(DbValue::Null, |value| DbValue::Text(value.into_owned())) + } + ColumnData::Binary(value) => { + value.map_or(DbValue::Null, |value| DbValue::Bytes(value.into_owned())) + } + ColumnData::Guid(value) => { + value.map_or(DbValue::Null, |value| DbValue::Text(value.to_string())) + } + ColumnData::Numeric(value) => { + value.map_or(DbValue::Null, |value| DbValue::Text(value.to_string())) + } + other => DbValue::Text(format!("{other:?}")), + } +} + +fn execute_odbc( + conn: &mut odbc_api::Connection<'static>, + sql: &str, + params: &[DbParam], +) -> Result, DbError> { + let parameters = OdbcParameters::from_params(params); + let cursor = if parameters.is_empty() { + conn.execute(sql, (), None).map_err(db_error)? + } else { + conn.execute(sql, parameters.as_slice(), None) + .map_err(db_error)? + }; + let Some(cursor) = cursor else { + return Ok(vec![DbStatementResult::Finished]); + }; + collect_odbc_rows(cursor) +} + +struct OdbcParameters { + values: Vec>, +} + +impl OdbcParameters { + fn from_params(params: &[DbParam]) -> Self { + Self { + values: params.iter().map(odbc_parameter).collect(), + } + } + + fn is_empty(&self) -> bool { + self.values.is_empty() + } + + fn as_slice(&self) -> &[Box] { + &self.values + } +} + +fn odbc_parameter(param: &DbParam) -> Box { + match param { + DbParam::Null => Box::new(WithDataType::new( + VarCharBox::null(), + odbc_api::DataType::Varchar { length: None }, + )), + DbParam::Text(value) => Box::new(value.clone().into_parameter()), + DbParam::Bool(value) => Box::new(i32::from(*value).into_parameter()), + DbParam::Integer(value) => Box::new((*value).into_parameter()), + DbParam::Bytes(value) => Box::new(value.clone().into_parameter()), + DbParam::Timestamp(value) => Box::new( + WithDataType::new( + odbc_api::Nullable::new(odbc_api::sys::Timestamp { + year: i16::try_from(value.year()).unwrap_or_default(), + month: u16::try_from(value.month()).unwrap_or_default(), + day: u16::try_from(value.day()).unwrap_or_default(), + hour: u16::try_from(value.hour()).unwrap_or_default(), + minute: u16::try_from(value.minute()).unwrap_or_default(), + second: u16::try_from(value.second()).unwrap_or_default(), + fraction: value.nanosecond(), + }), + odbc_api::DataType::Timestamp { precision: 6 }, + ) + .into_parameter(), + ), + } +} + +fn collect_odbc_rows(mut cursor: C) -> Result, DbError> +where + C: Cursor + ResultSetMetadata, +{ + let column_count = cursor.num_result_cols().map_err(db_error)?; + let column_count = usize::try_from(column_count).map_err(db_error)?; + let mut columns = Vec::with_capacity(column_count); + for idx in 0..column_count { + let mut description = odbc_api::ColumnDescription::default(); + let column_number = u16::try_from(idx + 1).map_err(db_error)?; + cursor + .describe_col(column_number, &mut description) + .map_err(db_error)?; + columns.push(DbColumn { + name: description + .name_to_string() + .unwrap_or_else(|_| format!("col{idx}")), + type_name: Some(format!("{:?}", description.data_type)), + }); + } + let mut result = Vec::new(); + while let Some(mut row) = cursor.next_row().map_err(db_error)? { + let mut values = Vec::with_capacity(column_count); + for idx in 0..column_count { + let column_number = u16::try_from(idx + 1).map_err(db_error)?; + let mut value = Vec::new(); + if row + .get_binary(column_number, &mut value) + .map_err(db_error)? + { + values.push( + String::from_utf8(value) + .map_or_else(|err| DbValue::Bytes(err.into_bytes()), DbValue::Text), + ); + } else { + values.push(DbValue::Null); + } + } + result.push(DbStatementResult::Row(DbRow { + columns: columns.clone(), + values, + kind: DbKind::Odbc, + })); + } + if result.is_empty() { + result.push(DbStatementResult::Finished); + } + Ok(result) +} + +fn sqlite_value_from_param(param: DbParam) -> rusqlite::types::Value { + match param { + DbParam::Null => rusqlite::types::Value::Null, + DbParam::Bool(value) => rusqlite::types::Value::Integer(i64::from(value)), + DbParam::Integer(value) => rusqlite::types::Value::Integer(value), + DbParam::Text(s) => rusqlite::types::Value::Text(s), + DbParam::Bytes(bytes) => rusqlite::types::Value::Blob(bytes), + DbParam::Timestamp(ts) => { + rusqlite::types::Value::Text(ts.naive_utc().format("%F %T").to_string()) + } + } +} + +fn sqlite_value(value: rusqlite::types::ValueRef<'_>) -> DbValue { + match value { + rusqlite::types::ValueRef::Null => DbValue::Null, + rusqlite::types::ValueRef::Integer(i) => DbValue::Integer(i), + rusqlite::types::ValueRef::Real(f) => DbValue::Real(f), + rusqlite::types::ValueRef::Text(s) => { + DbValue::Text(String::from_utf8_lossy(s).into_owned()) + } + rusqlite::types::ValueRef::Blob(b) => DbValue::Bytes(b.to_vec()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures_util::StreamExt; + use std::time::Duration; + + #[tokio::test] + async fn sqlite_stream_yields_first_row_before_statement_finishes() { + let conn = tokio_rusqlite::Connection::open_in_memory().await.unwrap(); + conn.call(|conn| { + conn.create_scalar_function( + "wait_ms", + 1, + rusqlite::functions::FunctionFlags::SQLITE_UTF8, + |ctx| { + let millis = ctx.get::(0)?; + std::thread::sleep(Duration::from_millis(millis.cast_unsigned())); + Ok(millis) + }, + ) + }) + .await + .unwrap(); + + let mut stream = stream_sqlite(&conn, "SELECT 1 AS n UNION ALL SELECT wait_ms(500)", &[]); + + let first = tokio::time::timeout(Duration::from_millis(100), stream.next()) + .await + .expect("SQLite should yield the first row before the second row has been computed") + .unwrap() + .unwrap(); + let DbStatementResult::Row(row) = first else { + panic!("expected first SQLite stream item to be a row"); + }; + assert!(matches!(row.values.first(), Some(DbValue::Integer(1)))); + } +} diff --git a/src/webserver/database/error_highlighting.rs b/src/webserver/database/error_highlighting.rs index c2b7595f..463d1572 100644 --- a/src/webserver/database/error_highlighting.rs +++ b/src/webserver/database/error_highlighting.rs @@ -3,6 +3,7 @@ use std::{ path::{Path, PathBuf}, }; +use super::DbError; use super::sql::{SourceSpan, StmtWithParams}; #[derive(Debug)] @@ -10,7 +11,7 @@ struct NiceDatabaseError { /// The source file that contains the query. source_file: PathBuf, /// The error that occurred. - db_err: sqlx::error::Error, + db_err: DbError, /// The query that was executed. query: String, /// The start location of the query in the source file, if the query was extracted from a larger file. @@ -43,12 +44,12 @@ impl std::fmt::Display for NiceDatabaseError { self.source_file.display(), self.db_err )?; - if let sqlx::error::Error::Database(db_err) = &self.db_err { - let Some(mut offset) = db_err.offset() else { - write!(f, "{}", self.query)?; - self.show_position_info(f)?; - return Ok(()); - }; + if let DbError::Database { + offset: Some(offset), + .. + } = &self.db_err + { + let mut offset = *offset; for line in self.query.lines() { if offset > line.len() { offset -= line.len() + 1; @@ -97,11 +98,7 @@ impl std::error::Error for NicePositionedError { /// Display a database error without any position information #[must_use] -pub fn display_db_error( - source_file: &Path, - query: &str, - db_err: sqlx::error::Error, -) -> anyhow::Error { +pub fn display_db_error(source_file: &Path, query: &str, db_err: DbError) -> anyhow::Error { anyhow::Error::new(NiceDatabaseError { source_file: source_file.to_path_buf(), db_err, @@ -115,7 +112,7 @@ pub fn display_db_error( pub fn display_stmt_db_error( source_file: &Path, stmt: &StmtWithParams, - db_err: sqlx::error::Error, + db_err: DbError, ) -> anyhow::Error { anyhow::Error::new(NiceDatabaseError { source_file: source_file.to_path_buf(), diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index 32ff97ab..260e61cc 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -20,15 +20,11 @@ use crate::webserver::http_request_info::ExecutionContext; use crate::webserver::request_variables::SetVariablesMap; use crate::webserver::single_or_vec::SingleOrVec; +use super::driver::{DbParam, DbStatementResult}; use super::syntax_tree::{StmtParam, extract_req_param}; -use super::{Database, DbItem, error_highlighting::display_db_error}; -use sqlx::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo}; -use sqlx::pool::PoolConnection; -use sqlx::{ - Any, AnyConnection, Arguments, Column, Either, Executor, Row as _, Statement, ValueRef, -}; +use super::{Database, DbConnection, DbItem}; -pub type DbConn = Option>; +pub type DbConn = Option; fn source_line_number(line: usize) -> i64 { i64::try_from(line).unwrap_or(i64::MAX) @@ -133,20 +129,7 @@ fn create_db_query_span( (span, operation_name) } -impl Database { - pub(crate) async fn prepare_with( - &self, - query: &str, - param_types: &[AnyTypeInfo], - ) -> anyhow::Result> { - self.connection - .prepare_with(query, param_types) - .await - .map(|s| s.to_owned()) - .map_err(|e| display_db_error(Path::new("autogenerated sqlpage query"), query, e)) - } -} - +#[allow(clippy::too_many_lines)] pub fn stream_query_results_with_conn<'a>( sql_file: &'a ParsedSqlFile, request: &'a ExecutionContext, @@ -182,15 +165,24 @@ pub fn stream_query_results_with_conn<'a>( &request.app_state.telemetry_metrics, ); record_query_params(&query_metrics.span, &query.param_values); - let mut stream = connection.fetch_many(query); let mut error = None; let mut returned_rows: i64 = 0; - loop { - let start_next = std::time::Instant::now(); - let next_elem = stream.next().instrument(query_span.clone()).await; - query_metrics.add_duration(start_next.elapsed()); - let Some(elem) = next_elem else { break; }; - + { + let mut results = connection.execute_stream(query.sql, &query.arguments); + loop { + let start_next = std::time::Instant::now(); + let elem = results.next().instrument(query_span.clone()).await; + query_metrics.add_duration(start_next.elapsed()); + let Some(elem) = elem else { + break; + }; + let elem = match elem { + Ok(elem) => elem, + Err(e) => { + error = Some(display_stmt_db_error(source_file, stmt, e)); + break; + } + }; let mut query_result = parse_single_sql_result(source_file, stmt, elem); if let DbItem::Error(e) = query_result { error = Some(e); @@ -210,8 +202,8 @@ pub fn stream_query_results_with_conn<'a>( for db_item in parse_dynamic_rows(query_result) { yield db_item; } - } - drop(stream); + } + } if let Some(error) = error { query_metrics.record_error(returned_rows, &error); try_rollback_transaction(connection).await; @@ -301,10 +293,10 @@ async fn exec_static_simple_select( Ok(serde_json::Value::Object(map)) } -async fn try_rollback_transaction(db_connection: &mut AnyConnection) { +async fn try_rollback_transaction(db_connection: &mut DbConnection) { log::debug!("Attempting to rollback transaction"); - match db_connection.execute("ROLLBACK").await { - Ok(_) => log::debug!("Rolled back transaction"), + match db_connection.execute_command("ROLLBACK", &[]).await { + Ok(()) => log::debug!("Rolled back transaction"), Err(e) => { log::debug!("There was probably no transaction in progress when this happened: {e:?}"); } @@ -369,7 +361,7 @@ async fn execute_set_variable_query<'a>( record_query_params(&query_metrics.span, &query.param_values); let start_time = std::time::Instant::now(); let value = match connection - .fetch_optional(query) + .fetch_optional(query.sql, &query.arguments) .instrument(query_span.clone()) .await { @@ -451,7 +443,7 @@ async fn take_connection<'a>( db: &'a Database, conn: &'a mut DbConn, request: &ExecutionContext, -) -> anyhow::Result<&'a mut PoolConnection> { +) -> anyhow::Result<&'a mut DbConnection> { if let Some(c) = conn { return Ok(c); } @@ -472,7 +464,7 @@ async fn take_connection<'a>( Ok(connection) } Err(e) => { - let db_name = db.connection.any_kind(); + let db_name = db.connection.kind(); let active_count = db.connection.size(); let err_msg = format!( "Unable to connect to {db_name:?}. The connection pool currently has {active_count} active connections." @@ -485,7 +477,7 @@ async fn take_connection<'a>( /// Sets the current `OTel` trace context on the database connection so it is visible /// in `pg_stat_activity.application_name` (`PostgreSQL`) or as a session variable (`MySQL`). /// This allows correlating `SQLPage` traces with database-side monitoring. -async fn set_trace_context(connection: &mut AnyConnection, db: &Database) { +async fn set_trace_context(connection: &mut DbConnection, db: &Database) { use opentelemetry::trace::TraceContextExt; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -503,60 +495,58 @@ async fn set_trace_context(connection: &mut AnyConnection, db: &Database) { span_context.trace_flags() ); let sql = match db.info.kind { - sqlx::any::AnyKind::Postgres => { + super::DbKind::Postgres => { // postgresqlreceiver expects application_name to be a raw W3C traceparent value. format!("SET application_name = '{traceparent}'") } - sqlx::any::AnyKind::MySql => { + super::DbKind::MySql => { format!("SET @traceparent = '{traceparent}'") } _ => return, }; - if let Err(e) = connection.execute(sql.as_str()).await { + if let Err(e) = connection.execute_command(sql.as_str(), &[]).await { log::debug!("Failed to set trace context on connection: {e}"); } } #[inline] fn parse_single_sql_result( - source_file: &Path, - stmt: &StmtWithParams, - res: sqlx::Result>, + _source_file: &Path, + _stmt: &StmtWithParams, + res: DbStatementResult, ) -> DbItem { match res { - Ok(Either::Right(r)) => { + DbStatementResult::Row(r) => { if log::log_enabled!(log::Level::Trace) { debug_row(&r); } DbItem::Row(super::sql_to_json::row_to_json(&r)) } - Ok(Either::Left(res)) => { + DbStatementResult::Finished => { + let res = "finished"; log::debug!("Finished query with result: {res:?}"); DbItem::FinishedQuery } - Err(err) => { - let nice_err = display_stmt_db_error(source_file, stmt, err); - DbItem::Error(nice_err) - } } } -fn debug_row(r: &AnyRow) { +fn debug_row(r: &super::driver::DbRow) { use std::fmt::Write; - let columns = r.columns(); let mut row_str = String::new(); - for (i, col) in columns.iter().enumerate() { - if let Ok(value) = r.try_get_raw(i) { - write!( - &mut row_str, - "[{:?} ({}): {:?}: {:?}]", - col.name(), - if value.is_null() { "NULL" } else { "NOT NULL" }, - col, - value.type_info() - ) - .unwrap(); - } + for (col, value) in r.columns.iter().zip(r.values.iter()) { + write!( + &mut row_str, + "[{:?} ({}): {:?}: {:?}]", + col.name, + if matches!(value, super::driver::DbValue::Null) { + "NULL" + } else { + "NOT NULL" + }, + col, + value + ) + .unwrap(); } log::trace!("Received db row: {row_str}"); } @@ -589,7 +579,7 @@ async fn bind_parameters<'a>( ) -> anyhow::Result> { let sql = stmt.query.as_str(); log::debug!("Preparing statement: {sql}"); - let mut arguments = AnyArguments::default(); + let mut arguments = Vec::with_capacity(stmt.params.len()); let mut param_values = Vec::with_capacity(stmt.params.len()); for (param_idx, param) in stmt.params.iter().enumerate() { log::trace!("\tevaluating parameter {}: {}", param_idx + 1, param); @@ -600,17 +590,11 @@ async fn bind_parameters<'a>( argument.as_ref().unwrap_or(&Cow::Borrowed("NULL")) ); param_values.push(argument.as_deref().map(str::to_owned)); - match argument { - None => arguments.add(None::), - Some(Cow::Owned(s)) => arguments.add(s), - Some(Cow::Borrowed(v)) => arguments.add(v), - } + arguments.push(DbParam::from(argument.map(Cow::into_owned))); } - let has_arguments = !stmt.params.is_empty(); Ok(StatementWithParams { sql, arguments, - has_arguments, param_values, }) } @@ -700,34 +684,10 @@ fn apply_json_columns(item: &mut DbItem, json_columns: &[String]) { pub struct StatementWithParams<'a> { sql: &'a str, - arguments: AnyArguments<'a>, - has_arguments: bool, + arguments: Vec, param_values: Vec>, } -impl<'q> sqlx::Execute<'q, Any> for StatementWithParams<'q> { - fn sql(&self) -> &'q str { - self.sql - } - - fn statement(&self) -> Option<&>::Statement> { - None - } - - fn take_arguments(&mut self) -> Option<>::Arguments> { - if self.has_arguments { - Some(std::mem::take(&mut self.arguments)) - } else { - None - } - } - - fn persistent(&self) -> bool { - // Let sqlx create a prepared statement the first time it is executed, and then reuse it. - true - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/webserver/database/migrations.rs b/src/webserver/database/migrations.rs index 426045b9..fcb91b6d 100644 --- a/src/webserver/database/migrations.rs +++ b/src/webserver/database/migrations.rs @@ -1,11 +1,21 @@ -use super::Database; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use anyhow::Context; +use sha2::{Digest, Sha384}; + use super::error_highlighting::display_db_error; +use super::{Database, DbKind, DbParam, make_placeholder}; use crate::MIGRATIONS_DIR; -use anyhow; -use anyhow::Context; -use sqlx::migrate::MigrateError; -use sqlx::migrate::Migration; -use sqlx::migrate::Migrator; + +#[derive(Debug)] +struct Migration { + version: i64, + description: String, + path: PathBuf, + sql: String, + checksum: Vec, +} pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyhow::Result<()> { let migrations_dir = config.configuration_directory.join(MIGRATIONS_DIR); @@ -17,10 +27,9 @@ pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyh return Ok(()); } log::debug!("Applying migrations from '{}'", migrations_dir.display()); - let migrator = Migrator::new(migrations_dir.clone()) - .await + let migrations = load_migrations(&migrations_dir) .with_context(|| migration_err("preparing the database migration"))?; - if migrator.migrations.is_empty() { + if migrations.is_empty() { log::debug!( "No migration found in {}. \ You can specify database operations to apply when the server first starts by creating files \ @@ -30,28 +39,168 @@ pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyh ); return Ok(()); } - log::info!("Found {} migrations:", migrator.migrations.len()); - for m in migrator.iter() { - log::info!("\t{}", DisplayMigration(m)); + log::info!("Found {} migrations:", migrations.len()); + for migration in &migrations { + log::info!("\t{}", DisplayMigration(migration)); } - migrator.run(&db.connection).await.map_err(|err| { - match err { - MigrateError::Execute(n, source) => { - let migration = migrator.iter().find(|&m| m.version == n).unwrap(); - let source_file = - migrations_dir.join(format!("{:04}_{}.sql", n, migration.description)); - display_db_error(&source_file, &migration.sql, source).context(format!( + + let mut conn = db.connection.acquire().await?; + ensure_migrations_table(&mut conn, db.info.kind).await?; + for migration in migrations { + let applied = migration_row(&mut conn, db, migration.version).await?; + if let Some(applied_checksum) = applied { + anyhow::ensure!( + applied_checksum == migration.checksum, + "Migration {} has already been applied, but its checksum changed", + DisplayMigration(&migration) + ); + continue; + } + + let start = Instant::now(); + if let Err(err) = conn.execute_batch(&migration.sql).await { + return Err( + display_db_error(&migration.path, &migration.sql, err).context(format!( "Failed to apply {} migration {}", db, - DisplayMigration(migration) - )) - } - source => anyhow::Error::new(source), + DisplayMigration(&migration) + )), + ); + } + let execution_time = i64::try_from(start.elapsed().as_millis()).unwrap_or(i64::MAX); + record_migration(&mut conn, db, &migration, execution_time).await?; + } + Ok(()) +} + +fn load_migrations(migrations_dir: &Path) -> anyhow::Result> { + let mut migrations = Vec::new(); + for entry in std::fs::read_dir(migrations_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("sql") { + continue; + } + let file_name = path + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid migration file name: {}", path.display()))?; + let Some((version, description)) = file_name.split_once('_') else { + anyhow::bail!("Invalid migration file name: {}", path.display()); + }; + let version = version.parse::().with_context(|| { + format!("Invalid migration version in file name: {}", path.display()) + })?; + let sql = std::fs::read_to_string(&path) + .with_context(|| format!("Unable to read migration {}", path.display()))?; + let checksum = Sha384::digest(sql.as_bytes()).to_vec(); + migrations.push(Migration { + version, + description: description.to_string(), + path, + sql, + checksum, + }); + } + migrations.sort_by_key(|migration| migration.version); + Ok(migrations) +} + +async fn ensure_migrations_table( + conn: &mut super::DbConnection, + kind: DbKind, +) -> anyhow::Result<()> { + let sql = match kind { + DbKind::Sqlite => { + "CREATE TABLE IF NOT EXISTS _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description TEXT NOT NULL, + installed_on TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN NOT NULL, + checksum BLOB NOT NULL, + execution_time BIGINT NOT NULL + )" } - .context(format!( - "Failed to apply database migrations from {MIGRATIONS_DIR:?}" - )) - })?; + DbKind::Postgres => { + "CREATE TABLE IF NOT EXISTS _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description TEXT NOT NULL, + installed_on TIMESTAMPTZ NOT NULL DEFAULT now(), + success BOOLEAN NOT NULL, + checksum BYTEA NOT NULL, + execution_time BIGINT NOT NULL + )" + } + DbKind::MySql | DbKind::Odbc => { + "CREATE TABLE IF NOT EXISTS _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description VARCHAR(255) NOT NULL, + installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN NOT NULL, + checksum BLOB NOT NULL, + execution_time BIGINT NOT NULL + )" + } + DbKind::Mssql => { + "IF OBJECT_ID(N'_sqlx_migrations', N'U') IS NULL + CREATE TABLE _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description NVARCHAR(255) NOT NULL, + installed_on DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + success BIT NOT NULL, + checksum VARBINARY(MAX) NOT NULL, + execution_time BIGINT NOT NULL + )" + } + }; + conn.execute_command(sql, &[]).await?; + Ok(()) +} + +async fn migration_row( + conn: &mut super::DbConnection, + db: &Database, + version: i64, +) -> anyhow::Result>> { + let sql = format!( + "SELECT checksum FROM _sqlx_migrations WHERE version = {}", + make_placeholder(db.info.kind, 1) + ); + let row = conn + .fetch_optional(&sql, &[DbParam::Integer(version)]) + .await?; + Ok(row.and_then(|row| match row.values.first() { + Some(super::driver::DbValue::Bytes(bytes)) => Some(bytes.clone()), + Some(super::driver::DbValue::Text(text)) => Some(text.as_bytes().to_vec()), + _ => None, + })) +} + +async fn record_migration( + conn: &mut super::DbConnection, + db: &Database, + migration: &Migration, + execution_time: i64, +) -> anyhow::Result<()> { + let sql = format!( + "INSERT INTO _sqlx_migrations(version, description, success, checksum, execution_time) VALUES ({}, {}, {}, {}, {})", + make_placeholder(db.info.kind, 1), + make_placeholder(db.info.kind, 2), + make_placeholder(db.info.kind, 3), + make_placeholder(db.info.kind, 4), + make_placeholder(db.info.kind, 5), + ); + conn.execute_command( + &sql, + &[ + DbParam::Integer(migration.version), + DbParam::Text(migration.description.clone()), + DbParam::Bool(true), + DbParam::Bytes(migration.checksum.clone()), + DbParam::Integer(execution_time), + ], + ) + .await?; Ok(()) } @@ -59,18 +208,7 @@ struct DisplayMigration<'a>(&'a Migration); impl std::fmt::Display for DisplayMigration<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Migration { - version, - migration_type, - description, - .. - } = &self.0; - write!(f, "[{version:04}]")?; - if migration_type != &sqlx::migrate::MigrationType::Simple { - write!(f, " ({migration_type:?})")?; - } - write!(f, " {description}")?; - Ok(()) + write!(f, "[{:04}] {}", self.0.version, self.0.description) } } diff --git a/src/webserver/database/mod.rs b/src/webserver/database/mod.rs index b73e0cb1..049f8d19 100644 --- a/src/webserver/database/mod.rs +++ b/src/webserver/database/mod.rs @@ -1,6 +1,7 @@ pub mod blob_to_data_url; mod connect; mod csv_import; +pub mod driver; pub mod execute_queries; pub mod migrations; mod sql; @@ -10,12 +11,12 @@ mod syntax_tree; mod error_highlighting; mod sql_to_json; +pub use driver::{DbConnection, DbError, DbKind, DbParam, DbPool}; pub use sql::ParsedSqlFile; use sql::{DB_PLACEHOLDERS, DbPlaceHolder}; -use sqlx::any::AnyKind; // SupportedDatabase is defined in this module -/// Supported database types in `SQLPage`. Represents an actual DBMS, not a sqlx backend kind (like "Odbc") +/// Supported database types in `SQLPage`. Represents an actual DBMS, not a driver kind like ODBC. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SupportedDatabase { Sqlite, @@ -81,20 +82,20 @@ impl SupportedDatabase { } } -impl From for SupportedDatabase { - fn from(kind: AnyKind) -> Self { +impl From for SupportedDatabase { + fn from(kind: DbKind) -> Self { match kind { - AnyKind::Postgres => Self::Postgres, - AnyKind::MySql => Self::MySql, - AnyKind::Sqlite => Self::Sqlite, - AnyKind::Mssql => Self::Mssql, - AnyKind::Odbc => Self::Generic, + DbKind::Postgres => Self::Postgres, + DbKind::MySql => Self::MySql, + DbKind::Sqlite => Self::Sqlite, + DbKind::Mssql => Self::Mssql, + DbKind::Odbc => Self::Generic, } } } pub struct Database { - pub connection: sqlx::AnyPool, + pub connection: DbPool, pub info: DbInfo, } @@ -103,14 +104,14 @@ pub struct DbInfo { pub dbms_name: String, /// The actual database we are connected to. Can be "Generic" when using an unknown ODBC driver pub database_type: SupportedDatabase, - /// The sqlx database backend we are using. Can be "Odbc", in which case we need to use `database_type` to know what database we are actually using. - pub kind: AnyKind, + /// The native driver backend we are using. Can be "Odbc", in which case we need to use `database_type` to know what database we are actually using. + pub kind: DbKind, } impl Database { - pub async fn close(&self) -> anyhow::Result<()> { + pub fn close(&self) -> anyhow::Result<()> { log::info!("Closing all database connections..."); - self.connection.close().await; + self.connection.close(); Ok(()) } } @@ -124,13 +125,13 @@ pub enum DbItem { impl std::fmt::Display for Database { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.connection.any_kind()) + write!(f, "{:?}", self.connection.kind()) } } #[inline] #[must_use] -pub fn make_placeholder(dbms: AnyKind, arg_number: usize) -> String { +pub fn make_placeholder(dbms: DbKind, arg_number: usize) -> String { if let Some((_, placeholder)) = DB_PLACEHOLDERS.iter().find(|(kind, _)| *kind == dbms) { match *placeholder { DbPlaceHolder::PrefixedNumber { prefix } => format!("{prefix}{arg_number}"), diff --git a/src/webserver/database/sql.rs b/src/webserver/database/sql.rs index 0d4bdfef..941ef485 100644 --- a/src/webserver/database/sql.rs +++ b/src/webserver/database/sql.rs @@ -20,7 +20,6 @@ use sqlparser::dialect::{ use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::Token::{self, EOF, SemiColon}; use sqlparser::tokenizer::{Location, Span, TokenWithSpan, Tokenizer}; -use sqlx::any::AnyKind; use std::fmt::Write; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -169,7 +168,7 @@ fn parse_sql<'a>( })) } -fn transform_to_positional_placeholders(stmt: &mut StmtWithParams, kind: AnyKind) { +fn transform_to_positional_placeholders(stmt: &mut StmtWithParams, kind: super::DbKind) { if let Some((_, DbPlaceHolder::Positional { placeholder })) = DB_PLACEHOLDERS .iter() .find(|(placeholder_kind, _)| *placeholder_kind == kind) @@ -784,11 +783,11 @@ mod test { fn create_test_db_info(database_type: SupportedDatabase) -> DbInfo { let kind = match database_type { - SupportedDatabase::Postgres => AnyKind::Postgres, - SupportedDatabase::Mssql => AnyKind::Mssql, - SupportedDatabase::MySql => AnyKind::MySql, - SupportedDatabase::Sqlite => AnyKind::Sqlite, - _ => AnyKind::Odbc, + SupportedDatabase::Postgres => crate::webserver::database::DbKind::Postgres, + SupportedDatabase::Mssql => crate::webserver::database::DbKind::Mssql, + SupportedDatabase::MySql => crate::webserver::database::DbKind::MySql, + SupportedDatabase::Sqlite => crate::webserver::database::DbKind::Sqlite, + _ => crate::webserver::database::DbKind::Odbc, }; DbInfo { dbms_name: database_type.display_name().to_string(), @@ -1020,6 +1019,18 @@ mod test { assert_eq!(parameters, [StmtParam::PostOrGet("1".to_string()),]); } + #[test] + fn test_mysql_statement_rewrite() { + let mut ast = parse_stmt("select '' || $1 || 'x'", &MySqlDialect {}); + let db_info = create_test_db_info(SupportedDatabase::MySql); + let parameters = ParameterExtractor::extract_parameters(&mut ast, db_info).unwrap(); + assert_eq!( + ast.to_string(), + "SELECT CONCAT(CONCAT('', CAST(@SQLPAGE_TEMP1 AS CHAR)), 'x')" + ); + assert_eq!(parameters, [StmtParam::PostOrGet("1".to_string()),]); + } + #[test] fn test_static_extract() { use SimpleSelectValue::Static; @@ -1317,7 +1328,7 @@ mod test { delayed_functions: vec![], json_columns: vec![], }; - transform_to_positional_placeholders(&mut stmt, AnyKind::MySql); + transform_to_positional_placeholders(&mut stmt, crate::webserver::database::DbKind::MySql); assert_eq!( stmt.query, "select \ diff --git a/src/webserver/database/sql/parameter_extraction.rs b/src/webserver/database/sql/parameter_extraction.rs index 57bb08c4..d4b122d2 100644 --- a/src/webserver/database/sql/parameter_extraction.rs +++ b/src/webserver/database/sql/parameter_extraction.rs @@ -1,5 +1,6 @@ use super::super::{DbInfo, SupportedDatabase}; use super::{is_sqlpage_func, sqlpage_func_name}; +use crate::webserver::database::DbKind; use crate::webserver::database::sqlpage_functions::func_call_to_param; use crate::webserver::database::syntax_tree::StmtParam; use sqlparser::ast::{ @@ -7,7 +8,6 @@ use sqlparser::ast::{ FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident, ObjectName, ObjectNamePart, Spanned, Statement, Value, ValueWithSpan, Visit, VisitMut, Visitor, VisitorMut, }; -use sqlx::any::AnyKind; use std::ops::ControlFlow; pub(super) struct ParameterExtractor { @@ -22,34 +22,31 @@ pub(crate) enum DbPlaceHolder { Positional { placeholder: &'static str }, } -pub(crate) const DB_PLACEHOLDERS: [(AnyKind, DbPlaceHolder); 5] = [ +pub(crate) const DB_PLACEHOLDERS: [(DbKind, DbPlaceHolder); 5] = [ ( - AnyKind::Sqlite, + DbKind::Sqlite, DbPlaceHolder::PrefixedNumber { prefix: "?" }, ), ( - AnyKind::Postgres, + DbKind::Postgres, DbPlaceHolder::PrefixedNumber { prefix: "$" }, ), ( - AnyKind::MySql, + DbKind::MySql, DbPlaceHolder::Positional { placeholder: "?" }, ), ( - AnyKind::Mssql, + DbKind::Mssql, DbPlaceHolder::PrefixedNumber { prefix: "@p" }, ), - ( - AnyKind::Odbc, - DbPlaceHolder::Positional { placeholder: "?" }, - ), + (DbKind::Odbc, DbPlaceHolder::Positional { placeholder: "?" }), ]; /// For positional parameters, we use a temporary placeholder during parameter extraction, /// And then replace it with the actual placeholder during statement rewriting. pub(crate) const TEMP_PLACEHOLDER_PREFIX: &str = "@SQLPAGE_TEMP"; -fn get_placeholder_prefix(kind: AnyKind) -> &'static str { +fn get_placeholder_prefix(kind: DbKind) -> &'static str { if let Some((_, DbPlaceHolder::PrefixedNumber { prefix })) = DB_PLACEHOLDERS .iter() .find(|(placeholder_kind, _prefix)| *placeholder_kind == kind) @@ -463,7 +460,7 @@ fn function_arg_expr(arg: &mut FunctionArg) -> Option<&mut Expr> { #[inline] #[must_use] -pub(super) fn make_tmp_placeholder(kind: AnyKind, arg_number: usize) -> String { +pub(super) fn make_tmp_placeholder(kind: DbKind, arg_number: usize) -> String { let prefix = if let Some((_, DbPlaceHolder::PrefixedNumber { prefix })) = DB_PLACEHOLDERS.iter().find(|(db_typ, _)| *db_typ == kind) { @@ -541,12 +538,16 @@ impl VisitorMut for ParameterExtractor { } self.replace_with_placeholder(value, param); } - // Replace 'str1' || 'str2' with CONCAT('str1', 'str2') for MSSQL + // Replace 'str1' || 'str2' with CONCAT('str1', 'str2') where pipes are not string concatenation. Expr::BinaryOp { left, op: BinaryOperator::StringConcat, right, - } if self.db_info.database_type == SupportedDatabase::Mssql => { + } if matches!( + self.db_info.database_type, + SupportedDatabase::Mssql | SupportedDatabase::MySql + ) => + { let left = std::mem::replace(left.as_mut(), Expr::value(Value::Null)); let right = std::mem::replace(right.as_mut(), Expr::value(Value::Null)); *value = Expr::Function(Function { diff --git a/src/webserver/database/sql_to_json.rs b/src/webserver/database/sql_to_json.rs index 4c93dddd..e9de2932 100644 --- a/src/webserver/database/sql_to_json.rs +++ b/src/webserver/database/sql_to_json.rs @@ -1,712 +1,83 @@ use crate::utils::add_value_to_map; use crate::webserver::database::blob_to_data_url; -use bigdecimal::BigDecimal; -use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime}; +use crate::webserver::database::driver::{DbColumn, DbKind, DbRow, DbValue}; use serde_json::{self, Map, Value}; -use sqlx::any::{AnyColumn, AnyRow, AnyTypeInfo, AnyTypeInfoKind}; -use sqlx::postgres::PgValueRef; -use sqlx::postgres::types::PgRange; -use sqlx::{Column, Row, TypeInfo, ValueRef}; -use sqlx::{Decode, Type}; -pub fn row_to_json(row: &AnyRow) -> Value { - use Value::Object; - - let columns = row.columns(); +pub fn row_to_json(row: &DbRow) -> Value { let mut map = Map::new(); - for col in columns { - let key = canonical_col_name(col); - let value: Value = sql_to_json(row, col); + for (col, value) in row.columns.iter().zip(row.values.iter()) { + let key = canonical_col_name(col, row.kind); + let value = sql_value_to_json(value); map = add_value_to_map(map, (key, value)); } - Object(map) + Value::Object(map) } -fn canonical_col_name(col: &AnyColumn) -> String { - // Some databases fold all unquoted identifiers to uppercase but SQLPage uses lowercase property names - if matches!(col.type_info().0, AnyTypeInfoKind::Odbc(_)) - && col - .name() - .chars() - .all(|c| c.is_ascii_uppercase() || c == '_') +fn canonical_col_name(col: &DbColumn, kind: DbKind) -> String { + if matches!(kind, DbKind::Odbc) && col.name.chars().all(|c| c.is_ascii_uppercase() || c == '_') { - col.name().to_ascii_lowercase() + col.name.to_ascii_lowercase() } else { - col.name().to_owned() - } -} - -pub fn sql_to_json(row: &AnyRow, col: &sqlx::any::AnyColumn) -> Value { - let raw_value_result = row.try_get_raw(col.ordinal()); - match raw_value_result { - Ok(raw_value) if !raw_value.is_null() => { - let mut raw_value = Some(raw_value); - let decoded = sql_nonnull_to_json(|| { - raw_value - .take() - .unwrap_or_else(|| row.try_get_raw(col.ordinal()).unwrap()) - }); - log::trace!("Decoded value: {decoded:?}"); - decoded - } - Ok(_null) => Value::Null, - Err(e) => { - log::warn!("Unable to extract value from row: {e:?}"); - Value::Null - } - } -} - -fn decode_raw<'a, T: Decode<'a, sqlx::any::Any> + Default>( - raw_value: sqlx::any::AnyValueRef<'a>, -) -> T { - match T::decode(raw_value) { - Ok(v) => v, - Err(e) => { - let type_name = std::any::type_name::(); - log::error!("Failed to decode {type_name} value: {e}"); - T::default() - } + col.name.clone() } } -fn decode_pg_range<'r, T>(raw_value: sqlx::any::AnyValueRef<'r>) -> Value -where - T: std::fmt::Display - + Type - + for<'a> sqlx::Decode<'a, sqlx::postgres::Postgres>, -{ - let Ok(pg_val): Result, _> = raw_value.try_into() else { - log::error!("Only postgres range values are supported"); - return Value::Null; - }; - match as sqlx::Decode<'r, sqlx::postgres::Postgres>>::decode(pg_val) { - Ok(pg_range) => pg_range.to_string().into(), - Err(e) => { - log::error!("Failed to decode postgres range value: {e}"); - Value::Null - } +pub fn sql_value_to_json(value: &DbValue) -> Value { + match value { + DbValue::Null => Value::Null, + DbValue::Bool(b) => (*b).into(), + DbValue::Integer(i) => (*i).into(), + DbValue::Real(f) => (*f).into(), + DbValue::Text(s) => Value::String(s.clone()), + DbValue::Bytes(bytes) => blob_to_data_url::vec_to_data_uri_value(bytes), } } -fn decimal_to_json(decimal: &BigDecimal) -> Value { - // to_plain_string always returns a valid JSON string - Value::Number(serde_json::Number::from_string_unchecked( - decimal.normalized().to_plain_string(), - )) -} - -pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueRef<'r>) -> Value { - use AnyTypeInfoKind::{Mssql, MySql}; - let raw_value = get_ref(); - let type_info = raw_value.type_info(); - let type_name = type_info.name(); - log::trace!("Decoding a value of type {type_name:?} (type info: {type_info:?})"); - let AnyTypeInfo(ref db_type) = *type_info; - match type_name { - "REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" => decode_raw::(raw_value).into(), - "NUMERIC" | "DECIMAL" => decimal_to_json(&decode_raw(raw_value)), - "INT8" | "BIGINT" | "SERIAL8" | "BIGSERIAL" | "IDENTITY" | "INT64" | "INTEGER8" - | "BIGINT SIGNED" => decode_raw::(raw_value).into(), - "INT" | "INT4" | "INTEGER" | "MEDIUMINT" | "YEAR" => decode_raw::(raw_value).into(), - "INT2" | "SMALLINT" | "TINYINT" => decode_raw::(raw_value).into(), - "BIGINT UNSIGNED" => decode_raw::(raw_value).into(), - "INT UNSIGNED" | "MEDIUMINT UNSIGNED" | "SMALLINT UNSIGNED" | "TINYINT UNSIGNED" => { - decode_raw::(raw_value).into() - } - "BOOL" | "BOOLEAN" => decode_raw::(raw_value).into(), - "BIT" if matches!(db_type, Mssql(_)) => decode_raw::(raw_value).into(), - "BIT" if matches!(db_type, MySql(mysql_type) if mysql_type.max_size() == Some(1)) => { - decode_raw::(raw_value).into() - } - "BIT" if matches!(db_type, MySql(_)) => decode_raw::(raw_value).into(), - "DATE" => decode_raw::(raw_value) - .to_string() - .into(), - "TIME" | "TIMETZ" => decode_raw::(raw_value) - .to_string() - .into(), - "DATETIMEOFFSET" | "TIMESTAMP" | "TIMESTAMPTZ" => { - decode_raw::>(raw_value) - .to_rfc3339() - .into() - } - "DATETIME" | "DATETIME2" => decode_raw::(raw_value) - .format("%FT%T%.f") - .to_string() - .into(), - "MONEY" | "SMALLMONEY" if matches!(db_type, Mssql(_)) => { - decode_raw::(raw_value).into() - } - "UUID" | "UNIQUEIDENTIFIER" => decode_raw::(raw_value) - .to_string() - .into(), - "JSON" | "JSON[]" | "JSONB" | "JSONB[]" => decode_raw::(raw_value), - "BLOB" | "BYTEA" | "FILESTREAM" | "VARBINARY" | "BIGVARBINARY" | "BINARY" | "IMAGE" => { - blob_to_data_url::vec_to_data_uri_value(&decode_raw::>(raw_value)) - } - "INT4RANGE" => decode_pg_range::(raw_value), - "INT8RANGE" => decode_pg_range::(raw_value), - "NUMRANGE" => decode_pg_range::(raw_value), - "DATERANGE" => decode_pg_range::(raw_value), - "TSRANGE" => decode_pg_range::(raw_value), - "TSTZRANGE" => decode_pg_range::>(raw_value), - // Deserialize as a string by default - _ => decode_raw::(raw_value).into(), - } -} - -/// Takes the first column of a row and converts it to a string. -pub fn row_to_string(row: &AnyRow) -> Option { - let col = row.columns().first()?; - match sql_to_json(row, col) { - serde_json::Value::String(s) => Some(s), - serde_json::Value::Null => None, +pub fn row_to_string(row: &DbRow) -> Option { + let first = row.values.first()?; + match sql_value_to_json(first) { + Value::String(s) => Some(s), + Value::Null => None, other => Some(other.to_string()), } } #[cfg(test)] mod tests { - use crate::app_config::tests::test_database_url; - use super::*; - use sqlx::Connection; - - fn setup_logging() { - crate::telemetry::init_test_logging(); - } - - fn db_specific_test(db_type: &str) -> Option { - setup_logging(); - let db_url = test_database_url(); - if db_url.starts_with(db_type) { - Some(db_url) - } else { - log::warn!("Skipping test because DATABASE_URL is not set to a {db_type} database"); - None - } - } - - #[actix_web::test] - async fn test_row_to_json() -> anyhow::Result<()> { - use sqlx::Connection; - let db_url = test_database_url(); - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - 123.456 as one_value, - 1 as two_values, - 2 as two_values, - 'x' as three_values, - 'y' as three_values, - 'z' as three_values - ", - ) - .fetch_one(&mut c) - .await?; - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "one_value": 123.456, - "two_values": [1,2], - "three_values": ["x","y","z"], - }), - ); - Ok(()) - } - - #[actix_web::test] - async fn test_postgres_types() -> anyhow::Result<()> { - let Some(db_url) = db_specific_test("postgres") else { - return Ok(()); - }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - 42::INT2 as small_int, - 42::INT4 as integer, - 42::INT8 as big_int, - 42.25::FLOAT4 as float4, - 42.25::FLOAT8 as float8, - 123456789123456789123456789::NUMERIC as numeric, - TRUE as boolean, - '2024-03-14'::DATE as date, - '13:14:15'::TIME as time, - '2024-03-14 13:14:15'::TIMESTAMP as timestamp, - '2024-03-14 13:14:15+02:00'::TIMESTAMPTZ as timestamptz, - INTERVAL '1 year 2 months 3 days' as complex_interval, - INTERVAL '4 hours' as hour_interval, - INTERVAL '1.5 days' as fractional_interval, - '{\"key\": \"value\"}'::JSON as json, - '{\"key\": \"value\"}'::JSONB as jsonb, - age('2024-03-14'::timestamp, '2024-01-01'::timestamp) as age_interval, - justify_interval(interval '1 year 2 months 3 days') as justified_interval, - 1234.56::MONEY as money_val, - '\\x68656c6c6f20776f726c64'::BYTEA as blob_data, - '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid, - '[1,5)'::INT4RANGE as int4range, - '[1,5]'::INT8RANGE as int8range, - '[1.5,4.5)'::NUMRANGE as numrange, - -- '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, - -- '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, - '[2024-11-12,2024-11-13)'::DATERANGE as daterange - ", - ) - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "small_int": 42, - "integer": 42, - "big_int": 42, - "float4": 42.25, - "float8": 42.25, - "numeric": 123_456_789_123_456_789_123_456_789_u128, - "boolean": true, - "date": "2024-03-14", - "time": "13:14:15", - "timestamp": "2024-03-14T13:14:15+00:00", - "timestamptz": "2024-03-14T11:14:15+00:00", - "complex_interval": "1 year 2 mons 3 days", - "hour_interval": "04:00:00", - "fractional_interval": "1 day 12:00:00", - "json": {"key": "value"}, - "jsonb": {"key": "value"}, - "age_interval": "2 mons 13 days", - "justified_interval": "1 year 2 mons 3 days", - "money_val": "$1,234.56", - "blob_data": "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", - "uuid": "550e8400-e29b-41d4-a716-446655440000", - "int4range": "[1,5)", - "int8range": "[1,6)", - "numrange": "[1.5,4.5)", - //"tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", // todo: bug in sqlx datetime range parsing - //"tstzrange": "[\"2024-11-12 02:00:00 +01:00\",\"2024-11-12 23:00:00 +00:00\")", // todo: tz info is lost in sqlx - "daterange": "[2024-11-12,2024-11-13)" - }), - ); - Ok(()) - } - - /// Postgres encodes values differently in prepared statements and in "simple" queries - /// - #[actix_web::test] - async fn test_postgres_prepared_types() -> anyhow::Result<()> { - let Some(db_url) = db_specific_test("postgres") else { - return Ok(()); - }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - '2024-03-14'::DATE as date, - '13:14:15'::TIME as time, - '2024-03-14 13:14:15+02:00'::TIMESTAMPTZ as timestamptz, - INTERVAL '-01:02:03' as time_interval, - '{\"key\": \"value\"}'::JSON as json, - 1234.56::MONEY as money_val, - '\\x74657374'::BYTEA as blob_data, - '550e8400-e29b-41d4-a716-446655440000'::UUID as uuid - where $1", - ) - .bind(true) - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "date": "2024-03-14", - "time": "13:14:15", - "timestamptz": "2024-03-14T11:14:15+00:00", - "time_interval": "-01:02:03", - "json": {"key": "value"}, - "money_val": "", // TODO: fix this bug: https://github.com/sqlpage/SQLPage/issues/983 - "blob_data": "data:application/octet-stream;base64,dGVzdA==", - "uuid": "550e8400-e29b-41d4-a716-446655440000", - }), - ); - Ok(()) - } - - #[actix_web::test] - async fn test_postgres_prepared_range_types() -> anyhow::Result<()> { - let Some(db_url) = db_specific_test("postgres") else { - return Ok(()); - }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - '[1,5)'::INT4RANGE as int4range, - '[2024-11-12 01:02:03,2024-11-12 23:00:00)'::TSRANGE as tsrange, - '[2024-11-12 01:02:03+01:00,2024-11-12 23:00:00+00:00)'::TSTZRANGE as tstzrange, - '[2024-11-12,2024-11-13)'::DATERANGE as daterange - where $1", - ) - .bind(true) - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "int4range": "[1,5)", - "tsrange": "[2024-11-12 01:02:03,2024-11-12 23:00:00)", - "tstzrange": "[2024-11-12 00:02:03 +00:00,2024-11-12 23:00:00 +00:00)", // todo: tz info is lost in sqlx - "daterange": "[2024-11-12,2024-11-13)" - }), - ); - Ok(()) - } - - #[actix_web::test] - async fn test_mysql_types() -> anyhow::Result<()> { - let db_url = db_specific_test("mysql").or_else(|| db_specific_test("mariadb")); - let Some(db_url) = db_url else { - return Ok(()); - }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - sqlx::query( - "CREATE TEMPORARY TABLE _sqlp_t ( - tiny_int TINYINT, - small_int SMALLINT, - medium_int MEDIUMINT, - signed_int INTEGER, - big_int BIGINT, - unsigned_int INTEGER UNSIGNED, - tiny_int_unsigned TINYINT UNSIGNED, - small_int_unsigned SMALLINT UNSIGNED, - medium_int_unsigned MEDIUMINT UNSIGNED, - big_int_unsigned BIGINT UNSIGNED, - decimal_num DECIMAL(10,2), - float_num FLOAT, - double_num DOUBLE, - bit_val BIT(1), - date_val DATE, - time_val TIME, - datetime_val DATETIME, - timestamp_val TIMESTAMP, - year_val YEAR, - char_val CHAR(10), - varchar_val VARCHAR(50), - text_val TEXT, - blob_val BLOB - ) AS - SELECT - 127 as tiny_int, - 32767 as small_int, - 8388607 as medium_int, - -1000000 as signed_int, - 9223372036854775807 as big_int, - 1000000 as unsigned_int, - 255 as tiny_int_unsigned, - 65535 as small_int_unsigned, - 16777215 as medium_int_unsigned, - 18446744073709551615 as big_int_unsigned, - 123.45 as decimal_num, - 42.25 as float_num, - 42.25 as double_num, - 1 as bit_val, - '2024-03-14' as date_val, - '13:14:15' as time_val, - '2024-03-14 13:14:15' as datetime_val, - '2024-03-14 13:14:15' as timestamp_val, - 2024 as year_val, - 'CHAR' as char_val, - 'VARCHAR' as varchar_val, - 'TEXT' as text_val, - x'626c6f62' as blob_val", - ) - .execute(&mut c) - .await?; - - let row = sqlx::query("SELECT * FROM _sqlp_t") - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "tiny_int": 127, - "small_int": 32767, - "medium_int": 8_388_607, - "signed_int": -1_000_000, - "big_int": 9_223_372_036_854_775_807_u64, - "unsigned_int": 1_000_000, - "tiny_int_unsigned": 255, - "small_int_unsigned": 65_535, - "medium_int_unsigned": 16_777_215, - "big_int_unsigned": 18_446_744_073_709_551_615_u64, - "decimal_num": 123.45, - "float_num": 42.25, - "double_num": 42.25, - "bit_val": true, - "date_val": "2024-03-14", - "time_val": "13:14:15", - "datetime_val": "2024-03-14T13:14:15", - "timestamp_val": "2024-03-14T13:14:15+00:00", - "year_val": 2024, - "char_val": "CHAR", - "varchar_val": "VARCHAR", - "text_val": "TEXT", - "blob_val": "data:application/octet-stream;base64,YmxvYg==" - }), - ); - - sqlx::query("DROP TABLE _sqlp_t").execute(&mut c).await?; - - Ok(()) - } - - #[actix_web::test] - async fn test_sqlite_types() -> anyhow::Result<()> { - let Some(db_url) = db_specific_test("sqlite") else { - return Ok(()); + #[test] + fn duplicate_columns_become_arrays() { + let row = DbRow { + columns: vec![ + DbColumn { + name: "value".into(), + type_name: None, + }, + DbColumn { + name: "value".into(), + type_name: None, + }, + ], + values: vec![DbValue::Integer(1), DbValue::Integer(2)], + kind: DbKind::Sqlite, }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - 42 as integer, - 42.25 as real, - 'xxx' as string, - x'68656c6c6f20776f726c64' as blob", - ) - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "integer": 42, - "real": 42.25, - "string": "xxx", - "blob": "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", - }), - ); - Ok(()) - } - - #[actix_web::test] - async fn test_mssql_types() -> anyhow::Result<()> { - let Some(db_url) = db_specific_test("mssql") else { - return Ok(()); + assert_eq!(row_to_json(&row), serde_json::json!({"value": [1, 2]})); + } + + #[test] + fn odbc_uppercase_columns_are_lowercased() { + let row = DbRow { + columns: vec![DbColumn { + name: "TITLE_TEXT".into(), + type_name: None, + }], + values: vec![DbValue::Text("hello".into())], + kind: DbKind::Odbc, }; - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT - CAST(1 AS BIT) as true_bit, - CAST(0 AS BIT) as false_bit, - CAST(NULL AS BIT) as null_bit, - CAST(255 AS TINYINT) as tiny_int, - CAST(42 AS SMALLINT) as small_int, - CAST(42 AS INT) as integer, - CAST(42 AS BIGINT) as big_int, - CAST(42.25 AS REAL) as real, - CAST(42.25 AS FLOAT) as float, - CAST(42.25 AS DECIMAL(10,2)) as decimal, - CAST('2024-03-14' AS DATE) as date, - CAST('13:14:15' AS TIME) as time, - CAST('2024-03-14 13:14:15' AS DATETIME) as datetime, - CAST('2024-03-14 13:14:15' AS DATETIME2) as datetime2, - CAST('2024-03-14 13:14:15 +02:00' AS DATETIMEOFFSET) as datetimeoffset, - N'Unicode String' as nvarchar, - 'ASCII String' as varchar, - CAST(1234.56 AS MONEY) as money_val, - CAST(12.34 AS SMALLMONEY) as small_money_val, - CAST(0x6D7373716C AS VARBINARY(10)) as blob_data, - CONVERT(UNIQUEIDENTIFIER, '6F9619FF-8B86-D011-B42D-00C04FC964FF') as unique_identifier - " - ) - .fetch_one(&mut c) - .await?; - - expect_json_object_equal( - &row_to_json(&row), - &serde_json::json!({ - "true_bit": true, - "false_bit": false, - "null_bit": null, - "tiny_int": 255, - "small_int": 42, - "integer": 42, - "big_int": 42, - "real": 42.25, - "float": 42.25, - "decimal": 42.25, - "date": "2024-03-14", - "time": "13:14:15", - "datetime": "2024-03-14T13:14:15", - "datetime2": "2024-03-14T13:14:15", - "datetimeoffset": "2024-03-14T13:14:15+02:00", - "nvarchar": "Unicode String", - "varchar": "ASCII String", - "money_val": 1234.56, - "small_money_val": 12.34, - "blob_data": "data:application/octet-stream;base64,bXNzcWw=", - "unique_identifier": "6f9619ff-8b86-d011-b42d-00c04fc964ff" - }), - ); - Ok(()) - } - - fn expect_json_object_equal(actual: &Value, expected: &Value) { - use std::fmt::Write; - - if json_values_equal(actual, expected) { - return; - } - let actual = actual.as_object().unwrap(); - let expected = expected.as_object().unwrap(); - - let all_keys: std::collections::BTreeSet<_> = - actual.keys().chain(expected.keys()).collect(); - let max_key_len = all_keys.iter().map(|k| k.len()).max().unwrap_or(0); - - let mut comparison_string = String::new(); - for key in all_keys { - let actual_value = actual.get(key).unwrap_or(&Value::Null); - let expected_value = expected.get(key).unwrap_or(&Value::Null); - if json_values_equal(actual_value, expected_value) { - continue; - } - writeln!( - &mut comparison_string, - "{key: anyhow::Result<()> { - let db_url = test_database_url(); - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - - // Test various column name formats to ensure canonical_col_name works correctly - let row = sqlx::query( - r#"SELECT - 42 as "UPPERCASE_COL", - 42 as "lowercase_col", - 42 as "Mixed_Case_Col", - 42 as "COL_WITH_123_NUMBERS", - 42 as "col-with-dashes", - 42 as "col with spaces", - 42 as "_UNDERSCORE_PREFIX", - 42 as "123_NUMBER_PREFIX" - "#, - ) - .fetch_one(&mut c) - .await?; - - let json_result = row_to_json(&row); - - // For ODBC databases, uppercase columns should be converted to lowercase - // For other databases, names should remain as-is - let expected_json = if c.kind() == sqlx::any::AnyKind::Odbc { - // ODBC database - uppercase should be converted to lowercase - serde_json::json!({ - "uppercase_col": 42, - "lowercase_col": 42, - "Mixed_Case_Col": 42, - "COL_WITH_123_NUMBERS": 42, - "col-with-dashes": 42, - "col with spaces": 42, - "_underscore_prefix": 42, - "123_NUMBER_PREFIX": 42 - }) - } else { - // Non-ODBC database - names remain as-is - serde_json::json!({ - "UPPERCASE_COL": 42, - "lowercase_col": 42, - "Mixed_Case_Col": 42, - "COL_WITH_123_NUMBERS": 42, - "col-with-dashes": 42, - "col with spaces": 42, - "_UNDERSCORE_PREFIX": 42, - "123_NUMBER_PREFIX": 42 - }) - }; - - expect_json_object_equal(&json_result, &expected_json); - - Ok(()) - } - - #[actix_web::test] - async fn test_row_to_json_edge_cases() -> anyhow::Result<()> { - let db_url = test_database_url(); - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let dbms_name = c.dbms_name().await.expect("retrieve db name"); - - // Test edge cases for row_to_json - let row = sqlx::query( - "SELECT - NULL as null_col, - '' as empty_string, - 0 as zero_value, - -42 as negative_int, - 1.23456 as my_float, - 'special_chars_!@#$%^&*()' as special_chars, - 'line1 -line2' as multiline_string - ", - ) - .fetch_one(&mut c) - .await?; - - let json_result = row_to_json(&row); - - // For Oracle databases, empty string is treated as NULL. - let empty_str_is_null = dbms_name.to_lowercase().contains("oracle"); - - let expected_json = serde_json::json!({ - "null_col": null, - "empty_string": if empty_str_is_null { serde_json::Value::Null } else { serde_json::Value::String(String::new()) }, - "zero_value": 0, - "negative_int": -42, - "my_float": 1.23456, - "special_chars": "special_chars_!@#$%^&*()", - "multiline_string": "line1\nline2" - }); - - expect_json_object_equal(&json_result, &expected_json); - - Ok(()) - } - - /// Compare JSON values, treating integers and floats that are numerically equal as equal - fn json_values_equal(a: &Value, b: &Value) -> bool { - use Value::*; - - match (a, b) { - (Null, Null) => true, - (Bool(a), Bool(b)) => a == b, - (Number(a), Number(b)) => { - // Treat integers and floats as equal if they represent the same numerical value - a.as_f64() == b.as_f64() - } - (String(a), String(b)) => a == b, - (Array(a), Array(b)) => { - a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| json_values_equal(a, b)) - } - (Object(a), Object(b)) => { - if a.len() != b.len() { - return false; - } - a.iter().all(|(key, value)| { - b.get(key) - .is_some_and(|expected_value| json_values_equal(value, expected_value)) - }) - } - _ => false, - } - } } diff --git a/src/webserver/database/sqlpage_functions/function_definition_macro.rs b/src/webserver/database/sqlpage_functions/function_definition_macro.rs index 235b7ffa..f9696597 100644 --- a/src/webserver/database/sqlpage_functions/function_definition_macro.rs +++ b/src/webserver/database/sqlpage_functions/function_definition_macro.rs @@ -56,7 +56,7 @@ macro_rules! sqlpage_functions { &self, #[allow(unused_variables)] request: &'a $crate::webserver::http_request_info::ExecutionContext, - db_connection: &mut Option>, + db_connection: &mut $crate::webserver::database::execute_queries::DbConn, params: Vec>> ) -> anyhow::Result>> { use $crate::webserver::database::sqlpage_functions::function_traits::*; diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index bf669470..f1076ec9 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -7,7 +7,6 @@ use crate::webserver::{ sqlpage_functions::{http_fetch_request::HttpFetchRequest, url_parameters::URLParameters}, }, http_client::make_http_client, - request_variables::SetVariablesMap, single_or_vec::SingleOrVec, }; use anyhow::{Context, anyhow}; @@ -722,6 +721,17 @@ async fn run_sql<'a>( log::debug!("run_sql: first argument is NULL, returning NULL"); return Ok(None); }; + if request + .included_sql_files + .iter() + .any(|path| path == sql_file_path.as_ref()) + { + anyhow::bail!( + "Too many nested inclusions. run_sql cannot include a file that is already being executed in the same inclusion chain. \ + Executing sqlpage.run_sql('{sql_file_path}') would create a loop. \ + This is to prevent infinite loops and stack overflows." + ); + } let run_sql_span = tracing::info_span!( "sqlpage.file", otel.name = format!("SQL {sql_file_path}"), @@ -738,14 +748,14 @@ async fn run_sql<'a>( .instrument(run_sql_span.clone()) .await .with_context(|| format!("run_sql: invalid path {sql_file_path:?}"))?; - let tmp_req = if let Some(variables) = variables { - let variables: SetVariablesMap = serde_json::from_str(&variables).with_context(|| { + let variables = if let Some(variables) = variables { + serde_json::from_str(&variables).with_context(|| { format!("run_sql(\'{sql_file_path}\', \'{variables}\'): the second argument should be a JSON object with string keys and values") - })?; - request.fork_with_variables(variables) + })? } else { - request.fork() + request.set_variables.borrow().clone() }; + let tmp_req = request.fork_for_run_sql(sql_file_path.as_ref(), variables); let max_recursion_depth = app_state.config.max_recursion_depth; if tmp_req.clone_depth > max_recursion_depth { anyhow::bail!( diff --git a/src/webserver/error.rs b/src/webserver/error.rs index df1bf9c0..66d4a697 100644 --- a/src/webserver/error.rs +++ b/src/webserver/error.rs @@ -64,7 +64,10 @@ pub(super) fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> H "Basic realm=\"Authentication required\", charset=\"UTF-8\"", )); } - } else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() { + } else if e + .downcast_ref::() + .is_some_and(|err| matches!(err, crate::webserver::database::DbError::PoolTimedOut)) + { use rand::RngExt; resp.status(StatusCode::TOO_MANY_REQUESTS).insert_header(( header::RETRY_AFTER, @@ -92,7 +95,10 @@ pub(super) fn anyhow_err_to_actix_resp(e: &anyhow::Error, state: &AppState) -> H fn anyhow_error_status(e: &anyhow::Error) -> Option { if let Some(&ErrorWithStatus { status }) = e.downcast_ref() { Some(status) - } else if let Some(sqlx::Error::PoolTimedOut) = e.downcast_ref() { + } else if e + .downcast_ref::() + .is_some_and(|err| matches!(err, crate::webserver::database::DbError::PoolTimedOut)) + { Some(StatusCode::TOO_MANY_REQUESTS) } else { None diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 242d9613..2884b98b 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -275,11 +275,11 @@ async fn render_sql( let database_entries_stream = stream_query_results_with_conn(&sql_file, &exec_ctx, &mut conn); let database_entries_stream = stop_at_first_error(database_entries_stream); - let response_with_writer = build_response_header_and_stream( + let response_with_writer = Box::pin(build_response_header_and_stream( Arc::clone(&app_state), database_entries_stream, request_context, - ) + )) .await; match response_with_writer { Ok(ResponseWithWriter::RenderStream { @@ -678,7 +678,7 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( .with_context(|| "Unable to start the application")?; // We are done, we can close the database connection - final_state.db.close().await?; + final_state.db.close()?; Ok(()) } diff --git a/src/webserver/http_request_info.rs b/src/webserver/http_request_info.rs index da628736..6cba2e32 100644 --- a/src/webserver/http_request_info.rs +++ b/src/webserver/http_request_info.rs @@ -53,6 +53,7 @@ pub struct ExecutionContext { pub request: Rc, pub set_variables: RefCell, pub clone_depth: u8, + pub included_sql_files: Rc>, } impl ExecutionContext { @@ -62,6 +63,7 @@ impl ExecutionContext { request: Rc::new(request), set_variables: RefCell::new(SetVariablesMap::new()), clone_depth: 0, + included_sql_files: Rc::new(Vec::new()), } } @@ -71,6 +73,7 @@ impl ExecutionContext { request: Rc::clone(&self.request), set_variables: RefCell::new(self.set_variables.borrow().clone()), clone_depth: self.clone_depth + 1, + included_sql_files: Rc::clone(&self.included_sql_files), } } @@ -80,6 +83,19 @@ impl ExecutionContext { request: Rc::clone(&self.request), set_variables: RefCell::new(variables), clone_depth: self.clone_depth + 1, + included_sql_files: Rc::clone(&self.included_sql_files), + } + } + + #[must_use] + pub fn fork_for_run_sql(&self, sql_file_path: &str, variables: SetVariablesMap) -> Self { + let mut included_sql_files = self.included_sql_files.as_ref().clone(); + included_sql_files.push(sql_file_path.to_string()); + Self { + request: Rc::clone(&self.request), + set_variables: RefCell::new(variables), + clone_depth: self.clone_depth + 1, + included_sql_files: Rc::new(included_sql_files), } } diff --git a/tests/core/mod.rs b/tests/core/mod.rs index 229aeac2..159cabd0 100644 --- a/tests/core/mod.rs +++ b/tests/core/mod.rs @@ -1,9 +1,9 @@ use actix_web::{http::StatusCode, test}; use sqlpage::{ AppState, + webserver::database::DbParam, webserver::{self, make_placeholder}, }; -use sqlx::Executor as _; use crate::common::{make_app_data_from_config, req_path, req_path_with_app_data, test_config}; @@ -58,19 +58,25 @@ async fn test_routing_with_db_fs() { } let drop_sql = "DROP TABLE IF EXISTS sqlpage_files"; - state.db.connection.execute(drop_sql).await.unwrap(); + let mut conn = state.db.connection.acquire().await.unwrap(); + conn.execute_command(drop_sql, &[]).await.unwrap(); let create_table_sql = sqlpage::filesystem::DbFsQueries::get_create_table_sql(state.db.info.database_type); - state.db.connection.execute(create_table_sql).await.unwrap(); + conn.execute_command(create_table_sql, &[]).await.unwrap(); let insert_sql = format!( "INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', {})", make_placeholder(state.db.info.kind, 1) ); - sqlx::query(&insert_sql) - .bind("select ''text'' as component, ''Hi from db !'' AS contents;".as_bytes()) - .execute(&state.db.connection) - .await - .unwrap(); + conn.execute_command( + &insert_sql, + &[DbParam::Bytes( + "select ''text'' as component, ''Hi from db !'' AS contents;" + .as_bytes() + .to_vec(), + )], + ) + .await + .unwrap(); let state = AppState::init(&config).await.unwrap(); let app_data = actix_web::web::Data::new(state); @@ -101,23 +107,28 @@ async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() { let expected_db_path = "\u{FFFD}.txt"; let mut conn = state.db.connection.acquire().await.unwrap(); - (&mut *conn) - .execute(sqlpage::filesystem::DbFsQueries::get_create_table_sql( + conn.execute_command( + sqlpage::filesystem::DbFsQueries::get_create_table_sql( sqlpage::webserver::database::SupportedDatabase::Sqlite, - )) - .await - .unwrap(); + ), + &[], + ) + .await + .unwrap(); let insert_sql = format!( "INSERT INTO sqlpage_files(path, contents) VALUES ({}, {})", make_placeholder(state.db.info.kind, 1), make_placeholder(state.db.info.kind, 2) ); - sqlx::query(&insert_sql) - .bind(expected_db_path) - .bind("file from db fs".as_bytes()) - .execute(&mut *conn) - .await - .unwrap(); + conn.execute_command( + &insert_sql, + &[ + DbParam::Text(expected_db_path.into()), + DbParam::Bytes("file from db fs".as_bytes().to_vec()), + ], + ) + .await + .unwrap(); drop(conn); let state = AppState::init(&config).await.unwrap();