diff --git a/.gitignore b/.gitignore index 8731c850ff..ea195c5d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ node_modules/ # Build output dist/ - -# Tauri build output is handled by src-tauri/.gitignore +/target/ # OS files .DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..1cad4596c7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,10062 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aead" +version = "0.6.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" +dependencies = [ + "crypto-common 0.2.1", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher 0.5.1", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", + "subtle", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +dependencies = [ + "aead 0.6.0-rc.10", + "aes 0.9.0", + "cipher 0.5.1", + "ctr 0.10.0", + "ghash 0.6.0", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter 0.1.4", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "607e64bb911ee4f90483e044fe78f175989148c2892e659a2cd25429e782ec54" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e754319ed8a85d817fe7adf183227e0b5308b82790a737b426c1124626b48118" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841321891f247aa86c6112c80d83d89cb36e0addd020fa2425085b8eb6c3f579" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.17.0", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f955dfb73fae000425f49c8226d2044dab60fb7ad4af1e24f961756354d996c9" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5e686972523798f76bef355145bc1ae25a84c731e650268d31ab763c701663" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64 0.22.1", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3b5846209775b6dc8056d77ff9a032b27043383dd5488abd0b663e265b9373" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa70d9d6b1356f1fb9f1f651b84a725b7e0abb93f188cf7d31f14abfa2f2e6f" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faec88a945338192beffbbd4be0def70135422930caa244ac3cec0cd213b26b4" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18aa020f6bc8e5201dcd2d4b7f98c68f8a410ef37128263243e6ff2a47a67d4f" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "arrow-select" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a657ab5132e9c8ca3b24eb15a823d0ced38017fe3930ff50167466b02e2d592c" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "58.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6de2efbbd1a9f9780ceb8d1ff5d20421b35863b361e3386b4f571f1fc69fcb8" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-native-tls" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe" +dependencies = [ + "futures-util", + "native-tls", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atoi_simd" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a49e05797ca52e312a0c658938b7d00693ef037799ef7187678f212d7684cf" +dependencies = [ + "debug_unsafe", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2 0.12.2", + "sha2 0.10.9", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash 0.8.12", + "base64 0.22.1", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.14.0", + "js-sys", + "once_cell", + "rand 0.9.4", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calamine" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a9acfeb1555aa8def91fe8ff208aadaea850c109968ec35ac965edbe7d210b" +dependencies = [ + "atoi_simd", + "byteorder", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml 0.37.5", + "serde", + "zip 4.6.1", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +dependencies = [ + "cipher 0.5.1", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +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.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", + "inout 0.2.2", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "colorchoice" +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", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "connection-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.0-rc.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96dacf199529fb801ae62a9aafdc01b189e9504c0d1ee1512a4c16bcd8666a93" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "crypto-primes" +version = "0.7.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6081ce8b60c0e533e2bba42771b94eb6149052115f4179744d5779883dc98583" +dependencies = [ + "crypto-bigint", + "libm", + "rand_core 0.10.1", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +dependencies = [ + "cipher 0.5.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes 0.8.4", + "block-padding 0.3.3", + "cbc 0.1.2", + "dbus", + "fastrand", + "hkdf 0.12.4", + "num", + "once_cell", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "dbx" +version = "0.4.0" +dependencies = [ + "anyhow", + "calamine", + "chrono", + "csv", + "dbx-core", + "duckdb", + "futures", + "keyring", + "log", + "mongodb", + "oracle-rs", + "percent-encoding", + "portpicker", + "redis", + "reqwest 0.12.28", + "russh", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-log", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-updater", + "tiberius", + "tokio", + "tokio-util", + "uuid", +] + +[[package]] +name = "dbx-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "calamine", + "chrono", + "csv", + "duckdb", + "futures", + "log", + "mongodb", + "oracle-rs", + "percent-encoding", + "portpicker", + "redis", + "reqwest 0.12.28", + "russh", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sqlx", + "tiberius", + "tokio", + "tokio-util", + "uuid", +] + +[[package]] +name = "dbx-web" +version = "0.4.0" +dependencies = [ + "async-stream", + "axum", + "dbx-core", + "env_logger", + "futures", + "hex", + "log", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "tokio", + "tokio-util", + "tower-http", + "uuid", +] + +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "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 = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dotenvy" +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" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "duckdb" +version = "1.10502.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdc796383b176dd5a45353fbb5e64583c0ee4da12cb62c9e510b785324b2488" +dependencies = [ + "arrow", + "cast", + "comfy-table", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libduckdb-sys", + "num-integer", + "rust_decimal", + "strum", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.17.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829" +dependencies = [ + "der 0.8.0", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature 3.0.0", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8 0.11.0-rc.11", + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.10.1", + "serde", + "sha2 0.11.0", + "signature 3.0.0", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.0-rc.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde7860544606d222fd6bd6d9f9a0773321bf78072a637e1d560a058c0031978" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.1", + "digest 0.11.3", + "hkdf 0.13.0", + "hybrid-array", + "once_cell", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.1", + "rustcrypto-ff", + "rustcrypto-group", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[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 2.0.117", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter 1.0.1", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[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 = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[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 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-array" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +dependencies = [ + "generic-array 0.14.7", + "rustversion", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval 0.6.2", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval 0.7.1", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.2.1", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "ctutils", + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding 0.3.3", + "generic-array 0.14.7", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + +[[package]] +name = "internal-russh-forked-ssh-key" +version = "0.6.18+upstream-0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f8a978272e3cbdf4768f7363eb1c8e1e6ba63c52a3ed05e29e222da4aec7cb" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "crypto-bigint", + "ecdsa", + "ed25519-dalek", + "hex", + "hmac 0.13.0", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.10.1", + "rsa 0.10.0-rc.16", + "sec1", + "sha1 0.11.0", + "sha2 0.11.0", + "signature 3.0.0", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "internal-russh-num-bigint" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.10.1", + "rand_core 0.10.1", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[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 0.2.1", +] + +[[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 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core 0.10.1", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libduckdb-sys" +version = "1.10502.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7401630ae2abcff642f7156294289e50f2d222e061c026ad797b01bf20c215" +dependencies = [ + "cc", + "flate2", + "pkg-config", + "reqwest 0.12.28", + "serde", + "serde_json", + "tar", + "vcpkg", + "zip 6.0.0", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md5" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6bcd6433cff03a4bfc3d9834d504467db1f1cf6d0ea765d37d330249ed629d" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ml-kem" +version = "0.3.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8198b5db27ac9773534c371751a59dc18aec8b80aa141e69abfdd1dec2e3f78c" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c90d33a0dac244570c26461d761ffaeadb3bfc2b17cc625ae2185cafdffae" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.5+1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" + +[[package]] +name = "mongodb" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef2c933617431ad0246fb5b43c425ebdae18c7f7259c87de0726d93b0e7e91b" +dependencies = [ + "base64 0.22.1", + "bitflags 2.11.1", + "bson", + "derive-where", + "derive_more", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-proto", + "hickory-resolver", + "hmac 0.12.1", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2 0.12.2", + "percent-encoding", + "rand 0.9.4", + "rustc_version_runtime", + "rustls", + "rustversion", + "serde", + "serde_bytes", + "serde_with", + "sha1 0.10.6", + "sha2 0.10.9", + "socket2", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots 1.0.7", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5758dc828eb2d02ec30563cba365609d56ddd833190b192beaee2b475a7bb3" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "muda" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "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.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "oracle-rs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9ca364b441f92b717658c62e85207158b30285deb01811ccd923017f61612e" +dependencies = [ + "aes 0.8.4", + "async-trait", + "bytes", + "cbc 0.1.2", + "chrono", + "hex", + "hmac 0.12.1", + "hostname", + "indexmap 2.14.0", + "md-5", + "pbkdf2 0.12.2", + "pkcs8 0.10.2", + "rand 0.8.6", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "sha1 0.10.6", + "sha2 0.10.9", + "thiserror 1.0.69", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "p256" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018bfbb86e05fd70a83e985921241035ee09fcd369c4a2c3680b389a01d2ad28" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0", +] + +[[package]] +name = "p384" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c91df688211f5957dbe2ab599dcbcaade8d6d3cdc15c5b350d350d7d07ce423" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2 0.11.0", +] + +[[package]] +name = "p521" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6cd9451de522549d36cc78a1b45a699a3d55a872e8ea0c8f0318e502d99e2c" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0", +] + +[[package]] +name = "pageant" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" +dependencies = [ + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand 0.8.6", + "sha2 0.10.9", + "thiserror 1.0.69", + "tokio", + "windows 0.62.2", + "windows-strings 0.5.1", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[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-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.0", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes 0.8.4", + "cbc 0.1.2", + "der 0.7.10", + "pbkdf2 0.12.2", + "scrypt 0.11.0", + "sha2 0.10.9", + "spki 0.7.3", +] + +[[package]] +name = "pkcs5" +version = "0.8.0-rc.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a777c6e26664bc9504b3ce3f6133f8f20d9071f130a4f9fcbd3186959d8dd6" +dependencies = [ + "aes 0.9.0", + "aes-gcm 0.11.0-rc.3", + "cbc 0.2.0", + "der 0.8.0", + "pbkdf2 0.13.0", + "rand_core 0.10.1", + "scrypt 0.12.0", + "sha2 0.11.0", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "pkcs5 0.7.1", + "rand_core 0.6.4", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "pkcs5 0.8.0-rc.13", + "rand_core 0.10.1", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkg-config" +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 = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml 0.39.2", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.6", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primefield" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93401c13cc7ff24684571cfca9d3cf9ebabfaf3d4b7b9963ade41ec54da196b5" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.1", + "rand_core 0.10.1", + "rustcrypto-ff", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c5c8a39bcd764bfedf456e8d55e115fe86dda3e0f555371849f2a41cbc9706" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redis" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-rustls", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", +] + +[[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", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.5.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a3127ee32baec36af75b4107082d9bd823501ec14a4e016be4b6b37faa74ae" +dependencies = [ + "hmac 0.13.0", + "subtle", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb9fd8c1edd9e6a2693623baf0fe77ff05ce022a5d7746900ffc38a15c233de" +dependencies = [ + "const-oid 0.10.2", + "crypto-bigint", + "crypto-primes", + "digest 0.11.3", + "pkcs1 0.8.0-rc.4", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.1", + "sha2 0.11.0", + "signature 3.0.0", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "russh" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e358980fe9b079b99da387117864ee6f0a3fd02f39e5b5fde6af9c2895374" +dependencies = [ + "aead 0.6.0-rc.10", + "aes 0.8.4", + "aes 0.9.0", + "aes-gcm 0.11.0-rc.3", + "aws-lc-rs", + "bitflags 2.11.1", + "block-padding 0.3.3", + "byteorder", + "bytes", + "cbc 0.1.2", + "cbc 0.2.0", + "cipher 0.5.1", + "crypto-bigint", + "ctr 0.10.0", + "ctr 0.9.2", + "curve25519-dalek", + "data-encoding", + "delegate", + "der 0.8.0", + "digest 0.10.7", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "flate2", + "futures", + "generic-array 1.4.1", + "getrandom 0.2.17", + "ghash 0.6.0", + "hex-literal", + "hkdf 0.13.0", + "hmac 0.12.1", + "hmac 0.13.0", + "inout 0.1.4", + "internal-russh-forked-ssh-key", + "internal-russh-num-bigint", + "keccak", + "log", + "md5 0.7.0", + "ml-kem", + "module-lattice", + "num-bigint", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2 0.12.2", + "pbkdf2 0.13.0", + "pkcs1 0.8.0-rc.4", + "pkcs5 0.8.0-rc.13", + "pkcs8 0.11.0-rc.11", + "polyval 0.7.1", + "rand 0.10.1", + "rand_core 0.10.1", + "rsa 0.10.0-rc.16", + "russh-cryptovec", + "russh-util", + "salsa20 0.11.0", + "scrypt 0.12.0", + "sec1", + "sha1 0.10.6", + "sha1 0.11.0", + "sha2 0.10.9", + "sha2 0.11.0", + "sha3", + "signature 3.0.0", + "spki 0.8.0-rc.4", + "ssh-encoding", + "subtle", + "thiserror 2.0.18", + "tokio", + "typenum", + "universal-hash 0.6.1", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36140e8a20297bc2e8338807c3d9ca911f7fa49d7539cbcd6d48d3befd70efd8" +dependencies = [ + "log", + "nix 0.31.2", + "ssh-encoding", + "windows-sys 0.61.2", +] + +[[package]] +name = "russh-util" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" +dependencies = [ + "chrono", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustcrypto-ff" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "rustcrypto-group" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" +dependencies = [ + "rand_core 0.10.1", + "rustcrypto-ff", + "subtle", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +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", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[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 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "salsa20" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" +dependencies = [ + "cfg-if", + "cipher 0.5.1", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20 0.10.2", + "sha2 0.10.9", +] + +[[package]] +name = "scrypt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" +dependencies = [ + "cfg-if", + "pbkdf2 0.13.0", + "salsa20 0.11.0", + "sha2 0.11.0", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der 0.8.0", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes 0.8.4", + "cbc 0.1.2", + "futures-util", + "generic-array 0.14.7", + "hkdf 0.12.4", + "num", + "once_cell", + "rand 0.8.6", + "serde", + "sha2 0.10.9", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.11.1", + "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.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.18", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "native-tls", + "once_cell", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa 0.9.10", + "rust_decimal", + "serde", + "sha1 0.10.6", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes 0.8.4", + "aes-gcm 0.10.3", + "cbc 0.1.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "ctr 0.9.2", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "bytes", + "pem-rfc7468 0.7.0", + "sha2 0.10.9", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tao" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.3", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.3", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip 4.6.1", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf 0.11.3", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiberius" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d" +dependencies = [ + "async-native-tls", + "async-trait", + "asynchronous-codec", + "byteorder", + "bytes", + "chrono", + "connection-string", + "encoding_rs", + "enumflags2", + "futures-util", + "num-traits", + "once_cell", + "pin-project-lite", + "pretty-hex", + "rust_decimal", + "thiserror 1.0.69", + "tracing", + "uuid", + "winauth", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags 2.11.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.1", + "ctutils", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winauth" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f820cd208ce9c6b050812dc2d724ba98c6c1e9db5ce9b3f58d925ae5723a5e6" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "md5 0.6.1", + "rand 0.7.3", + "winapi", +] + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1 0.10.6", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.14.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.14.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..d01c6eaba3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +resolver = "2" +members = ["src-tauri", "crates/dbx-core", "src-web"] + +[profile.release] +panic = "abort" +strip = true +lto = true +codegen-units = 1 +opt-level = "s" diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml new file mode 100644 index 0000000000..eb226becc5 --- /dev/null +++ b/crates/dbx-core/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "dbx-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +percent-encoding = "2" +log = "0.4" +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["compat"] } +chrono = { version = "0.4", features = ["serde"] } +rust_decimal = { version = "1", features = ["serde"] } +anyhow = "1" +uuid = { version = "1", features = ["v4", "serde"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "mysql", "postgres", "sqlite", "json", "chrono", "uuid", "rust_decimal"] } +redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp"] } +rustls = { version = "0.23", features = ["aws-lc-rs"] } +duckdb = { version = "1.3.2", features = ["bundled"] } +tiberius = { version = "0.12.3", features = ["tds73", "chrono", "rust_decimal"] } +reqwest = { version = "0.12", features = ["json", "stream"] } +futures = "0.3" +mongodb = "3.2.5" +oracle-rs = "0.1" +russh = "0.60" +portpicker = "0.1.1" +csv = "1" +calamine = "0.30.1" diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs new file mode 100644 index 0000000000..93a35d13d5 --- /dev/null +++ b/crates/dbx-core/src/ai.rs @@ -0,0 +1,792 @@ +use futures::StreamExt; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock}; +use tokio::sync::RwLock; + +// --------------------------------------------------------------------------- +// Stream cancel registry +// --------------------------------------------------------------------------- + +static AI_STREAMS: LazyLock>>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +pub async fn register_stream(session_id: &str) -> Arc { + let cancelled = Arc::new(AtomicBool::new(false)); + AI_STREAMS + .write() + .await + .insert(session_id.to_string(), cancelled.clone()); + cancelled +} + +pub async fn cancel_stream(session_id: &str) -> bool { + if let Some(flag) = AI_STREAMS.read().await.get(session_id) { + flag.store(true, Ordering::Relaxed); + true + } else { + false + } +} + +pub async fn unregister_stream(session_id: &str) { + AI_STREAMS.write().await.remove(session_id); +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AiProvider { + Claude, + Openai, + Custom, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AiApiStyle { + Completions, + Responses, +} + +impl Default for AiApiStyle { + fn default() -> Self { + Self::Completions + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiConfig { + pub provider: AiProvider, + #[serde(default)] + pub api_key: String, + #[serde(default)] + pub endpoint: String, + #[serde(default)] + pub model: String, + #[serde(default)] + pub api_style: AiApiStyle, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiMessage { + pub role: String, + pub content: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiCompletionRequest { + pub config: AiConfig, + pub system_prompt: String, + pub messages: Vec, + pub max_tokens: Option, + pub temperature: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiStreamChunk { + pub session_id: String, + pub delta: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_delta: Option, + pub done: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiChatMessage { + pub role: String, + pub content: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiConversation { + pub id: String, + pub title: String, + pub connection_name: String, + pub database: String, + pub messages: Vec, + pub created_at: String, + pub updated_at: String, +} + +// --------------------------------------------------------------------------- +// Pure helpers +// --------------------------------------------------------------------------- + +pub fn resolve_endpoint(config: &AiConfig) -> String { + let ep = config.endpoint.trim().trim_end_matches('/'); + if ep.ends_with("/chat/completions") + || ep.ends_with("/responses") + || ep.ends_with("/messages") + { + return ep.to_string(); + } + match config.provider { + AiProvider::Claude => format!("{ep}/messages"), + AiProvider::Openai | AiProvider::Custom => { + if config.api_style == AiApiStyle::Responses { + format!("{ep}/responses") + } else { + format!("{ep}/chat/completions") + } + } + } +} + +pub fn stream_data_payload(line: &str) -> Option<&str> { + let line = line.trim(); + if line.is_empty() + || line.starts_with(':') + || line.starts_with("event:") + || line.starts_with("id:") + { + return None; + } + if let Some(data) = line.strip_prefix("data:") { + return Some(data.trim_start()); + } + if line.starts_with('{') { + return Some(line); + } + None +} + +pub fn claude_stream_text(event: &serde_json::Value) -> Option<&str> { + if event["type"] == "content_block_delta" { + return event["delta"]["text"].as_str(); + } + None +} + +pub fn openai_stream_text(event: &serde_json::Value) -> Option<&str> { + event["choices"] + .get(0) + .and_then(|choice| { + choice["delta"]["content"] + .as_str() + .or_else(|| choice["message"]["content"].as_str()) + }) + .or_else(|| event["content"].as_str()) + .filter(|text| !text.is_empty()) +} + +pub fn openai_stream_reasoning(event: &serde_json::Value) -> Option<&str> { + event["choices"] + .get(0) + .and_then(|choice| choice["delta"]["reasoning_content"].as_str()) + .filter(|text| !text.is_empty()) +} + +pub fn responses_stream_text(event: &serde_json::Value) -> Option<&str> { + event["delta"].as_str().filter(|s| !s.is_empty()) +} + +pub fn extract_error(data: &serde_json::Value) -> Option { + data["error"]["message"] + .as_str() + .or_else(|| data["error"].as_str()) + .map(ToString::to_string) +} + +pub fn build_responses_input(system_prompt: &str, messages: &[AiMessage]) -> serde_json::Value { + let mut input = Vec::new(); + if !system_prompt.is_empty() { + input.push(json!({ + "role": "developer", + "content": system_prompt, + })); + } + for m in messages { + input.push(json!({ + "role": m.role, + "content": m.content, + })); + } + json!(input) +} + +// --------------------------------------------------------------------------- +// Validation helper +// --------------------------------------------------------------------------- + +fn validate_config(config: &AiConfig) -> Result<(), String> { + if config.api_key.trim().is_empty() { + return Err("API key is required".to_string()); + } + if config.endpoint.trim().is_empty() { + return Err("Endpoint is required".to_string()); + } + if config.model.trim().is_empty() { + return Err("Model is required".to_string()); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Non-streaming calls +// --------------------------------------------------------------------------- + +pub async fn call_claude( + client: &reqwest::Client, + request: AiCompletionRequest, +) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + "x-api-key", + HeaderValue::from_str(&request.config.api_key).map_err(|e| e.to_string())?, + ); + headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01")); + + let body = json!({ + "model": request.config.model, + "max_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + "system": request.system_prompt, + "messages": request.messages, + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("Claude request failed: {e}"))?; + + let status = res.status(); + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + if !status.is_success() { + return Err(extract_error(&data).unwrap_or_else(|| format!("Claude API error: {status}"))); + } + + Ok(data["content"] + .as_array() + .and_then(|items| items.iter().find_map(|item| item["text"].as_str())) + .unwrap_or_default() + .to_string()) +} + +pub async fn call_openai_compatible( + client: &reqwest::Client, + request: AiCompletionRequest, +) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)) + .map_err(|e| e.to_string())?, + ); + + let mut messages = vec![json!({ "role": "system", "content": request.system_prompt })]; + messages.extend( + request + .messages + .iter() + .map(|message| json!({ "role": message.role, "content": message.content })), + ); + + let body = json!({ + "model": request.config.model, + "messages": messages, + "max_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("AI request failed: {e}"))?; + + let status = res.status(); + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + if !status.is_success() { + return Err(extract_error(&data).unwrap_or_else(|| format!("API error: {status}"))); + } + + Ok(data["choices"][0]["message"]["content"] + .as_str() + .unwrap_or_default() + .to_string()) +} + +pub async fn call_responses_api( + client: &reqwest::Client, + request: AiCompletionRequest, +) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)) + .map_err(|e| e.to_string())?, + ); + + let body = json!({ + "model": request.config.model, + "input": build_responses_input(&request.system_prompt, &request.messages), + "max_output_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("AI request failed: {e}"))?; + + let status = res.status(); + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + if !status.is_success() { + return Err(extract_error(&data).unwrap_or_else(|| format!("API error: {status}"))); + } + + Ok(data["output"] + .as_array() + .and_then(|items| { + items.iter().find_map(|item| { + item["content"] + .as_array() + .and_then(|parts| parts.iter().find_map(|p| p["text"].as_str())) + }) + }) + .unwrap_or_default() + .to_string()) +} + +// --------------------------------------------------------------------------- +// High-level: test_connection_core / complete +// --------------------------------------------------------------------------- + +pub async fn test_connection_core(config: &AiConfig) -> Result { + validate_config(config)?; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .map_err(|e| e.to_string())?; + + let request = AiCompletionRequest { + config: config.clone(), + system_prompt: String::new(), + messages: vec![AiMessage { + role: "user".into(), + content: "hi".into(), + }], + max_tokens: Some(1), + temperature: Some(0.0), + }; + + match request.config.provider { + AiProvider::Claude => call_claude(&client, request).await, + AiProvider::Openai | AiProvider::Custom => { + if request.config.api_style == AiApiStyle::Responses { + call_responses_api(&client, request).await + } else { + call_openai_compatible(&client, request).await + } + } + } + .map(|_| "OK".to_string()) +} + +pub async fn complete(request: &AiCompletionRequest) -> Result { + validate_config(&request.config)?; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build() + .map_err(|e| e.to_string())?; + + match request.config.provider { + AiProvider::Claude => call_claude(&client, request.clone()).await, + AiProvider::Openai | AiProvider::Custom => { + if request.config.api_style == AiApiStyle::Responses { + call_responses_api(&client, request.clone()).await + } else { + call_openai_compatible(&client, request.clone()).await + } + } + } +} + +// --------------------------------------------------------------------------- +// Streaming +// --------------------------------------------------------------------------- + +pub async fn stream( + session_id: &str, + request: &AiCompletionRequest, + cancelled: &AtomicBool, + on_chunk: impl Fn(AiStreamChunk), +) -> Result<(), String> { + validate_config(&request.config)?; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .build() + .map_err(|e| e.to_string())?; + + match request.config.provider { + AiProvider::Claude => { + stream_claude(&client, session_id, request, cancelled, &on_chunk).await + } + AiProvider::Openai | AiProvider::Custom => { + if request.config.api_style == AiApiStyle::Responses { + stream_responses_api(&client, session_id, request, cancelled, &on_chunk).await + } else { + stream_openai(&client, session_id, request, cancelled, &on_chunk).await + } + } + } +} + +async fn stream_claude( + client: &reqwest::Client, + session_id: &str, + request: &AiCompletionRequest, + cancelled: &AtomicBool, + on_chunk: &impl Fn(AiStreamChunk), +) -> Result<(), String> { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + "x-api-key", + HeaderValue::from_str(&request.config.api_key).map_err(|e| e.to_string())?, + ); + headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01")); + + let body = json!({ + "model": request.config.model, + "max_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + "system": request.system_prompt, + "messages": request.messages, + "stream": true, + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("Claude request failed: {e}"))?; + + if !res.status().is_success() { + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + return Err(extract_error(&data).unwrap_or_else(|| "Claude API error".to_string())); + } + + let mut byte_stream = res.bytes_stream(); + let mut buf = String::new(); + + let mut finished = false; + while let Some(chunk) = byte_stream.next().await { + if cancelled.load(Ordering::Relaxed) { + break; + } + let chunk = chunk.map_err(|e| e.to_string())?; + buf.push_str(&String::from_utf8_lossy(&chunk)); + + while let Some(pos) = buf.find('\n') { + let line = buf[..pos].to_string(); + buf = buf[pos + 1..].to_string(); + + let Some(data) = stream_data_payload(&line) else { + continue; + }; + if data == "[DONE]" { + finished = true; + break; + } + + if let Ok(event) = serde_json::from_str::(data) { + if let Some(text) = claude_stream_text(&event) { + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: text.to_string(), + reasoning_delta: None, + done: false, + }); + } + } + } + + if finished { + break; + } + } + + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: String::new(), + reasoning_delta: None, + done: true, + }); + + Ok(()) +} + +async fn stream_openai( + client: &reqwest::Client, + session_id: &str, + request: &AiCompletionRequest, + cancelled: &AtomicBool, + on_chunk: &impl Fn(AiStreamChunk), +) -> Result<(), String> { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)) + .map_err(|e| e.to_string())?, + ); + + let mut messages = vec![json!({ "role": "system", "content": request.system_prompt })]; + messages.extend( + request + .messages + .iter() + .map(|m| json!({ "role": m.role, "content": m.content })), + ); + + let body = json!({ + "model": request.config.model, + "messages": messages, + "max_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + "stream": true, + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("AI request failed: {e}"))?; + + if !res.status().is_success() { + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + return Err(extract_error(&data).unwrap_or_else(|| "API error".to_string())); + } + + let mut byte_stream = res.bytes_stream(); + let mut buf = String::new(); + + let mut finished = false; + while let Some(chunk) = byte_stream.next().await { + if cancelled.load(Ordering::Relaxed) { + break; + } + let chunk = chunk.map_err(|e| e.to_string())?; + buf.push_str(&String::from_utf8_lossy(&chunk)); + + while let Some(pos) = buf.find('\n') { + let line = buf[..pos].to_string(); + buf = buf[pos + 1..].to_string(); + + let Some(data) = stream_data_payload(&line) else { + continue; + }; + if data == "[DONE]" { + finished = true; + break; + } + + if let Ok(event) = serde_json::from_str::(data) { + if let Some(reasoning) = openai_stream_reasoning(&event) { + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: String::new(), + reasoning_delta: Some(reasoning.to_string()), + done: false, + }); + } + if let Some(text) = openai_stream_text(&event) { + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: text.to_string(), + reasoning_delta: None, + done: false, + }); + } + } + } + + if finished { + break; + } + } + + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: String::new(), + reasoning_delta: None, + done: true, + }); + + Ok(()) +} + +async fn stream_responses_api( + client: &reqwest::Client, + session_id: &str, + request: &AiCompletionRequest, + cancelled: &AtomicBool, + on_chunk: &impl Fn(AiStreamChunk), +) -> Result<(), String> { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)) + .map_err(|e| e.to_string())?, + ); + + let body = json!({ + "model": request.config.model, + "input": build_responses_input(&request.system_prompt, &request.messages), + "max_output_tokens": request.max_tokens.unwrap_or(2048), + "temperature": request.temperature.unwrap_or(0.2), + "stream": true, + }); + + let res = client + .post(&resolve_endpoint(&request.config)) + .headers(headers) + .json(&body) + .send() + .await + .map_err(|e| format!("AI request failed: {e}"))?; + + if !res.status().is_success() { + let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + return Err(extract_error(&data).unwrap_or_else(|| "API error".to_string())); + } + + let mut byte_stream = res.bytes_stream(); + let mut buf = String::new(); + + let mut finished = false; + while let Some(chunk) = byte_stream.next().await { + if cancelled.load(Ordering::Relaxed) { + break; + } + let chunk = chunk.map_err(|e| e.to_string())?; + buf.push_str(&String::from_utf8_lossy(&chunk)); + + while let Some(pos) = buf.find('\n') { + let line = buf[..pos].to_string(); + buf = buf[pos + 1..].to_string(); + + let Some(data) = stream_data_payload(&line) else { + continue; + }; + if data == "[DONE]" { + finished = true; + break; + } + + if let Ok(event) = serde_json::from_str::(data) { + if let Some(text) = responses_stream_text(&event) { + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: text.to_string(), + reasoning_delta: None, + done: false, + }); + } + } + } + + if finished { + break; + } + } + + on_chunk(AiStreamChunk { + session_id: session_id.to_string(), + delta: String::new(), + reasoning_delta: None, + done: true, + }); + + Ok(()) +} + +// --------------------------------------------------------------------------- +// Conversation persistence (path-based) +// --------------------------------------------------------------------------- + +const MAX_CONVERSATIONS: usize = 50; + +pub fn read_conversations(path: &Path) -> Result, String> { + if !path.exists() { + return Ok(vec![]); + } + let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + serde_json::from_str(&json).map_err(|e| e.to_string()) +} + +pub fn write_conversations(path: &Path, conversations: &[AiConversation]) -> Result<(), String> { + let json = serde_json::to_string(conversations).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) +} + +pub fn save_conversation(path: &Path, conversation: AiConversation) -> Result<(), String> { + let mut conversations = read_conversations(path)?; + if let Some(pos) = conversations.iter().position(|c| c.id == conversation.id) { + conversations[pos] = conversation; + } else { + conversations.insert(0, conversation); + conversations.truncate(MAX_CONVERSATIONS); + } + write_conversations(path, &conversations) +} + +pub fn load_conversations(path: &Path) -> Result, String> { + read_conversations(path) +} + +pub fn delete_conversation(path: &Path, id: &str) -> Result<(), String> { + let conversations: Vec = read_conversations(path)? + .into_iter() + .filter(|c| c.id != id) + .collect(); + write_conversations(path, &conversations) +} + +pub fn save_config(path: &Path, config: &AiConfig) -> Result<(), String> { + let json = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) +} + +pub fn load_config(path: &Path) -> Result, String> { + if !path.exists() { + return Ok(None); + } + let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + serde_json::from_str(&json).map(Some).map_err(|e| e.to_string()) +} diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs new file mode 100644 index 0000000000..94d889db3a --- /dev/null +++ b/crates/dbx-core/src/connection.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::db; +use crate::db::ssh_tunnel::TunnelManager; +use crate::models::connection::{ConnectionConfig, DatabaseType}; +use crate::query_cancel::RunningQueries; + +pub fn expand_tilde(path: &str) -> String { + if path == "~" || path.starts_with("~/") { + if let Ok(home) = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) { + return format!("{}{}", home, &path[1..]); + } + } + path.to_string() +} + +pub enum PoolKind { + Mysql(sqlx::mysql::MySqlPool, bool), + Postgres(sqlx::postgres::PgPool), + Sqlite(sqlx::sqlite::SqlitePool), + Redis(tokio::sync::Mutex), + DuckDb(Arc>), + MongoDb(mongodb::Client), + ClickHouse(db::clickhouse_driver::ChClient), + SqlServer(Arc>), + Oracle(Arc>), + Elasticsearch(db::elasticsearch_driver::EsClient), +} + +pub struct AppState { + pub connections: Mutex>, + pub configs: Mutex>, + pub running_queries: RunningQueries, + pub tunnels: TunnelManager, +} + +impl AppState { + pub fn new() -> Self { + Self { + connections: Mutex::new(HashMap::new()), + configs: Mutex::new(HashMap::new()), + running_queries: RunningQueries::default(), + tunnels: TunnelManager::new(), + } + } + + pub async fn get_or_create_pool( + &self, + connection_id: &str, + database: Option<&str>, + ) -> Result { + let db_type = { + let configs = self.configs.lock().await; + configs.get(connection_id).map(|c| c.db_type.clone()) + }; + + let is_embedded = matches!(db_type, Some(DatabaseType::Sqlite) | Some(DatabaseType::DuckDb)); + if is_embedded { + return Ok(connection_id.to_string()); + } + + let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle)); + let pool_key = if is_single_conn { + connection_id.to_string() + } else { + match database { + Some(db) => format!("{connection_id}:{db}"), + None => connection_id.to_string(), + } + }; + + let conns = self.connections.lock().await; + if conns.contains_key(&pool_key) { + return Ok(pool_key); + } + drop(conns); + + let configs = self.configs.lock().await; + let config = configs + .get(connection_id) + .ok_or("Connection config not found")? + .clone(); + drop(configs); + + let mut db_config = config.clone(); + if let Some(db) = database { + if db_config.db_type != DatabaseType::Oracle { + db_config.database = Some(db.to_string()); + } + } + + let (host, port) = self.connection_host_port(connection_id, &db_config).await?; + let url = connection_url_for_endpoint(&db_config, &host, port); + let pool = match db_config.db_type { + DatabaseType::Mysql if db_config.needs_bare_mysql() => PoolKind::Mysql(db::mysql::connect_bare(&url).await?, true), + DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?, false), + DatabaseType::Doris | DatabaseType::StarRocks => PoolKind::Mysql(db::mysql::connect_bare(&url).await?, true), + DatabaseType::Postgres | DatabaseType::Redshift => PoolKind::Postgres(db::postgres::connect(&url).await?), + DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect_path(&expand_tilde(&db_config.host)).await?), + DatabaseType::Redis => { + let con = db::redis_driver::connect(&url).await?; + PoolKind::Redis(tokio::sync::Mutex::new(con)) + } + DatabaseType::DuckDb => { + let con = duckdb::Connection::open(&expand_tilde(&db_config.host)).map_err(|e| e.to_string())?; + PoolKind::DuckDb(Arc::new(std::sync::Mutex::new(con))) + } + DatabaseType::MongoDb => { + let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?; + PoolKind::MongoDb(client) + } + DatabaseType::ClickHouse => { + let username = if db_config.username.is_empty() { None } else { Some(db_config.username.clone()) }; + let password = if db_config.password.is_empty() { None } else { Some(db_config.password.clone()) }; + let client = db::clickhouse_driver::ChClient::new(&url, username, password); + db::clickhouse_driver::test_connection(&client).await?; + PoolKind::ClickHouse(client) + } + DatabaseType::SqlServer => { + let client = db::sqlserver::connect( + &host, + port, + &db_config.username, + &db_config.password, + db_config.database.as_deref(), + ) + .await?; + PoolKind::SqlServer(Arc::new(tokio::sync::Mutex::new(client))) + } + DatabaseType::Oracle => { + let client = db::oracle_driver::connect( + &host, + port, + db_config.database.as_deref().unwrap_or("ORCL"), + &db_config.username, &db_config.password, + ) + .await?; + PoolKind::Oracle(Arc::new(tokio::sync::Mutex::new(client))) + } + DatabaseType::Elasticsearch => { + let client = db::elasticsearch_driver::EsClient::new( + &url, + Some(&db_config.username), + Some(&db_config.password), + ); + db::elasticsearch_driver::test_connection(&client).await?; + PoolKind::Elasticsearch(client) + } + }; + + self.connections.lock().await.insert(pool_key.clone(), pool); + Ok(pool_key) + } + + pub async fn connection_host_port( + &self, + connection_id: &str, + config: &ConnectionConfig, + ) -> Result<(String, u16), String> { + if !config.ssh_enabled || config.ssh_host.is_empty() { + return Ok((config.host.clone(), config.port)); + } + + if let Some(local_port) = self.tunnels.local_port(connection_id).await { + return Ok(("127.0.0.1".to_string(), local_port)); + } + + let local_port = self + .tunnels + .start_tunnel( + connection_id, + &config.ssh_host, + config.ssh_port, + &config.ssh_user, + &config.ssh_password, + &config.ssh_key_path, + &config.ssh_key_passphrase, + &config.host, + config.port, + config.ssh_expose_lan, + ) + .await?; + + Ok(("127.0.0.1".to_string(), local_port)) + } + + pub async fn reconnect_pool( + &self, + connection_id: &str, + database: Option<&str>, + ) -> Result { + let is_single_conn = { + let configs = self.configs.lock().await; + configs.get(connection_id) + .map(|c| c.db_type == DatabaseType::Oracle || c.db_type == DatabaseType::Elasticsearch) + .unwrap_or(false) + }; + let pool_key = if is_single_conn { + connection_id.to_string() + } else { + match database { + Some(db) => format!("{connection_id}:{db}"), + None => connection_id.to_string(), + } + }; + self.connections.lock().await.remove(&pool_key); + self.get_or_create_pool(connection_id, database).await + } +} + +pub fn connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> String { + if host == config.host && port == config.port { + config.connection_url() + } else { + config.connection_url_with_host(host, port) + } +} + +pub fn redacted_connection_url_for_endpoint( + config: &ConnectionConfig, + host: &str, + port: u16, +) -> String { + if host == config.host && port == config.port { + config.redacted_connection_url() + } else { + config.redacted_connection_url_with_host(host, port) + } +} diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs new file mode 100644 index 0000000000..cdeaccb2bf --- /dev/null +++ b/crates/dbx-core/src/connection_secrets.rs @@ -0,0 +1,435 @@ +use crate::models::connection::ConnectionConfig; +use std::collections::HashMap; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +pub const MAIN_PASSWORD_KEY: &str = "password"; +pub const SSH_PASSWORD_KEY: &str = "ssh_password"; +pub const SSH_KEY_PASSPHRASE_KEY: &str = "ssh_key_passphrase"; +pub const CONNECTION_STRING_KEY: &str = "connection_string"; + +pub trait ConnectionSecretStore { + fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String>; + fn get_secret(&self, connection_id: &str, key: &str) -> Result, String>; + fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String>; +} + +pub struct FileSecretStore { + path: PathBuf, +} + +impl FileSecretStore { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + fn read_store(&self) -> HashMap { + std::fs::read_to_string(&self.path) + .ok() + .and_then(|json| serde_json::from_str(&json).ok()) + .unwrap_or_default() + } + + fn write_store(&self, map: &HashMap) -> Result<(), String> { + let json = serde_json::to_string_pretty(map).map_err(|e| e.to_string())?; + std::fs::write(&self.path, json).map_err(|e| e.to_string()) + } +} + +impl ConnectionSecretStore for FileSecretStore { + fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> { + let mut map = self.read_store(); + map.insert(secret_account(connection_id, key), secret.to_string()); + self.write_store(&map) + } + + fn get_secret(&self, connection_id: &str, key: &str) -> Result, String> { + Ok(self.read_store().get(&secret_account(connection_id, key)).cloned()) + } + + fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> { + let mut map = self.read_store(); + map.remove(&secret_account(connection_id, key)); + self.write_store(&map) + } +} + +pub fn save_connections_to_file( + path: &Path, + configs: &[ConnectionConfig], + store: &dyn ConnectionSecretStore, +) -> Result<(), String> { + delete_removed_connection_secrets(path, configs, store)?; + for config in configs { + persist_secret(store, &config.id, MAIN_PASSWORD_KEY, &config.password)?; + persist_secret(store, &config.id, SSH_PASSWORD_KEY, &config.ssh_password)?; + persist_secret(store, &config.id, SSH_KEY_PASSPHRASE_KEY, &config.ssh_key_passphrase)?; + persist_optional_secret( + store, + &config.id, + CONNECTION_STRING_KEY, + config.connection_string.as_deref(), + )?; + } + + write_sanitized_connections(path, configs) +} + +pub fn load_connections_from_file( + path: &Path, + store: &dyn ConnectionSecretStore, +) -> Result, String> { + if !path.exists() { + return Ok(vec![]); + } + + let mut configs = read_connections(path)?; + let mut needs_rewrite = false; + for config in &mut configs { + if config.password.is_empty() { + if let Some(secret) = store.get_secret(&config.id, MAIN_PASSWORD_KEY)? { + config.password = secret; + } + } else { + store.set_secret(&config.id, MAIN_PASSWORD_KEY, &config.password)?; + needs_rewrite = true; + } + + if config.ssh_password.is_empty() { + if let Some(secret) = store.get_secret(&config.id, SSH_PASSWORD_KEY)? { + config.ssh_password = secret; + } + } else { + store.set_secret(&config.id, SSH_PASSWORD_KEY, &config.ssh_password)?; + needs_rewrite = true; + } + + if config.ssh_key_passphrase.is_empty() { + if let Some(secret) = store.get_secret(&config.id, SSH_KEY_PASSPHRASE_KEY)? { + config.ssh_key_passphrase = secret; + } + } else { + store.set_secret(&config.id, SSH_KEY_PASSPHRASE_KEY, &config.ssh_key_passphrase)?; + needs_rewrite = true; + } + + match config + .connection_string + .as_deref() + .filter(|secret| !secret.is_empty()) + { + Some(secret) => { + store.set_secret(&config.id, CONNECTION_STRING_KEY, secret)?; + needs_rewrite = true; + } + None => { + if let Some(secret) = store.get_secret(&config.id, CONNECTION_STRING_KEY)? { + config.connection_string = Some(secret); + } + } + } + } + + if needs_rewrite { + write_sanitized_connections(path, &configs)?; + } + + Ok(configs) +} + +fn delete_removed_connection_secrets( + path: &Path, + configs: &[ConnectionConfig], + store: &dyn ConnectionSecretStore, +) -> Result<(), String> { + if !path.exists() { + return Ok(()); + } + + let previous = match read_connections(path) { + Ok(configs) => configs, + Err(_) => return Ok(()), + }; + let current_ids: HashSet<&str> = configs.iter().map(|config| config.id.as_str()).collect(); + for config in previous { + if current_ids.contains(config.id.as_str()) { + continue; + } + store.delete_secret(&config.id, MAIN_PASSWORD_KEY)?; + store.delete_secret(&config.id, SSH_PASSWORD_KEY)?; + store.delete_secret(&config.id, SSH_KEY_PASSPHRASE_KEY)?; + store.delete_secret(&config.id, CONNECTION_STRING_KEY)?; + } + Ok(()) +} + +fn persist_secret( + store: &dyn ConnectionSecretStore, + connection_id: &str, + key: &str, + secret: &str, +) -> Result<(), String> { + if secret.is_empty() { + store.delete_secret(connection_id, key) + } else { + store.set_secret(connection_id, key, secret) + } +} + +fn persist_optional_secret( + store: &dyn ConnectionSecretStore, + connection_id: &str, + key: &str, + secret: Option<&str>, +) -> Result<(), String> { + match secret.filter(|secret| !secret.is_empty()) { + Some(secret) => store.set_secret(connection_id, key, secret), + None => store.delete_secret(connection_id, key), + } +} + +fn read_connections(path: &Path) -> Result, String> { + let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + serde_json::from_str(&json).map_err(|e| e.to_string()) +} + +fn write_sanitized_connections(path: &Path, configs: &[ConnectionConfig]) -> Result<(), String> { + let sanitized = sanitize_connections(configs); + let json = serde_json::to_string_pretty(&sanitized).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) +} + +fn sanitize_connections(configs: &[ConnectionConfig]) -> Vec { + configs + .iter() + .cloned() + .map(|mut config| { + config.password.clear(); + config.ssh_password.clear(); + config.ssh_key_passphrase.clear(); + config.connection_string = None; + config + }) + .collect() +} + +pub fn secret_account(connection_id: &str, key: &str) -> String { + format!("connection:{connection_id}:{key}") +} + +#[cfg(test)] +mod tests { + use super::{ + load_connections_from_file, save_connections_to_file, ConnectionSecretStore, + CONNECTION_STRING_KEY, MAIN_PASSWORD_KEY, SSH_PASSWORD_KEY, + }; + use crate::models::connection::{ConnectionConfig, DatabaseType}; + use std::cell::RefCell; + use std::collections::HashMap; + use std::path::Path; + + #[derive(Default)] + struct MemorySecretStore { + values: RefCell>, + deleted: RefCell>, + } + + impl MemorySecretStore { + fn set_existing(&self, connection_id: &str, key: &str, value: &str) { + self.values + .borrow_mut() + .insert(secret_key(connection_id, key), value.to_string()); + } + + fn get_existing(&self, connection_id: &str, key: &str) -> Option { + self.values + .borrow() + .get(&secret_key(connection_id, key)) + .cloned() + } + + fn was_deleted(&self, connection_id: &str, key: &str) -> bool { + self.deleted + .borrow() + .contains(&secret_key(connection_id, key)) + } + } + + impl ConnectionSecretStore for MemorySecretStore { + fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> { + self.values + .borrow_mut() + .insert(secret_key(connection_id, key), secret.to_string()); + Ok(()) + } + + fn get_secret(&self, connection_id: &str, key: &str) -> Result, String> { + Ok(self + .values + .borrow() + .get(&secret_key(connection_id, key)) + .cloned()) + } + + fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> { + self.values + .borrow_mut() + .remove(&secret_key(connection_id, key)); + self.deleted + .borrow_mut() + .push(secret_key(connection_id, key)); + Ok(()) + } + } + + fn secret_key(connection_id: &str, key: &str) -> String { + format!("{connection_id}:{key}") + } + + fn temp_connections_file(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!( + "dbx-connection-secrets-test-{}-{name}", + std::process::id() + )); + std::fs::create_dir_all(&dir).unwrap(); + dir.join("connections.json") + } + + fn connection(id: &str, password: &str, ssh_password: &str) -> ConnectionConfig { + ConnectionConfig { + id: id.to_string(), + name: format!("{id} connection"), + db_type: DatabaseType::Postgres, + driver_profile: None, + driver_label: None, + url_params: None, + host: "localhost".to_string(), + port: 5432, + username: "postgres".to_string(), + password: password.to_string(), + database: Some("postgres".to_string()), + color: None, + ssh_enabled: !ssh_password.is_empty(), + ssh_host: String::new(), + ssh_port: 22, + ssh_user: String::new(), + ssh_password: ssh_password.to_string(), + ssh_key_path: String::new(), + ssh_key_passphrase: String::new(), + ssh_expose_lan: false, + ssl: false, + connection_string: None, + } + } + + fn read_configs(path: &Path) -> Vec { + let json = std::fs::read_to_string(path).unwrap(); + serde_json::from_str(&json).unwrap() + } + + #[test] + fn save_connections_moves_passwords_to_secret_store_and_redacts_file() { + let path = temp_connections_file("save-redacts"); + let store = MemorySecretStore::default(); + let configs = vec![connection("main", "db-secret", "ssh-secret")]; + + save_connections_to_file(&path, &configs, &store).unwrap(); + + assert_eq!( + store.get_existing("main", MAIN_PASSWORD_KEY).as_deref(), + Some("db-secret") + ); + assert_eq!( + store.get_existing("main", SSH_PASSWORD_KEY).as_deref(), + Some("ssh-secret") + ); + let persisted = read_configs(&path); + assert_eq!(persisted[0].password, ""); + assert_eq!(persisted[0].ssh_password, ""); + } + + #[test] + fn load_connections_restores_passwords_from_secret_store() { + let path = temp_connections_file("load-restores"); + let store = MemorySecretStore::default(); + store.set_existing("main", MAIN_PASSWORD_KEY, "db-secret"); + store.set_existing("main", SSH_PASSWORD_KEY, "ssh-secret"); + let sanitized = vec![connection("main", "", "")]; + std::fs::write(&path, serde_json::to_string_pretty(&sanitized).unwrap()).unwrap(); + + let loaded = load_connections_from_file(&path, &store).unwrap(); + + assert_eq!(loaded[0].password, "db-secret"); + assert_eq!(loaded[0].ssh_password, "ssh-secret"); + } + + #[test] + fn load_connections_migrates_plaintext_passwords_and_rewrites_sanitized_file() { + let path = temp_connections_file("migrates-plaintext"); + let store = MemorySecretStore::default(); + let legacy = vec![connection("legacy", "plain-db", "plain-ssh")]; + std::fs::write(&path, serde_json::to_string_pretty(&legacy).unwrap()).unwrap(); + + let loaded = load_connections_from_file(&path, &store).unwrap(); + + assert_eq!(loaded[0].password, "plain-db"); + assert_eq!(loaded[0].ssh_password, "plain-ssh"); + assert_eq!( + store.get_existing("legacy", MAIN_PASSWORD_KEY).as_deref(), + Some("plain-db") + ); + assert_eq!( + store.get_existing("legacy", SSH_PASSWORD_KEY).as_deref(), + Some("plain-ssh") + ); + let persisted = read_configs(&path); + assert_eq!(persisted[0].password, ""); + assert_eq!(persisted[0].ssh_password, ""); + } + + #[test] + fn save_connections_deletes_secrets_for_removed_connections() { + let path = temp_connections_file("deletes-removed"); + let store = MemorySecretStore::default(); + let previous = vec![connection("old", "", ""), connection("kept", "", "")]; + std::fs::write(&path, serde_json::to_string_pretty(&previous).unwrap()).unwrap(); + store.set_existing("old", MAIN_PASSWORD_KEY, "old-db"); + store.set_existing("old", SSH_PASSWORD_KEY, "old-ssh"); + store.set_existing("kept", MAIN_PASSWORD_KEY, "kept-db"); + + save_connections_to_file(&path, &[connection("kept", "new-db", "")], &store).unwrap(); + + assert!(store.was_deleted("old", MAIN_PASSWORD_KEY)); + assert!(store.was_deleted("old", SSH_PASSWORD_KEY)); + assert_eq!( + store.get_existing("kept", MAIN_PASSWORD_KEY).as_deref(), + Some("new-db") + ); + } + + #[test] + fn save_connections_moves_connection_string_to_secret_store_and_restores_it() { + let path = temp_connections_file("connection-string"); + let store = MemorySecretStore::default(); + let mut config = connection("mongo", "", ""); + config.db_type = DatabaseType::MongoDb; + config.connection_string = Some("mongodb://user:secret@localhost/app".to_string()); + + save_connections_to_file(&path, &[config], &store).unwrap(); + + assert_eq!( + store + .get_existing("mongo", CONNECTION_STRING_KEY) + .as_deref(), + Some("mongodb://user:secret@localhost/app") + ); + let persisted = read_configs(&path); + assert_eq!(persisted[0].connection_string, None); + + let loaded = load_connections_from_file(&path, &store).unwrap(); + assert_eq!( + loaded[0].connection_string.as_deref(), + Some("mongodb://user:secret@localhost/app") + ); + } +} diff --git a/src-tauri/src/db/clickhouse_driver.rs b/crates/dbx-core/src/db/clickhouse_driver.rs similarity index 98% rename from src-tauri/src/db/clickhouse_driver.rs rename to crates/dbx-core/src/db/clickhouse_driver.rs index 357b87d694..5b906901da 100644 --- a/src-tauri/src/db/clickhouse_driver.rs +++ b/crates/dbx-core/src/db/clickhouse_driver.rs @@ -2,7 +2,7 @@ use reqwest::Client as HttpClient; use serde::Deserialize; use std::time::Instant; -use super::{ColumnInfo, DatabaseInfo, QueryResult, TableInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, QueryResult, TableInfo}; pub struct ChClient { http: HttpClient, diff --git a/src-tauri/src/db/elasticsearch_driver.rs b/crates/dbx-core/src/db/elasticsearch_driver.rs similarity index 99% rename from src-tauri/src/db/elasticsearch_driver.rs rename to crates/dbx-core/src/db/elasticsearch_driver.rs index f6094c2d5a..d1ea6d1e25 100644 --- a/src-tauri/src/db/elasticsearch_driver.rs +++ b/crates/dbx-core/src/db/elasticsearch_driver.rs @@ -1,7 +1,7 @@ use reqwest::Client as HttpClient; use serde::Deserialize; -use super::mongo_driver::MongoDocumentResult; +use crate::db::mongo_driver::MongoDocumentResult; pub struct EsClient { http: HttpClient, diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs new file mode 100644 index 0000000000..1219d42623 --- /dev/null +++ b/crates/dbx-core/src/db/mod.rs @@ -0,0 +1,13 @@ +pub mod clickhouse_driver; +pub mod elasticsearch_driver; +pub mod mongo_driver; +pub mod mysql; +pub mod oracle_driver; +pub mod postgres; +pub mod redis_driver; +pub mod sqlite; +pub mod sqlserver; +pub mod ssh_tunnel; + +// Re-export types so that `db::QueryResult` etc. work within dbx-core +pub use crate::types::*; diff --git a/src-tauri/src/db/mongo_driver.rs b/crates/dbx-core/src/db/mongo_driver.rs similarity index 100% rename from src-tauri/src/db/mongo_driver.rs rename to crates/dbx-core/src/db/mongo_driver.rs diff --git a/src-tauri/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs similarity index 99% rename from src-tauri/src/db/mysql.rs rename to crates/dbx-core/src/db/mysql.rs index c7d8c8a366..d11860d126 100644 --- a/src-tauri/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -4,7 +4,7 @@ use sqlx::mysql::{MySqlPool, MySqlPoolOptions, MySqlRow}; use sqlx::{Column, Executor, Row, TypeInfo, ValueRef}; use std::time::{Duration, Instant}; -use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; fn quote_value(s: &str) -> String { format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")) diff --git a/src-tauri/src/db/oracle_driver.rs b/crates/dbx-core/src/db/oracle_driver.rs similarity index 98% rename from src-tauri/src/db/oracle_driver.rs rename to crates/dbx-core/src/db/oracle_driver.rs index 2ae7d87d23..9e34ad8db1 100644 --- a/src-tauri/src/db/oracle_driver.rs +++ b/crates/dbx-core/src/db/oracle_driver.rs @@ -1,7 +1,7 @@ use oracle_rs::{Config, Connection}; use std::time::Instant; -use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; pub type OracleClient = Connection; diff --git a/src-tauri/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs similarity index 99% rename from src-tauri/src/db/postgres.rs rename to crates/dbx-core/src/db/postgres.rs index 6c90481de7..6c1bd7040c 100644 --- a/src-tauri/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -4,7 +4,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions, PgRow}; use sqlx::{Column, Executor, Row, TypeInfo, ValueRef}; use std::time::{Duration, Instant}; -use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; fn pg_temporal_to_json_value(row: &PgRow, idx: usize) -> Option { if let Ok(v) = row.try_get::, _>(idx) { diff --git a/src-tauri/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs similarity index 100% rename from src-tauri/src/db/redis_driver.rs rename to crates/dbx-core/src/db/redis_driver.rs diff --git a/src-tauri/src/db/sqlite.rs b/crates/dbx-core/src/db/sqlite.rs similarity index 98% rename from src-tauri/src/db/sqlite.rs rename to crates/dbx-core/src/db/sqlite.rs index 0cd77ac1fa..dd6a50d000 100644 --- a/src-tauri/src/db/sqlite.rs +++ b/crates/dbx-core/src/db/sqlite.rs @@ -2,7 +2,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions, SqliteRo use sqlx::{Column, Executor, Row}; use std::time::{Duration, Instant}; -use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; pub async fn connect_path(path: &str) -> Result { let mut options = SqliteConnectOptions::new() diff --git a/src-tauri/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs similarity index 99% rename from src-tauri/src/db/sqlserver.rs rename to crates/dbx-core/src/db/sqlserver.rs index bcc03092a0..90ee7936c6 100644 --- a/src-tauri/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -4,7 +4,7 @@ use tokio::net::TcpStream; use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt}; use std::time::Instant; -use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; pub type SqlServerClient = Client>; diff --git a/src-tauri/src/db/ssh_tunnel.rs b/crates/dbx-core/src/db/ssh_tunnel.rs similarity index 100% rename from src-tauri/src/db/ssh_tunnel.rs rename to crates/dbx-core/src/db/ssh_tunnel.rs diff --git a/crates/dbx-core/src/history.rs b/crates/dbx-core/src/history.rs new file mode 100644 index 0000000000..9901ab6e18 --- /dev/null +++ b/crates/dbx-core/src/history.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HistoryEntry { + pub id: String, + pub connection_name: String, + pub database: String, + pub sql: String, + pub executed_at: String, + pub execution_time_ms: u128, + pub success: bool, + pub error: Option, +} + +pub const MAX_HISTORY: usize = 1000; + +pub fn read_all(path: &Path) -> Result, String> { + if !path.exists() { + return Ok(vec![]); + } + let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + serde_json::from_str(&json).map_err(|e| e.to_string()) +} + +pub fn write_all(path: &Path, entries: &[HistoryEntry]) -> Result<(), String> { + let json = serde_json::to_string(entries).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) +} + +pub fn save_history_entry(path: &Path, entry: HistoryEntry) -> Result<(), String> { + let mut entries = read_all(path)?; + entries.insert(0, entry); + entries.truncate(MAX_HISTORY); + write_all(path, &entries) +} + +pub fn load_history_entries(path: &Path, limit: usize, offset: usize) -> Result, String> { + let entries = read_all(path)?; + Ok(entries.into_iter().skip(offset).take(limit).collect()) +} + +pub fn clear_history_entries(path: &Path) -> Result<(), String> { + write_all(path, &[]) +} + +pub fn delete_history_entry_by_id(path: &Path, id: &str) -> Result<(), String> { + let entries: Vec = read_all(path)? + .into_iter() + .filter(|e| e.id != id) + .collect(); + write_all(path, &entries) +} diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs new file mode 100644 index 0000000000..7d76510284 --- /dev/null +++ b/crates/dbx-core/src/lib.rs @@ -0,0 +1,16 @@ +pub mod ai; +pub mod connection; +pub mod connection_secrets; +pub mod db; +pub mod history; +pub mod models; +pub mod mongo_ops; +pub mod query; +pub mod query_cancel; +pub mod redis_ops; +pub mod schema; +pub mod sql; +pub mod table_import; +pub mod transfer; +pub mod types; +pub mod update; diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs new file mode 100644 index 0000000000..be075eb0d0 --- /dev/null +++ b/crates/dbx-core/src/models/connection.rs @@ -0,0 +1,353 @@ +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionConfig { + pub id: String, + pub name: String, + pub db_type: DatabaseType, + #[serde(default)] + pub driver_profile: Option, + #[serde(default)] + pub driver_label: Option, + #[serde(default)] + pub url_params: Option, + pub host: String, + pub port: u16, + pub username: String, + pub password: String, + pub database: Option, + #[serde(default)] + pub color: Option, + #[serde(default)] + pub ssh_enabled: bool, + #[serde(default)] + pub ssh_host: String, + #[serde(default = "default_ssh_port")] + pub ssh_port: u16, + #[serde(default)] + pub ssh_user: String, + #[serde(default)] + pub ssh_password: String, + #[serde(default)] + pub ssh_key_path: String, + #[serde(default)] + pub ssh_key_passphrase: String, + #[serde(default)] + pub ssh_expose_lan: bool, + #[serde(default)] + pub ssl: bool, + #[serde(default)] + pub connection_string: Option, +} + +fn default_ssh_port() -> u16 { + 22 +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DatabaseType { + Mysql, + Postgres, + Sqlite, + Redis, + #[serde(rename = "duckdb")] + DuckDb, + #[serde(rename = "clickhouse")] + ClickHouse, + #[serde(rename = "sqlserver")] + SqlServer, + #[serde(rename = "mongodb")] + MongoDb, + #[serde(rename = "oracle")] + Oracle, + #[serde(rename = "elasticsearch")] + Elasticsearch, + Doris, + #[serde(rename = "starrocks")] + StarRocks, + Redshift, +} + +impl ConnectionConfig { + pub fn needs_bare_mysql(&self) -> bool { + matches!(self.db_type, DatabaseType::Doris | DatabaseType::StarRocks) + || self.driver_profile.as_deref().map(|p| p.to_lowercase()) + .is_some_and(|p| matches!(p.as_str(), "doris" | "starrocks" | "selectdb" | "tdengine")) + } + + pub fn connection_url(&self) -> String { + self.connection_url_with_host(&self.host, self.port) + } + + pub fn redacted_connection_url(&self) -> String { + self.redacted_connection_url_with_host(&self.host, self.port) + } + + pub fn redacted_connection_url_with_host(&self, host: &str, port: u16) -> String { + let host = bracket_ipv6(host); + let db_part = self + .database + .as_deref() + .filter(|d| !d.is_empty()) + .map(|d| format!("/{}", encode_url_part(d))) + .unwrap_or_default(); + let params = self.normalized_url_params(); + + match self.db_type { + DatabaseType::Sqlite | DatabaseType::DuckDb => { + format!("{}?mode=rwc", self.host) + } + DatabaseType::Redis => { + let scheme = if self.ssl { "rediss" } else { "redis" }; + format!("{scheme}://{host}:{port}/") + } + DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks => format!("mysql://{host}:{port}{db_part}?{params}"), + DatabaseType::Postgres | DatabaseType::Redshift => { + let suffix = if params.is_empty() { + String::new() + } else { + format!("?{params}") + }; + format!("postgres://{host}:{port}{db_part}{suffix}") + } + DatabaseType::ClickHouse => format!("http://{host}:{port}"), + DatabaseType::SqlServer => format!( + "server=tcp:{host},{port};database={}", + self.database.as_deref().unwrap_or("master") + ), + DatabaseType::MongoDb => { + if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { + return cs.to_string(); + } + format!("mongodb://{host}:{port}{db_part}") + } + DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), + DatabaseType::Elasticsearch => format!("http://{host}:{port}"), + } + } + + pub fn connection_url_with_host(&self, host: &str, port: u16) -> String { + let host = bracket_ipv6(host); + let db_part = self + .database + .as_deref() + .filter(|d| !d.is_empty()) + .map(|d| format!("/{}", encode_url_part(d))) + .unwrap_or_default(); + let username = encode_url_part(&self.username); + let password = encode_url_part(&self.password); + let params = self.normalized_url_params(); + + match self.db_type { + DatabaseType::Sqlite | DatabaseType::DuckDb => { + format!("{}?mode=rwc", self.host) + } + DatabaseType::Redis => { + let scheme = if self.ssl { "rediss" } else { "redis" }; + if self.username.is_empty() && self.password.is_empty() { + format!("{scheme}://{host}:{port}/") + } else if self.username.is_empty() { + format!("{scheme}://:{password}@{host}:{port}/") + } else { + format!("{scheme}://{username}:{password}@{host}:{port}/") + } + } + DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks => format!( + "mysql://{}:{}@{host}:{port}{db_part}?{params}", + username, password + ), + DatabaseType::Postgres | DatabaseType::Redshift => { + let suffix = if params.is_empty() { + String::new() + } else { + format!("?{params}") + }; + format!( + "postgres://{}:{}@{host}:{port}{db_part}{suffix}", + username, password + ) + } + DatabaseType::ClickHouse => format!("http://{host}:{port}"), + DatabaseType::SqlServer => format!( + "server=tcp:{host},{port};user={};password={};database={}", + self.username, + self.password, + self.database.as_deref().unwrap_or("master") + ), + DatabaseType::MongoDb => { + if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { + return cs.to_string(); + } + if self.username.is_empty() { + format!("mongodb://{host}:{port}{db_part}") + } else { + format!("mongodb://{username}:{password}@{host}:{port}{db_part}") + } + } + DatabaseType::Oracle => { + format!("oracle://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Elasticsearch => format!("http://{host}:{port}"), + } + } + + fn normalized_url_params(&self) -> String { + let value = self.url_params.as_deref().unwrap_or("").trim(); + if self.needs_bare_mysql() { + let v = value.trim_start_matches('?'); + let filtered: Vec<&str> = v.split('&') + .filter(|p| !p.is_empty() && !p.starts_with("charset=") && !p.starts_with("ssl-mode=preferred")) + .collect(); + return if filtered.is_empty() { "ssl-mode=disabled".to_string() } else { format!("ssl-mode=disabled&{}", filtered.join("&")) }; + } + match self.db_type { + DatabaseType::Mysql => { + let base = "ssl-mode=preferred&charset=utf8mb4"; + if value.is_empty() { + base.to_string() + } else if value.contains("ssl-mode=") { + let v = value.trim_start_matches('?'); + if v.contains("charset=") { v.to_string() } else { format!("{v}&charset=utf8mb4") } + } else { + let v = value.trim_start_matches('?'); + if v.contains("charset=") { format!("ssl-mode=preferred&{v}") } else { format!("{base}&{v}") } + } + } + DatabaseType::Doris | DatabaseType::StarRocks => { + let v = value.trim_start_matches('?'); + let filtered: Vec<&str> = v.split('&') + .filter(|p| !p.is_empty() && !p.starts_with("charset=") && !p.starts_with("ssl-mode=preferred")) + .collect(); + if filtered.is_empty() { "ssl-mode=disabled".to_string() } else { format!("ssl-mode=disabled&{}", filtered.join("&")) } + } + DatabaseType::Postgres | DatabaseType::Redshift => value.trim_start_matches('?').to_string(), + _ => value.trim_start_matches('?').to_string(), + } + } +} + +fn encode_url_part(value: &str) -> String { + utf8_percent_encode(value, NON_ALPHANUMERIC).to_string() +} + +fn bracket_ipv6(host: &str) -> String { + if host.contains(':') && !host.starts_with('[') { + format!("[{host}]") + } else { + host.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::{ConnectionConfig, DatabaseType}; + + fn mysql_config(username: &str, password: &str, database: Option<&str>) -> ConnectionConfig { + ConnectionConfig { + id: "id".to_string(), + name: "name".to_string(), + db_type: DatabaseType::Mysql, + driver_profile: None, + driver_label: None, + url_params: None, + host: "10.1.2.3".to_string(), + port: 2883, + username: username.to_string(), + password: password.to_string(), + database: database.map(str::to_string), + color: None, + ssh_enabled: false, + ssh_host: String::new(), + ssh_port: 22, + ssh_user: String::new(), + ssh_password: String::new(), + ssh_key_path: String::new(), + ssh_key_passphrase: String::new(), + ssh_expose_lan: false, + ssl: false, + connection_string: None, + } + } + + #[test] + fn mysql_url_encodes_oceanbase_username() { + let config = mysql_config("user@tenant#cluster", "secret", None); + + assert_eq!( + config.connection_url(), + "mysql://user%40tenant%23cluster:secret@10.1.2.3:2883?ssl-mode=preferred&charset=utf8mb4" + ); + } + + #[test] + fn mysql_url_encodes_password_and_database() { + let config = mysql_config("root", "p@ss:word#1", Some("db/name")); + + assert_eq!( + config.connection_url(), + "mysql://root:p%40ss%3Aword%231@10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4" + ); + } + + #[test] + fn mysql_url_appends_custom_params() { + let mut config = mysql_config("root", "secret", Some("test")); + config.url_params = Some("charset=utf8mb4".to_string()); + + assert_eq!( + config.connection_url(), + "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4" + ); + } + + #[test] + fn postgres_url_appends_custom_params() { + let mut config = mysql_config("postgres", "secret", Some("test")); + config.db_type = DatabaseType::Postgres; + config.url_params = Some("sslmode=disable".to_string()); + + assert_eq!( + config.connection_url(), + "postgres://postgres:secret@10.1.2.3:2883/test?sslmode=disable" + ); + } + + #[test] + fn redacted_mysql_url_omits_credentials() { + let config = mysql_config("user@tenant#cluster", "p@ss:word#1", Some("db/name")); + + let url = config.redacted_connection_url(); + + assert_eq!(url, "mysql://10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4"); + assert!(!url.contains("user")); + assert!(!url.contains("p%40ss")); + assert!(!url.contains("p@ss")); + } + + #[test] + fn redacted_sqlserver_url_omits_credentials() { + let mut config = mysql_config("sa", "super-secret", Some("master")); + config.db_type = DatabaseType::SqlServer; + + let url = config.redacted_connection_url(); + + assert_eq!(url, "server=tcp:10.1.2.3,2883;database=master"); + assert!(!url.contains("sa")); + assert!(!url.contains("super-secret")); + } + + #[test] + fn redacted_redis_url_omits_credentials_and_keeps_tls_scheme() { + let mut config = mysql_config("default", "redis-secret", None); + config.db_type = DatabaseType::Redis; + config.ssl = true; + + let url = config.redacted_connection_url(); + + assert_eq!(url, "rediss://10.1.2.3:2883/"); + assert!(!url.contains("default")); + assert!(!url.contains("redis-secret")); + } +} diff --git a/crates/dbx-core/src/models/mod.rs b/crates/dbx-core/src/models/mod.rs new file mode 100644 index 0000000000..b3b606b40c --- /dev/null +++ b/crates/dbx-core/src/models/mod.rs @@ -0,0 +1 @@ +pub mod connection; diff --git a/crates/dbx-core/src/mongo_ops.rs b/crates/dbx-core/src/mongo_ops.rs new file mode 100644 index 0000000000..2e2e5b3e47 --- /dev/null +++ b/crates/dbx-core/src/mongo_ops.rs @@ -0,0 +1,114 @@ +use crate::connection::{AppState, PoolKind}; +use crate::db::mongo_driver::{self, MongoDocumentResult}; +use crate::db::elasticsearch_driver; + +pub async fn mongo_list_databases_core( + state: &AppState, + connection_id: &str, +) -> Result, String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => mongo_driver::list_databases(client).await, + PoolKind::Elasticsearch(_) => Ok(vec!["default".to_string()]), + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} + +pub async fn mongo_list_collections_core( + state: &AppState, + connection_id: &str, + database: &str, +) -> Result, String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => mongo_driver::list_collections(client, database).await, + PoolKind::Elasticsearch(client) => elasticsearch_driver::list_indices(client).await, + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} + +pub async fn mongo_find_documents_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + skip: u64, + limit: i64, +) -> Result { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => { + mongo_driver::find_documents(client, database, collection, skip, limit).await + } + PoolKind::Elasticsearch(client) => { + let client = client.clone(); + drop(connections); + elasticsearch_driver::find_documents(&client, collection, skip, limit).await + } + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} + +pub async fn mongo_insert_document_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + doc_json: &str, +) -> Result { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => { + mongo_driver::insert_document(client, database, collection, doc_json).await + } + PoolKind::Elasticsearch(client) => { + let client = client.clone(); + drop(connections); + elasticsearch_driver::insert_document(&client, collection, doc_json).await + } + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} + +pub async fn mongo_update_document_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + id: &str, + doc_json: &str, +) -> Result { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => { + mongo_driver::update_document(client, database, collection, id, doc_json).await + } + PoolKind::Elasticsearch(client) => { + let client = client.clone(); + drop(connections); + elasticsearch_driver::update_document(&client, collection, id, doc_json).await + } + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} + +pub async fn mongo_delete_document_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + id: &str, +) -> Result { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => { + mongo_driver::delete_document(client, database, collection, id).await + } + PoolKind::Elasticsearch(client) => { + let client = client.clone(); + drop(connections); + elasticsearch_driver::delete_document(&client, collection, id).await + } + _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), + } +} diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs new file mode 100644 index 0000000000..f74f3a8374 --- /dev/null +++ b/crates/dbx-core/src/query.rs @@ -0,0 +1,366 @@ +use std::future::Future; +use std::time::Duration; +use tokio::time::timeout; +use tokio_util::sync::CancellationToken; + +use crate::connection::{AppState, PoolKind}; +use crate::db; +use crate::sql::split_sql_statements; + +pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30); +pub const MAX_ROWS: usize = 10000; +pub const QUERY_CANCELED: &str = "Query canceled"; + +pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result { + let start = std::time::Instant::now(); + let trimmed = sql.trim().to_uppercase(); + + if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") || trimmed.starts_with("DESCRIBE") + || trimmed.starts_with("EXPLAIN") || trimmed.starts_with("WITH") || trimmed.starts_with("PRAGMA") + { + let mut stmt = con.prepare(sql).map_err(|e| e.to_string())?; + let mut rows = stmt.query([]).map_err(|e| e.to_string())?; + let stmt_ref = rows.as_ref().ok_or("DuckDB statement unavailable")?; + let col_count = stmt_ref.column_count(); + let columns: Vec = (0..col_count) + .map(|i| stmt_ref.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "?".to_string())) + .collect(); + + let mut result_rows = Vec::new(); + while let Some(row) = rows.next().map_err(|e| e.to_string())? { + if result_rows.len() >= MAX_ROWS { break; } + let vals: Vec = (0..col_count).map(|i| { + row.get::<_, String>(i) + .map(serde_json::Value::String) + .or_else(|_| row.get::<_, i64>(i).map(|v| serde_json::Value::Number(v.into()))) + .or_else(|_| row.get::<_, f64>(i).map(|v| { + serde_json::Number::from_f64(v) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null) + })) + .or_else(|_| row.get::<_, bool>(i).map(serde_json::Value::Bool)) + .unwrap_or(serde_json::Value::Null) + }).collect(); + result_rows.push(vals); + } + + let truncated = result_rows.len() >= MAX_ROWS; + Ok(db::QueryResult { columns, rows: result_rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated }) + } else { + let affected = con.execute(sql, []).map_err(|e| e.to_string())?; + Ok(db::QueryResult { columns: vec![], rows: vec![], affected_rows: affected as u64, execution_time_ms: start.elapsed().as_millis(), truncated: false }) + } +} + +pub fn truncate_result(mut result: db::QueryResult) -> db::QueryResult { + if result.rows.len() > MAX_ROWS { + result.rows.truncate(MAX_ROWS); + result.truncated = true; + } + result +} + +pub fn is_connection_error(err: &str) -> bool { + let lower = err.to_lowercase(); + lower.contains("connection") + || lower.contains("broken pipe") + || lower.contains("reset by peer") + || lower.contains("timed out") + || lower.contains("closed") + || lower.contains("eof") +} + +pub fn timeout_error() -> String { + format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()) +} + +pub fn canceled_error() -> String { + QUERY_CANCELED.to_string() +} + +pub fn is_canceled(cancel_token: &Option) -> bool { + cancel_token + .as_ref() + .map(|token| token.is_cancelled()) + .unwrap_or(false) +} + +pub async fn wait_for_query( + cancel_token: Option, + future: F, +) -> Result +where + F: Future>, +{ + wait_for_query_with_timeout(cancel_token, QUERY_TIMEOUT, future).await +} + +pub async fn wait_for_query_with_timeout( + cancel_token: Option, + timeout_duration: Duration, + future: F, +) -> Result +where + F: Future>, +{ + if let Some(token) = cancel_token { + tokio::select! { + biased; + _ = token.cancelled() => Err(canceled_error()), + result = timeout(timeout_duration, future) => result.map_err(|_| timeout_error())?, + } + } else { + timeout(timeout_duration, future) + .await + .map_err(|_| timeout_error())? + } +} + +pub async fn do_execute( + state: &AppState, + pool_key: &str, + sql: &str, + cancel_token: Option, +) -> Result { + let connections = state.connections.lock().await; + let pool = connections.get(pool_key).ok_or("Connection not found")?; + + match pool { + PoolKind::DuckDb(con) => { + let con = con.clone(); + let sql = sql.to_string(); + drop(connections); + wait_for_query(cancel_token, async move { + let task = tokio::task::spawn_blocking(move || { + let con = con.lock().map_err(|e| e.to_string())?; + duckdb_execute(&con, &sql) + }); + task.await.map_err(|e| e.to_string())? + }) + .await + } + PoolKind::Mysql(p, bare) => { + let p = p.clone(); + let bare = *bare; + drop(connections); + wait_for_query(cancel_token, db::mysql::execute_query(&p, sql, bare)) + .await + .map(truncate_result) + } + PoolKind::Postgres(p) => { + let p = p.clone(); + drop(connections); + wait_for_query(cancel_token, db::postgres::execute_query(&p, sql)) + .await + .map(truncate_result) + } + PoolKind::Sqlite(p) => { + let p = p.clone(); + drop(connections); + wait_for_query(cancel_token, db::sqlite::execute_query(&p, sql)) + .await + .map(truncate_result) + } + PoolKind::ClickHouse(client) => { + let client = client.clone(); + let database = pool_key.split(':').nth(1).unwrap_or("default").to_string(); + drop(connections); + wait_for_query(cancel_token, db::clickhouse_driver::execute_query(&client, &database, sql)) + .await + .map(truncate_result) + } + PoolKind::SqlServer(client) => { + let client = client.clone(); + drop(connections); + let mut client = match cancel_token.as_ref() { + Some(token) => tokio::select! { + biased; + _ = token.cancelled() => return Err(canceled_error()), + guard = client.lock() => guard, + }, + None => client.lock().await, + }; + wait_for_query(cancel_token, db::sqlserver::execute_query(&mut client, sql)) + .await + .map(truncate_result) + } + PoolKind::Oracle(client) => { + let client = client.clone(); + drop(connections); + let client = match cancel_token.as_ref() { + Some(token) => tokio::select! { + biased; + _ = token.cancelled() => return Err(canceled_error()), + guard = client.lock() => guard, + }, + None => client.lock().await, + }; + wait_for_query(cancel_token, db::oracle_driver::execute_query(&*client, sql)) + .await + .map(truncate_result) + } + PoolKind::Elasticsearch(_) => Err("Use document browser for Elasticsearch".to_string()), + PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()), + PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()), + } +} + +pub async fn execute_sql_statement( + state: &AppState, + connection_id: &str, + database: &str, + sql: &str, + cancel_token: Option, +) -> Result { + let pool_key = if database.is_empty() { + connection_id.to_string() + } else { + state.get_or_create_pool(connection_id, Some(database)).await? + }; + + if is_canceled(&cancel_token) { + return Err(canceled_error()); + } + + let result = do_execute(state, &pool_key, sql, cancel_token.clone()).await; + + match &result { + Err(e) if is_connection_error(e) && !is_canceled(&cancel_token) => { + let db_opt = if database.is_empty() { None } else { Some(database) }; + let new_key = state.reconnect_pool(connection_id, db_opt).await?; + do_execute(state, &new_key, sql, cancel_token).await + } + _ => result, + } +} + +pub async fn execute_multi_core( + state: &AppState, + connection_id: &str, + database: &str, + sql: &str, + cancel_token: Option, +) -> Result, String> { + let statements = split_sql_statements(sql); + if statements.len() <= 1 { + let single_sql = statements.into_iter().next().unwrap_or_default(); + let result = execute_sql_statement( + state, connection_id, database, &single_sql, cancel_token, + ).await?; + return Ok(vec![result]); + } + + let mut results = Vec::with_capacity(statements.len()); + for stmt in &statements { + if is_canceled(&cancel_token) { + results.push(db::QueryResult { + columns: vec!["Error".to_string()], + rows: vec![vec![serde_json::Value::String(canceled_error())]], + affected_rows: 0, + execution_time_ms: 0, + truncated: false, + }); + break; + } + match execute_sql_statement( + state, connection_id, database, stmt, cancel_token.clone(), + ).await { + Ok(r) => results.push(r), + Err(e) => { + results.push(db::QueryResult { + columns: vec!["Error".to_string()], + rows: vec![vec![serde_json::Value::String(e)]], + affected_rows: 0, + execution_time_ms: 0, + truncated: false, + }); + } + } + } + + Ok(results) +} + +pub async fn execute_statements( + state: &AppState, + connection_id: &str, + database: &str, + statements: &[String], +) -> Result { + let pool_key = if database.is_empty() { + connection_id.to_string() + } else { + state.get_or_create_pool(connection_id, Some(database)).await? + }; + + let mut total_affected: u64 = 0; + let start = std::time::Instant::now(); + + for (i, sql) in statements.iter().enumerate() { + match do_execute(state, &pool_key, sql, None).await { + Ok(result) => { + total_affected += result.affected_rows; + } + Err(e) => { + if is_connection_error(&e) { + let db_opt = if database.is_empty() { None } else { Some(database) }; + let _ = state.reconnect_pool(connection_id, db_opt).await; + } + return Err(format!( + "Statement {} failed: {}. Previous {} statement(s) may have been committed.", + i + 1, e, i + )); + } + } + } + + Ok(db::QueryResult { + columns: vec![], + rows: vec![], + affected_rows: total_affected, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn wait_for_query_returns_cancelled_when_token_is_cancelled() { + let token = CancellationToken::new(); + token.cancel(); + + let result = wait_for_query(Some(token), async { + tokio::time::sleep(Duration::from_secs(30)).await; + Ok(db::QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: 0, + truncated: false, + }) + }) + .await; + + assert_eq!(result.unwrap_err(), QUERY_CANCELED); + } + + #[tokio::test] + async fn wait_for_query_without_token_still_times_out() { + let result = wait_for_query_with_timeout(None, Duration::from_millis(10), async { + tokio::time::sleep(Duration::from_secs(1)).await; + Ok(db::QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: 0, + truncated: false, + }) + }) + .await; + + assert_eq!(result.unwrap_err(), timeout_error()); + } +} diff --git a/crates/dbx-core/src/query_cancel.rs b/crates/dbx-core/src/query_cancel.rs new file mode 100644 index 0000000000..fbb1c5ef35 --- /dev/null +++ b/crates/dbx-core/src/query_cancel.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio_util::sync::CancellationToken; + +#[derive(Clone, Default)] +pub struct RunningQueries { + inner: Arc>>, +} + +impl RunningQueries { + pub fn register(&self, execution_id: String) -> RegisteredQuery { + let token = CancellationToken::new(); + self.inner + .lock() + .expect("running query registry poisoned") + .insert(execution_id.clone(), token.clone()); + + RegisteredQuery { + execution_id, + token, + running_queries: self.clone(), + } + } + + pub fn cancel(&self, execution_id: &str) -> bool { + let token = self + .inner + .lock() + .expect("running query registry poisoned") + .get(execution_id) + .cloned(); + + if let Some(token) = token { + token.cancel(); + true + } else { + false + } + } + + #[cfg(test)] + pub fn has(&self, execution_id: &str) -> bool { + self.inner + .lock() + .expect("running query registry poisoned") + .contains_key(execution_id) + } + + fn remove(&self, execution_id: &str) { + self.inner + .lock() + .expect("running query registry poisoned") + .remove(execution_id); + } +} + +pub struct RegisteredQuery { + execution_id: String, + token: CancellationToken, + running_queries: RunningQueries, +} + +impl RegisteredQuery { + pub fn token(&self) -> CancellationToken { + self.token.clone() + } +} + +impl Drop for RegisteredQuery { + fn drop(&mut self) { + self.running_queries.remove(&self.execution_id); + } +} + +#[cfg(test)] +mod tests { + use super::RunningQueries; + + #[test] + fn cancel_marks_registered_query_as_cancelled() { + let running = RunningQueries::default(); + let registered = running.register("exec-1".to_string()); + + assert!(running.cancel("exec-1")); + assert!(registered.token().is_cancelled()); + } + + #[test] + fn dropping_registration_removes_running_query() { + let running = RunningQueries::default(); + let registered = running.register("exec-1".to_string()); + + assert!(running.has("exec-1")); + drop(registered); + + assert!(!running.has("exec-1")); + } +} diff --git a/crates/dbx-core/src/redis_ops.rs b/crates/dbx-core/src/redis_ops.rs new file mode 100644 index 0000000000..f862d7b758 --- /dev/null +++ b/crates/dbx-core/src/redis_ops.rs @@ -0,0 +1,166 @@ +use crate::connection::{AppState, PoolKind}; +use crate::db::redis_driver::{self, RedisScanResult, RedisValue}; + +pub async fn redis_list_databases_core( + state: &AppState, + connection_id: &str, +) -> Result, String> { + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + match pool { + PoolKind::Redis(con) => { + let mut con = con.lock().await; + redis_driver::list_databases(&mut con).await + } + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_scan_keys_core( + state: &AppState, + connection_id: &str, + db: u32, + cursor: u64, + pattern: &str, + count: usize, +) -> Result { + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + match pool { + PoolKind::Redis(con) => { + let mut con = con.lock().await; + redis_driver::select_db(&mut con, db).await?; + redis_driver::scan_keys_page(&mut con, cursor, pattern, count).await + } + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_get_value_core( + state: &AppState, + connection_id: &str, + key: &str, +) -> Result { + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + match pool { + PoolKind::Redis(con) => { + let mut con = con.lock().await; + redis_driver::get_value(&mut con, key).await + } + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_set_string_core( + state: &AppState, + connection_id: &str, + key: &str, + value: &str, + ttl: Option, +) -> Result<(), String> { + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + match pool { + PoolKind::Redis(con) => { + let mut con = con.lock().await; + redis_driver::set_string(&mut con, key, value, ttl).await + } + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_delete_key_core( + state: &AppState, + connection_id: &str, + key: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + match pool { + PoolKind::Redis(con) => { + let mut con = con.lock().await; + redis_driver::delete_key(&mut con, key).await + } + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_hash_set_core( + state: &AppState, + connection_id: &str, + key: &str, + field: &str, + value: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::hash_set(&mut *con.lock().await, key, field, value).await, + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_hash_del_core( + state: &AppState, + connection_id: &str, + key: &str, + field: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::hash_del(&mut *con.lock().await, key, field).await, + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_list_push_core( + state: &AppState, + connection_id: &str, + key: &str, + value: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::list_push(&mut *con.lock().await, key, value).await, + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_list_remove_core( + state: &AppState, + connection_id: &str, + key: &str, + index: i64, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::list_remove(&mut *con.lock().await, key, index).await, + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_set_add_core( + state: &AppState, + connection_id: &str, + key: &str, + member: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::set_add(&mut *con.lock().await, key, member).await, + _ => Err("Not a Redis connection".to_string()), + } +} + +pub async fn redis_set_remove_core( + state: &AppState, + connection_id: &str, + key: &str, + member: &str, +) -> Result<(), String> { + let connections = state.connections.lock().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::Redis(con) => redis_driver::set_remove(&mut *con.lock().await, key, member).await, + _ => Err("Not a Redis connection".to_string()), + } +} diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs new file mode 100644 index 0000000000..d11c2c0cb9 --- /dev/null +++ b/crates/dbx-core/src/schema.rs @@ -0,0 +1,515 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use crate::connection::{AppState, PoolKind}; +use crate::db; + +pub fn duckdb_query_tables(con: &duckdb::Connection) -> Result, String> { + let mut stmt = con.prepare( + "SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name" + ).map_err(|e| e.to_string())?; + let rows = stmt.query_map([], |row| { + Ok(db::TableInfo { + name: row.get::<_, String>(0)?, + table_type: row.get::<_, String>(1)?, + }) + }).map_err(|e| e.to_string())?; + Ok(rows.filter_map(|r| r.ok()).collect()) +} + +pub fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result, String> { + let mut pk_stmt = con.prepare( + "SELECT kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + AND tc.table_name = kcu.table_name + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = 'main' + AND tc.table_name = ? + ORDER BY kcu.ordinal_position" + ).map_err(|e| e.to_string())?; + let pk_rows = pk_stmt.query_map([table], |row| row.get::<_, String>(0)) + .map_err(|e| e.to_string())?; + let primary_keys: std::collections::HashSet = pk_rows.filter_map(|r| r.ok()).collect(); + + let mut stmt = con.prepare( + "SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'main' AND table_name = ? + ORDER BY ordinal_position" + ).map_err(|e| e.to_string())?; + let rows = stmt.query_map([table], |row| { + let name = row.get::<_, String>(0)?; + Ok(db::ColumnInfo { + is_primary_key: primary_keys.contains(&name), + name, + data_type: row.get::<_, String>(1)?, + is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES", + column_default: row.get::<_, Option>(3)?, + extra: None, comment: None, + numeric_precision: None, + numeric_scale: None, + character_maximum_length: None, + }) + }).map_err(|e| e.to_string())?; + Ok(rows.filter_map(|r| r.ok()).collect()) +} + +pub fn extract_duckdb(connections: &HashMap, key: &str) -> Option>> { + match connections.get(key)? { + PoolKind::DuckDb(con) => Some(con.clone()), + _ => None, + } +} + +pub fn extract_sqlserver(connections: &HashMap, key: &str) -> Option>> { + match connections.get(key)? { + PoolKind::SqlServer(client) => Some(client.clone()), + _ => None, + } +} + +pub fn extract_clickhouse(connections: &HashMap, key: &str) -> Option { + match connections.get(key)? { + PoolKind::ClickHouse(client) => Some(client.clone()), + _ => None, + } +} + +pub fn extract_oracle(connections: &HashMap, key: &str) -> Option>> { + match connections.get(key)? { + PoolKind::Oracle(client) => Some(client.clone()), + _ => None, + } +} + +pub async fn list_databases_core( + state: &AppState, + connection_id: &str, +) -> Result, String> { + { + let connections = state.connections.lock().await; + if let Some(client) = extract_clickhouse(&connections, connection_id) { + drop(connections); + return db::clickhouse_driver::list_databases(&client).await; + } + if let Some(client) = extract_sqlserver(&connections, connection_id) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_databases(&mut client).await; + } + if let Some(client) = extract_oracle(&connections, connection_id) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_databases(&*client).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::list_databases(p).await, + PoolKind::Postgres(p) => db::postgres::list_databases(p).await, + PoolKind::Sqlite(p) => db::sqlite::list_databases(p).await, + PoolKind::DuckDb(_) => Ok(vec![db::DatabaseInfo { name: "main".to_string() }]), + _ => Ok(vec![]), + } +} + +pub async fn list_schemas_core( + state: &AppState, + connection_id: &str, + database: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_schemas(&mut client).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_schemas(&*client).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Postgres(p) => db::postgres::list_schemas(p).await, + _ => Ok(vec![]), + } +} + +pub async fn list_tables_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(con) = extract_duckdb(&connections, &pool_key) { + drop(connections); + let con = con.lock().map_err(|e| e.to_string())?; + return duckdb_query_tables(&con); + } + if let Some(client) = extract_clickhouse(&connections, &pool_key) { + drop(connections); + return db::clickhouse_driver::list_tables(&client, database).await; + } + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_tables(&mut client, schema).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_tables(&*client, schema).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::list_tables(p, schema).await, + PoolKind::Postgres(p) => db::postgres::list_tables(p, schema).await, + PoolKind::Sqlite(p) => db::sqlite::list_tables(p, schema).await, + _ => Ok(vec![]), + } +} + +pub async fn get_columns_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(con) = extract_duckdb(&connections, &pool_key) { + drop(connections); + let con = con.lock().map_err(|e| e.to_string())?; + return duckdb_query_columns(&con, table); + } + if let Some(client) = extract_clickhouse(&connections, &pool_key) { + drop(connections); + return db::clickhouse_driver::get_columns(&client, database, table).await; + } + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::get_columns(&mut client, schema, table).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::get_columns(&*client, schema, table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::get_columns(p, schema, table).await, + PoolKind::Postgres(p) => db::postgres::get_columns(p, schema, table).await, + PoolKind::Sqlite(p) => db::sqlite::get_columns(p, schema, table).await, + _ => Ok(vec![]), + } +} + +pub async fn list_indexes_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_indexes(&mut client, schema, table).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_indexes(&*client, schema, table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::list_indexes(p, schema, table).await, + PoolKind::Postgres(p) => db::postgres::list_indexes(p, schema, table).await, + PoolKind::Sqlite(p) => db::sqlite::list_indexes(p, schema, table).await, + _ => Ok(vec![]), + } +} + +pub async fn list_foreign_keys_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_foreign_keys(&mut client, schema, table).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_foreign_keys(&*client, schema, table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::list_foreign_keys(p, schema, table).await, + PoolKind::Postgres(p) => db::postgres::list_foreign_keys(p, schema, table).await, + PoolKind::Sqlite(p) => db::sqlite::list_foreign_keys(p, schema, table).await, + _ => Ok(vec![]), + } +} + +pub async fn list_triggers_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::list_triggers(&mut client, schema, table).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return db::oracle_driver::list_triggers(&*client, schema, table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => db::mysql::list_triggers(p, schema, table).await, + PoolKind::Postgres(p) => db::postgres::list_triggers(p, schema, table).await, + PoolKind::Sqlite(p) => db::sqlite::list_triggers(p, schema, table).await, + _ => Ok(vec![]), + } +} + +pub async fn get_table_ddl_core( + state: &AppState, + connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(con) = extract_duckdb(&connections, &pool_key) { + drop(connections); + let tbl = table.replace('\'', "''"); + let con = con.lock().map_err(|e| e.to_string())?; + let mut stmt = con.prepare(&format!("SELECT sql FROM duckdb_tables() WHERE table_name = '{tbl}'")) + .map_err(|e| e.to_string())?; + let mut rows = stmt.query([]).map_err(|e| e.to_string())?; + if let Some(row) = rows.next().map_err(|e| e.to_string())? { + return row.get::<_, String>(0).map_err(|e| e.to_string()); + } + return Err("Table not found".to_string()); + } + if let Some(client) = extract_clickhouse(&connections, &pool_key) { + drop(connections); + let result = db::clickhouse_driver::execute_query(&client, database, &format!("SHOW CREATE TABLE `{table}`")).await?; + return result.rows.first() + .and_then(|r| r.first()) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "Table not found".to_string()); + } + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return build_sqlserver_ddl(&mut client, schema, table).await; + } + if let Some(client) = extract_oracle(&connections, &pool_key) { + drop(connections); + let client = client.lock().await; + return build_oracle_ddl(&*client, schema, table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p, _) => mysql_ddl(p, table).await, + PoolKind::Postgres(p) => pg_ddl(p, schema, table).await, + PoolKind::Sqlite(p) => sqlite_ddl(p, table).await, + _ => Err("DDL not supported for this database type".to_string()), + } +} + +pub async fn mysql_ddl(pool: &sqlx::mysql::MySqlPool, table: &str) -> Result { + use sqlx::Row; + let sql = format!("SHOW CREATE TABLE `{}`", table.replace('`', "``")); + let row: sqlx::mysql::MySqlRow = sqlx::raw_sql(&sql) + .fetch_one(pool).await.map_err(|e| e.to_string())?; + row.try_get::(1) + .or_else(|_| row.try_get::, _>(1).map(|b| String::from_utf8_lossy(&b).to_string())) + .map_err(|e| e.to_string()) +} + +pub async fn sqlite_ddl(pool: &sqlx::sqlite::SqlitePool, table: &str) -> Result { + use sqlx::Row; + let row: sqlx::sqlite::SqliteRow = sqlx::query("SELECT sql FROM sqlite_master WHERE type='table' AND name=?") + .bind(table) + .fetch_one(pool).await.map_err(|e| e.to_string())?; + row.try_get::(0).map_err(|e| e.to_string()) +} + +pub async fn pg_ddl(pool: &sqlx::postgres::PgPool, schema: &str, table: &str) -> Result { + let columns = db::postgres::get_columns(pool, schema, table).await?; + let indexes = db::postgres::list_indexes(pool, schema, table).await?; + let fkeys = db::postgres::list_foreign_keys(pool, schema, table).await?; + + let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); + let col_lines: Vec = columns.iter().map(|c| { + let mut line = format!(" \"{}\" {}", c.name, c.data_type); + if !c.is_nullable { line.push_str(" NOT NULL"); } + if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } + line + }).collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", "))); + } + for fk in &fkeys { + ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column)); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { continue; } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); + let using = idx.index_type.as_deref().map(|t| format!(" USING {t}")).unwrap_or_default(); + let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "))).unwrap_or_default(); + let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\"{using} ({cols}){include}{filter};", idx.name)); + if let Some(ref c) = idx.comment { + ddl.push_str(&format!("\nCOMMENT ON INDEX \"{schema}\".\"{}\" IS '{}';", idx.name, c.replace('\'', "''"))); + } + } + Ok(ddl) +} + +pub async fn build_sqlserver_ddl(client: &mut db::sqlserver::SqlServerClient, schema: &str, table: &str) -> Result { + let columns = db::sqlserver::get_columns(client, schema, table).await?; + let indexes = db::sqlserver::list_indexes(client, schema, table).await?; + let fkeys = db::sqlserver::list_foreign_keys(client, schema, table).await?; + + let mut ddl = format!("CREATE TABLE [{schema}].[{table}] (\n"); + let col_lines: Vec = columns.iter().map(|c| { + let mut line = format!(" [{}] {}", c.name, c.data_type); + if !c.is_nullable { line.push_str(" NOT NULL"); } + if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } + line + }).collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("[{k}]")).collect::>().join(", "))); + } + for fk in &fkeys { + ddl.push_str(&format!(",\n CONSTRAINT [{}] FOREIGN KEY ([{}]) REFERENCES [{}]([{}])", fk.name, fk.column, fk.ref_table, fk.ref_column)); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { continue; } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let idx_type = idx.index_type.as_deref().map(|t| format!("{t} ")).unwrap_or_default(); + let cols = idx.columns.iter().map(|c| format!("[{c}]")).collect::>().join(", "); + let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("[{c}]")).collect::>().join(", "))).unwrap_or_default(); + let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); + ddl.push_str(&format!("\nCREATE {unique}{idx_type}INDEX [{}] ON [{schema}].[{table}] ({cols}){include}{filter};", idx.name)); + } + Ok(ddl) +} + +pub async fn build_oracle_ddl(client: &db::oracle_driver::OracleClient, schema: &str, table: &str) -> Result { + let columns = db::oracle_driver::get_columns(client, schema, table).await?; + let indexes = db::oracle_driver::list_indexes(client, schema, table).await?; + let fkeys = db::oracle_driver::list_foreign_keys(client, schema, table).await?; + + let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); + let col_lines: Vec = columns.iter().map(|c| { + let mut line = format!(" \"{}\" {}", c.name, c.data_type); + if !c.is_nullable { line.push_str(" NOT NULL"); } + if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } + line + }).collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", "))); + } + for fk in &fkeys { + ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column)); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { continue; } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); + } + Ok(ddl) +} diff --git a/crates/dbx-core/src/sql.rs b/crates/dbx-core/src/sql.rs new file mode 100644 index 0000000000..454d32d2c2 --- /dev/null +++ b/crates/dbx-core/src/sql.rs @@ -0,0 +1,415 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SqlFileRequest { + pub execution_id: String, + pub connection_id: String, + pub database: String, + pub file_path: String, + pub continue_on_error: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SqlFilePreview { + pub file_name: String, + pub file_path: String, + pub size_bytes: u64, + pub preview: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SqlFileStatus { + Started, + Running, + StatementDone, + StatementFailed, + Done, + Error, + Cancelled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SqlFileProgress { + pub execution_id: String, + pub status: SqlFileStatus, + pub statement_index: usize, + pub success_count: usize, + pub failure_count: usize, + pub affected_rows: u64, + pub elapsed_ms: u128, + pub statement_summary: String, + pub error: Option, +} + +#[derive(Default)] +pub struct SqlStatementSplitter { + buffer: String, + in_single_quote: bool, + in_double_quote: bool, + in_backtick: bool, + in_line_comment: bool, + in_block_comment: bool, + dollar_quote_tag: Option, + previous: Option, +} + +impl SqlStatementSplitter { + pub fn push_chunk(&mut self, chunk: &str) -> Vec { + let mut statements = Vec::new(); + let chars = chunk.chars().collect::>(); + let mut i = 0; + + while i < chars.len() { + if let Some(tag) = &self.dollar_quote_tag { + let tag_chars = tag.chars().collect::>(); + if starts_with_chars(&chars, i, &tag_chars) { + for tag_ch in &tag_chars { + self.buffer.push(*tag_ch); + self.previous = Some(*tag_ch); + } + i += tag_chars.len(); + self.dollar_quote_tag = None; + continue; + } + + let ch = chars[i]; + self.buffer.push(ch); + self.previous = Some(ch); + i += 1; + continue; + } + + let ch = chars[i]; + let next = chars.get(i + 1).copied(); + + if self.in_line_comment { + self.buffer.push(ch); + if ch == '\n' { + self.in_line_comment = false; + } + self.previous = Some(ch); + i += 1; + continue; + } + + if self.in_block_comment { + self.buffer.push(ch); + if self.previous == Some('*') && ch == '/' { + self.in_block_comment = false; + } + self.previous = Some(ch); + i += 1; + continue; + } + + if !self.in_single_quote && !self.in_double_quote && !self.in_backtick { + if self.previous == Some('-') && ch == '-' { + self.in_line_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + i += 1; + continue; + } + if self.previous == Some('/') && ch == '*' { + self.in_block_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + i += 1; + continue; + } + if ch == '-' && next == Some('-') { + self.in_line_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + i += 1; + continue; + } + if ch == '/' && next == Some('*') { + self.in_block_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + i += 1; + continue; + } + if let Some(tag) = dollar_quote_tag_at(&chars, i) { + for tag_ch in tag.chars() { + self.buffer.push(tag_ch); + self.previous = Some(tag_ch); + } + i += tag.chars().count(); + self.dollar_quote_tag = Some(tag); + continue; + } + } + + match ch { + '\'' if !self.in_double_quote + && !self.in_backtick + && self.previous != Some('\\') => + { + self.in_single_quote = !self.in_single_quote; + self.buffer.push(ch); + } + '"' if !self.in_single_quote + && !self.in_backtick + && self.previous != Some('\\') => + { + self.in_double_quote = !self.in_double_quote; + self.buffer.push(ch); + } + '`' if !self.in_single_quote && !self.in_double_quote => { + self.in_backtick = !self.in_backtick; + self.buffer.push(ch); + } + ';' if !self.in_single_quote && !self.in_double_quote && !self.in_backtick => { + self.push_current_statement(&mut statements); + } + _ => self.buffer.push(ch), + } + + self.previous = Some(ch); + i += 1; + } + + statements + } + + pub fn finish(mut self) -> Vec { + let mut statements = Vec::new(); + self.push_current_statement(&mut statements); + statements + } + + fn push_current_statement(&mut self, statements: &mut Vec) { + let statement = self.buffer.trim(); + if has_executable_sql(statement) { + statements.push(statement.to_string()); + } + self.buffer.clear(); + self.previous = None; + } +} + +pub fn split_sql_statements(sql: &str) -> Vec { + let mut splitter = SqlStatementSplitter::default(); + let mut statements = splitter.push_chunk(sql); + statements.extend(splitter.finish()); + statements +} + +pub fn statement_summary(statement: &str) -> String { + const MAX_LEN: usize = 120; + + let collapsed = statement.split_whitespace().collect::>().join(" "); + if collapsed.chars().count() <= MAX_LEN { + return collapsed; + } + + collapsed.chars().take(MAX_LEN).collect() +} + +fn starts_with_chars(chars: &[char], start: usize, needle: &[char]) -> bool { + start + needle.len() <= chars.len() && chars[start..start + needle.len()] == *needle +} + +fn dollar_quote_tag_at(chars: &[char], start: usize) -> Option { + if chars.get(start) != Some(&'$') { + return None; + } + + match chars.get(start + 1) { + Some('$') => return Some("$$".to_string()), + Some(ch) if ch.is_ascii_alphabetic() || *ch == '_' => {} + _ => return None, + } + + let mut end = start + 2; + while let Some(ch) = chars.get(end) { + if *ch == '$' { + return Some(chars[start..=end].iter().collect()); + } + if !ch.is_ascii_alphanumeric() && *ch != '_' { + return None; + } + end += 1; + } + + None +} + +fn has_executable_sql(statement: &str) -> bool { + let chars = statement.chars().collect::>(); + let mut in_line_comment = false; + let mut in_block_comment = false; + let mut previous = None; + let mut i = 0; + + while i < chars.len() { + let ch = chars[i]; + let next = chars.get(i + 1).copied(); + + if in_line_comment { + if ch == '\n' { + in_line_comment = false; + } + previous = Some(ch); + i += 1; + continue; + } + + if in_block_comment { + if previous == Some('*') && ch == '/' { + in_block_comment = false; + } + previous = Some(ch); + i += 1; + continue; + } + + if ch == '-' && next == Some('-') { + in_line_comment = true; + previous = Some(ch); + i += 1; + continue; + } + + if ch == '/' && next == Some('*') { + if is_mysql_executable_comment_start(&chars, i) { + return true; + } + in_block_comment = true; + previous = Some(ch); + i += 1; + continue; + } + + if !ch.is_whitespace() { + return true; + } + + previous = Some(ch); + i += 1; + } + + false +} + +fn is_mysql_executable_comment_start(chars: &[char], start: usize) -> bool { + chars.get(start) == Some(&'/') + && chars.get(start + 1) == Some(&'*') + && (chars.get(start + 2) == Some(&'!') + || (chars.get(start + 2) == Some(&'M') && chars.get(start + 3) == Some(&'!'))) +} + +#[cfg(test)] +fn split_sql_script(sql: &str) -> Result, String> { + Ok(split_sql_statements(sql)) +} + +#[cfg(test)] +mod tests { + use super::{split_sql_script, SqlStatementSplitter}; + + #[test] + fn splits_semicolon_delimited_statements() { + assert_eq!( + split_sql_script("CREATE TABLE a(id int); INSERT INTO a VALUES (1);").unwrap(), + vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] + ); + } + + #[test] + fn ignores_semicolons_inside_quotes_and_comments() { + let sql = "\ + INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`);\n\ + -- comment ; ignored\n\ + /* block ; ignored */\n\ + SELECT 1;"; + assert_eq!( + split_sql_script(sql).unwrap(), + vec![ + "INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`)", + "-- comment ; ignored\n/* block ; ignored */\nSELECT 1", + ] + ); + } + + #[test] + fn emits_trailing_statement_without_semicolon() { + assert_eq!( + split_sql_script("CREATE TABLE a(id int);\nINSERT INTO a VALUES (1)").unwrap(), + vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] + ); + } + + #[test] + fn line_comment_openers_can_span_chunks() { + let mut splitter = SqlStatementSplitter::default(); + + assert_eq!(splitter.push_chunk("SELECT 1; -"), vec!["SELECT 1"]); + assert_eq!( + splitter.push_chunk("- comment ; ignored\nSELECT 2;"), + vec!["-- comment ; ignored\nSELECT 2"] + ); + assert_eq!(splitter.finish(), Vec::::new()); + } + + #[test] + fn block_comment_openers_can_span_chunks() { + let mut splitter = SqlStatementSplitter::default(); + + assert_eq!(splitter.push_chunk("SELECT 1; /"), vec!["SELECT 1"]); + assert_eq!( + splitter.push_chunk("* comment ; ignored */\nSELECT 2;"), + vec!["/* comment ; ignored */\nSELECT 2"] + ); + assert_eq!(splitter.finish(), Vec::::new()); + } + + #[test] + fn skips_comment_only_tail_after_statement() { + assert_eq!( + split_sql_script("CREATE TABLE a(id int); -- done\n/* no more sql */").unwrap(), + vec!["CREATE TABLE a(id int)"] + ); + } + + #[test] + fn keeps_postgres_dollar_quoted_function_body_together() { + let sql = "\ + CREATE FUNCTION bump_counter()\n\ + RETURNS trigger AS $$\n\ + BEGIN\n\ + PERFORM 1;\n\ + RETURN NEW;\n\ + END;\n\ + $$ LANGUAGE plpgsql;\n\ + SELECT 1;"; + + assert_eq!( + split_sql_script(sql).unwrap(), + vec![ + "CREATE FUNCTION bump_counter()\nRETURNS trigger AS $$\nBEGIN\nPERFORM 1;\nRETURN NEW;\nEND;\n$$ LANGUAGE plpgsql", + "SELECT 1", + ] + ); + } + + #[test] + fn keeps_mysql_executable_comments_as_statements() { + assert_eq!( + split_sql_script( + "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\nSELECT 1;", + ) + .unwrap(), + vec![ + "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */", + "SELECT 1", + ] + ); + } +} diff --git a/crates/dbx-core/src/table_import.rs b/crates/dbx-core/src/table_import.rs new file mode 100644 index 0000000000..d3bef40eec --- /dev/null +++ b/crates/dbx-core/src/table_import.rs @@ -0,0 +1,717 @@ +use std::collections::HashSet; +use std::path::Path; + +use calamine::{open_workbook_auto, Data, Reader}; +use serde::{Deserialize, Serialize}; + +use crate::models::connection::DatabaseType; +use crate::transfer::{execute_on_pool, generate_insert, qualified_table}; +use crate::connection::AppState; + +pub const DEFAULT_PREVIEW_LIMIT: usize = 50; +pub const DEFAULT_BATCH_SIZE: usize = 500; + +#[derive(Debug, Clone)] +pub struct ParsedImportFile { + pub columns: Vec, + pub rows: Vec>, + pub total_rows: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportSqlBatch { + pub sql: String, + pub row_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TableImportColumnMapping { + pub source_column: String, + pub target_column: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TableImportMode { + Append, + Truncate, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TableImportRequest { + pub import_id: String, + pub connection_id: String, + pub database: String, + pub schema: String, + pub table: String, + pub file_path: String, + pub mappings: Vec, + pub mode: TableImportMode, + pub batch_size: usize, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TableImportPreview { + pub file_name: String, + pub file_path: String, + pub file_type: String, + pub size_bytes: u64, + pub columns: Vec, + pub rows: Vec>, + pub total_rows: usize, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TableImportSummary { + pub import_id: String, + pub rows_imported: usize, + pub total_rows: usize, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TableImportProgress { + pub import_id: String, + pub status: TableImportStatus, + pub rows_imported: usize, + pub total_rows: usize, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TableImportStatus { + Running, + Done, + Error, + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ImportFileKind { + Csv, + Tsv, + Json, + Xlsx, +} + +impl ImportFileKind { + pub fn label(self) -> &'static str { + match self { + ImportFileKind::Csv => "csv", + ImportFileKind::Tsv => "tsv", + ImportFileKind::Json => "json", + ImportFileKind::Xlsx => "xlsx", + } + } +} + +pub fn import_file_kind(path: &str) -> Result { + let lower = path.to_lowercase(); + if lower.ends_with(".csv") { + Ok(ImportFileKind::Csv) + } else if lower.ends_with(".tsv") { + Ok(ImportFileKind::Tsv) + } else if lower.ends_with(".json") { + Ok(ImportFileKind::Json) + } else if lower.ends_with(".xlsx") || lower.ends_with(".xlsm") || lower.ends_with(".xls") { + Ok(ImportFileKind::Xlsx) + } else { + Err("Unsupported import file type".to_string()) + } +} + +pub fn normalize_header(value: &str, index: usize) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + format!("column_{}", index + 1) + } else { + trimmed.to_string() + } +} + +pub fn csv_value(value: &str) -> serde_json::Value { + if value.is_empty() { + serde_json::Value::Null + } else { + serde_json::Value::String(value.to_string()) + } +} + +pub fn parse_delimited_bytes( + bytes: &[u8], + delimiter: u8, + preview_limit: usize, +) -> Result { + let mut reader = csv::ReaderBuilder::new() + .delimiter(delimiter) + .flexible(true) + .from_reader(bytes); + let columns = reader + .headers() + .map_err(|e| e.to_string())? + .iter() + .enumerate() + .map(|(index, header)| normalize_header(header, index)) + .collect::>(); + if columns.is_empty() { + return Err("Import file has no columns".to_string()); + } + + let mut rows = Vec::new(); + let mut total_rows = 0; + for record in reader.records() { + let record = record.map_err(|e| e.to_string())?; + total_rows += 1; + if rows.len() >= preview_limit { + continue; + } + let mut row = Vec::with_capacity(columns.len()); + for index in 0..columns.len() { + row.push( + record + .get(index) + .map(csv_value) + .unwrap_or(serde_json::Value::Null), + ); + } + rows.push(row); + } + + Ok(ParsedImportFile { + columns, + rows, + total_rows, + }) +} + +pub fn parse_csv_bytes(bytes: &[u8], preview_limit: usize) -> Result { + parse_delimited_bytes(bytes, b',', preview_limit) +} + +pub fn parse_json_bytes(bytes: &[u8], preview_limit: usize) -> Result { + let value: serde_json::Value = serde_json::from_slice(bytes).map_err(|e| e.to_string())?; + let items = match value { + serde_json::Value::Array(items) => items, + serde_json::Value::Object(_) => vec![value], + _ => return Err("JSON import must be an object or an array".to_string()), + }; + if items.is_empty() { + return Err("Import file has no rows".to_string()); + } + + if items.iter().all(|item| item.is_object()) { + let mut columns = Vec::new(); + for item in &items { + if let Some(obj) = item.as_object() { + for key in obj.keys() { + if !columns.contains(key) { + columns.push(key.clone()); + } + } + } + } + if columns.is_empty() { + return Err("Import file has no columns".to_string()); + } + let rows = items + .iter() + .take(preview_limit) + .map(|item| { + let obj = item.as_object().expect("checked object JSON row"); + columns + .iter() + .map(|column| obj.get(column).cloned().unwrap_or(serde_json::Value::Null)) + .collect::>() + }) + .collect::>(); + return Ok(ParsedImportFile { + columns, + rows, + total_rows: items.len(), + }); + } + + if items.iter().all(|item| item.is_array()) { + let max_cols = items + .iter() + .filter_map(|item| item.as_array().map(|row| row.len())) + .max() + .unwrap_or(0); + if max_cols == 0 { + return Err("Import file has no columns".to_string()); + } + let columns = (0..max_cols) + .map(|index| format!("column_{}", index + 1)) + .collect::>(); + let rows = items + .iter() + .take(preview_limit) + .map(|item| { + let arr = item.as_array().expect("checked array JSON row"); + (0..max_cols) + .map(|index| arr.get(index).cloned().unwrap_or(serde_json::Value::Null)) + .collect::>() + }) + .collect::>(); + return Ok(ParsedImportFile { + columns, + rows, + total_rows: items.len(), + }); + } + + Err("JSON rows must all be objects or all be arrays".to_string()) +} + +pub fn xlsx_cell_value(cell: &Data) -> serde_json::Value { + match cell { + Data::Empty => serde_json::Value::Null, + Data::String(s) => csv_value(s), + Data::Float(n) => serde_json::Number::from_f64(*n) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Data::Int(n) => serde_json::Value::Number((*n).into()), + Data::Bool(v) => serde_json::Value::Bool(*v), + Data::DateTime(v) => serde_json::Value::String(v.to_string()), + Data::DateTimeIso(v) => serde_json::Value::String(v.clone()), + Data::DurationIso(v) => serde_json::Value::String(v.clone()), + Data::Error(v) => serde_json::Value::String(v.to_string()), + } +} + +pub fn xlsx_cell_label(cell: &Data) -> String { + match cell { + Data::Empty => String::new(), + Data::String(s) => s.clone(), + Data::Float(n) => n.to_string(), + Data::Int(n) => n.to_string(), + Data::Bool(v) => v.to_string(), + Data::DateTime(v) => v.to_string(), + Data::DateTimeIso(v) => v.clone(), + Data::DurationIso(v) => v.clone(), + Data::Error(v) => v.to_string(), + } +} + +pub fn parse_xlsx_file(path: &str, preview_limit: usize) -> Result { + let mut workbook = open_workbook_auto(path).map_err(|e| e.to_string())?; + let sheet_name = workbook + .sheet_names() + .first() + .cloned() + .ok_or_else(|| "Workbook has no sheets".to_string())?; + let range = workbook + .worksheet_range(&sheet_name) + .map_err(|e| e.to_string())?; + let mut rows_iter = range.rows(); + let header = rows_iter + .next() + .ok_or_else(|| "Import file has no rows".to_string())?; + let columns = header + .iter() + .enumerate() + .map(|(index, cell)| normalize_header(&xlsx_cell_label(cell), index)) + .collect::>(); + if columns.is_empty() { + return Err("Import file has no columns".to_string()); + } + + let mut rows = Vec::new(); + let mut total_rows = 0; + for source_row in rows_iter { + total_rows += 1; + if rows.len() >= preview_limit { + continue; + } + let mut row = Vec::with_capacity(columns.len()); + for index in 0..columns.len() { + row.push( + source_row + .get(index) + .map(xlsx_cell_value) + .unwrap_or(serde_json::Value::Null), + ); + } + rows.push(row); + } + + Ok(ParsedImportFile { + columns, + rows, + total_rows, + }) +} + +pub fn parse_import_file(path: &str, preview_limit: usize) -> Result { + match import_file_kind(path)? { + ImportFileKind::Csv => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + parse_csv_bytes(&bytes, preview_limit) + } + ImportFileKind::Tsv => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + parse_delimited_bytes(&bytes, b'\t', preview_limit) + } + ImportFileKind::Json => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + parse_json_bytes(&bytes, preview_limit) + } + ImportFileKind::Xlsx => parse_xlsx_file(path, preview_limit), + } +} + +pub fn mapping_indexes( + data: &ParsedImportFile, + mappings: &[TableImportColumnMapping], +) -> Result, String> { + if mappings.is_empty() { + return Err("No columns mapped for import".to_string()); + } + let mut mapped = Vec::new(); + let mut target_seen = HashSet::new(); + for mapping in mappings { + let source_index = data + .columns + .iter() + .position(|column| column == &mapping.source_column) + .ok_or_else(|| format!("Source column not found: {}", mapping.source_column))?; + if mapping.target_column.trim().is_empty() { + return Err("Target column cannot be empty".to_string()); + } + if !target_seen.insert(mapping.target_column.clone()) { + return Err(format!( + "Target column mapped more than once: {}", + mapping.target_column + )); + } + mapped.push((source_index, mapping.target_column.clone())); + } + Ok(mapped) +} + +pub fn build_import_insert_batches( + data: &ParsedImportFile, + mappings: &[TableImportColumnMapping], + table: &str, + schema: &str, + db_type: &DatabaseType, + batch_size: usize, +) -> Result, String> { + let mapped = mapping_indexes(data, mappings)?; + let columns = mapped + .iter() + .map(|(_, target)| target.clone()) + .collect::>(); + let batch_size = batch_size.max(1); + let mut batches = Vec::new(); + + for chunk in data.rows.chunks(batch_size) { + let rows = chunk + .iter() + .map(|row| { + mapped + .iter() + .map(|(source_index, _)| { + row.get(*source_index) + .cloned() + .unwrap_or(serde_json::Value::Null) + }) + .collect::>() + }) + .collect::>(); + let sql = generate_insert(&columns, &rows, table, schema, db_type); + if !sql.trim().is_empty() { + batches.push(ImportSqlBatch { + sql, + row_count: chunk.len(), + }); + } + } + + Ok(batches) +} + +pub fn truncate_sql(table: &str, schema: &str, db_type: &DatabaseType) -> String { + let full_table = qualified_table(table, schema, db_type); + match db_type { + DatabaseType::Sqlite => format!("DELETE FROM {full_table}"), + _ => format!("TRUNCATE TABLE {full_table}"), + } +} + +pub fn preview_table_import_file_core(file_path: &str) -> Result { + let kind = import_file_kind(file_path)?; + let parsed = parse_import_file(file_path, DEFAULT_PREVIEW_LIMIT)?; + let metadata = std::fs::metadata(file_path).map_err(|e| e.to_string())?; + let file_name = Path::new(file_path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(file_path) + .to_string(); + + Ok(TableImportPreview { + file_name, + file_path: file_path.to_string(), + file_type: kind.label().to_string(), + size_bytes: metadata.len(), + columns: parsed.columns, + rows: parsed.rows, + total_rows: parsed.total_rows, + }) +} + +/// Core import logic. Returns (rows_imported, total_rows). +/// `progress_callback` is invoked for progress updates. +pub async fn import_table_file_core( + state: &AppState, + request: &TableImportRequest, + db_type: &DatabaseType, + pool_key: &str, + is_cancelled: impl Fn(&str) -> std::pin::Pin + Send + '_>>, + mut progress_callback: F, +) -> Result +where + F: FnMut(TableImportProgress), +{ + let batch_size = if request.batch_size == 0 { + DEFAULT_BATCH_SIZE + } else { + request.batch_size + }; + + let parsed = match parse_import_file(&request.file_path, usize::MAX) { + Ok(parsed) => parsed, + Err(error) => { + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Error, + rows_imported: 0, + total_rows: 0, + error: Some(error.clone()), + }); + return Err(error); + } + }; + + let total_rows = parsed.total_rows; + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Running, + rows_imported: 0, + total_rows, + error: None, + }); + + let batches = match build_import_insert_batches( + &parsed, + &request.mappings, + &request.table, + &request.schema, + db_type, + batch_size, + ) { + Ok(batches) => batches, + Err(error) => { + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Error, + rows_imported: 0, + total_rows, + error: Some(error.clone()), + }); + return Err(error); + } + }; + + if matches!(request.mode, TableImportMode::Truncate) { + let sql = truncate_sql(&request.table, &request.schema, db_type); + if let Err(error) = execute_on_pool(state, pool_key, &sql).await { + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Error, + rows_imported: 0, + total_rows, + error: Some(error.clone()), + }); + return Err(error); + } + } + + let mut rows_imported = 0; + for batch in batches { + if is_cancelled(&request.import_id).await { + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Cancelled, + rows_imported, + total_rows, + error: None, + }); + return Err("Import cancelled".to_string()); + } + + if let Err(error) = execute_on_pool(state, pool_key, &batch.sql).await { + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Error, + rows_imported, + total_rows, + error: Some(error.clone()), + }); + return Err(error); + } + rows_imported = (rows_imported + batch.row_count).min(total_rows); + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Running, + rows_imported, + total_rows, + error: None, + }); + } + + progress_callback(TableImportProgress { + import_id: request.import_id.clone(), + status: TableImportStatus::Done, + rows_imported, + total_rows, + error: None, + }); + + Ok(TableImportSummary { + import_id: request.import_id.clone(), + rows_imported, + total_rows, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::connection::DatabaseType; + + #[test] + fn parses_csv_headers_and_preview_rows() { + let parsed = parse_csv_bytes(b"id,name,active\n1,Ada,true\n2,,false\n", 10).unwrap(); + + assert_eq!(parsed.columns, vec!["id", "name", "active"]); + assert_eq!(parsed.total_rows, 2); + assert_eq!( + parsed.rows[0], + vec![ + serde_json::Value::String("1".to_string()), + serde_json::Value::String("Ada".to_string()), + serde_json::Value::String("true".to_string()), + ] + ); + assert_eq!( + parsed.rows[1], + vec![ + serde_json::Value::String("2".to_string()), + serde_json::Value::Null, + serde_json::Value::String("false".to_string()), + ] + ); + } + + #[test] + fn parses_tsv_with_tab_delimiter() { + let parsed = parse_delimited_bytes(b"id\tname\n1\tAda\n", b'\t', 10).unwrap(); + + assert_eq!(parsed.columns, vec!["id", "name"]); + assert_eq!(parsed.total_rows, 1); + assert_eq!( + parsed.rows[0], + vec![ + serde_json::Value::String("1".to_string()), + serde_json::Value::String("Ada".to_string()), + ] + ); + } + + #[test] + fn parses_json_array_objects_with_union_columns() { + let parsed = + parse_json_bytes(br#"[{"id":1,"name":"Ada"},{"id":2,"active":true}]"#, 10).unwrap(); + + assert_eq!(parsed.columns, vec!["id", "name", "active"]); + assert_eq!(parsed.total_rows, 2); + assert_eq!( + parsed.rows[0], + vec![ + serde_json::json!(1), + serde_json::json!("Ada"), + serde_json::Value::Null, + ] + ); + assert_eq!( + parsed.rows[1], + vec![ + serde_json::json!(2), + serde_json::Value::Null, + serde_json::json!(true), + ] + ); + } + + #[test] + fn builds_import_insert_batches_from_mapped_columns() { + let mappings = vec![ + TableImportColumnMapping { + source_column: "id".to_string(), + target_column: "user_id".to_string(), + }, + TableImportColumnMapping { + source_column: "name".to_string(), + target_column: "display_name".to_string(), + }, + ]; + let data = ParsedImportFile { + columns: vec!["id".to_string(), "name".to_string(), "ignored".to_string()], + rows: vec![ + vec![ + serde_json::json!(1), + serde_json::json!("Ada"), + serde_json::json!("x"), + ], + vec![ + serde_json::json!(2), + serde_json::json!("O'Hara"), + serde_json::json!("y"), + ], + vec![ + serde_json::json!(3), + serde_json::Value::Null, + serde_json::json!("z"), + ], + ], + total_rows: 3, + }; + + let batches = build_import_insert_batches( + &data, + &mappings, + "users", + "public", + &DatabaseType::Postgres, + 2, + ) + .unwrap(); + + assert_eq!(batches, vec![ + ImportSqlBatch { + sql: "INSERT INTO \"public\".\"users\" (\"user_id\", \"display_name\") VALUES\n(1, 'Ada'),\n(2, 'O''Hara')".to_string(), + row_count: 2, + }, + ImportSqlBatch { + sql: "INSERT INTO \"public\".\"users\" (\"user_id\", \"display_name\") VALUES\n(3, NULL)".to_string(), + row_count: 1, + }, + ]); + } +} diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs new file mode 100644 index 0000000000..5bb028ea85 --- /dev/null +++ b/crates/dbx-core/src/transfer.rs @@ -0,0 +1,630 @@ +use std::collections::HashSet; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::connection::{AppState, PoolKind}; +use crate::db; +use crate::models::connection::DatabaseType; + +static CANCELLED: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| RwLock::new(HashSet::new())); + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransferRequest { + pub transfer_id: String, + pub source_connection_id: String, + pub source_database: String, + pub source_schema: String, + pub target_connection_id: String, + pub target_database: String, + pub target_schema: String, + pub tables: Vec, + pub create_table: bool, + pub truncate_before: bool, + pub batch_size: usize, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransferProgress { + pub transfer_id: String, + pub table: String, + pub table_index: usize, + pub total_tables: usize, + pub rows_transferred: u64, + pub total_rows: Option, + pub status: TransferStatus, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TransferStatus { + Running, + TableDone, + Done, + Error, + Cancelled, +} + +pub fn quote_identifier(name: &str, db_type: &DatabaseType) -> String { + match db_type { + DatabaseType::Mysql | DatabaseType::ClickHouse | DatabaseType::Doris | DatabaseType::StarRocks => format!("`{}`", name.replace('`', "``")), + DatabaseType::SqlServer => format!("[{}]", name.replace(']', "]]")), + _ => format!("\"{}\"", name.replace('"', "\"\"")), + } +} + +pub fn qualified_table(table: &str, schema: &str, db_type: &DatabaseType) -> String { + let qt = quote_identifier(table, db_type); + if schema.is_empty() { + qt + } else { + format!("{}.{}", quote_identifier(schema, db_type), qt) + } +} + +pub fn escape_value(val: &serde_json::Value, db_type: &DatabaseType) -> String { + match val { + serde_json::Value::Null => "NULL".to_string(), + serde_json::Value::Bool(b) => match db_type { + DatabaseType::Mysql | DatabaseType::Sqlite | DatabaseType::DuckDb | DatabaseType::Doris | DatabaseType::StarRocks => { + if *b { "1".to_string() } else { "0".to_string() } + } + _ => if *b { "TRUE".to_string() } else { "FALSE".to_string() }, + }, + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::String(s) => { + format!("'{}'", s.replace('\'', "''")) + } + _ => { + let s = val.to_string(); + format!("'{}'", s.replace('\'', "''")) + } + } +} + +pub fn map_column_type(source_type: &str, _source_db: &DatabaseType, target_db: &DatabaseType) -> String { + let t = source_type.to_lowercase(); + let base = t.split('(').next().unwrap_or(&t).trim(); + + match base { + "int" | "integer" | "int4" | "mediumint" => match target_db { + DatabaseType::Postgres => "INTEGER".into(), + DatabaseType::Mysql => "INT".into(), + DatabaseType::SqlServer => "INT".into(), + _ => "INTEGER".into(), + }, + "bigint" | "int8" => "BIGINT".into(), + "smallint" | "int2" => "SMALLINT".into(), + "tinyint" => match target_db { + DatabaseType::Postgres => "SMALLINT".into(), + _ => "TINYINT".into(), + }, + "serial" | "bigserial" | "smallserial" => match target_db { + DatabaseType::Postgres => source_type.to_uppercase(), + DatabaseType::Mysql => "BIGINT AUTO_INCREMENT".into(), + _ => "INTEGER".into(), + }, + "float" | "float4" | "real" => match target_db { + DatabaseType::Postgres => "REAL".into(), + _ => "FLOAT".into(), + }, + "double" | "double precision" | "float8" => match target_db { + DatabaseType::Postgres => "DOUBLE PRECISION".into(), + _ => "DOUBLE".into(), + }, + "decimal" | "numeric" | "number" => { + if t.contains('(') { + match target_db { + DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::SqlServer | DatabaseType::Oracle => { + format!("DECIMAL{}", &t[t.find('(').unwrap()..]) + } + _ => "NUMERIC".into(), + } + } else { + "NUMERIC".into() + } + } + "varchar" | "nvarchar" | "character varying" | "varchar2" => { + if t.contains('(') { + let len_part = &t[t.find('(').unwrap()..]; + match target_db { + DatabaseType::Postgres => format!("VARCHAR{len_part}"), + DatabaseType::Mysql => format!("VARCHAR{len_part}"), + DatabaseType::SqlServer => format!("NVARCHAR{len_part}"), + _ => format!("VARCHAR{len_part}"), + } + } else { + "VARCHAR(255)".into() + } + } + "char" | "nchar" | "character" => { + if t.contains('(') { + let len_part = &t[t.find('(').unwrap()..]; + format!("CHAR{len_part}") + } else { + "CHAR(1)".into() + } + } + "text" | "longtext" | "mediumtext" | "tinytext" | "clob" | "ntext" => "TEXT".into(), + "bool" | "boolean" => match target_db { + DatabaseType::Mysql => "TINYINT(1)".into(), + DatabaseType::SqlServer => "BIT".into(), + _ => "BOOLEAN".into(), + }, + "date" => "DATE".into(), + "time" => "TIME".into(), + "datetime" => match target_db { + DatabaseType::Postgres => "TIMESTAMP".into(), + _ => "DATETIME".into(), + }, + "timestamp" | "timestamptz" | "timestamp with time zone" + | "timestamp without time zone" => match target_db { + DatabaseType::Mysql => "DATETIME".into(), + DatabaseType::SqlServer => "DATETIME2".into(), + _ => "TIMESTAMP".into(), + }, + "blob" | "longblob" | "mediumblob" | "tinyblob" | "binary" | "varbinary" | "image" => { + match target_db { + DatabaseType::Postgres => "BYTEA".into(), + DatabaseType::Mysql => "BLOB".into(), + DatabaseType::SqlServer => "VARBINARY(MAX)".into(), + _ => "BLOB".into(), + } + } + "bytea" => match target_db { + DatabaseType::Postgres => "BYTEA".into(), + DatabaseType::Mysql => "BLOB".into(), + _ => "BLOB".into(), + }, + "json" | "jsonb" => match target_db { + DatabaseType::Postgres => "JSONB".into(), + DatabaseType::Mysql => "JSON".into(), + _ => "TEXT".into(), + }, + "uuid" => match target_db { + DatabaseType::Postgres => "UUID".into(), + _ => "VARCHAR(36)".into(), + }, + "bit" => match target_db { + DatabaseType::Postgres => "BOOLEAN".into(), + _ => "BIT".into(), + }, + _ => "TEXT".into(), + } +} + +pub fn generate_create_table_ddl( + columns: &[db::ColumnInfo], + table: &str, + schema: &str, + target_db: &DatabaseType, + source_db: &DatabaseType, +) -> String { + let full_table = qualified_table(table, schema, target_db); + + let col_lines: Vec = columns + .iter() + .map(|c| { + let mapped_type = map_column_type(&c.data_type, source_db, target_db); + let mut line = format!(" {} {}", quote_identifier(&c.name, target_db), mapped_type); + if !c.is_nullable { + line.push_str(" NOT NULL"); + } + line + }) + .collect(); + + let pks: Vec = columns + .iter() + .filter(|c| c.is_primary_key) + .map(|c| quote_identifier(&c.name, target_db)) + .collect(); + + let mut ddl = match target_db { + DatabaseType::SqlServer => format!("IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{table}')\n"), + _ => String::new(), + }; + + let create_prefix = match target_db { + DatabaseType::SqlServer => "CREATE TABLE", + _ => "CREATE TABLE IF NOT EXISTS", + }; + + ddl.push_str(&format!("{create_prefix} {full_table} (\n")); + ddl.push_str(&col_lines.join(",\n")); + + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.join(", "))); + } + + ddl.push_str("\n)"); + + if matches!(target_db, DatabaseType::ClickHouse) { + ddl.push_str(" ENGINE = MergeTree() ORDER BY tuple()"); + } + + ddl +} + +pub fn generate_insert( + columns: &[String], + rows: &[Vec], + table: &str, + schema: &str, + db_type: &DatabaseType, +) -> String { + if rows.is_empty() { + return String::new(); + } + + let full_table = qualified_table(table, schema, db_type); + let col_list = columns + .iter() + .map(|c| quote_identifier(c, db_type)) + .collect::>() + .join(", "); + + let value_rows: Vec = rows + .iter() + .map(|row| { + let vals: Vec = row.iter().map(|v| escape_value(v, db_type)).collect(); + format!("({})", vals.join(", ")) + }) + .collect(); + + format!("INSERT INTO {full_table} ({col_list}) VALUES\n{}", value_rows.join(",\n")) +} + +pub fn pagination_sql( + columns: &[String], + table: &str, + schema: &str, + db_type: &DatabaseType, + offset: u64, + limit: usize, +) -> String { + let full_table = qualified_table(table, schema, db_type); + let col_list = columns + .iter() + .map(|c| quote_identifier(c, db_type)) + .collect::>() + .join(", "); + + match db_type { + DatabaseType::SqlServer | DatabaseType::Oracle => { + format!( + "SELECT {col_list} FROM {full_table} ORDER BY (SELECT NULL) OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" + ) + } + _ => { + format!("SELECT {col_list} FROM {full_table} LIMIT {limit} OFFSET {offset}") + } + } +} + +pub fn count_sql(table: &str, schema: &str, db_type: &DatabaseType) -> String { + let full_table = qualified_table(table, schema, db_type); + format!("SELECT COUNT(*) FROM {full_table}") +} + +pub async fn execute_on_pool( + state: &AppState, + pool_key: &str, + sql: &str, +) -> Result { + let connections = state.connections.lock().await; + let pool = connections.get(pool_key).ok_or("Connection not found")?; + + match pool { + PoolKind::Mysql(p, bare) => { + let p = p.clone(); + let bare = *bare; + drop(connections); + db::mysql::execute_query(&p, sql, bare).await + } + PoolKind::Postgres(p) => { + let p = p.clone(); + drop(connections); + db::postgres::execute_query(&p, sql).await + } + PoolKind::Sqlite(p) => { + let p = p.clone(); + drop(connections); + db::sqlite::execute_query(&p, sql).await + } + PoolKind::ClickHouse(client) => { + let client = client.clone(); + let database = pool_key.split(':').nth(1).unwrap_or("default").to_string(); + drop(connections); + db::clickhouse_driver::execute_query(&client, &database, sql).await + } + PoolKind::SqlServer(client) => { + let client = client.clone(); + drop(connections); + let mut client = client.lock().await; + db::sqlserver::execute_query(&mut client, sql).await + } + PoolKind::Oracle(client) => { + let client = client.clone(); + drop(connections); + let client = client.lock().await; + db::oracle_driver::execute_query(&*client, sql).await + } + PoolKind::DuckDb(con) => { + let con = con.clone(); + let sql = sql.to_string(); + drop(connections); + tokio::task::spawn_blocking(move || { + let con = con.lock().map_err(|e| e.to_string())?; + let start = std::time::Instant::now(); + let trimmed = sql.trim().to_uppercase(); + if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") + || trimmed.starts_with("DESCRIBE") || trimmed.starts_with("WITH") + || trimmed.starts_with("PRAGMA") + { + let mut stmt = con.prepare(&sql).map_err(|e| e.to_string())?; + let mut rows = stmt.query([]).map_err(|e| e.to_string())?; + let stmt_ref = rows.as_ref().ok_or("DuckDB statement unavailable")?; + let col_count = stmt_ref.column_count(); + let columns: Vec = (0..col_count) + .map(|i| stmt_ref.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "?".to_string())) + .collect(); + let mut result_rows = Vec::new(); + while let Some(row) = rows.next().map_err(|e| e.to_string())? { + let vals: Vec = (0..col_count).map(|i| { + row.get::<_, String>(i).map(serde_json::Value::String) + .or_else(|_| row.get::<_, i64>(i).map(|v| serde_json::Value::Number(v.into()))) + .or_else(|_| row.get::<_, f64>(i).map(|v| { + serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null) + })) + .or_else(|_| row.get::<_, bool>(i).map(serde_json::Value::Bool)) + .unwrap_or(serde_json::Value::Null) + }).collect(); + result_rows.push(vals); + } + Ok(db::QueryResult { columns, rows: result_rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated: false }) + } else { + let affected = con.execute(&sql, []).map_err(|e| e.to_string())?; + Ok(db::QueryResult { columns: vec![], rows: vec![], affected_rows: affected as u64, execution_time_ms: start.elapsed().as_millis(), truncated: false }) + } + }) + .await + .map_err(|e| e.to_string())? + } + _ => Err("Unsupported database type for transfer".to_string()), + } +} + +pub async fn get_db_type(state: &AppState, connection_id: &str) -> Result { + let configs = state.configs.lock().await; + configs + .get(connection_id) + .map(|c| c.db_type.clone()) + .ok_or_else(|| format!("Connection config not found: {connection_id}")) +} + +pub async fn get_columns_for_transfer( + state: &AppState, + pool_key: &str, + _connection_id: &str, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + let connections = state.connections.lock().await; + + if let Some(PoolKind::DuckDb(con)) = connections.get(pool_key) { + let con = con.clone(); + drop(connections); + let table = table.to_string(); + return tokio::task::spawn_blocking(move || { + let con = con.lock().map_err(|e| e.to_string())?; + let mut stmt = con.prepare( + "SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'main' AND table_name = ? + ORDER BY ordinal_position" + ).map_err(|e| e.to_string())?; + let rows = stmt.query_map([&table], |row| { + Ok(db::ColumnInfo { + name: row.get::<_, String>(0)?, + data_type: row.get::<_, String>(1)?, + is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES", + column_default: row.get::<_, Option>(3)?, + is_primary_key: false, + extra: None, + comment: None, + numeric_precision: None, + numeric_scale: None, + character_maximum_length: None, + }) + }).map_err(|e| e.to_string())?; + Ok(rows.filter_map(|r| r.ok()).collect()) + }).await.map_err(|e| e.to_string())?; + } + + if let Some(PoolKind::ClickHouse(client)) = connections.get(pool_key) { + let client = client.clone(); + let database = database.to_string(); + let table = table.to_string(); + drop(connections); + return db::clickhouse_driver::get_columns(&client, &database, &table).await; + } + if let Some(PoolKind::SqlServer(client)) = connections.get(pool_key) { + let client = client.clone(); + let schema = schema.to_string(); + let table = table.to_string(); + drop(connections); + let mut client = client.lock().await; + return db::sqlserver::get_columns(&mut client, &schema, &table).await; + } + if let Some(PoolKind::Oracle(client)) = connections.get(pool_key) { + let client = client.clone(); + let schema = schema.to_string(); + let table = table.to_string(); + drop(connections); + let client = client.lock().await; + return db::oracle_driver::get_columns(&*client, &schema, &table).await; + } + + let pool = connections.get(pool_key).ok_or("Pool not found")?; + let schema = schema.to_string(); + let table = table.to_string(); + match pool { + PoolKind::Mysql(p, _) => { + let p = p.clone(); + drop(connections); + db::mysql::get_columns(&p, &schema, &table).await + } + PoolKind::Postgres(p) => { + let p = p.clone(); + drop(connections); + db::postgres::get_columns(&p, &schema, &table).await + } + PoolKind::Sqlite(p) => { + let p = p.clone(); + drop(connections); + db::sqlite::get_columns(&p, &schema, &table).await + } + _ => Err("Unsupported database type".to_string()), + } +} + +pub async fn is_cancelled(transfer_id: &str) -> bool { + CANCELLED.read().await.contains(transfer_id) +} + +pub async fn set_cancelled(transfer_id: &str) { + CANCELLED.write().await.insert(transfer_id.to_string()); +} + +pub async fn clear_cancelled(transfer_id: &str) { + CANCELLED.write().await.remove(transfer_id); +} + +/// Transfer a single table. Returns rows transferred. +/// `progress_callback` is invoked for progress updates. +pub async fn transfer_table( + state: &AppState, + request: &TransferRequest, + table: &str, + table_index: usize, + source_db_type: &DatabaseType, + target_db_type: &DatabaseType, + source_pool_key: &str, + target_pool_key: &str, + mut progress_callback: F, +) -> Result +where + F: FnMut(TransferProgress), +{ + let total_tables = request.tables.len(); + + // Get source columns (deduplicate by name) + let columns = { + let raw = get_columns_for_transfer( + state, source_pool_key, &request.source_connection_id, + &request.source_database, &request.source_schema, table, + ).await?; + let mut seen = std::collections::HashSet::new(); + raw.into_iter().filter(|c| seen.insert(c.name.clone())).collect::>() + }; + + if columns.is_empty() { + return Err(format!("No columns found for table {table}")); + } + + let col_names: Vec = columns.iter().map(|c| c.name.clone()).collect(); + log::info!("[transfer] {} has {} columns, counting rows...", table, columns.len()); + + // Count source rows + let total_rows = { + let sql = count_sql(table, &request.source_schema, source_db_type); + match execute_on_pool(state, source_pool_key, &sql).await { + Ok(result) => result.rows.first() + .and_then(|r| r.first()) + .and_then(|v| match v { + serde_json::Value::Number(n) => n.as_u64(), + serde_json::Value::String(s) => s.parse::().ok(), + _ => None, + }), + Err(e) => { + log::warn!("[transfer] count failed for {}: {}", table, e); + None + } + } + }; + log::info!("[transfer] {} total_rows={:?}", table, total_rows); + + // Create table on target if requested + if request.create_table { + let ddl = generate_create_table_ddl(&columns, table, &request.target_schema, target_db_type, source_db_type); + log::info!("[transfer] creating target table: {}", &ddl[..ddl.len().min(200)]); + if let Err(e) = execute_on_pool(state, target_pool_key, &ddl).await { + let err_lower = e.to_lowercase(); + if !err_lower.contains("already exists") && !err_lower.contains("there is already") { + return Err(format!("Failed to create table: {e}")); + } + } + } + + // Truncate target if requested + if request.truncate_before { + let full_table = qualified_table(table, &request.target_schema, target_db_type); + let truncate_sql = match target_db_type { + DatabaseType::Sqlite | DatabaseType::DuckDb => format!("DELETE FROM {full_table}"), + _ => format!("TRUNCATE TABLE {full_table}"), + }; + execute_on_pool(state, target_pool_key, &truncate_sql).await + .map_err(|e| format!("Failed to truncate: {e}"))?; + } + + // Transfer data in batches + let batch_size = if request.batch_size == 0 { 1000 } else { request.batch_size }; + let mut offset: u64 = 0; + let mut total_transferred: u64 = 0; + + loop { + if is_cancelled(&request.transfer_id).await { + return Err("Cancelled".to_string()); + } + + let sql = pagination_sql(&col_names, table, &request.source_schema, source_db_type, offset, batch_size); + let result = execute_on_pool(state, source_pool_key, &sql).await?; + let row_count = result.rows.len(); + + if row_count == 0 { + break; + } + + let insert_sql = generate_insert(&col_names, &result.rows, table, &request.target_schema, target_db_type); + if !insert_sql.is_empty() { + execute_on_pool(state, target_pool_key, &insert_sql).await + .map_err(|e| format!("Insert failed at offset {offset}: {e}"))?; + } + + total_transferred += row_count as u64; + log::info!("[transfer] {} batch +{} rows (total {})", table, row_count, total_transferred); + offset += row_count as u64; + + progress_callback(TransferProgress { + transfer_id: request.transfer_id.clone(), + table: table.to_string(), + table_index, + total_tables, + rows_transferred: total_transferred, + total_rows, + status: TransferStatus::Running, + error: None, + }); + + if row_count < batch_size { + break; + } + } + + Ok(total_transferred) +} diff --git a/crates/dbx-core/src/types.rs b/crates/dbx-core/src/types.rs new file mode 100644 index 0000000000..aedd248fb1 --- /dev/null +++ b/crates/dbx-core/src/types.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DatabaseInfo { + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TableInfo { + pub name: String, + pub table_type: String, // "TABLE" or "VIEW" +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColumnInfo { + pub name: String, + pub data_type: String, + pub is_nullable: bool, + pub column_default: Option, + pub is_primary_key: bool, + pub extra: Option, + pub comment: Option, + pub numeric_precision: Option, + pub numeric_scale: Option, + pub character_maximum_length: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryResult { + pub columns: Vec, + pub rows: Vec>, + pub affected_rows: u64, + pub execution_time_ms: u128, + #[serde(default)] + pub truncated: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexInfo { + pub name: String, + pub columns: Vec, + pub is_unique: bool, + pub is_primary: bool, + pub filter: Option, + pub index_type: Option, + pub included_columns: Option>, + pub comment: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ForeignKeyInfo { + pub name: String, + pub column: String, + pub ref_table: String, + pub ref_column: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TriggerInfo { + pub name: String, + pub event: String, + pub timing: String, +} diff --git a/crates/dbx-core/src/update.rs b/crates/dbx-core/src/update.rs new file mode 100644 index 0000000000..b1e704d722 --- /dev/null +++ b/crates/dbx-core/src/update.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; + +pub const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/t8y2/dbx/releases/latest"; + +#[derive(Debug, Deserialize)] +pub struct GithubRelease { + pub tag_name: String, + pub name: Option, + pub html_url: String, + pub body: Option, +} + +#[derive(Debug, Serialize)] +pub struct UpdateInfo { + pub current_version: String, + pub latest_version: String, + pub update_available: bool, + pub release_name: String, + pub release_url: String, + pub release_notes: String, +} + +pub async fn fetch_latest_release() -> Result { + let client = reqwest::Client::new(); + client + .get(LATEST_RELEASE_URL) + .header(reqwest::header::USER_AGENT, "dbx-update-checker") + .send() + .await + .map_err(|e| format!("Failed to check updates: {e}"))? + .error_for_status() + .map_err(|e| format!("Failed to check updates: {e}"))? + .json::() + .await + .map_err(|e| format!("Failed to parse update response: {e}")) +} + +pub fn build_update_info(release: GithubRelease, current_version: &str) -> UpdateInfo { + let latest_version = normalize_version(&release.tag_name); + + UpdateInfo { + update_available: is_newer_version(&latest_version, current_version), + current_version: current_version.to_string(), + latest_version, + release_name: release.name.unwrap_or_else(|| release.tag_name.clone()), + release_url: release.html_url, + release_notes: release.body.unwrap_or_default(), + } +} + +pub fn normalize_version(version: &str) -> String { + version.trim().trim_start_matches('v').to_string() +} + +pub fn parse_version(version: &str) -> Vec { + normalize_version(version) + .split(['.', '-', '+']) + .map(|part| part.parse::().unwrap_or(0)) + .collect() +} + +pub fn is_newer_version(latest: &str, current: &str) -> bool { + let latest_parts = parse_version(latest); + let current_parts = parse_version(current); + let max_len = latest_parts.len().max(current_parts.len()); + + for i in 0..max_len { + let latest_part = *latest_parts.get(i).unwrap_or(&0); + let current_part = *current_parts.get(i).unwrap_or(&0); + if latest_part > current_part { + return true; + } + if latest_part < current_part { + return false; + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::{is_newer_version, normalize_version}; + + #[test] + fn normalizes_tag_versions() { + assert_eq!(normalize_version("v1.2.3"), "1.2.3"); + assert_eq!(normalize_version(" 0.2.0 "), "0.2.0"); + } + + #[test] + fn compares_semver_like_versions() { + assert!(is_newer_version("0.2.1", "0.2.0")); + assert!(is_newer_version("1.0.0", "0.9.9")); + assert!(!is_newer_version("0.2.0", "0.2.0")); + assert!(!is_newer_version("0.1.9", "0.2.0")); + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 480425d27d..36d4119de1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,13 +15,6 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2.5.6", features = [] } -[profile.release] -panic = "abort" -strip = true -lto = true -codegen-units = 1 -opt-level = "s" - [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -54,3 +47,4 @@ russh = "0.60" keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] } csv = "1.4.0" calamine = "0.30.1" +dbx-core = { path = "../crates/dbx-core" } diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index d7283edb00..c0ce529789 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -1,64 +1,6 @@ -use futures::StreamExt; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock}; use tauri::{AppHandle, Emitter, Manager}; -use tokio::sync::RwLock; -static AI_STREAMS: LazyLock>>> = - LazyLock::new(|| RwLock::new(HashMap::new())); - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum AiProvider { - Claude, - Openai, - Custom, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum AiApiStyle { - Completions, - Responses, -} - -impl Default for AiApiStyle { - fn default() -> Self { - Self::Completions - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AiConfig { - pub provider: AiProvider, - pub api_key: String, - pub endpoint: String, - pub model: String, - #[serde(default)] - pub api_style: AiApiStyle, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AiMessage { - pub role: String, - pub content: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AiCompletionRequest { - pub config: AiConfig, - pub system_prompt: String, - pub messages: Vec, - pub max_tokens: Option, - pub temperature: Option, -} +pub use dbx_core::ai::*; fn ai_config_file(app: &AppHandle) -> Result { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; @@ -66,674 +8,68 @@ fn ai_config_file(app: &AppHandle) -> Result { Ok(dir.join("ai_config.json")) } -#[tauri::command] -pub async fn ai_test_connection(config: AiConfig) -> Result { - if config.api_key.trim().is_empty() { - return Err("API key is required".to_string()); - } - if config.endpoint.trim().is_empty() { - return Err("Endpoint is required".to_string()); - } - if config.model.trim().is_empty() { - return Err("Model is required".to_string()); - } - - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(15)) - .build() - .map_err(|e| e.to_string())?; - - let request = AiCompletionRequest { - config, - system_prompt: String::new(), - messages: vec![AiMessage { role: "user".into(), content: "hi".into() }], - max_tokens: Some(1), - temperature: Some(0.0), - }; - - match request.config.provider { - AiProvider::Claude => call_claude(&client, request).await, - AiProvider::Openai | AiProvider::Custom => { - if request.config.api_style == AiApiStyle::Responses { - call_responses_api(&client, request).await - } else { - call_openai_compatible(&client, request).await - } - } - }.map(|_| "OK".to_string()) -} - -#[tauri::command] -pub async fn save_ai_config(app: AppHandle, config: AiConfig) -> Result<(), String> { - let json = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?; - std::fs::write(ai_config_file(&app)?, json).map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn load_ai_config(app: AppHandle) -> Result, String> { - let path = ai_config_file(&app)?; - if !path.exists() { - return Ok(None); - } - let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - serde_json::from_str(&json).map(Some).map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn ai_complete(request: AiCompletionRequest) -> Result { - if request.config.api_key.trim().is_empty() { - return Err("API key is required".to_string()); - } - if request.config.endpoint.trim().is_empty() { - return Err("Endpoint is required".to_string()); - } - if request.config.model.trim().is_empty() { - return Err("Model is required".to_string()); - } - - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(60)) - .build() - .map_err(|e| e.to_string())?; - - match request.config.provider { - AiProvider::Claude => call_claude(&client, request).await, - AiProvider::Openai | AiProvider::Custom => { - if request.config.api_style == AiApiStyle::Responses { - call_responses_api(&client, request).await - } else { - call_openai_compatible(&client, request).await - } - } - } -} - -#[derive(Debug, Clone, Serialize)] -struct AiStreamChunk { - session_id: String, - delta: String, - #[serde(skip_serializing_if = "Option::is_none")] - reasoning_delta: Option, - done: bool, -} - -#[tauri::command] -pub async fn ai_stream(app: AppHandle, session_id: String, request: AiCompletionRequest) -> Result<(), String> { - if request.config.api_key.trim().is_empty() { - return Err("API key is required".to_string()); - } - if request.config.endpoint.trim().is_empty() { - return Err("Endpoint is required".to_string()); - } - if request.config.model.trim().is_empty() { - return Err("Model is required".to_string()); - } - - let cancelled = Arc::new(AtomicBool::new(false)); - AI_STREAMS.write().await.insert(session_id.clone(), cancelled.clone()); - - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .build() - .map_err(|e| e.to_string())?; - - let result = match request.config.provider { - AiProvider::Claude => stream_claude(&app, &client, &session_id, request, &cancelled).await, - AiProvider::Openai | AiProvider::Custom => { - if request.config.api_style == AiApiStyle::Responses { - stream_responses_api(&app, &client, &session_id, request, &cancelled).await - } else { - stream_openai(&app, &client, &session_id, request, &cancelled).await - } - } - }; - - AI_STREAMS.write().await.remove(&session_id); - result -} - -#[tauri::command] -pub async fn ai_cancel_stream(session_id: String) -> Result { - if let Some(flag) = AI_STREAMS.read().await.get(&session_id) { - flag.store(true, Ordering::Relaxed); - Ok(true) - } else { - Ok(false) - } -} - -async fn stream_claude(app: &AppHandle, client: &reqwest::Client, session_id: &str, request: AiCompletionRequest, cancelled: &AtomicBool) -> Result<(), String> { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - "x-api-key", - HeaderValue::from_str(&request.config.api_key).map_err(|e| e.to_string())?, - ); - headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01")); - - let body = json!({ - "model": request.config.model, - "max_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - "system": request.system_prompt, - "messages": request.messages, - "stream": true, - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("Claude request failed: {e}"))?; - - if !res.status().is_success() { - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - return Err(extract_error(&data).unwrap_or_else(|| "Claude API error".to_string())); - } - - let mut stream = res.bytes_stream(); - let mut buf = String::new(); - - let mut finished = false; - while let Some(chunk) = stream.next().await { - if cancelled.load(Ordering::Relaxed) { - break; - } - let chunk = chunk.map_err(|e| e.to_string())?; - buf.push_str(&String::from_utf8_lossy(&chunk)); - - while let Some(pos) = buf.find('\n') { - let line = buf[..pos].to_string(); - buf = buf[pos + 1..].to_string(); - - let Some(data) = stream_data_payload(&line) else { - continue; - }; - if data == "[DONE]" { - finished = true; - break; - } - - if let Ok(event) = serde_json::from_str::(data) { - if let Some(text) = claude_stream_text(&event) { - emit_stream_delta(app, session_id, text); - } - } - } - - if finished { - break; - } - } - - let _ = app.emit("ai-stream-chunk", AiStreamChunk { - session_id: session_id.to_string(), - delta: String::new(), - reasoning_delta: None, - done: true, - }); - - Ok(()) -} - -async fn stream_openai(app: &AppHandle, client: &reqwest::Client, session_id: &str, request: AiCompletionRequest, cancelled: &AtomicBool) -> Result<(), String> { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)).map_err(|e| e.to_string())?, - ); - - let mut messages = vec![json!({ "role": "system", "content": request.system_prompt })]; - messages.extend( - request.messages.iter().map(|m| json!({ "role": m.role, "content": m.content })), - ); - - let body = json!({ - "model": request.config.model, - "messages": messages, - "max_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - "stream": true, - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("AI request failed: {e}"))?; - - if !res.status().is_success() { - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - return Err(extract_error(&data).unwrap_or_else(|| "API error".to_string())); - } - - let mut stream = res.bytes_stream(); - let mut buf = String::new(); - - let mut finished = false; - while let Some(chunk) = stream.next().await { - if cancelled.load(Ordering::Relaxed) { - break; - } - let chunk = chunk.map_err(|e| e.to_string())?; - buf.push_str(&String::from_utf8_lossy(&chunk)); - - while let Some(pos) = buf.find('\n') { - let line = buf[..pos].to_string(); - buf = buf[pos + 1..].to_string(); - - let Some(data) = stream_data_payload(&line) else { - continue; - }; - if data == "[DONE]" { - finished = true; - break; - } - - if let Ok(event) = serde_json::from_str::(data) { - if let Some(reasoning) = openai_stream_reasoning(&event) { - emit_stream_reasoning(app, session_id, reasoning); - } - if let Some(text) = openai_stream_text(&event) { - emit_stream_delta(app, session_id, text); - } - } - } - - if finished { - break; - } - } - - let _ = app.emit("ai-stream-chunk", AiStreamChunk { - session_id: session_id.to_string(), - delta: String::new(), - reasoning_delta: None, - done: true, - }); - - Ok(()) -} - -async fn call_claude(client: &reqwest::Client, request: AiCompletionRequest) -> Result { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - "x-api-key", - HeaderValue::from_str(&request.config.api_key).map_err(|e| e.to_string())?, - ); - headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01")); - - let body = json!({ - "model": request.config.model, - "max_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - "system": request.system_prompt, - "messages": request.messages, - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("Claude request failed: {e}"))?; - - let status = res.status(); - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - if !status.is_success() { - return Err(extract_error(&data).unwrap_or_else(|| format!("Claude API error: {status}"))); - } - - Ok(data["content"] - .as_array() - .and_then(|items| items.iter().find_map(|item| item["text"].as_str())) - .unwrap_or_default() - .to_string()) -} - -async fn call_openai_compatible( - client: &reqwest::Client, - request: AiCompletionRequest, -) -> Result { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)).map_err(|e| e.to_string())?, - ); - - let mut messages = vec![json!({ "role": "system", "content": request.system_prompt })]; - messages.extend( - request - .messages - .iter() - .map(|message| json!({ "role": message.role, "content": message.content })), - ); - - let body = json!({ - "model": request.config.model, - "messages": messages, - "max_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("AI request failed: {e}"))?; - - let status = res.status(); - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - if !status.is_success() { - return Err(extract_error(&data).unwrap_or_else(|| format!("API error: {status}"))); - } - - Ok(data["choices"][0]["message"]["content"] - .as_str() - .unwrap_or_default() - .to_string()) -} - -fn extract_error(data: &serde_json::Value) -> Option { - data["error"]["message"] - .as_str() - .or_else(|| data["error"].as_str()) - .map(ToString::to_string) -} - -fn resolve_endpoint(config: &AiConfig) -> String { - let ep = config.endpoint.trim().trim_end_matches('/'); - if ep.ends_with("/chat/completions") || ep.ends_with("/responses") || ep.ends_with("/messages") { - return ep.to_string(); - } - match config.provider { - AiProvider::Claude => format!("{ep}/messages"), - AiProvider::Openai | AiProvider::Custom => { - if config.api_style == AiApiStyle::Responses { - format!("{ep}/responses") - } else { - format!("{ep}/chat/completions") - } - } - } -} - -fn stream_data_payload(line: &str) -> Option<&str> { - let line = line.trim(); - if line.is_empty() || line.starts_with(':') || line.starts_with("event:") || line.starts_with("id:") { - return None; - } - if let Some(data) = line.strip_prefix("data:") { - return Some(data.trim_start()); - } - if line.starts_with('{') { - return Some(line); - } - None -} - -fn claude_stream_text(event: &serde_json::Value) -> Option<&str> { - if event["type"] == "content_block_delta" { - return event["delta"]["text"].as_str(); - } - None -} - -fn openai_stream_text(event: &serde_json::Value) -> Option<&str> { - event["choices"] - .get(0) - .and_then(|choice| { - choice["delta"]["content"] - .as_str() - .or_else(|| choice["message"]["content"].as_str()) - }) - .or_else(|| event["content"].as_str()) - .filter(|text| !text.is_empty()) -} - -fn openai_stream_reasoning(event: &serde_json::Value) -> Option<&str> { - event["choices"] - .get(0) - .and_then(|choice| choice["delta"]["reasoning_content"].as_str()) - .filter(|text| !text.is_empty()) -} - -fn emit_stream_delta(app: &AppHandle, session_id: &str, delta: &str) { - let _ = app.emit("ai-stream-chunk", AiStreamChunk { - session_id: session_id.to_string(), - delta: delta.to_string(), - reasoning_delta: None, - done: false, - }); -} - -fn emit_stream_reasoning(app: &AppHandle, session_id: &str, reasoning: &str) { - let _ = app.emit("ai-stream-chunk", AiStreamChunk { - session_id: session_id.to_string(), - delta: String::new(), - reasoning_delta: Some(reasoning.to_string()), - done: false, - }); -} - -fn build_responses_input(system_prompt: &str, messages: &[AiMessage]) -> serde_json::Value { - let mut input = Vec::new(); - if !system_prompt.is_empty() { - input.push(json!({ - "role": "developer", - "content": system_prompt, - })); - } - for m in messages { - input.push(json!({ - "role": m.role, - "content": m.content, - })); - } - json!(input) -} - -async fn call_responses_api( - client: &reqwest::Client, - request: AiCompletionRequest, -) -> Result { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)).map_err(|e| e.to_string())?, - ); - - let body = json!({ - "model": request.config.model, - "input": build_responses_input(&request.system_prompt, &request.messages), - "max_output_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("AI request failed: {e}"))?; - - let status = res.status(); - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - if !status.is_success() { - return Err(extract_error(&data).unwrap_or_else(|| format!("API error: {status}"))); - } - - Ok(data["output"] - .as_array() - .and_then(|items| { - items.iter().find_map(|item| { - item["content"] - .as_array() - .and_then(|parts| parts.iter().find_map(|p| p["text"].as_str())) - }) - }) - .unwrap_or_default() - .to_string()) -} - -async fn stream_responses_api(app: &AppHandle, client: &reqwest::Client, session_id: &str, request: AiCompletionRequest, cancelled: &AtomicBool) -> Result<(), String> { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", request.config.api_key)).map_err(|e| e.to_string())?, - ); - - let body = json!({ - "model": request.config.model, - "input": build_responses_input(&request.system_prompt, &request.messages), - "max_output_tokens": request.max_tokens.unwrap_or(2048), - "temperature": request.temperature.unwrap_or(0.2), - "stream": true, - }); - - let res = client - .post(&resolve_endpoint(&request.config)) - .headers(headers) - .json(&body) - .send() - .await - .map_err(|e| format!("AI request failed: {e}"))?; - - if !res.status().is_success() { - let data: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - return Err(extract_error(&data).unwrap_or_else(|| "API error".to_string())); - } - - let mut stream = res.bytes_stream(); - let mut buf = String::new(); - - let mut finished = false; - while let Some(chunk) = stream.next().await { - if cancelled.load(Ordering::Relaxed) { - break; - } - let chunk = chunk.map_err(|e| e.to_string())?; - buf.push_str(&String::from_utf8_lossy(&chunk)); - - while let Some(pos) = buf.find('\n') { - let line = buf[..pos].to_string(); - buf = buf[pos + 1..].to_string(); - - let Some(data) = stream_data_payload(&line) else { - continue; - }; - if data == "[DONE]" { - finished = true; - break; - } - - if let Ok(event) = serde_json::from_str::(data) { - if let Some(text) = responses_stream_text(&event) { - emit_stream_delta(app, session_id, text); - } - } - } - - if finished { - break; - } - } - - let _ = app.emit("ai-stream-chunk", AiStreamChunk { - session_id: session_id.to_string(), - delta: String::new(), - reasoning_delta: None, - done: true, - }); - - Ok(()) -} - -fn responses_stream_text(event: &serde_json::Value) -> Option<&str> { - event["delta"].as_str().filter(|s| !s.is_empty()) -} - -// --- AI Conversation Persistence --- - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AiChatMessage { - pub role: String, - pub content: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AiConversation { - pub id: String, - pub title: String, - pub connection_name: String, - pub database: String, - pub messages: Vec, - pub created_at: String, - pub updated_at: String, -} - fn conversations_file(app: &AppHandle) -> Result { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; Ok(dir.join("ai_conversations.json")) } -fn read_conversations(app: &AppHandle) -> Result, String> { - let path = conversations_file(app)?; - if !path.exists() { - return Ok(vec![]); - } - let json = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; - serde_json::from_str(&json).map_err(|e| e.to_string()) +#[tauri::command] +pub async fn ai_test_connection(config: AiConfig) -> Result { + dbx_core::ai::test_connection_core(&config).await } -fn write_conversations(app: &AppHandle, conversations: &[AiConversation]) -> Result<(), String> { - let path = conversations_file(app)?; - let json = serde_json::to_string(conversations).map_err(|e| e.to_string())?; - std::fs::write(path, json).map_err(|e| e.to_string()) -} - -const MAX_CONVERSATIONS: usize = 50; - #[tauri::command] -pub async fn save_ai_conversation(app: AppHandle, conversation: AiConversation) -> Result<(), String> { - let mut conversations = read_conversations(&app)?; - if let Some(pos) = conversations.iter().position(|c| c.id == conversation.id) { - conversations[pos] = conversation; - } else { - conversations.insert(0, conversation); - conversations.truncate(MAX_CONVERSATIONS); - } - write_conversations(&app, &conversations) +pub async fn save_ai_config(app: AppHandle, config: AiConfig) -> Result<(), String> { + dbx_core::ai::save_config(&ai_config_file(&app)?, &config) +} + +#[tauri::command] +pub async fn load_ai_config(app: AppHandle) -> Result, String> { + dbx_core::ai::load_config(&ai_config_file(&app)?) +} + +#[tauri::command] +pub async fn ai_complete(request: AiCompletionRequest) -> Result { + dbx_core::ai::complete(&request).await +} + +#[tauri::command] +pub async fn ai_stream( + app: AppHandle, + session_id: String, + request: AiCompletionRequest, +) -> Result<(), String> { + let cancelled = dbx_core::ai::register_stream(&session_id).await; + + let result = dbx_core::ai::stream(&session_id, &request, &cancelled, |chunk| { + let _ = app.emit("ai-stream-chunk", &chunk); + }) + .await; + + dbx_core::ai::unregister_stream(&session_id).await; + result +} + +#[tauri::command] +pub async fn ai_cancel_stream(session_id: String) -> Result { + Ok(dbx_core::ai::cancel_stream(&session_id).await) +} + +#[tauri::command] +pub async fn save_ai_conversation( + app: AppHandle, + conversation: AiConversation, +) -> Result<(), String> { + dbx_core::ai::save_conversation(&conversations_file(&app)?, conversation) } #[tauri::command] pub async fn load_ai_conversations(app: AppHandle) -> Result, String> { - read_conversations(&app) + dbx_core::ai::load_conversations(&conversations_file(&app)?) } #[tauri::command] pub async fn delete_ai_conversation(app: AppHandle, id: String) -> Result<(), String> { - let conversations: Vec = read_conversations(&app)? - .into_iter() - .filter(|c| c.id != id) - .collect(); - write_conversations(&app, &conversations) + dbx_core::ai::delete_conversation(&conversations_file(&app)?, &id) } diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 54aaf3ceb6..d3bb73b40e 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -1,218 +1,16 @@ -use std::collections::HashMap; use std::sync::Arc; use tauri::{AppHandle, Manager, State}; -use tokio::sync::Mutex; use crate::commands::connection_secrets::{ create_secret_store, load_connections_from_file, save_connections_to_file, }; -use crate::commands::query_cancel::RunningQueries; -use crate::db; -use crate::db::ssh_tunnel::TunnelManager; -use crate::models::connection::{ConnectionConfig, DatabaseType}; -fn expand_tilde(path: &str) -> String { - if path == "~" || path.starts_with("~/") { - if let Ok(home) = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) { - return format!("{}{}", home, &path[1..]); - } - } - path.to_string() -} - -pub enum PoolKind { - Mysql(sqlx::mysql::MySqlPool, bool), - Postgres(sqlx::postgres::PgPool), - Sqlite(sqlx::sqlite::SqlitePool), - Redis(tokio::sync::Mutex), - DuckDb(std::sync::Arc>), - MongoDb(mongodb::Client), - ClickHouse(db::clickhouse_driver::ChClient), - SqlServer(std::sync::Arc>), - Oracle(std::sync::Arc>), - Elasticsearch(db::elasticsearch_driver::EsClient), -} - -pub struct AppState { - pub connections: Mutex>, - pub configs: Mutex>, - pub running_queries: RunningQueries, - pub tunnels: TunnelManager, -} - -impl AppState { - pub fn new() -> Self { - Self { - connections: Mutex::new(HashMap::new()), - configs: Mutex::new(HashMap::new()), - running_queries: RunningQueries::default(), - tunnels: TunnelManager::new(), - } - } - - pub async fn get_or_create_pool( - &self, - connection_id: &str, - database: Option<&str>, - ) -> Result { - let db_type = { - let configs = self.configs.lock().await; - configs.get(connection_id).map(|c| c.db_type.clone()) - }; - - let is_embedded = matches!(db_type, Some(DatabaseType::Sqlite) | Some(DatabaseType::DuckDb)); - if is_embedded { - return Ok(connection_id.to_string()); - } - - let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle)); - let pool_key = if is_single_conn { - connection_id.to_string() - } else { - match database { - Some(db) => format!("{connection_id}:{db}"), - None => connection_id.to_string(), - } - }; - - let conns = self.connections.lock().await; - if conns.contains_key(&pool_key) { - return Ok(pool_key); - } - drop(conns); - - let configs = self.configs.lock().await; - let config = configs - .get(connection_id) - .ok_or("Connection config not found")? - .clone(); - drop(configs); - - let mut db_config = config.clone(); - if let Some(db) = database { - if db_config.db_type != DatabaseType::Oracle { - db_config.database = Some(db.to_string()); - } - } - - let (host, port) = self.connection_host_port(connection_id, &db_config).await?; - let url = connection_url_for_endpoint(&db_config, &host, port); - let pool = match db_config.db_type { - DatabaseType::Mysql if db_config.needs_bare_mysql() => PoolKind::Mysql(db::mysql::connect_bare(&url).await?, true), - DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?, false), - DatabaseType::Doris | DatabaseType::StarRocks => PoolKind::Mysql(db::mysql::connect_bare(&url).await?, true), - DatabaseType::Postgres | DatabaseType::Redshift => PoolKind::Postgres(db::postgres::connect(&url).await?), - DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect_path(&expand_tilde(&db_config.host)).await?), - DatabaseType::Redis => { - let con = db::redis_driver::connect(&url).await?; - PoolKind::Redis(tokio::sync::Mutex::new(con)) - } - DatabaseType::DuckDb => { - let con = duckdb::Connection::open(&expand_tilde(&db_config.host)).map_err(|e| e.to_string())?; - PoolKind::DuckDb(std::sync::Arc::new(std::sync::Mutex::new(con))) - } - DatabaseType::MongoDb => { - let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?; - PoolKind::MongoDb(client) - } - DatabaseType::ClickHouse => { - let username = if db_config.username.is_empty() { None } else { Some(db_config.username.clone()) }; - let password = if db_config.password.is_empty() { None } else { Some(db_config.password.clone()) }; - let client = db::clickhouse_driver::ChClient::new(&url, username, password); - db::clickhouse_driver::test_connection(&client).await?; - PoolKind::ClickHouse(client) - } - DatabaseType::SqlServer => { - let client = db::sqlserver::connect( - &host, - port, - &db_config.username, - &db_config.password, - db_config.database.as_deref(), - ) - .await?; - PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client))) - } - DatabaseType::Oracle => { - let client = db::oracle_driver::connect( - &host, - port, - db_config.database.as_deref().unwrap_or("ORCL"), - &db_config.username, &db_config.password, - ) - .await?; - PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(client))) - } - DatabaseType::Elasticsearch => { - let client = db::elasticsearch_driver::EsClient::new( - &url, - Some(&db_config.username), - Some(&db_config.password), - ); - db::elasticsearch_driver::test_connection(&client).await?; - PoolKind::Elasticsearch(client) - } - }; - - self.connections.lock().await.insert(pool_key.clone(), pool); - Ok(pool_key) - } - - async fn connection_host_port( - &self, - connection_id: &str, - config: &ConnectionConfig, - ) -> Result<(String, u16), String> { - if !config.ssh_enabled || config.ssh_host.is_empty() { - return Ok((config.host.clone(), config.port)); - } - - if let Some(local_port) = self.tunnels.local_port(connection_id).await { - return Ok(("127.0.0.1".to_string(), local_port)); - } - - let local_port = self - .tunnels - .start_tunnel( - connection_id, - &config.ssh_host, - config.ssh_port, - &config.ssh_user, - &config.ssh_password, - &config.ssh_key_path, - &config.ssh_key_passphrase, - &config.host, - config.port, - config.ssh_expose_lan, - ) - .await?; - - Ok(("127.0.0.1".to_string(), local_port)) - } - - pub async fn reconnect_pool( - &self, - connection_id: &str, - database: Option<&str>, - ) -> Result { - let is_single_conn = { - let configs = self.configs.lock().await; - configs.get(connection_id) - .map(|c| c.db_type == DatabaseType::Oracle || c.db_type == DatabaseType::Elasticsearch) - .unwrap_or(false) - }; - let pool_key = if is_single_conn { - connection_id.to_string() - } else { - match database { - Some(db) => format!("{connection_id}:{db}"), - None => connection_id.to_string(), - } - }; - self.connections.lock().await.remove(&pool_key); - self.get_or_create_pool(connection_id, database).await - } -} +pub use dbx_core::connection::{ + connection_url_for_endpoint, expand_tilde, redacted_connection_url_for_endpoint, AppState, + PoolKind, +}; +use dbx_core::db; +use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; fn connections_file(app: &AppHandle) -> Result { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; @@ -220,24 +18,10 @@ fn connections_file(app: &AppHandle) -> Result { Ok(dir.join("connections.json")) } -fn connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> String { - if host == config.host && port == config.port { - config.connection_url() - } else { - config.connection_url_with_host(host, port) - } -} - -fn redacted_connection_url_for_endpoint( - config: &ConnectionConfig, - host: &str, - port: u16, -) -> String { - if host == config.host && port == config.port { - config.redacted_connection_url() - } else { - config.redacted_connection_url_with_host(host, port) - } +fn sidebar_layout_file(app: &AppHandle) -> Result { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + Ok(dir.join("sidebar_layout.json")) } #[tauri::command] @@ -257,12 +41,6 @@ pub async fn load_connections(app: AppHandle) -> Result, S load_connections_from_file(&path, &*store) } -fn sidebar_layout_file(app: &AppHandle) -> Result { - let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; - std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - Ok(dir.join("sidebar_layout.json")) -} - #[tauri::command] pub async fn save_sidebar_layout( app: AppHandle, diff --git a/src-tauri/src/commands/connection_secrets.rs b/src-tauri/src/commands/connection_secrets.rs index 2b2154ef84..409d1048dd 100644 --- a/src-tauri/src/commands/connection_secrets.rs +++ b/src-tauri/src/commands/connection_secrets.rs @@ -1,22 +1,14 @@ -use crate::models::connection::ConnectionConfig; -use std::collections::HashMap; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager}; -pub(super) const MAIN_PASSWORD_KEY: &str = "password"; -pub(super) const SSH_PASSWORD_KEY: &str = "ssh_password"; -pub(super) const SSH_KEY_PASSPHRASE_KEY: &str = "ssh_key_passphrase"; -pub(super) const CONNECTION_STRING_KEY: &str = "connection_string"; +// Re-export core types and functions +pub(super) use dbx_core::connection_secrets::{ + save_connections_to_file, load_connections_from_file, + ConnectionSecretStore, FileSecretStore, + secret_account, +}; const KEYRING_SERVICE: &str = "dev.dbx.connections"; -pub(super) trait ConnectionSecretStore { - fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String>; - fn get_secret(&self, connection_id: &str, key: &str) -> Result, String>; - fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String>; -} - pub(super) struct KeyringConnectionSecretStore; impl ConnectionSecretStore for KeyringConnectionSecretStore { @@ -45,46 +37,6 @@ impl ConnectionSecretStore for KeyringConnectionSecretStore { } } -pub(super) struct FileSecretStore { - path: PathBuf, -} - -impl FileSecretStore { - fn new(path: PathBuf) -> Self { - Self { path } - } - - fn read_store(&self) -> HashMap { - std::fs::read_to_string(&self.path) - .ok() - .and_then(|json| serde_json::from_str(&json).ok()) - .unwrap_or_default() - } - - fn write_store(&self, map: &HashMap) -> Result<(), String> { - let json = serde_json::to_string_pretty(map).map_err(|e| e.to_string())?; - std::fs::write(&self.path, json).map_err(|e| e.to_string()) - } -} - -impl ConnectionSecretStore for FileSecretStore { - fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> { - let mut map = self.read_store(); - map.insert(secret_account(connection_id, key), secret.to_string()); - self.write_store(&map) - } - - fn get_secret(&self, connection_id: &str, key: &str) -> Result, String> { - Ok(self.read_store().get(&secret_account(connection_id, key)).cloned()) - } - - fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> { - let mut map = self.read_store(); - map.remove(&secret_account(connection_id, key)); - self.write_store(&map) - } -} - pub(super) fn create_secret_store(app: &AppHandle) -> Box { if cfg!(debug_assertions) { let dir = app.path().app_data_dir().expect("failed to resolve app data dir"); @@ -95,391 +47,11 @@ pub(super) fn create_secret_store(app: &AppHandle) -> Box Result<(), String> { - delete_removed_connection_secrets(path, configs, store)?; - for config in configs { - persist_secret(store, &config.id, MAIN_PASSWORD_KEY, &config.password)?; - persist_secret(store, &config.id, SSH_PASSWORD_KEY, &config.ssh_password)?; - persist_secret(store, &config.id, SSH_KEY_PASSPHRASE_KEY, &config.ssh_key_passphrase)?; - persist_optional_secret( - store, - &config.id, - CONNECTION_STRING_KEY, - config.connection_string.as_deref(), - )?; - } - - write_sanitized_connections(path, configs) -} - -pub(super) fn load_connections_from_file( - path: &Path, - store: &dyn ConnectionSecretStore, -) -> Result, String> { - if !path.exists() { - return Ok(vec![]); - } - - let mut configs = read_connections(path)?; - let mut needs_rewrite = false; - for config in &mut configs { - if config.password.is_empty() { - if let Some(secret) = store.get_secret(&config.id, MAIN_PASSWORD_KEY)? { - config.password = secret; - } - } else { - store.set_secret(&config.id, MAIN_PASSWORD_KEY, &config.password)?; - needs_rewrite = true; - } - - if config.ssh_password.is_empty() { - if let Some(secret) = store.get_secret(&config.id, SSH_PASSWORD_KEY)? { - config.ssh_password = secret; - } - } else { - store.set_secret(&config.id, SSH_PASSWORD_KEY, &config.ssh_password)?; - needs_rewrite = true; - } - - if config.ssh_key_passphrase.is_empty() { - if let Some(secret) = store.get_secret(&config.id, SSH_KEY_PASSPHRASE_KEY)? { - config.ssh_key_passphrase = secret; - } - } else { - store.set_secret(&config.id, SSH_KEY_PASSPHRASE_KEY, &config.ssh_key_passphrase)?; - needs_rewrite = true; - } - - match config - .connection_string - .as_deref() - .filter(|secret| !secret.is_empty()) - { - Some(secret) => { - store.set_secret(&config.id, CONNECTION_STRING_KEY, secret)?; - needs_rewrite = true; - } - None => { - if let Some(secret) = store.get_secret(&config.id, CONNECTION_STRING_KEY)? { - config.connection_string = Some(secret); - } - } - } - } - - if needs_rewrite { - write_sanitized_connections(path, &configs)?; - } - - Ok(configs) -} - -fn delete_removed_connection_secrets( - path: &Path, - configs: &[ConnectionConfig], - store: &dyn ConnectionSecretStore, -) -> Result<(), String> { - if !path.exists() { - return Ok(()); - } - - let previous = match read_connections(path) { - Ok(configs) => configs, - Err(_) => return Ok(()), - }; - let current_ids: HashSet<&str> = configs.iter().map(|config| config.id.as_str()).collect(); - for config in previous { - if current_ids.contains(config.id.as_str()) { - continue; - } - store.delete_secret(&config.id, MAIN_PASSWORD_KEY)?; - store.delete_secret(&config.id, SSH_PASSWORD_KEY)?; - store.delete_secret(&config.id, SSH_KEY_PASSPHRASE_KEY)?; - store.delete_secret(&config.id, CONNECTION_STRING_KEY)?; - } - Ok(()) -} - -fn persist_secret( - store: &dyn ConnectionSecretStore, - connection_id: &str, - key: &str, - secret: &str, -) -> Result<(), String> { - if secret.is_empty() { - store.delete_secret(connection_id, key) - } else { - store.set_secret(connection_id, key, secret) - } -} - -fn persist_optional_secret( - store: &dyn ConnectionSecretStore, - connection_id: &str, - key: &str, - secret: Option<&str>, -) -> Result<(), String> { - match secret.filter(|secret| !secret.is_empty()) { - Some(secret) => store.set_secret(connection_id, key, secret), - None => store.delete_secret(connection_id, key), - } -} - -fn read_connections(path: &Path) -> Result, String> { - let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - serde_json::from_str(&json).map_err(|e| e.to_string()) -} - -fn write_sanitized_connections(path: &Path, configs: &[ConnectionConfig]) -> Result<(), String> { - let sanitized = sanitize_connections(configs); - let json = serde_json::to_string_pretty(&sanitized).map_err(|e| e.to_string())?; - std::fs::write(path, json).map_err(|e| e.to_string()) -} - -fn sanitize_connections(configs: &[ConnectionConfig]) -> Vec { - configs - .iter() - .cloned() - .map(|mut config| { - config.password.clear(); - config.ssh_password.clear(); - config.ssh_key_passphrase.clear(); - config.connection_string = None; - config - }) - .collect() -} - fn keyring_entry(connection_id: &str, key: &str, action: &str) -> Result { keyring::Entry::new(KEYRING_SERVICE, &secret_account(connection_id, key)) .map_err(|err| keyring_error(action, connection_id, key, err)) } -fn secret_account(connection_id: &str, key: &str) -> String { - format!("connection:{connection_id}:{key}") -} - fn keyring_error(action: &str, connection_id: &str, key: &str, err: keyring::Error) -> String { format!("Failed to {action} saved {key} for connection {connection_id}: {err}") } - -#[cfg(test)] -mod tests { - use super::{ - load_connections_from_file, save_connections_to_file, ConnectionSecretStore, - CONNECTION_STRING_KEY, MAIN_PASSWORD_KEY, SSH_PASSWORD_KEY, - }; - use crate::models::connection::{ConnectionConfig, DatabaseType}; - use std::cell::RefCell; - use std::collections::HashMap; - use std::path::Path; - - #[derive(Default)] - struct MemorySecretStore { - values: RefCell>, - deleted: RefCell>, - } - - impl MemorySecretStore { - fn set_existing(&self, connection_id: &str, key: &str, value: &str) { - self.values - .borrow_mut() - .insert(secret_key(connection_id, key), value.to_string()); - } - - fn get_existing(&self, connection_id: &str, key: &str) -> Option { - self.values - .borrow() - .get(&secret_key(connection_id, key)) - .cloned() - } - - fn was_deleted(&self, connection_id: &str, key: &str) -> bool { - self.deleted - .borrow() - .contains(&secret_key(connection_id, key)) - } - } - - impl ConnectionSecretStore for MemorySecretStore { - fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> { - self.values - .borrow_mut() - .insert(secret_key(connection_id, key), secret.to_string()); - Ok(()) - } - - fn get_secret(&self, connection_id: &str, key: &str) -> Result, String> { - Ok(self - .values - .borrow() - .get(&secret_key(connection_id, key)) - .cloned()) - } - - fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> { - self.values - .borrow_mut() - .remove(&secret_key(connection_id, key)); - self.deleted - .borrow_mut() - .push(secret_key(connection_id, key)); - Ok(()) - } - } - - fn secret_key(connection_id: &str, key: &str) -> String { - format!("{connection_id}:{key}") - } - - fn temp_connections_file(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!( - "dbx-connection-secrets-test-{}-{name}", - std::process::id() - )); - std::fs::create_dir_all(&dir).unwrap(); - dir.join("connections.json") - } - - fn connection(id: &str, password: &str, ssh_password: &str) -> ConnectionConfig { - ConnectionConfig { - id: id.to_string(), - name: format!("{id} connection"), - db_type: DatabaseType::Postgres, - driver_profile: None, - driver_label: None, - url_params: None, - host: "localhost".to_string(), - port: 5432, - username: "postgres".to_string(), - password: password.to_string(), - database: Some("postgres".to_string()), - color: None, - ssh_enabled: !ssh_password.is_empty(), - ssh_host: String::new(), - ssh_port: 22, - ssh_user: String::new(), - ssh_password: ssh_password.to_string(), - ssh_key_path: String::new(), - ssh_key_passphrase: String::new(), - ssh_expose_lan: false, - ssl: false, - connection_string: None, - } - } - - fn read_configs(path: &Path) -> Vec { - let json = std::fs::read_to_string(path).unwrap(); - serde_json::from_str(&json).unwrap() - } - - #[test] - fn save_connections_moves_passwords_to_secret_store_and_redacts_file() { - let path = temp_connections_file("save-redacts"); - let store = MemorySecretStore::default(); - let configs = vec![connection("main", "db-secret", "ssh-secret")]; - - save_connections_to_file(&path, &configs, &store).unwrap(); - - assert_eq!( - store.get_existing("main", MAIN_PASSWORD_KEY).as_deref(), - Some("db-secret") - ); - assert_eq!( - store.get_existing("main", SSH_PASSWORD_KEY).as_deref(), - Some("ssh-secret") - ); - let persisted = read_configs(&path); - assert_eq!(persisted[0].password, ""); - assert_eq!(persisted[0].ssh_password, ""); - } - - #[test] - fn load_connections_restores_passwords_from_secret_store() { - let path = temp_connections_file("load-restores"); - let store = MemorySecretStore::default(); - store.set_existing("main", MAIN_PASSWORD_KEY, "db-secret"); - store.set_existing("main", SSH_PASSWORD_KEY, "ssh-secret"); - let sanitized = vec![connection("main", "", "")]; - std::fs::write(&path, serde_json::to_string_pretty(&sanitized).unwrap()).unwrap(); - - let loaded = load_connections_from_file(&path, &store).unwrap(); - - assert_eq!(loaded[0].password, "db-secret"); - assert_eq!(loaded[0].ssh_password, "ssh-secret"); - } - - #[test] - fn load_connections_migrates_plaintext_passwords_and_rewrites_sanitized_file() { - let path = temp_connections_file("migrates-plaintext"); - let store = MemorySecretStore::default(); - let legacy = vec![connection("legacy", "plain-db", "plain-ssh")]; - std::fs::write(&path, serde_json::to_string_pretty(&legacy).unwrap()).unwrap(); - - let loaded = load_connections_from_file(&path, &store).unwrap(); - - assert_eq!(loaded[0].password, "plain-db"); - assert_eq!(loaded[0].ssh_password, "plain-ssh"); - assert_eq!( - store.get_existing("legacy", MAIN_PASSWORD_KEY).as_deref(), - Some("plain-db") - ); - assert_eq!( - store.get_existing("legacy", SSH_PASSWORD_KEY).as_deref(), - Some("plain-ssh") - ); - let persisted = read_configs(&path); - assert_eq!(persisted[0].password, ""); - assert_eq!(persisted[0].ssh_password, ""); - } - - #[test] - fn save_connections_deletes_secrets_for_removed_connections() { - let path = temp_connections_file("deletes-removed"); - let store = MemorySecretStore::default(); - let previous = vec![connection("old", "", ""), connection("kept", "", "")]; - std::fs::write(&path, serde_json::to_string_pretty(&previous).unwrap()).unwrap(); - store.set_existing("old", MAIN_PASSWORD_KEY, "old-db"); - store.set_existing("old", SSH_PASSWORD_KEY, "old-ssh"); - store.set_existing("kept", MAIN_PASSWORD_KEY, "kept-db"); - - save_connections_to_file(&path, &[connection("kept", "new-db", "")], &store).unwrap(); - - assert!(store.was_deleted("old", MAIN_PASSWORD_KEY)); - assert!(store.was_deleted("old", SSH_PASSWORD_KEY)); - assert_eq!( - store.get_existing("kept", MAIN_PASSWORD_KEY).as_deref(), - Some("new-db") - ); - } - - #[test] - fn save_connections_moves_connection_string_to_secret_store_and_restores_it() { - let path = temp_connections_file("connection-string"); - let store = MemorySecretStore::default(); - let mut config = connection("mongo", "", ""); - config.db_type = DatabaseType::MongoDb; - config.connection_string = Some("mongodb://user:secret@localhost/app".to_string()); - - save_connections_to_file(&path, &[config], &store).unwrap(); - - assert_eq!( - store - .get_existing("mongo", CONNECTION_STRING_KEY) - .as_deref(), - Some("mongodb://user:secret@localhost/app") - ); - let persisted = read_configs(&path); - assert_eq!(persisted[0].connection_string, None); - - let loaded = load_connections_from_file(&path, &store).unwrap(); - assert_eq!( - loaded[0].connection_string.as_deref(), - Some("mongodb://user:secret@localhost/app") - ); - } -} diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index b5c60985f9..fbb603e5d8 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -1,17 +1,6 @@ -use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HistoryEntry { - pub id: String, - pub connection_name: String, - pub database: String, - pub sql: String, - pub executed_at: String, - pub execution_time_ms: u128, - pub success: bool, - pub error: Option, -} +pub use dbx_core::history::HistoryEntry; fn history_file(app: &AppHandle) -> Result { let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; @@ -19,29 +8,10 @@ fn history_file(app: &AppHandle) -> Result { Ok(dir.join("query_history.json")) } -fn read_all(app: &AppHandle) -> Result, String> { - let path = history_file(app)?; - if !path.exists() { - return Ok(vec![]); - } - let json = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; - serde_json::from_str(&json).map_err(|e| e.to_string()) -} - -fn write_all(app: &AppHandle, entries: &[HistoryEntry]) -> Result<(), String> { - let path = history_file(app)?; - let json = serde_json::to_string(entries).map_err(|e| e.to_string())?; - std::fs::write(path, json).map_err(|e| e.to_string()) -} - -const MAX_HISTORY: usize = 1000; - #[tauri::command] pub async fn save_history(app: AppHandle, entry: HistoryEntry) -> Result<(), String> { - let mut entries = read_all(&app)?; - entries.insert(0, entry); - entries.truncate(MAX_HISTORY); - write_all(&app, &entries) + let path = history_file(&app)?; + dbx_core::history::save_history_entry(&path, entry) } #[tauri::command] @@ -50,20 +20,18 @@ pub async fn load_history( limit: usize, offset: usize, ) -> Result, String> { - let entries = read_all(&app)?; - Ok(entries.into_iter().skip(offset).take(limit).collect()) + let path = history_file(&app)?; + dbx_core::history::load_history_entries(&path, limit, offset) } #[tauri::command] pub async fn clear_history(app: AppHandle) -> Result<(), String> { - write_all(&app, &[]) + let path = history_file(&app)?; + dbx_core::history::clear_history_entries(&path) } #[tauri::command] pub async fn delete_history_entry(app: AppHandle, id: String) -> Result<(), String> { - let entries: Vec = read_all(&app)? - .into_iter() - .filter(|e| e.id != id) - .collect(); - write_all(&app, &entries) + let path = history_file(&app)?; + dbx_core::history::delete_history_entry_by_id(&path, &id) } diff --git a/src-tauri/src/commands/mongo_cmd.rs b/src-tauri/src/commands/mongo_cmd.rs index 9c1c72cab7..21e721679b 100644 --- a/src-tauri/src/commands/mongo_cmd.rs +++ b/src-tauri/src/commands/mongo_cmd.rs @@ -1,21 +1,15 @@ use std::sync::Arc; use tauri::State; -use crate::commands::connection::{AppState, PoolKind}; -use crate::db::mongo_driver::{self, MongoDocumentResult}; -use crate::db::elasticsearch_driver; +use crate::commands::connection::AppState; +use dbx_core::db::mongo_driver::MongoDocumentResult; #[tauri::command] pub async fn mongo_list_databases( state: State<'_, Arc>, connection_id: String, ) -> Result, String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => mongo_driver::list_databases(client).await, - PoolKind::Elasticsearch(_) => Ok(vec!["default".to_string()]), - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_list_databases_core(&state, &connection_id).await } #[tauri::command] @@ -24,12 +18,7 @@ pub async fn mongo_list_collections( connection_id: String, database: String, ) -> Result, String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => mongo_driver::list_collections(client, &database).await, - PoolKind::Elasticsearch(client) => elasticsearch_driver::list_indices(client).await, - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_list_collections_core(&state, &connection_id, &database).await } #[tauri::command] @@ -41,18 +30,7 @@ pub async fn mongo_find_documents( skip: u64, limit: i64, ) -> Result { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => { - mongo_driver::find_documents(client, &database, &collection, skip, limit).await - } - PoolKind::Elasticsearch(client) => { - let client = client.clone(); - drop(connections); - elasticsearch_driver::find_documents(&client, &collection, skip, limit).await - } - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_find_documents_core(&state, &connection_id, &database, &collection, skip, limit).await } #[tauri::command] @@ -63,18 +41,7 @@ pub async fn mongo_insert_document( collection: String, doc_json: String, ) -> Result { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => { - mongo_driver::insert_document(client, &database, &collection, &doc_json).await - } - PoolKind::Elasticsearch(client) => { - let client = client.clone(); - drop(connections); - elasticsearch_driver::insert_document(&client, &collection, &doc_json).await - } - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_insert_document_core(&state, &connection_id, &database, &collection, &doc_json).await } #[tauri::command] @@ -86,18 +53,7 @@ pub async fn mongo_update_document( id: String, doc_json: String, ) -> Result { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => { - mongo_driver::update_document(client, &database, &collection, &id, &doc_json).await - } - PoolKind::Elasticsearch(client) => { - let client = client.clone(); - drop(connections); - elasticsearch_driver::update_document(&client, &collection, &id, &doc_json).await - } - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_update_document_core(&state, &connection_id, &database, &collection, &id, &doc_json).await } #[tauri::command] @@ -108,16 +64,5 @@ pub async fn mongo_delete_document( collection: String, id: String, ) -> Result { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::MongoDb(client) => { - mongo_driver::delete_document(client, &database, &collection, &id).await - } - PoolKind::Elasticsearch(client) => { - let client = client.clone(); - drop(connections); - elasticsearch_driver::delete_document(&client, &collection, &id).await - } - _ => Err("Not a MongoDB/Elasticsearch connection".to_string()), - } + dbx_core::mongo_ops::mongo_delete_document_core(&state, &connection_id, &database, &collection, &id).await } diff --git a/src-tauri/src/commands/query.rs b/src-tauri/src/commands/query.rs index d988902814..608252bbd3 100644 --- a/src-tauri/src/commands/query.rs +++ b/src-tauri/src/commands/query.rs @@ -1,240 +1,12 @@ -use std::future::Future; use std::sync::Arc; -use std::time::Duration; use tauri::State; -use tokio::time::timeout; -use tokio_util::sync::CancellationToken; -use crate::commands::connection::{AppState, PoolKind}; -use crate::commands::sql_file::split_sql_statements; -use crate::db; +use crate::commands::connection::AppState; +use dbx_core::db; +use dbx_core::sql::split_sql_statements; -const QUERY_TIMEOUT: Duration = Duration::from_secs(30); -const MAX_ROWS: usize = 10000; -const QUERY_CANCELED: &str = "Query canceled"; - -fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result { - let start = std::time::Instant::now(); - let trimmed = sql.trim().to_uppercase(); - - if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") || trimmed.starts_with("DESCRIBE") - || trimmed.starts_with("EXPLAIN") || trimmed.starts_with("WITH") || trimmed.starts_with("PRAGMA") - { - let mut stmt = con.prepare(sql).map_err(|e| e.to_string())?; - let mut rows = stmt.query([]).map_err(|e| e.to_string())?; - let stmt_ref = rows.as_ref().ok_or("DuckDB statement unavailable")?; - let col_count = stmt_ref.column_count(); - let columns: Vec = (0..col_count) - .map(|i| stmt_ref.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "?".to_string())) - .collect(); - - let mut result_rows = Vec::new(); - while let Some(row) = rows.next().map_err(|e| e.to_string())? { - if result_rows.len() >= MAX_ROWS { break; } - let vals: Vec = (0..col_count).map(|i| { - row.get::<_, String>(i) - .map(serde_json::Value::String) - .or_else(|_| row.get::<_, i64>(i).map(|v| serde_json::Value::Number(v.into()))) - .or_else(|_| row.get::<_, f64>(i).map(|v| { - serde_json::Number::from_f64(v) - .map(serde_json::Value::Number) - .unwrap_or(serde_json::Value::Null) - })) - .or_else(|_| row.get::<_, bool>(i).map(serde_json::Value::Bool)) - .unwrap_or(serde_json::Value::Null) - }).collect(); - result_rows.push(vals); - } - - let truncated = result_rows.len() >= MAX_ROWS; - Ok(db::QueryResult { columns, rows: result_rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated }) - } else { - let affected = con.execute(sql, []).map_err(|e| e.to_string())?; - Ok(db::QueryResult { columns: vec![], rows: vec![], affected_rows: affected as u64, execution_time_ms: start.elapsed().as_millis(), truncated: false }) - } -} - -fn truncate_result(mut result: db::QueryResult) -> db::QueryResult { - if result.rows.len() > MAX_ROWS { - result.rows.truncate(MAX_ROWS); - result.truncated = true; - } - result -} - -fn is_connection_error(err: &str) -> bool { - let lower = err.to_lowercase(); - lower.contains("connection") - || lower.contains("broken pipe") - || lower.contains("reset by peer") - || lower.contains("timed out") - || lower.contains("closed") - || lower.contains("eof") -} - -fn timeout_error() -> String { - format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()) -} - -fn canceled_error() -> String { - QUERY_CANCELED.to_string() -} - -fn is_canceled(cancel_token: &Option) -> bool { - cancel_token - .as_ref() - .map(|token| token.is_cancelled()) - .unwrap_or(false) -} - -async fn wait_for_query( - cancel_token: Option, - future: F, -) -> Result -where - F: Future>, -{ - wait_for_query_with_timeout(cancel_token, QUERY_TIMEOUT, future).await -} - -async fn wait_for_query_with_timeout( - cancel_token: Option, - timeout_duration: Duration, - future: F, -) -> Result -where - F: Future>, -{ - if let Some(token) = cancel_token { - tokio::select! { - biased; - _ = token.cancelled() => Err(canceled_error()), - result = timeout(timeout_duration, future) => result.map_err(|_| timeout_error())?, - } - } else { - timeout(timeout_duration, future) - .await - .map_err(|_| timeout_error())? - } -} - -async fn do_execute( - state: &AppState, - pool_key: &str, - sql: &str, - cancel_token: Option, -) -> Result { - let connections = state.connections.lock().await; - let pool = connections.get(pool_key).ok_or("Connection not found")?; - - match pool { - PoolKind::DuckDb(con) => { - let con = con.clone(); - let sql = sql.to_string(); - drop(connections); - wait_for_query(cancel_token, async move { - let task = tokio::task::spawn_blocking(move || { - let con = con.lock().map_err(|e| e.to_string())?; - duckdb_execute(&con, &sql) - }); - task.await.map_err(|e| e.to_string())? - }) - .await - } - PoolKind::Mysql(p, bare) => { - let p = p.clone(); - let bare = *bare; - drop(connections); - wait_for_query(cancel_token, db::mysql::execute_query(&p, sql, bare)) - .await - .map(truncate_result) - } - PoolKind::Postgres(p) => { - let p = p.clone(); - drop(connections); - wait_for_query(cancel_token, db::postgres::execute_query(&p, sql)) - .await - .map(truncate_result) - } - PoolKind::Sqlite(p) => { - let p = p.clone(); - drop(connections); - wait_for_query(cancel_token, db::sqlite::execute_query(&p, sql)) - .await - .map(truncate_result) - } - PoolKind::ClickHouse(client) => { - let client = client.clone(); - let database = pool_key.split(':').nth(1).unwrap_or("default").to_string(); - drop(connections); - wait_for_query(cancel_token, db::clickhouse_driver::execute_query(&client, &database, sql)) - .await - .map(truncate_result) - } - PoolKind::SqlServer(client) => { - let client = client.clone(); - drop(connections); - let mut client = match cancel_token.as_ref() { - Some(token) => tokio::select! { - biased; - _ = token.cancelled() => return Err(canceled_error()), - guard = client.lock() => guard, - }, - None => client.lock().await, - }; - wait_for_query(cancel_token, db::sqlserver::execute_query(&mut client, sql)) - .await - .map(truncate_result) - } - PoolKind::Oracle(client) => { - let client = client.clone(); - drop(connections); - let client = match cancel_token.as_ref() { - Some(token) => tokio::select! { - biased; - _ = token.cancelled() => return Err(canceled_error()), - guard = client.lock() => guard, - }, - None => client.lock().await, - }; - wait_for_query(cancel_token, db::oracle_driver::execute_query(&*client, sql)) - .await - .map(truncate_result) - } - PoolKind::Elasticsearch(_) => Err("Use document browser for Elasticsearch".to_string()), - PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()), - PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()), - } -} - -pub(super) async fn execute_sql_statement( - state: &AppState, - connection_id: &str, - database: &str, - sql: &str, - cancel_token: Option, -) -> Result { - let pool_key = if database.is_empty() { - connection_id.to_string() - } else { - state.get_or_create_pool(connection_id, Some(database)).await? - }; - - if is_canceled(&cancel_token) { - return Err(canceled_error()); - } - - let result = do_execute(state, &pool_key, sql, cancel_token.clone()).await; - - match &result { - Err(e) if is_connection_error(e) && !is_canceled(&cancel_token) => { - let db_opt = if database.is_empty() { None } else { Some(database) }; - let new_key = state.reconnect_pool(connection_id, db_opt).await?; - do_execute(state, &new_key, sql, cancel_token).await - } - _ => result, - } -} +// Re-export core functions for use by other modules (e.g., sql_file.rs) +pub use dbx_core::query::execute_sql_statement; #[tauri::command] pub async fn execute_query( @@ -250,16 +22,14 @@ pub async fn execute_query( .map(|id| state.running_queries.register(id.clone())); let cancel_token = registered_query.as_ref().map(|query| query.token()); - let result = execute_sql_statement( + dbx_core::query::execute_sql_statement( &state, &connection_id, &database, &sql, cancel_token, ) - .await; - - result + .await } #[tauri::command] @@ -276,44 +46,14 @@ pub async fn execute_multi( .map(|id| state.running_queries.register(id.clone())); let cancel_token = registered_query.as_ref().map(|query| query.token()); - let statements = split_sql_statements(&sql); - if statements.len() <= 1 { - let single_sql = statements.into_iter().next().unwrap_or_default(); - let result = execute_sql_statement( - &state, &connection_id, &database, &single_sql, cancel_token, - ).await?; - return Ok(vec![result]); - } - - let mut results = Vec::with_capacity(statements.len()); - for stmt in &statements { - if is_canceled(&cancel_token) { - results.push(db::QueryResult { - columns: vec!["Error".to_string()], - rows: vec![vec![serde_json::Value::String(canceled_error())]], - affected_rows: 0, - execution_time_ms: 0, - truncated: false, - }); - break; - } - match execute_sql_statement( - &state, &connection_id, &database, stmt, cancel_token.clone(), - ).await { - Ok(r) => results.push(r), - Err(e) => { - results.push(db::QueryResult { - columns: vec!["Error".to_string()], - rows: vec![vec![serde_json::Value::String(e)]], - affected_rows: 0, - execution_time_ms: 0, - truncated: false, - }); - } - } - } - - Ok(results) + dbx_core::query::execute_multi_core( + &state, + &connection_id, + &database, + &sql, + cancel_token, + ) + .await } #[tauri::command] @@ -324,48 +64,6 @@ pub async fn cancel_query( Ok(state.running_queries.cancel(&execution_id)) } -async fn execute_statements( - state: &Arc, - connection_id: String, - database: String, - statements: Vec, -) -> Result { - let pool_key = if database.is_empty() { - connection_id.clone() - } else { - state.get_or_create_pool(&connection_id, Some(&database)).await? - }; - - let mut total_affected: u64 = 0; - let start = std::time::Instant::now(); - - for (i, sql) in statements.iter().enumerate() { - match do_execute(&state, &pool_key, sql, None).await { - Ok(result) => { - total_affected += result.affected_rows; - } - Err(e) => { - if is_connection_error(&e) { - let db_opt = if database.is_empty() { None } else { Some(database.as_str()) }; - let _ = state.reconnect_pool(&connection_id, db_opt).await; - } - return Err(format!( - "Statement {} failed: {}. Previous {} statement(s) may have been committed.", - i + 1, e, i - )); - } - } - } - - Ok(db::QueryResult { - columns: vec![], - rows: vec![], - affected_rows: total_affected, - execution_time_ms: start.elapsed().as_millis(), - truncated: false, - }) -} - #[tauri::command] pub async fn execute_batch( state: State<'_, Arc>, @@ -373,7 +71,7 @@ pub async fn execute_batch( database: String, statements: Vec, ) -> Result { - execute_statements(&state, connection_id, database, statements).await + dbx_core::query::execute_statements(&state, &connection_id, &database, &statements).await } #[tauri::command] @@ -383,47 +81,5 @@ pub async fn execute_script( database: String, sql: String, ) -> Result { - execute_statements(&state, connection_id, database, split_sql_statements(&sql)).await -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn wait_for_query_returns_cancelled_when_token_is_cancelled() { - let token = CancellationToken::new(); - token.cancel(); - - let result = wait_for_query(Some(token), async { - tokio::time::sleep(Duration::from_secs(30)).await; - Ok(db::QueryResult { - columns: vec![], - rows: vec![], - affected_rows: 0, - execution_time_ms: 0, - truncated: false, - }) - }) - .await; - - assert_eq!(result.unwrap_err(), QUERY_CANCELED); - } - - #[tokio::test] - async fn wait_for_query_without_token_still_times_out() { - let result = wait_for_query_with_timeout(None, Duration::from_millis(10), async { - tokio::time::sleep(Duration::from_secs(1)).await; - Ok(db::QueryResult { - columns: vec![], - rows: vec![], - affected_rows: 0, - execution_time_ms: 0, - truncated: false, - }) - }) - .await; - - assert_eq!(result.unwrap_err(), timeout_error()); - } + dbx_core::query::execute_statements(&state, &connection_id, &database, &split_sql_statements(&sql)).await } diff --git a/src-tauri/src/commands/query_cancel.rs b/src-tauri/src/commands/query_cancel.rs index fbb1c5ef35..b5f8fd770d 100644 --- a/src-tauri/src/commands/query_cancel.rs +++ b/src-tauri/src/commands/query_cancel.rs @@ -1,98 +1,2 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use tokio_util::sync::CancellationToken; - -#[derive(Clone, Default)] -pub struct RunningQueries { - inner: Arc>>, -} - -impl RunningQueries { - pub fn register(&self, execution_id: String) -> RegisteredQuery { - let token = CancellationToken::new(); - self.inner - .lock() - .expect("running query registry poisoned") - .insert(execution_id.clone(), token.clone()); - - RegisteredQuery { - execution_id, - token, - running_queries: self.clone(), - } - } - - pub fn cancel(&self, execution_id: &str) -> bool { - let token = self - .inner - .lock() - .expect("running query registry poisoned") - .get(execution_id) - .cloned(); - - if let Some(token) = token { - token.cancel(); - true - } else { - false - } - } - - #[cfg(test)] - pub fn has(&self, execution_id: &str) -> bool { - self.inner - .lock() - .expect("running query registry poisoned") - .contains_key(execution_id) - } - - fn remove(&self, execution_id: &str) { - self.inner - .lock() - .expect("running query registry poisoned") - .remove(execution_id); - } -} - -pub struct RegisteredQuery { - execution_id: String, - token: CancellationToken, - running_queries: RunningQueries, -} - -impl RegisteredQuery { - pub fn token(&self) -> CancellationToken { - self.token.clone() - } -} - -impl Drop for RegisteredQuery { - fn drop(&mut self) { - self.running_queries.remove(&self.execution_id); - } -} - -#[cfg(test)] -mod tests { - use super::RunningQueries; - - #[test] - fn cancel_marks_registered_query_as_cancelled() { - let running = RunningQueries::default(); - let registered = running.register("exec-1".to_string()); - - assert!(running.cancel("exec-1")); - assert!(registered.token().is_cancelled()); - } - - #[test] - fn dropping_registration_removes_running_query() { - let running = RunningQueries::default(); - let registered = running.register("exec-1".to_string()); - - assert!(running.has("exec-1")); - drop(registered); - - assert!(!running.has("exec-1")); - } -} +#[allow(unused_imports)] +pub use dbx_core::query_cancel::*; diff --git a/src-tauri/src/commands/redis_cmd.rs b/src-tauri/src/commands/redis_cmd.rs index 7212108313..1c6a691c5f 100644 --- a/src-tauri/src/commands/redis_cmd.rs +++ b/src-tauri/src/commands/redis_cmd.rs @@ -1,23 +1,15 @@ use std::sync::Arc; use tauri::State; -use crate::commands::connection::{AppState, PoolKind}; -use crate::db::redis_driver::{self, RedisScanResult, RedisValue}; +use crate::commands::connection::AppState; +use dbx_core::db::redis_driver::{RedisScanResult, RedisValue}; #[tauri::command] pub async fn redis_list_databases( state: State<'_, Arc>, connection_id: String, ) -> Result, String> { - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - match pool { - PoolKind::Redis(con) => { - let mut con = con.lock().await; - redis_driver::list_databases(&mut con).await - } - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_list_databases_core(&state, &connection_id).await } #[tauri::command] @@ -29,16 +21,7 @@ pub async fn redis_scan_keys( pattern: String, count: usize, ) -> Result { - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - match pool { - PoolKind::Redis(con) => { - let mut con = con.lock().await; - redis_driver::select_db(&mut con, db).await?; - redis_driver::scan_keys_page(&mut con, cursor, &pattern, count).await - } - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_scan_keys_core(&state, &connection_id, db, cursor, &pattern, count).await } #[tauri::command] @@ -47,15 +30,7 @@ pub async fn redis_get_value( connection_id: String, key: String, ) -> Result { - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - match pool { - PoolKind::Redis(con) => { - let mut con = con.lock().await; - redis_driver::get_value(&mut con, &key).await - } - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_get_value_core(&state, &connection_id, &key).await } #[tauri::command] @@ -66,15 +41,7 @@ pub async fn redis_set_string( value: String, ttl: Option, ) -> Result<(), String> { - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - match pool { - PoolKind::Redis(con) => { - let mut con = con.lock().await; - redis_driver::set_string(&mut con, &key, &value, ttl).await - } - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_set_string_core(&state, &connection_id, &key, &value, ttl).await } #[tauri::command] @@ -83,15 +50,7 @@ pub async fn redis_delete_key( connection_id: String, key: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - match pool { - PoolKind::Redis(con) => { - let mut con = con.lock().await; - redis_driver::delete_key(&mut con, &key).await - } - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_delete_key_core(&state, &connection_id, &key).await } #[tauri::command] @@ -99,11 +58,7 @@ pub async fn redis_hash_set( state: State<'_, Arc>, connection_id: String, key: String, field: String, value: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::hash_set(&mut *con.lock().await, &key, &field, &value).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_hash_set_core(&state, &connection_id, &key, &field, &value).await } #[tauri::command] @@ -111,11 +66,7 @@ pub async fn redis_hash_del( state: State<'_, Arc>, connection_id: String, key: String, field: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::hash_del(&mut *con.lock().await, &key, &field).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_hash_del_core(&state, &connection_id, &key, &field).await } #[tauri::command] @@ -123,11 +74,7 @@ pub async fn redis_list_push( state: State<'_, Arc>, connection_id: String, key: String, value: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::list_push(&mut *con.lock().await, &key, &value).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_list_push_core(&state, &connection_id, &key, &value).await } #[tauri::command] @@ -135,11 +82,7 @@ pub async fn redis_list_remove( state: State<'_, Arc>, connection_id: String, key: String, index: i64, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::list_remove(&mut *con.lock().await, &key, index).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_list_remove_core(&state, &connection_id, &key, index).await } #[tauri::command] @@ -147,11 +90,7 @@ pub async fn redis_set_add( state: State<'_, Arc>, connection_id: String, key: String, member: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::set_add(&mut *con.lock().await, &key, &member).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_set_add_core(&state, &connection_id, &key, &member).await } #[tauri::command] @@ -159,9 +98,5 @@ pub async fn redis_set_remove( state: State<'_, Arc>, connection_id: String, key: String, member: String, ) -> Result<(), String> { - let connections = state.connections.lock().await; - match connections.get(&connection_id).ok_or("Not found")? { - PoolKind::Redis(con) => redis_driver::set_remove(&mut *con.lock().await, &key, &member).await, - _ => Err("Not a Redis connection".to_string()), - } + dbx_core::redis_ops::redis_set_remove_core(&state, &connection_id, &key, &member).await } diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index be4920a652..27159a257b 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -1,123 +1,15 @@ use std::sync::Arc; use tauri::State; -use crate::commands::connection::{AppState, PoolKind}; -use crate::db; - -fn duckdb_query_tables(con: &duckdb::Connection) -> Result, String> { - let mut stmt = con.prepare( - "SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name" - ).map_err(|e| e.to_string())?; - let rows = stmt.query_map([], |row| { - Ok(db::TableInfo { - name: row.get::<_, String>(0)?, - table_type: row.get::<_, String>(1)?, - }) - }).map_err(|e| e.to_string())?; - Ok(rows.filter_map(|r| r.ok()).collect()) -} - -fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result, String> { - let mut pk_stmt = con.prepare( - "SELECT kcu.column_name - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - AND tc.table_name = kcu.table_name - WHERE tc.constraint_type = 'PRIMARY KEY' - AND tc.table_schema = 'main' - AND tc.table_name = ? - ORDER BY kcu.ordinal_position" - ).map_err(|e| e.to_string())?; - let pk_rows = pk_stmt.query_map([table], |row| row.get::<_, String>(0)) - .map_err(|e| e.to_string())?; - let primary_keys: std::collections::HashSet = pk_rows.filter_map(|r| r.ok()).collect(); - - let mut stmt = con.prepare( - "SELECT column_name, data_type, is_nullable, column_default - FROM information_schema.columns - WHERE table_schema = 'main' AND table_name = ? - ORDER BY ordinal_position" - ).map_err(|e| e.to_string())?; - let rows = stmt.query_map([table], |row| { - let name = row.get::<_, String>(0)?; - Ok(db::ColumnInfo { - is_primary_key: primary_keys.contains(&name), - name, - data_type: row.get::<_, String>(1)?, - is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES", - column_default: row.get::<_, Option>(3)?, - extra: None, comment: None, - numeric_precision: None, - numeric_scale: None, - character_maximum_length: None, - }) - }).map_err(|e| e.to_string())?; - Ok(rows.filter_map(|r| r.ok()).collect()) -} - -fn extract_duckdb(connections: &std::collections::HashMap, key: &str) -> Option>> { - match connections.get(key)? { - PoolKind::DuckDb(con) => Some(con.clone()), - _ => None, - } -} - -fn extract_sqlserver(connections: &std::collections::HashMap, key: &str) -> Option>> { - match connections.get(key)? { - PoolKind::SqlServer(client) => Some(client.clone()), - _ => None, - } -} - -fn extract_clickhouse(connections: &std::collections::HashMap, key: &str) -> Option { - match connections.get(key)? { - PoolKind::ClickHouse(client) => Some(client.clone()), - _ => None, - } -} - -fn extract_oracle(connections: &std::collections::HashMap, key: &str) -> Option>> { - match connections.get(key)? { - PoolKind::Oracle(client) => Some(client.clone()), - _ => None, - } -} +use crate::commands::connection::AppState; +use dbx_core::db; #[tauri::command] pub async fn list_databases( state: State<'_, Arc>, connection_id: String, ) -> Result, String> { - { - let connections = state.connections.lock().await; - if let Some(client) = extract_clickhouse(&connections, &connection_id) { - drop(connections); - return db::clickhouse_driver::list_databases(&client).await; - } - if let Some(client) = extract_sqlserver(&connections, &connection_id) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_databases(&mut client).await; - } - if let Some(client) = extract_oracle(&connections, &connection_id) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_databases(&*client).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&connection_id).ok_or("Connection not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::list_databases(p).await, - PoolKind::Postgres(p) => db::postgres::list_databases(p).await, - PoolKind::Sqlite(p) => db::sqlite::list_databases(p).await, - PoolKind::DuckDb(_) => Ok(vec![db::DatabaseInfo { name: "main".to_string() }]), - _ => Ok(vec![]), - } + dbx_core::schema::list_databases_core(&state, &connection_id).await } #[tauri::command] @@ -126,29 +18,7 @@ pub async fn list_schemas( connection_id: String, database: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_schemas(&mut client).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_schemas(&*client).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Postgres(p) => db::postgres::list_schemas(p).await, - _ => Ok(vec![]), - } + dbx_core::schema::list_schemas_core(&state, &connection_id, &database).await } #[tauri::command] @@ -158,40 +28,7 @@ pub async fn list_tables( database: String, schema: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(con) = extract_duckdb(&connections, &pool_key) { - drop(connections); - let con = con.lock().map_err(|e| e.to_string())?; - return duckdb_query_tables(&con); - } - if let Some(client) = extract_clickhouse(&connections, &pool_key) { - drop(connections); - return db::clickhouse_driver::list_tables(&client, &database).await; - } - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_tables(&mut client, &schema).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_tables(&*client, &schema).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::list_tables(p, &schema).await, - PoolKind::Postgres(p) => db::postgres::list_tables(p, &schema).await, - PoolKind::Sqlite(p) => db::sqlite::list_tables(p, &schema).await, - _ => Ok(vec![]), - } + dbx_core::schema::list_tables_core(&state, &connection_id, &database, &schema).await } #[tauri::command] @@ -202,40 +39,7 @@ pub async fn get_columns( schema: String, table: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(con) = extract_duckdb(&connections, &pool_key) { - drop(connections); - let con = con.lock().map_err(|e| e.to_string())?; - return duckdb_query_columns(&con, &table); - } - if let Some(client) = extract_clickhouse(&connections, &pool_key) { - drop(connections); - return db::clickhouse_driver::get_columns(&client, &database, &table).await; - } - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::get_columns(&mut client, &schema, &table).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::get_columns(&*client, &schema, &table).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::get_columns(p, &schema, &table).await, - PoolKind::Postgres(p) => db::postgres::get_columns(p, &schema, &table).await, - PoolKind::Sqlite(p) => db::sqlite::get_columns(p, &schema, &table).await, - _ => Ok(vec![]), - } + dbx_core::schema::get_columns_core(&state, &connection_id, &database, &schema, &table).await } #[tauri::command] @@ -246,31 +50,7 @@ pub async fn list_indexes( schema: String, table: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_indexes(&mut client, &schema, &table).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_indexes(&*client, &schema, &table).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::list_indexes(p, &schema, &table).await, - PoolKind::Postgres(p) => db::postgres::list_indexes(p, &schema, &table).await, - PoolKind::Sqlite(p) => db::sqlite::list_indexes(p, &schema, &table).await, - _ => Ok(vec![]), - } + dbx_core::schema::list_indexes_core(&state, &connection_id, &database, &schema, &table).await } #[tauri::command] @@ -281,31 +61,7 @@ pub async fn list_foreign_keys( schema: String, table: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_foreign_keys(&mut client, &schema, &table).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_foreign_keys(&*client, &schema, &table).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::list_foreign_keys(p, &schema, &table).await, - PoolKind::Postgres(p) => db::postgres::list_foreign_keys(p, &schema, &table).await, - PoolKind::Sqlite(p) => db::sqlite::list_foreign_keys(p, &schema, &table).await, - _ => Ok(vec![]), - } + dbx_core::schema::list_foreign_keys_core(&state, &connection_id, &database, &schema, &table).await } #[tauri::command] @@ -316,31 +72,7 @@ pub async fn list_triggers( schema: String, table: String, ) -> Result, String> { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::list_triggers(&mut client, &schema, &table).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return db::oracle_driver::list_triggers(&*client, &schema, &table).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => db::mysql::list_triggers(p, &schema, &table).await, - PoolKind::Postgres(p) => db::postgres::list_triggers(p, &schema, &table).await, - PoolKind::Sqlite(p) => db::sqlite::list_triggers(p, &schema, &table).await, - _ => Ok(vec![]), - } + dbx_core::schema::list_triggers_core(&state, &connection_id, &database, &schema, &table).await } #[tauri::command] @@ -351,173 +83,5 @@ pub async fn get_table_ddl( schema: String, table: String, ) -> Result { - let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; - - { - let connections = state.connections.lock().await; - if let Some(con) = extract_duckdb(&connections, &pool_key) { - drop(connections); - let tbl = table.replace('\'', "''"); - let con = con.lock().map_err(|e| e.to_string())?; - let mut stmt = con.prepare(&format!("SELECT sql FROM duckdb_tables() WHERE table_name = '{tbl}'")) - .map_err(|e| e.to_string())?; - let mut rows = stmt.query([]).map_err(|e| e.to_string())?; - if let Some(row) = rows.next().map_err(|e| e.to_string())? { - return row.get::<_, String>(0).map_err(|e| e.to_string()); - } - return Err("Table not found".to_string()); - } - if let Some(client) = extract_clickhouse(&connections, &pool_key) { - drop(connections); - let result = db::clickhouse_driver::execute_query(&client, &database, &format!("SHOW CREATE TABLE `{table}`")).await?; - return result.rows.first() - .and_then(|r| r.first()) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .ok_or_else(|| "Table not found".to_string()); - } - if let Some(client) = extract_sqlserver(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return build_sqlserver_ddl(&mut client, &schema, &table).await; - } - if let Some(client) = extract_oracle(&connections, &pool_key) { - drop(connections); - let client = client.lock().await; - return build_oracle_ddl(&*client, &schema, &table).await; - } - } - - let connections = state.connections.lock().await; - let pool = connections.get(&pool_key).ok_or("Pool not found")?; - - match pool { - PoolKind::Mysql(p, _) => mysql_ddl(p, &table).await, - PoolKind::Postgres(p) => pg_ddl(p, &schema, &table).await, - PoolKind::Sqlite(p) => sqlite_ddl(p, &table).await, - _ => Err("DDL not supported for this database type".to_string()), - } -} - -async fn mysql_ddl(pool: &sqlx::mysql::MySqlPool, table: &str) -> Result { - use sqlx::Row; - let sql = format!("SHOW CREATE TABLE `{}`", table.replace('`', "``")); - let row: sqlx::mysql::MySqlRow = sqlx::raw_sql(&sql) - .fetch_one(pool).await.map_err(|e| e.to_string())?; - row.try_get::(1) - .or_else(|_| row.try_get::, _>(1).map(|b| String::from_utf8_lossy(&b).to_string())) - .map_err(|e| e.to_string()) -} - -async fn sqlite_ddl(pool: &sqlx::sqlite::SqlitePool, table: &str) -> Result { - use sqlx::Row; - let row: sqlx::sqlite::SqliteRow = sqlx::query("SELECT sql FROM sqlite_master WHERE type='table' AND name=?") - .bind(table) - .fetch_one(pool).await.map_err(|e| e.to_string())?; - row.try_get::(0).map_err(|e| e.to_string()) -} - -async fn pg_ddl(pool: &sqlx::postgres::PgPool, schema: &str, table: &str) -> Result { - let columns = db::postgres::get_columns(pool, schema, table).await?; - let indexes = db::postgres::list_indexes(pool, schema, table).await?; - let fkeys = db::postgres::list_foreign_keys(pool, schema, table).await?; - - let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); - let col_lines: Vec = columns.iter().map(|c| { - let mut line = format!(" \"{}\" {}", c.name, c.data_type); - if !c.is_nullable { line.push_str(" NOT NULL"); } - if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } - line - }).collect(); - ddl.push_str(&col_lines.join(",\n")); - - let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); - if !pks.is_empty() { - ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", "))); - } - for fk in &fkeys { - ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column)); - } - ddl.push_str("\n);\n"); - - for idx in &indexes { - if idx.is_primary { continue; } - let unique = if idx.is_unique { "UNIQUE " } else { "" }; - let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); - let using = idx.index_type.as_deref().map(|t| format!(" USING {t}")).unwrap_or_default(); - let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "))).unwrap_or_default(); - let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); - ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\"{using} ({cols}){include}{filter};", idx.name)); - if let Some(ref c) = idx.comment { - ddl.push_str(&format!("\nCOMMENT ON INDEX \"{schema}\".\"{}\" IS '{}';", idx.name, c.replace('\'', "''"))); - } - } - Ok(ddl) -} - -async fn build_sqlserver_ddl(client: &mut db::sqlserver::SqlServerClient, schema: &str, table: &str) -> Result { - let columns = db::sqlserver::get_columns(client, schema, table).await?; - let indexes = db::sqlserver::list_indexes(client, schema, table).await?; - let fkeys = db::sqlserver::list_foreign_keys(client, schema, table).await?; - - let mut ddl = format!("CREATE TABLE [{schema}].[{table}] (\n"); - let col_lines: Vec = columns.iter().map(|c| { - let mut line = format!(" [{}] {}", c.name, c.data_type); - if !c.is_nullable { line.push_str(" NOT NULL"); } - if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } - line - }).collect(); - ddl.push_str(&col_lines.join(",\n")); - - let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); - if !pks.is_empty() { - ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("[{k}]")).collect::>().join(", "))); - } - for fk in &fkeys { - ddl.push_str(&format!(",\n CONSTRAINT [{}] FOREIGN KEY ([{}]) REFERENCES [{}]([{}])", fk.name, fk.column, fk.ref_table, fk.ref_column)); - } - ddl.push_str("\n);\n"); - - for idx in &indexes { - if idx.is_primary { continue; } - let unique = if idx.is_unique { "UNIQUE " } else { "" }; - let idx_type = idx.index_type.as_deref().map(|t| format!("{t} ")).unwrap_or_default(); - let cols = idx.columns.iter().map(|c| format!("[{c}]")).collect::>().join(", "); - let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("[{c}]")).collect::>().join(", "))).unwrap_or_default(); - let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); - ddl.push_str(&format!("\nCREATE {unique}{idx_type}INDEX [{}] ON [{schema}].[{table}] ({cols}){include}{filter};", idx.name)); - } - Ok(ddl) -} - -async fn build_oracle_ddl(client: &db::oracle_driver::OracleClient, schema: &str, table: &str) -> Result { - let columns = db::oracle_driver::get_columns(client, schema, table).await?; - let indexes = db::oracle_driver::list_indexes(client, schema, table).await?; - let fkeys = db::oracle_driver::list_foreign_keys(client, schema, table).await?; - - let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); - let col_lines: Vec = columns.iter().map(|c| { - let mut line = format!(" \"{}\" {}", c.name, c.data_type); - if !c.is_nullable { line.push_str(" NOT NULL"); } - if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } - line - }).collect(); - ddl.push_str(&col_lines.join(",\n")); - - let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); - if !pks.is_empty() { - ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", "))); - } - for fk in &fkeys { - ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column)); - } - ddl.push_str("\n);\n"); - - for idx in &indexes { - if idx.is_primary { continue; } - let unique = if idx.is_unique { "UNIQUE " } else { "" }; - let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); - ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); - } - Ok(ddl) + dbx_core::schema::get_table_ddl_core(&state, &connection_id, &database, &schema, &table).await } diff --git a/src-tauri/src/commands/sql_file.rs b/src-tauri/src/commands/sql_file.rs index 8e84601334..f68de43f43 100644 --- a/src-tauri/src/commands/sql_file.rs +++ b/src-tauri/src/commands/sql_file.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; -use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, State}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::sync::RwLock; @@ -12,6 +11,11 @@ use tokio_util::sync::CancellationToken; use crate::commands::connection::AppState; use crate::commands::query::execute_sql_statement; +pub use dbx_core::sql::{ + statement_summary, SqlFilePreview, SqlFileProgress, SqlFileRequest, + SqlFileStatus, SqlStatementSplitter, +}; + static SQL_FILE_EXECUTIONS: std::sync::LazyLock>> = std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); @@ -22,51 +26,6 @@ struct StatementErrorDecision { result: Result, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFileRequest { - pub execution_id: String, - pub connection_id: String, - pub database: String, - pub file_path: String, - pub continue_on_error: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFilePreview { - pub file_name: String, - pub file_path: String, - pub size_bytes: u64, - pub preview: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SqlFileStatus { - Started, - Running, - StatementDone, - StatementFailed, - Done, - Error, - Cancelled, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFileProgress { - pub execution_id: String, - pub status: SqlFileStatus, - pub statement_index: usize, - pub success_count: usize, - pub failure_count: usize, - pub affected_rows: u64, - pub elapsed_ms: u128, - pub statement_summary: String, - pub error: Option, -} - #[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] struct SqlFileSummary { @@ -76,255 +35,6 @@ struct SqlFileSummary { failed_statement_index: Option, } -#[derive(Default)] -struct SqlStatementSplitter { - buffer: String, - in_single_quote: bool, - in_double_quote: bool, - in_backtick: bool, - in_line_comment: bool, - in_block_comment: bool, - dollar_quote_tag: Option, - previous: Option, -} - -impl SqlStatementSplitter { - fn push_chunk(&mut self, chunk: &str) -> Vec { - let mut statements = Vec::new(); - let chars = chunk.chars().collect::>(); - let mut i = 0; - - while i < chars.len() { - if let Some(tag) = &self.dollar_quote_tag { - let tag_chars = tag.chars().collect::>(); - if starts_with_chars(&chars, i, &tag_chars) { - for tag_ch in &tag_chars { - self.buffer.push(*tag_ch); - self.previous = Some(*tag_ch); - } - i += tag_chars.len(); - self.dollar_quote_tag = None; - continue; - } - - let ch = chars[i]; - self.buffer.push(ch); - self.previous = Some(ch); - i += 1; - continue; - } - - let ch = chars[i]; - let next = chars.get(i + 1).copied(); - - if self.in_line_comment { - self.buffer.push(ch); - if ch == '\n' { - self.in_line_comment = false; - } - self.previous = Some(ch); - i += 1; - continue; - } - - if self.in_block_comment { - self.buffer.push(ch); - if self.previous == Some('*') && ch == '/' { - self.in_block_comment = false; - } - self.previous = Some(ch); - i += 1; - continue; - } - - if !self.in_single_quote && !self.in_double_quote && !self.in_backtick { - if self.previous == Some('-') && ch == '-' { - self.in_line_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - i += 1; - continue; - } - if self.previous == Some('/') && ch == '*' { - self.in_block_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - i += 1; - continue; - } - if ch == '-' && next == Some('-') { - self.in_line_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - i += 1; - continue; - } - if ch == '/' && next == Some('*') { - self.in_block_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - i += 1; - continue; - } - if let Some(tag) = dollar_quote_tag_at(&chars, i) { - for tag_ch in tag.chars() { - self.buffer.push(tag_ch); - self.previous = Some(tag_ch); - } - i += tag.chars().count(); - self.dollar_quote_tag = Some(tag); - continue; - } - } - - match ch { - '\'' if !self.in_double_quote - && !self.in_backtick - && self.previous != Some('\\') => - { - self.in_single_quote = !self.in_single_quote; - self.buffer.push(ch); - } - '"' if !self.in_single_quote - && !self.in_backtick - && self.previous != Some('\\') => - { - self.in_double_quote = !self.in_double_quote; - self.buffer.push(ch); - } - '`' if !self.in_single_quote && !self.in_double_quote => { - self.in_backtick = !self.in_backtick; - self.buffer.push(ch); - } - ';' if !self.in_single_quote && !self.in_double_quote && !self.in_backtick => { - self.push_current_statement(&mut statements); - } - _ => self.buffer.push(ch), - } - - self.previous = Some(ch); - i += 1; - } - - statements - } - - fn finish(mut self) -> Vec { - let mut statements = Vec::new(); - self.push_current_statement(&mut statements); - statements - } - - fn push_current_statement(&mut self, statements: &mut Vec) { - let statement = self.buffer.trim(); - if has_executable_sql(statement) { - statements.push(statement.to_string()); - } - self.buffer.clear(); - self.previous = None; - } -} - -pub(crate) fn split_sql_statements(sql: &str) -> Vec { - let mut splitter = SqlStatementSplitter::default(); - let mut statements = splitter.push_chunk(sql); - statements.extend(splitter.finish()); - statements -} - -fn starts_with_chars(chars: &[char], start: usize, needle: &[char]) -> bool { - start + needle.len() <= chars.len() && chars[start..start + needle.len()] == *needle -} - -fn dollar_quote_tag_at(chars: &[char], start: usize) -> Option { - if chars.get(start) != Some(&'$') { - return None; - } - - match chars.get(start + 1) { - Some('$') => return Some("$$".to_string()), - Some(ch) if ch.is_ascii_alphabetic() || *ch == '_' => {} - _ => return None, - } - - let mut end = start + 2; - while let Some(ch) = chars.get(end) { - if *ch == '$' { - return Some(chars[start..=end].iter().collect()); - } - if !ch.is_ascii_alphanumeric() && *ch != '_' { - return None; - } - end += 1; - } - - None -} - -fn has_executable_sql(statement: &str) -> bool { - let chars = statement.chars().collect::>(); - let mut in_line_comment = false; - let mut in_block_comment = false; - let mut previous = None; - let mut i = 0; - - while i < chars.len() { - let ch = chars[i]; - let next = chars.get(i + 1).copied(); - - if in_line_comment { - if ch == '\n' { - in_line_comment = false; - } - previous = Some(ch); - i += 1; - continue; - } - - if in_block_comment { - if previous == Some('*') && ch == '/' { - in_block_comment = false; - } - previous = Some(ch); - i += 1; - continue; - } - - if ch == '-' && next == Some('-') { - in_line_comment = true; - previous = Some(ch); - i += 1; - continue; - } - - if ch == '/' && next == Some('*') { - if is_mysql_executable_comment_start(&chars, i) { - return true; - } - in_block_comment = true; - previous = Some(ch); - i += 1; - continue; - } - - if !ch.is_whitespace() { - return true; - } - - previous = Some(ch); - i += 1; - } - - false -} - -fn is_mysql_executable_comment_start(chars: &[char], start: usize) -> bool { - chars.get(start) == Some(&'/') - && chars.get(start + 1) == Some(&'*') - && (chars.get(start + 2) == Some(&'!') - || (chars.get(start + 2) == Some(&'M') && chars.get(start + 3) == Some(&'!'))) -} - #[tauri::command] pub async fn preview_sql_file(file_path: String) -> Result { let path = PathBuf::from(&file_path); @@ -808,17 +518,6 @@ fn sql_file_progress( } } -fn statement_summary(statement: &str) -> String { - const MAX_LEN: usize = 120; - - let collapsed = statement.split_whitespace().collect::>().join(" "); - if collapsed.chars().count() <= MAX_LEN { - return collapsed; - } - - collapsed.chars().take(MAX_LEN).collect() -} - #[cfg(test)] async fn run_statements_for_test( statements: Vec, @@ -871,118 +570,6 @@ async fn run_statements_for_test( } } -#[cfg(test)] -fn split_sql_script(sql: &str) -> Result, String> { - let mut splitter = SqlStatementSplitter::default(); - let mut statements = splitter.push_chunk(sql); - statements.extend(splitter.finish()); - Ok(statements) -} - -#[cfg(test)] -mod tests { - use super::{split_sql_script, SqlStatementSplitter}; - - #[test] - fn splits_semicolon_delimited_statements() { - assert_eq!( - split_sql_script("CREATE TABLE a(id int); INSERT INTO a VALUES (1);").unwrap(), - vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] - ); - } - - #[test] - fn ignores_semicolons_inside_quotes_and_comments() { - let sql = "\ - INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`);\n\ - -- comment ; ignored\n\ - /* block ; ignored */\n\ - SELECT 1;"; - assert_eq!( - split_sql_script(sql).unwrap(), - vec![ - "INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`)", - "-- comment ; ignored\n/* block ; ignored */\nSELECT 1", - ] - ); - } - - #[test] - fn emits_trailing_statement_without_semicolon() { - assert_eq!( - split_sql_script("CREATE TABLE a(id int);\nINSERT INTO a VALUES (1)").unwrap(), - vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] - ); - } - - #[test] - fn line_comment_openers_can_span_chunks() { - let mut splitter = SqlStatementSplitter::default(); - - assert_eq!(splitter.push_chunk("SELECT 1; -"), vec!["SELECT 1"]); - assert_eq!( - splitter.push_chunk("- comment ; ignored\nSELECT 2;"), - vec!["-- comment ; ignored\nSELECT 2"] - ); - assert_eq!(splitter.finish(), Vec::::new()); - } - - #[test] - fn block_comment_openers_can_span_chunks() { - let mut splitter = SqlStatementSplitter::default(); - - assert_eq!(splitter.push_chunk("SELECT 1; /"), vec!["SELECT 1"]); - assert_eq!( - splitter.push_chunk("* comment ; ignored */\nSELECT 2;"), - vec!["/* comment ; ignored */\nSELECT 2"] - ); - assert_eq!(splitter.finish(), Vec::::new()); - } - - #[test] - fn skips_comment_only_tail_after_statement() { - assert_eq!( - split_sql_script("CREATE TABLE a(id int); -- done\n/* no more sql */").unwrap(), - vec!["CREATE TABLE a(id int)"] - ); - } - - #[test] - fn keeps_postgres_dollar_quoted_function_body_together() { - let sql = "\ - CREATE FUNCTION bump_counter()\n\ - RETURNS trigger AS $$\n\ - BEGIN\n\ - PERFORM 1;\n\ - RETURN NEW;\n\ - END;\n\ - $$ LANGUAGE plpgsql;\n\ - SELECT 1;"; - - assert_eq!( - split_sql_script(sql).unwrap(), - vec![ - "CREATE FUNCTION bump_counter()\nRETURNS trigger AS $$\nBEGIN\nPERFORM 1;\nRETURN NEW;\nEND;\n$$ LANGUAGE plpgsql", - "SELECT 1", - ] - ); - } - - #[test] - fn keeps_mysql_executable_comments_as_statements() { - assert_eq!( - split_sql_script( - "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\nSELECT 1;", - ) - .unwrap(), - vec![ - "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */", - "SELECT 1", - ] - ); - } -} - #[cfg(test)] mod execution_tests { use super::*; diff --git a/src-tauri/src/commands/table_import.rs b/src-tauri/src/commands/table_import.rs index 672d77dfd7..f0d65da669 100644 --- a/src-tauri/src/commands/table_import.rs +++ b/src-tauri/src/commands/table_import.rs @@ -1,455 +1,21 @@ use std::collections::HashSet; -use std::path::Path; use std::sync::Arc; -use calamine::{open_workbook_auto, Data, Reader}; -use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, State}; use tokio::sync::RwLock; use crate::commands::connection::AppState; -use crate::commands::transfer::{execute_on_pool, generate_insert, get_db_type, qualified_table}; -use crate::models::connection::DatabaseType; +use crate::commands::transfer::get_db_type; -const DEFAULT_PREVIEW_LIMIT: usize = 50; -const DEFAULT_BATCH_SIZE: usize = 500; +// Re-export types for backward compatibility +pub use dbx_core::table_import::{ + TableImportPreview, TableImportProgress, + TableImportRequest, TableImportSummary, +}; static CANCELLED_IMPORTS: std::sync::LazyLock>> = std::sync::LazyLock::new(|| RwLock::new(HashSet::new())); -#[derive(Debug, Clone)] -struct ParsedImportFile { - columns: Vec, - rows: Vec>, - total_rows: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ImportSqlBatch { - sql: String, - row_count: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TableImportColumnMapping { - pub source_column: String, - pub target_column: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum TableImportMode { - Append, - Truncate, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TableImportRequest { - pub import_id: String, - pub connection_id: String, - pub database: String, - pub schema: String, - pub table: String, - pub file_path: String, - pub mappings: Vec, - pub mode: TableImportMode, - pub batch_size: usize, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TableImportPreview { - pub file_name: String, - pub file_path: String, - pub file_type: String, - pub size_bytes: u64, - pub columns: Vec, - pub rows: Vec>, - pub total_rows: usize, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TableImportSummary { - pub import_id: String, - pub rows_imported: usize, - pub total_rows: usize, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TableImportProgress { - pub import_id: String, - pub status: TableImportStatus, - pub rows_imported: usize, - pub total_rows: usize, - pub error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum TableImportStatus { - Running, - Done, - Error, - Cancelled, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ImportFileKind { - Csv, - Tsv, - Json, - Xlsx, -} - -impl ImportFileKind { - fn label(self) -> &'static str { - match self { - ImportFileKind::Csv => "csv", - ImportFileKind::Tsv => "tsv", - ImportFileKind::Json => "json", - ImportFileKind::Xlsx => "xlsx", - } - } -} - -fn import_file_kind(path: &str) -> Result { - let lower = path.to_lowercase(); - if lower.ends_with(".csv") { - Ok(ImportFileKind::Csv) - } else if lower.ends_with(".tsv") { - Ok(ImportFileKind::Tsv) - } else if lower.ends_with(".json") { - Ok(ImportFileKind::Json) - } else if lower.ends_with(".xlsx") || lower.ends_with(".xlsm") || lower.ends_with(".xls") { - Ok(ImportFileKind::Xlsx) - } else { - Err("Unsupported import file type".to_string()) - } -} - -fn normalize_header(value: &str, index: usize) -> String { - let trimmed = value.trim(); - if trimmed.is_empty() { - format!("column_{}", index + 1) - } else { - trimmed.to_string() - } -} - -fn csv_value(value: &str) -> serde_json::Value { - if value.is_empty() { - serde_json::Value::Null - } else { - serde_json::Value::String(value.to_string()) - } -} - -fn parse_delimited_bytes( - bytes: &[u8], - delimiter: u8, - preview_limit: usize, -) -> Result { - let mut reader = csv::ReaderBuilder::new() - .delimiter(delimiter) - .flexible(true) - .from_reader(bytes); - let columns = reader - .headers() - .map_err(|e| e.to_string())? - .iter() - .enumerate() - .map(|(index, header)| normalize_header(header, index)) - .collect::>(); - if columns.is_empty() { - return Err("Import file has no columns".to_string()); - } - - let mut rows = Vec::new(); - let mut total_rows = 0; - for record in reader.records() { - let record = record.map_err(|e| e.to_string())?; - total_rows += 1; - if rows.len() >= preview_limit { - continue; - } - let mut row = Vec::with_capacity(columns.len()); - for index in 0..columns.len() { - row.push( - record - .get(index) - .map(csv_value) - .unwrap_or(serde_json::Value::Null), - ); - } - rows.push(row); - } - - Ok(ParsedImportFile { - columns, - rows, - total_rows, - }) -} - -fn parse_csv_bytes(bytes: &[u8], preview_limit: usize) -> Result { - parse_delimited_bytes(bytes, b',', preview_limit) -} - -fn parse_json_bytes(bytes: &[u8], preview_limit: usize) -> Result { - let value: serde_json::Value = serde_json::from_slice(bytes).map_err(|e| e.to_string())?; - let items = match value { - serde_json::Value::Array(items) => items, - serde_json::Value::Object(_) => vec![value], - _ => return Err("JSON import must be an object or an array".to_string()), - }; - if items.is_empty() { - return Err("Import file has no rows".to_string()); - } - - if items.iter().all(|item| item.is_object()) { - let mut columns = Vec::new(); - for item in &items { - if let Some(obj) = item.as_object() { - for key in obj.keys() { - if !columns.contains(key) { - columns.push(key.clone()); - } - } - } - } - if columns.is_empty() { - return Err("Import file has no columns".to_string()); - } - let rows = items - .iter() - .take(preview_limit) - .map(|item| { - let obj = item.as_object().expect("checked object JSON row"); - columns - .iter() - .map(|column| obj.get(column).cloned().unwrap_or(serde_json::Value::Null)) - .collect::>() - }) - .collect::>(); - return Ok(ParsedImportFile { - columns, - rows, - total_rows: items.len(), - }); - } - - if items.iter().all(|item| item.is_array()) { - let max_cols = items - .iter() - .filter_map(|item| item.as_array().map(|row| row.len())) - .max() - .unwrap_or(0); - if max_cols == 0 { - return Err("Import file has no columns".to_string()); - } - let columns = (0..max_cols) - .map(|index| format!("column_{}", index + 1)) - .collect::>(); - let rows = items - .iter() - .take(preview_limit) - .map(|item| { - let arr = item.as_array().expect("checked array JSON row"); - (0..max_cols) - .map(|index| arr.get(index).cloned().unwrap_or(serde_json::Value::Null)) - .collect::>() - }) - .collect::>(); - return Ok(ParsedImportFile { - columns, - rows, - total_rows: items.len(), - }); - } - - Err("JSON rows must all be objects or all be arrays".to_string()) -} - -fn xlsx_cell_value(cell: &Data) -> serde_json::Value { - match cell { - Data::Empty => serde_json::Value::Null, - Data::String(s) => csv_value(s), - Data::Float(n) => serde_json::Number::from_f64(*n) - .map(serde_json::Value::Number) - .unwrap_or(serde_json::Value::Null), - Data::Int(n) => serde_json::Value::Number((*n).into()), - Data::Bool(v) => serde_json::Value::Bool(*v), - Data::DateTime(v) => serde_json::Value::String(v.to_string()), - Data::DateTimeIso(v) => serde_json::Value::String(v.clone()), - Data::DurationIso(v) => serde_json::Value::String(v.clone()), - Data::Error(v) => serde_json::Value::String(v.to_string()), - } -} - -fn xlsx_cell_label(cell: &Data) -> String { - match cell { - Data::Empty => String::new(), - Data::String(s) => s.clone(), - Data::Float(n) => n.to_string(), - Data::Int(n) => n.to_string(), - Data::Bool(v) => v.to_string(), - Data::DateTime(v) => v.to_string(), - Data::DateTimeIso(v) => v.clone(), - Data::DurationIso(v) => v.clone(), - Data::Error(v) => v.to_string(), - } -} - -fn parse_xlsx_file(path: &str, preview_limit: usize) -> Result { - let mut workbook = open_workbook_auto(path).map_err(|e| e.to_string())?; - let sheet_name = workbook - .sheet_names() - .first() - .cloned() - .ok_or_else(|| "Workbook has no sheets".to_string())?; - let range = workbook - .worksheet_range(&sheet_name) - .map_err(|e| e.to_string())?; - let mut rows_iter = range.rows(); - let header = rows_iter - .next() - .ok_or_else(|| "Import file has no rows".to_string())?; - let columns = header - .iter() - .enumerate() - .map(|(index, cell)| normalize_header(&xlsx_cell_label(cell), index)) - .collect::>(); - if columns.is_empty() { - return Err("Import file has no columns".to_string()); - } - - let mut rows = Vec::new(); - let mut total_rows = 0; - for source_row in rows_iter { - total_rows += 1; - if rows.len() >= preview_limit { - continue; - } - let mut row = Vec::with_capacity(columns.len()); - for index in 0..columns.len() { - row.push( - source_row - .get(index) - .map(xlsx_cell_value) - .unwrap_or(serde_json::Value::Null), - ); - } - rows.push(row); - } - - Ok(ParsedImportFile { - columns, - rows, - total_rows, - }) -} - -fn parse_import_file(path: &str, preview_limit: usize) -> Result { - match import_file_kind(path)? { - ImportFileKind::Csv => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - parse_csv_bytes(&bytes, preview_limit) - } - ImportFileKind::Tsv => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - parse_delimited_bytes(&bytes, b'\t', preview_limit) - } - ImportFileKind::Json => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - parse_json_bytes(&bytes, preview_limit) - } - ImportFileKind::Xlsx => parse_xlsx_file(path, preview_limit), - } -} - -fn mapping_indexes( - data: &ParsedImportFile, - mappings: &[TableImportColumnMapping], -) -> Result, String> { - if mappings.is_empty() { - return Err("No columns mapped for import".to_string()); - } - let mut mapped = Vec::new(); - let mut target_seen = HashSet::new(); - for mapping in mappings { - let source_index = data - .columns - .iter() - .position(|column| column == &mapping.source_column) - .ok_or_else(|| format!("Source column not found: {}", mapping.source_column))?; - if mapping.target_column.trim().is_empty() { - return Err("Target column cannot be empty".to_string()); - } - if !target_seen.insert(mapping.target_column.clone()) { - return Err(format!( - "Target column mapped more than once: {}", - mapping.target_column - )); - } - mapped.push((source_index, mapping.target_column.clone())); - } - Ok(mapped) -} - -fn build_import_insert_batches( - data: &ParsedImportFile, - mappings: &[TableImportColumnMapping], - table: &str, - schema: &str, - db_type: &DatabaseType, - batch_size: usize, -) -> Result, String> { - let mapped = mapping_indexes(data, mappings)?; - let columns = mapped - .iter() - .map(|(_, target)| target.clone()) - .collect::>(); - let batch_size = batch_size.max(1); - let mut batches = Vec::new(); - - for chunk in data.rows.chunks(batch_size) { - let rows = chunk - .iter() - .map(|row| { - mapped - .iter() - .map(|(source_index, _)| { - row.get(*source_index) - .cloned() - .unwrap_or(serde_json::Value::Null) - }) - .collect::>() - }) - .collect::>(); - let sql = generate_insert(&columns, &rows, table, schema, db_type); - if !sql.trim().is_empty() { - batches.push(ImportSqlBatch { - sql, - row_count: chunk.len(), - }); - } - } - - Ok(batches) -} - -fn truncate_sql(table: &str, schema: &str, db_type: &DatabaseType) -> String { - let full_table = qualified_table(table, schema, db_type); - match db_type { - DatabaseType::Sqlite => format!("DELETE FROM {full_table}"), - _ => format!("TRUNCATE TABLE {full_table}"), - } -} - fn emit_progress(app: &AppHandle, progress: TableImportProgress) { let _ = app.emit("table-import-progress", progress); } @@ -464,24 +30,7 @@ async fn clear_cancelled(import_id: &str) { #[tauri::command] pub async fn preview_table_import_file(file_path: String) -> Result { - let kind = import_file_kind(&file_path)?; - let parsed = parse_import_file(&file_path, DEFAULT_PREVIEW_LIMIT)?; - let metadata = std::fs::metadata(&file_path).map_err(|e| e.to_string())?; - let file_name = Path::new(&file_path) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&file_path) - .to_string(); - - Ok(TableImportPreview { - file_name, - file_path, - file_type: kind.label().to_string(), - size_bytes: metadata.len(), - columns: parsed.columns, - rows: parsed.rows, - total_rows: parsed.total_rows, - }) + dbx_core::table_import::preview_table_import_file_core(&file_path) } #[tauri::command] @@ -499,142 +48,19 @@ pub async fn import_table_file( .get_or_create_pool(&request.connection_id, Some(&request.database)) .await? }; - let batch_size = if request.batch_size == 0 { - DEFAULT_BATCH_SIZE - } else { - request.batch_size - }; - let parsed = match parse_import_file(&request.file_path, usize::MAX) { - Ok(parsed) => parsed, - Err(error) => { - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Error, - rows_imported: 0, - total_rows: 0, - error: Some(error.clone()), - }, - ); - return Err(error); - } - }; - - let total_rows = parsed.total_rows; - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Running, - rows_imported: 0, - total_rows, - error: None, - }, - ); - - let batches = match build_import_insert_batches( - &parsed, - &request.mappings, - &request.table, - &request.schema, + let result = dbx_core::table_import::import_table_file_core( + &state, + &request, &db_type, - batch_size, - ) { - Ok(batches) => batches, - Err(error) => { - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Error, - rows_imported: 0, - total_rows, - error: Some(error.clone()), - }, - ); - return Err(error); - } - }; + &pool_key, + |import_id| Box::pin(is_cancelled(import_id)), + |progress| emit_progress(&app, progress), + ) + .await; - if matches!(request.mode, TableImportMode::Truncate) { - let sql = truncate_sql(&request.table, &request.schema, &db_type); - if let Err(error) = execute_on_pool(&state, &pool_key, &sql).await { - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Error, - rows_imported: 0, - total_rows, - error: Some(error.clone()), - }, - ); - return Err(error); - } - } - - let mut rows_imported = 0; - for batch in batches { - if is_cancelled(&request.import_id).await { - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Cancelled, - rows_imported, - total_rows, - error: None, - }, - ); - clear_cancelled(&request.import_id).await; - return Err("Import cancelled".to_string()); - } - - if let Err(error) = execute_on_pool(&state, &pool_key, &batch.sql).await { - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Error, - rows_imported, - total_rows, - error: Some(error.clone()), - }, - ); - return Err(error); - } - rows_imported = (rows_imported + batch.row_count).min(total_rows); - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Running, - rows_imported, - total_rows, - error: None, - }, - ); - } - - emit_progress( - &app, - TableImportProgress { - import_id: request.import_id.clone(), - status: TableImportStatus::Done, - rows_imported, - total_rows, - error: None, - }, - ); clear_cancelled(&request.import_id).await; - - Ok(TableImportSummary { - import_id: request.import_id, - rows_imported, - total_rows, - }) + result } #[tauri::command] @@ -642,129 +68,3 @@ pub async fn cancel_table_import(import_id: String) -> Result { CANCELLED_IMPORTS.write().await.insert(import_id); Ok(true) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::models::connection::DatabaseType; - - #[test] - fn parses_csv_headers_and_preview_rows() { - let parsed = parse_csv_bytes(b"id,name,active\n1,Ada,true\n2,,false\n", 10).unwrap(); - - assert_eq!(parsed.columns, vec!["id", "name", "active"]); - assert_eq!(parsed.total_rows, 2); - assert_eq!( - parsed.rows[0], - vec![ - serde_json::Value::String("1".to_string()), - serde_json::Value::String("Ada".to_string()), - serde_json::Value::String("true".to_string()), - ] - ); - assert_eq!( - parsed.rows[1], - vec![ - serde_json::Value::String("2".to_string()), - serde_json::Value::Null, - serde_json::Value::String("false".to_string()), - ] - ); - } - - #[test] - fn parses_tsv_with_tab_delimiter() { - let parsed = parse_delimited_bytes(b"id\tname\n1\tAda\n", b'\t', 10).unwrap(); - - assert_eq!(parsed.columns, vec!["id", "name"]); - assert_eq!(parsed.total_rows, 1); - assert_eq!( - parsed.rows[0], - vec![ - serde_json::Value::String("1".to_string()), - serde_json::Value::String("Ada".to_string()), - ] - ); - } - - #[test] - fn parses_json_array_objects_with_union_columns() { - let parsed = - parse_json_bytes(br#"[{"id":1,"name":"Ada"},{"id":2,"active":true}]"#, 10).unwrap(); - - assert_eq!(parsed.columns, vec!["id", "name", "active"]); - assert_eq!(parsed.total_rows, 2); - assert_eq!( - parsed.rows[0], - vec![ - serde_json::json!(1), - serde_json::json!("Ada"), - serde_json::Value::Null, - ] - ); - assert_eq!( - parsed.rows[1], - vec![ - serde_json::json!(2), - serde_json::Value::Null, - serde_json::json!(true), - ] - ); - } - - #[test] - fn builds_import_insert_batches_from_mapped_columns() { - let mappings = vec![ - TableImportColumnMapping { - source_column: "id".to_string(), - target_column: "user_id".to_string(), - }, - TableImportColumnMapping { - source_column: "name".to_string(), - target_column: "display_name".to_string(), - }, - ]; - let data = ParsedImportFile { - columns: vec!["id".to_string(), "name".to_string(), "ignored".to_string()], - rows: vec![ - vec![ - serde_json::json!(1), - serde_json::json!("Ada"), - serde_json::json!("x"), - ], - vec![ - serde_json::json!(2), - serde_json::json!("O'Hara"), - serde_json::json!("y"), - ], - vec![ - serde_json::json!(3), - serde_json::Value::Null, - serde_json::json!("z"), - ], - ], - total_rows: 3, - }; - - let batches = build_import_insert_batches( - &data, - &mappings, - "users", - "public", - &DatabaseType::Postgres, - 2, - ) - .unwrap(); - - assert_eq!(batches, vec![ - ImportSqlBatch { - sql: "INSERT INTO \"public\".\"users\" (\"user_id\", \"display_name\") VALUES\n(1, 'Ada'),\n(2, 'O''Hara')".to_string(), - row_count: 2, - }, - ImportSqlBatch { - sql: "INSERT INTO \"public\".\"users\" (\"user_id\", \"display_name\") VALUES\n(3, NULL)".to_string(), - row_count: 1, - }, - ]); - } -} diff --git a/src-tauri/src/commands/transfer.rs b/src-tauri/src/commands/transfer.rs index 770d663ab1..e91db3bea5 100644 --- a/src-tauri/src/commands/transfer.rs +++ b/src-tauri/src/commands/transfer.rs @@ -1,628 +1,18 @@ -use std::collections::HashSet; use std::sync::Arc; -use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, State}; -use tokio::sync::RwLock; -use crate::commands::connection::{AppState, PoolKind}; -use crate::db; -use crate::models::connection::DatabaseType; +use crate::commands::connection::AppState; -static CANCELLED: std::sync::LazyLock>> = - std::sync::LazyLock::new(|| RwLock::new(HashSet::new())); - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransferRequest { - pub transfer_id: String, - pub source_connection_id: String, - pub source_database: String, - pub source_schema: String, - pub target_connection_id: String, - pub target_database: String, - pub target_schema: String, - pub tables: Vec, - pub create_table: bool, - pub truncate_before: bool, - pub batch_size: usize, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TransferProgress { - pub transfer_id: String, - pub table: String, - pub table_index: usize, - pub total_tables: usize, - pub rows_transferred: u64, - pub total_rows: Option, - pub status: TransferStatus, - pub error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum TransferStatus { - Running, - TableDone, - Done, - Error, - Cancelled, -} - -pub(crate) fn quote_identifier(name: &str, db_type: &DatabaseType) -> String { - match db_type { - DatabaseType::Mysql | DatabaseType::ClickHouse | DatabaseType::Doris | DatabaseType::StarRocks => format!("`{}`", name.replace('`', "``")), - DatabaseType::SqlServer => format!("[{}]", name.replace(']', "]]")), - _ => format!("\"{}\"", name.replace('"', "\"\"")), - } -} - -pub(crate) fn qualified_table(table: &str, schema: &str, db_type: &DatabaseType) -> String { - let qt = quote_identifier(table, db_type); - if schema.is_empty() { - qt - } else { - format!("{}.{}", quote_identifier(schema, db_type), qt) - } -} - -fn escape_value(val: &serde_json::Value, db_type: &DatabaseType) -> String { - match val { - serde_json::Value::Null => "NULL".to_string(), - serde_json::Value::Bool(b) => match db_type { - DatabaseType::Mysql | DatabaseType::Sqlite | DatabaseType::DuckDb | DatabaseType::Doris | DatabaseType::StarRocks => { - if *b { "1".to_string() } else { "0".to_string() } - } - _ => if *b { "TRUE".to_string() } else { "FALSE".to_string() }, - }, - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::String(s) => { - format!("'{}'", s.replace('\'', "''")) - } - _ => { - let s = val.to_string(); - format!("'{}'", s.replace('\'', "''")) - } - } -} - -fn map_column_type(source_type: &str, _source_db: &DatabaseType, target_db: &DatabaseType) -> String { - let t = source_type.to_lowercase(); - let base = t.split('(').next().unwrap_or(&t).trim(); - - match base { - "int" | "integer" | "int4" | "mediumint" => match target_db { - DatabaseType::Postgres => "INTEGER".into(), - DatabaseType::Mysql => "INT".into(), - DatabaseType::SqlServer => "INT".into(), - _ => "INTEGER".into(), - }, - "bigint" | "int8" => "BIGINT".into(), - "smallint" | "int2" => "SMALLINT".into(), - "tinyint" => match target_db { - DatabaseType::Postgres => "SMALLINT".into(), - _ => "TINYINT".into(), - }, - "serial" | "bigserial" | "smallserial" => match target_db { - DatabaseType::Postgres => source_type.to_uppercase(), - DatabaseType::Mysql => "BIGINT AUTO_INCREMENT".into(), - _ => "INTEGER".into(), - }, - "float" | "float4" | "real" => match target_db { - DatabaseType::Postgres => "REAL".into(), - _ => "FLOAT".into(), - }, - "double" | "double precision" | "float8" => match target_db { - DatabaseType::Postgres => "DOUBLE PRECISION".into(), - _ => "DOUBLE".into(), - }, - "decimal" | "numeric" | "number" => { - if t.contains('(') { - match target_db { - DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::SqlServer | DatabaseType::Oracle => { - format!("DECIMAL{}", &t[t.find('(').unwrap()..]) - } - _ => "NUMERIC".into(), - } - } else { - "NUMERIC".into() - } - } - "varchar" | "nvarchar" | "character varying" | "varchar2" => { - if t.contains('(') { - let len_part = &t[t.find('(').unwrap()..]; - match target_db { - DatabaseType::Postgres => format!("VARCHAR{len_part}"), - DatabaseType::Mysql => format!("VARCHAR{len_part}"), - DatabaseType::SqlServer => format!("NVARCHAR{len_part}"), - _ => format!("VARCHAR{len_part}"), - } - } else { - "VARCHAR(255)".into() - } - } - "char" | "nchar" | "character" => { - if t.contains('(') { - let len_part = &t[t.find('(').unwrap()..]; - format!("CHAR{len_part}") - } else { - "CHAR(1)".into() - } - } - "text" | "longtext" | "mediumtext" | "tinytext" | "clob" | "ntext" => "TEXT".into(), - "bool" | "boolean" => match target_db { - DatabaseType::Mysql => "TINYINT(1)".into(), - DatabaseType::SqlServer => "BIT".into(), - _ => "BOOLEAN".into(), - }, - "date" => "DATE".into(), - "time" => "TIME".into(), - "datetime" => match target_db { - DatabaseType::Postgres => "TIMESTAMP".into(), - _ => "DATETIME".into(), - }, - "timestamp" | "timestamptz" | "timestamp with time zone" - | "timestamp without time zone" => match target_db { - DatabaseType::Mysql => "DATETIME".into(), - DatabaseType::SqlServer => "DATETIME2".into(), - _ => "TIMESTAMP".into(), - }, - "blob" | "longblob" | "mediumblob" | "tinyblob" | "binary" | "varbinary" | "image" => { - match target_db { - DatabaseType::Postgres => "BYTEA".into(), - DatabaseType::Mysql => "BLOB".into(), - DatabaseType::SqlServer => "VARBINARY(MAX)".into(), - _ => "BLOB".into(), - } - } - "bytea" => match target_db { - DatabaseType::Postgres => "BYTEA".into(), - DatabaseType::Mysql => "BLOB".into(), - _ => "BLOB".into(), - }, - "json" | "jsonb" => match target_db { - DatabaseType::Postgres => "JSONB".into(), - DatabaseType::Mysql => "JSON".into(), - _ => "TEXT".into(), - }, - "uuid" => match target_db { - DatabaseType::Postgres => "UUID".into(), - _ => "VARCHAR(36)".into(), - }, - "bit" => match target_db { - DatabaseType::Postgres => "BOOLEAN".into(), - _ => "BIT".into(), - }, - _ => "TEXT".into(), - } -} - -fn generate_create_table_ddl( - columns: &[db::ColumnInfo], - table: &str, - schema: &str, - target_db: &DatabaseType, - source_db: &DatabaseType, -) -> String { - let full_table = qualified_table(table, schema, target_db); - - let col_lines: Vec = columns - .iter() - .map(|c| { - let mapped_type = map_column_type(&c.data_type, source_db, target_db); - let mut line = format!(" {} {}", quote_identifier(&c.name, target_db), mapped_type); - if !c.is_nullable { - line.push_str(" NOT NULL"); - } - line - }) - .collect(); - - let pks: Vec = columns - .iter() - .filter(|c| c.is_primary_key) - .map(|c| quote_identifier(&c.name, target_db)) - .collect(); - - let mut ddl = match target_db { - DatabaseType::SqlServer => format!("IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{table}')\n"), - _ => String::new(), - }; - - let create_prefix = match target_db { - DatabaseType::SqlServer => "CREATE TABLE", - _ => "CREATE TABLE IF NOT EXISTS", - }; - - ddl.push_str(&format!("{create_prefix} {full_table} (\n")); - ddl.push_str(&col_lines.join(",\n")); - - if !pks.is_empty() { - ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.join(", "))); - } - - ddl.push_str("\n)"); - - if matches!(target_db, DatabaseType::ClickHouse) { - ddl.push_str(" ENGINE = MergeTree() ORDER BY tuple()"); - } - - ddl -} - -pub(crate) fn generate_insert( - columns: &[String], - rows: &[Vec], - table: &str, - schema: &str, - db_type: &DatabaseType, -) -> String { - if rows.is_empty() { - return String::new(); - } - - let full_table = qualified_table(table, schema, db_type); - let col_list = columns - .iter() - .map(|c| quote_identifier(c, db_type)) - .collect::>() - .join(", "); - - let value_rows: Vec = rows - .iter() - .map(|row| { - let vals: Vec = row.iter().map(|v| escape_value(v, db_type)).collect(); - format!("({})", vals.join(", ")) - }) - .collect(); - - format!("INSERT INTO {full_table} ({col_list}) VALUES\n{}", value_rows.join(",\n")) -} - -fn pagination_sql( - columns: &[String], - table: &str, - schema: &str, - db_type: &DatabaseType, - offset: u64, - limit: usize, -) -> String { - let full_table = qualified_table(table, schema, db_type); - let col_list = columns - .iter() - .map(|c| quote_identifier(c, db_type)) - .collect::>() - .join(", "); - - match db_type { - DatabaseType::SqlServer | DatabaseType::Oracle => { - format!( - "SELECT {col_list} FROM {full_table} ORDER BY (SELECT NULL) OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" - ) - } - _ => { - format!("SELECT {col_list} FROM {full_table} LIMIT {limit} OFFSET {offset}") - } - } -} - -fn count_sql(table: &str, schema: &str, db_type: &DatabaseType) -> String { - let full_table = qualified_table(table, schema, db_type); - format!("SELECT COUNT(*) FROM {full_table}") -} - -pub(crate) async fn execute_on_pool( - state: &AppState, - pool_key: &str, - sql: &str, -) -> Result { - let connections = state.connections.lock().await; - let pool = connections.get(pool_key).ok_or("Connection not found")?; - - match pool { - PoolKind::Mysql(p, bare) => { - let p = p.clone(); - let bare = *bare; - drop(connections); - db::mysql::execute_query(&p, sql, bare).await - } - PoolKind::Postgres(p) => { - let p = p.clone(); - drop(connections); - db::postgres::execute_query(&p, sql).await - } - PoolKind::Sqlite(p) => { - let p = p.clone(); - drop(connections); - db::sqlite::execute_query(&p, sql).await - } - PoolKind::ClickHouse(client) => { - let client = client.clone(); - let database = pool_key.split(':').nth(1).unwrap_or("default").to_string(); - drop(connections); - db::clickhouse_driver::execute_query(&client, &database, sql).await - } - PoolKind::SqlServer(client) => { - let client = client.clone(); - drop(connections); - let mut client = client.lock().await; - db::sqlserver::execute_query(&mut client, sql).await - } - PoolKind::Oracle(client) => { - let client = client.clone(); - drop(connections); - let client = client.lock().await; - db::oracle_driver::execute_query(&*client, sql).await - } - PoolKind::DuckDb(con) => { - let con = con.clone(); - let sql = sql.to_string(); - drop(connections); - tokio::task::spawn_blocking(move || { - let con = con.lock().map_err(|e| e.to_string())?; - let start = std::time::Instant::now(); - let trimmed = sql.trim().to_uppercase(); - if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") - || trimmed.starts_with("DESCRIBE") || trimmed.starts_with("WITH") - || trimmed.starts_with("PRAGMA") - { - let mut stmt = con.prepare(&sql).map_err(|e| e.to_string())?; - let mut rows = stmt.query([]).map_err(|e| e.to_string())?; - let stmt_ref = rows.as_ref().ok_or("DuckDB statement unavailable")?; - let col_count = stmt_ref.column_count(); - let columns: Vec = (0..col_count) - .map(|i| stmt_ref.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "?".to_string())) - .collect(); - let mut result_rows = Vec::new(); - while let Some(row) = rows.next().map_err(|e| e.to_string())? { - let vals: Vec = (0..col_count).map(|i| { - row.get::<_, String>(i).map(serde_json::Value::String) - .or_else(|_| row.get::<_, i64>(i).map(|v| serde_json::Value::Number(v.into()))) - .or_else(|_| row.get::<_, f64>(i).map(|v| { - serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null) - })) - .or_else(|_| row.get::<_, bool>(i).map(serde_json::Value::Bool)) - .unwrap_or(serde_json::Value::Null) - }).collect(); - result_rows.push(vals); - } - Ok(db::QueryResult { columns, rows: result_rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated: false }) - } else { - let affected = con.execute(&sql, []).map_err(|e| e.to_string())?; - Ok(db::QueryResult { columns: vec![], rows: vec![], affected_rows: affected as u64, execution_time_ms: start.elapsed().as_millis(), truncated: false }) - } - }) - .await - .map_err(|e| e.to_string())? - } - _ => Err("Unsupported database type for transfer".to_string()), - } -} - -pub(crate) async fn get_db_type(state: &AppState, connection_id: &str) -> Result { - let configs = state.configs.lock().await; - configs - .get(connection_id) - .map(|c| c.db_type.clone()) - .ok_or_else(|| format!("Connection config not found: {connection_id}")) -} - -async fn get_columns_for_transfer( - state: &AppState, - pool_key: &str, - _connection_id: &str, - database: &str, - schema: &str, - table: &str, -) -> Result, String> { - let connections = state.connections.lock().await; - - if let Some(PoolKind::DuckDb(con)) = connections.get(pool_key) { - let con = con.clone(); - drop(connections); - let table = table.to_string(); - return tokio::task::spawn_blocking(move || { - let con = con.lock().map_err(|e| e.to_string())?; - // Inline DuckDB column query - let mut stmt = con.prepare( - "SELECT column_name, data_type, is_nullable, column_default - FROM information_schema.columns - WHERE table_schema = 'main' AND table_name = ? - ORDER BY ordinal_position" - ).map_err(|e| e.to_string())?; - let rows = stmt.query_map([&table], |row| { - Ok(db::ColumnInfo { - name: row.get::<_, String>(0)?, - data_type: row.get::<_, String>(1)?, - is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES", - column_default: row.get::<_, Option>(3)?, - is_primary_key: false, - extra: None, - comment: None, - numeric_precision: None, - numeric_scale: None, - character_maximum_length: None, - }) - }).map_err(|e| e.to_string())?; - Ok(rows.filter_map(|r| r.ok()).collect()) - }).await.map_err(|e| e.to_string())?; - } - - if let Some(PoolKind::ClickHouse(client)) = connections.get(pool_key) { - let client = client.clone(); - let database = database.to_string(); - let table = table.to_string(); - drop(connections); - return db::clickhouse_driver::get_columns(&client, &database, &table).await; - } - if let Some(PoolKind::SqlServer(client)) = connections.get(pool_key) { - let client = client.clone(); - let schema = schema.to_string(); - let table = table.to_string(); - drop(connections); - let mut client = client.lock().await; - return db::sqlserver::get_columns(&mut client, &schema, &table).await; - } - if let Some(PoolKind::Oracle(client)) = connections.get(pool_key) { - let client = client.clone(); - let schema = schema.to_string(); - let table = table.to_string(); - drop(connections); - let client = client.lock().await; - return db::oracle_driver::get_columns(&*client, &schema, &table).await; - } - - let pool = connections.get(pool_key).ok_or("Pool not found")?; - let schema = schema.to_string(); - let table = table.to_string(); - match pool { - PoolKind::Mysql(p, _) => { - let p = p.clone(); - drop(connections); - db::mysql::get_columns(&p, &schema, &table).await - } - PoolKind::Postgres(p) => { - let p = p.clone(); - drop(connections); - db::postgres::get_columns(&p, &schema, &table).await - } - PoolKind::Sqlite(p) => { - let p = p.clone(); - drop(connections); - db::sqlite::get_columns(&p, &schema, &table).await - } - _ => Err("Unsupported database type".to_string()), - } -} +// Re-export types and functions used by other modules +pub use dbx_core::transfer::{ + get_db_type, + TransferProgress, TransferRequest, TransferStatus, +}; fn emit_progress(app: &AppHandle, progress: TransferProgress) { let _ = app.emit("transfer-progress", progress); } -async fn is_cancelled(transfer_id: &str) -> bool { - CANCELLED.read().await.contains(transfer_id) -} - -async fn transfer_table( - app: &AppHandle, - state: &AppState, - request: &TransferRequest, - table: &str, - table_index: usize, - source_db_type: &DatabaseType, - target_db_type: &DatabaseType, - source_pool_key: &str, - target_pool_key: &str, -) -> Result { - let total_tables = request.tables.len(); - - // Get source columns (deduplicate by name) - let columns = { - let raw = get_columns_for_transfer( - state, source_pool_key, &request.source_connection_id, - &request.source_database, &request.source_schema, table, - ).await?; - let mut seen = std::collections::HashSet::new(); - raw.into_iter().filter(|c| seen.insert(c.name.clone())).collect::>() - }; - - if columns.is_empty() { - return Err(format!("No columns found for table {table}")); - } - - let col_names: Vec = columns.iter().map(|c| c.name.clone()).collect(); - log::info!("[transfer] {} has {} columns, counting rows...", table, columns.len()); - - // Count source rows - let total_rows = { - let sql = count_sql(table, &request.source_schema, source_db_type); - match execute_on_pool(state, source_pool_key, &sql).await { - Ok(result) => result.rows.first() - .and_then(|r| r.first()) - .and_then(|v| match v { - serde_json::Value::Number(n) => n.as_u64(), - serde_json::Value::String(s) => s.parse::().ok(), - _ => None, - }), - Err(e) => { - log::warn!("[transfer] count failed for {}: {}", table, e); - None - } - } - }; - log::info!("[transfer] {} total_rows={:?}", table, total_rows); - - // Create table on target if requested - if request.create_table { - let ddl = generate_create_table_ddl(&columns, table, &request.target_schema, target_db_type, source_db_type); - log::info!("[transfer] creating target table: {}", &ddl[..ddl.len().min(200)]); - if let Err(e) = execute_on_pool(state, target_pool_key, &ddl).await { - let err_lower = e.to_lowercase(); - if !err_lower.contains("already exists") && !err_lower.contains("there is already") { - return Err(format!("Failed to create table: {e}")); - } - } - } - - // Truncate target if requested - if request.truncate_before { - let full_table = qualified_table(table, &request.target_schema, target_db_type); - let truncate_sql = match target_db_type { - DatabaseType::Sqlite | DatabaseType::DuckDb => format!("DELETE FROM {full_table}"), - _ => format!("TRUNCATE TABLE {full_table}"), - }; - execute_on_pool(state, target_pool_key, &truncate_sql).await - .map_err(|e| format!("Failed to truncate: {e}"))?; - } - - // Transfer data in batches - let batch_size = if request.batch_size == 0 { 1000 } else { request.batch_size }; - let mut offset: u64 = 0; - let mut total_transferred: u64 = 0; - - loop { - if is_cancelled(&request.transfer_id).await { - return Err("Cancelled".to_string()); - } - - let sql = pagination_sql(&col_names, table, &request.source_schema, source_db_type, offset, batch_size); - let result = execute_on_pool(state, source_pool_key, &sql).await?; - let row_count = result.rows.len(); - - if row_count == 0 { - break; - } - - let insert_sql = generate_insert(&col_names, &result.rows, table, &request.target_schema, target_db_type); - if !insert_sql.is_empty() { - execute_on_pool(state, target_pool_key, &insert_sql).await - .map_err(|e| format!("Insert failed at offset {offset}: {e}"))?; - } - - total_transferred += row_count as u64; - log::info!("[transfer] {} batch +{} rows (total {})", table, row_count, total_transferred); - offset += row_count as u64; - - emit_progress(app, TransferProgress { - transfer_id: request.transfer_id.clone(), - table: table.to_string(), - table_index, - total_tables, - rows_transferred: total_transferred, - total_rows, - status: TransferStatus::Running, - error: None, - }); - - if row_count < batch_size { - break; - } - } - - Ok(total_transferred) -} - #[tauri::command] pub async fn start_transfer( app: AppHandle, @@ -649,7 +39,7 @@ pub async fn start_transfer( log::info!("[transfer] starting transfer_id={} tables={}", transfer_id, total_tables); for (i, table) in request.tables.iter().enumerate() { - if is_cancelled(&transfer_id).await { + if dbx_core::transfer::is_cancelled(&transfer_id).await { emit_progress(&app, TransferProgress { transfer_id: transfer_id.clone(), table: table.clone(), @@ -660,16 +50,17 @@ pub async fn start_transfer( status: TransferStatus::Cancelled, error: None, }); - CANCELLED.write().await.remove(&transfer_id); + dbx_core::transfer::clear_cancelled(&transfer_id).await; return; } log::info!("[transfer] table {}/{}: {}", i + 1, total_tables, table); - match transfer_table( - &app, &state, &request, table, i, + match dbx_core::transfer::transfer_table( + &state, &request, table, i, &source_db_type, &target_db_type, &source_pool_key, &target_pool_key, + |progress| emit_progress(&app, progress), ).await { Ok(rows) => { emit_progress(&app, TransferProgress { @@ -695,7 +86,7 @@ pub async fn start_transfer( status: TransferStatus::Cancelled, error: None, }); - CANCELLED.write().await.remove(&transfer_id); + dbx_core::transfer::clear_cancelled(&transfer_id).await; return; } emit_progress(&app, TransferProgress { @@ -708,13 +99,13 @@ pub async fn start_transfer( status: TransferStatus::Error, error: Some(e), }); - CANCELLED.write().await.remove(&transfer_id); + dbx_core::transfer::clear_cancelled(&transfer_id).await; return; } } } - CANCELLED.write().await.remove(&transfer_id); + dbx_core::transfer::clear_cancelled(&transfer_id).await; }); Ok(()) @@ -722,6 +113,6 @@ pub async fn start_transfer( #[tauri::command] pub async fn cancel_transfer(transfer_id: String) -> Result<(), String> { - CANCELLED.write().await.insert(transfer_id); + dbx_core::transfer::set_cancelled(&transfer_id).await; Ok(()) } diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index fdf9ef7c23..b001118c53 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -1,98 +1,8 @@ -use serde::{Deserialize, Serialize}; - -const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/t8y2/dbx/releases/latest"; - -#[derive(Debug, Deserialize)] -struct GithubRelease { - tag_name: String, - name: Option, - html_url: String, - body: Option, -} - -#[derive(Debug, Serialize)] -pub struct UpdateInfo { - pub current_version: String, - pub latest_version: String, - pub update_available: bool, - pub release_name: String, - pub release_url: String, - pub release_notes: String, -} +pub use dbx_core::update::UpdateInfo; #[tauri::command] pub async fn check_for_updates() -> Result { - let client = reqwest::Client::new(); - let release = client - .get(LATEST_RELEASE_URL) - .header(reqwest::header::USER_AGENT, "dbx-update-checker") - .send() - .await - .map_err(|e| format!("Failed to check updates: {e}"))? - .error_for_status() - .map_err(|e| format!("Failed to check updates: {e}"))? - .json::() - .await - .map_err(|e| format!("Failed to parse update response: {e}"))?; - - let current_version = env!("CARGO_PKG_VERSION").to_string(); - let latest_version = normalize_version(&release.tag_name); - - Ok(UpdateInfo { - update_available: is_newer_version(&latest_version, ¤t_version), - current_version, - latest_version, - release_name: release.name.unwrap_or_else(|| release.tag_name.clone()), - release_url: release.html_url, - release_notes: release.body.unwrap_or_default(), - }) -} - -fn normalize_version(version: &str) -> String { - version.trim().trim_start_matches('v').to_string() -} - -fn parse_version(version: &str) -> Vec { - normalize_version(version) - .split(['.', '-', '+']) - .map(|part| part.parse::().unwrap_or(0)) - .collect() -} - -fn is_newer_version(latest: &str, current: &str) -> bool { - let latest_parts = parse_version(latest); - let current_parts = parse_version(current); - let max_len = latest_parts.len().max(current_parts.len()); - - for i in 0..max_len { - let latest_part = *latest_parts.get(i).unwrap_or(&0); - let current_part = *current_parts.get(i).unwrap_or(&0); - if latest_part > current_part { - return true; - } - if latest_part < current_part { - return false; - } - } - - false -} - -#[cfg(test)] -mod tests { - use super::{is_newer_version, normalize_version}; - - #[test] - fn normalizes_tag_versions() { - assert_eq!(normalize_version("v1.2.3"), "1.2.3"); - assert_eq!(normalize_version(" 0.2.0 "), "0.2.0"); - } - - #[test] - fn compares_semver_like_versions() { - assert!(is_newer_version("0.2.1", "0.2.0")); - assert!(is_newer_version("1.0.0", "0.9.9")); - assert!(!is_newer_version("0.2.0", "0.2.0")); - assert!(!is_newer_version("0.1.9", "0.2.0")); - } + let release = dbx_core::update::fetch_latest_release().await?; + let current_version = env!("CARGO_PKG_VERSION"); + Ok(dbx_core::update::build_update_info(release, current_version)) } diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index c437419260..a74bedade7 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -1,74 +1,4 @@ -pub mod clickhouse_driver; -pub mod elasticsearch_driver; -pub mod mongo_driver; -pub mod mysql; -pub mod oracle_driver; -pub mod postgres; -pub mod redis_driver; -pub mod sqlite; -pub mod sqlserver; -pub mod ssh_tunnel; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseInfo { - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TableInfo { - pub name: String, - pub table_type: String, // "TABLE" or "VIEW" -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ColumnInfo { - pub name: String, - pub data_type: String, - pub is_nullable: bool, - pub column_default: Option, - pub is_primary_key: bool, - pub extra: Option, - pub comment: Option, - pub numeric_precision: Option, - pub numeric_scale: Option, - pub character_maximum_length: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QueryResult { - pub columns: Vec, - pub rows: Vec>, - pub affected_rows: u64, - pub execution_time_ms: u128, - #[serde(default)] - pub truncated: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IndexInfo { - pub name: String, - pub columns: Vec, - pub is_unique: bool, - pub is_primary: bool, - pub filter: Option, - pub index_type: Option, - pub included_columns: Option>, - pub comment: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ForeignKeyInfo { - pub name: String, - pub column: String, - pub ref_table: String, - pub ref_column: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TriggerInfo { - pub name: String, - pub event: String, - pub timing: String, -} +#[allow(unused_imports)] +pub use dbx_core::db::*; +#[allow(unused_imports)] +pub use dbx_core::types::*; diff --git a/src-tauri/src/models/connection.rs b/src-tauri/src/models/connection.rs index eefec7764c..f0c6f5d612 100644 --- a/src-tauri/src/models/connection.rs +++ b/src-tauri/src/models/connection.rs @@ -1,352 +1 @@ -use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConnectionConfig { - pub id: String, - pub name: String, - pub db_type: DatabaseType, - #[serde(default)] - pub driver_profile: Option, - #[serde(default)] - pub driver_label: Option, - #[serde(default)] - pub url_params: Option, - pub host: String, - pub port: u16, - pub username: String, - pub password: String, - pub database: Option, - #[serde(default)] - pub color: Option, - #[serde(default)] - pub ssh_enabled: bool, - #[serde(default)] - pub ssh_host: String, - #[serde(default = "default_ssh_port")] - pub ssh_port: u16, - #[serde(default)] - pub ssh_user: String, - #[serde(default)] - pub ssh_password: String, - #[serde(default)] - pub ssh_key_path: String, - #[serde(default)] - pub ssh_key_passphrase: String, - #[serde(default)] - pub ssh_expose_lan: bool, - #[serde(default)] - pub ssl: bool, - #[serde(default)] - pub connection_string: Option, -} - -fn default_ssh_port() -> u16 { - 22 -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum DatabaseType { - Mysql, - Postgres, - Sqlite, - Redis, - #[serde(rename = "duckdb")] - DuckDb, - #[serde(rename = "clickhouse")] - ClickHouse, - #[serde(rename = "sqlserver")] - SqlServer, - #[serde(rename = "mongodb")] - MongoDb, - #[serde(rename = "oracle")] - Oracle, - #[serde(rename = "elasticsearch")] - Elasticsearch, - Doris, - #[serde(rename = "starrocks")] - StarRocks, - Redshift, -} - -impl ConnectionConfig { - pub fn needs_bare_mysql(&self) -> bool { - matches!(self.db_type, DatabaseType::Doris | DatabaseType::StarRocks) - || self.driver_profile.as_deref().map(|p| p.to_lowercase()) - .is_some_and(|p| matches!(p.as_str(), "doris" | "starrocks" | "selectdb" | "tdengine")) - } - - pub fn connection_url(&self) -> String { - self.connection_url_with_host(&self.host, self.port) - } - - pub fn redacted_connection_url(&self) -> String { - self.redacted_connection_url_with_host(&self.host, self.port) - } - - pub fn redacted_connection_url_with_host(&self, host: &str, port: u16) -> String { - let host = bracket_ipv6(host); - let db_part = self - .database - .as_deref() - .filter(|d| !d.is_empty()) - .map(|d| format!("/{}", encode_url_part(d))) - .unwrap_or_default(); - let params = self.normalized_url_params(); - - match self.db_type { - DatabaseType::Sqlite | DatabaseType::DuckDb => { - format!("{}?mode=rwc", self.host) - } - DatabaseType::Redis => { - let scheme = if self.ssl { "rediss" } else { "redis" }; - format!("{scheme}://{host}:{port}/") - } - DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks => format!("mysql://{host}:{port}{db_part}?{params}"), - DatabaseType::Postgres | DatabaseType::Redshift => { - let suffix = if params.is_empty() { - String::new() - } else { - format!("?{params}") - }; - format!("postgres://{host}:{port}{db_part}{suffix}") - } - DatabaseType::ClickHouse => format!("http://{host}:{port}"), - DatabaseType::SqlServer => format!( - "server=tcp:{host},{port};database={}", - self.database.as_deref().unwrap_or("master") - ), - DatabaseType::MongoDb => { - if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { - return cs.to_string(); - } - format!("mongodb://{host}:{port}{db_part}") - } - DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), - DatabaseType::Elasticsearch => format!("http://{host}:{port}"), - } - } - - pub fn connection_url_with_host(&self, host: &str, port: u16) -> String { - let host = bracket_ipv6(host); - let db_part = self - .database - .as_deref() - .filter(|d| !d.is_empty()) - .map(|d| format!("/{}", encode_url_part(d))) - .unwrap_or_default(); - let username = encode_url_part(&self.username); - let password = encode_url_part(&self.password); - let params = self.normalized_url_params(); - - match self.db_type { - DatabaseType::Sqlite | DatabaseType::DuckDb => { - format!("{}?mode=rwc", self.host) - } - DatabaseType::Redis => { - let scheme = if self.ssl { "rediss" } else { "redis" }; - if self.username.is_empty() && self.password.is_empty() { - format!("{scheme}://{host}:{port}/") - } else if self.username.is_empty() { - format!("{scheme}://:{password}@{host}:{port}/") - } else { - format!("{scheme}://{username}:{password}@{host}:{port}/") - } - } - DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks => format!( - "mysql://{}:{}@{host}:{port}{db_part}?{params}", - username, password - ), - DatabaseType::Postgres | DatabaseType::Redshift => { - let suffix = if params.is_empty() { - String::new() - } else { - format!("?{params}") - }; - format!( - "postgres://{}:{}@{host}:{port}{db_part}{suffix}", - username, password - ) - } - DatabaseType::ClickHouse => format!("http://{host}:{port}"), - DatabaseType::SqlServer => format!( - "server=tcp:{host},{port};user={};password={};database={}", - self.username, - self.password, - self.database.as_deref().unwrap_or("master") - ), - DatabaseType::MongoDb => { - if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) { - return cs.to_string(); - } - if self.username.is_empty() { - format!("mongodb://{host}:{port}{db_part}") - } else { - format!("mongodb://{username}:{password}@{host}:{port}{db_part}") - } - } - DatabaseType::Oracle => { - format!("oracle://{}:{}@{host}:{port}{db_part}", username, password) - } - DatabaseType::Elasticsearch => format!("http://{host}:{port}"), - } - } - - fn normalized_url_params(&self) -> String { - let value = self.url_params.as_deref().unwrap_or("").trim(); - if self.needs_bare_mysql() { - let v = value.trim_start_matches('?'); - let filtered: Vec<&str> = v.split('&') - .filter(|p| !p.is_empty() && !p.starts_with("charset=") && !p.starts_with("ssl-mode=preferred")) - .collect(); - return if filtered.is_empty() { "ssl-mode=disabled".to_string() } else { format!("ssl-mode=disabled&{}", filtered.join("&")) }; - } - match self.db_type { - DatabaseType::Mysql => { - let base = "ssl-mode=preferred&charset=utf8mb4"; - if value.is_empty() { - base.to_string() - } else if value.contains("ssl-mode=") { - let v = value.trim_start_matches('?'); - if v.contains("charset=") { v.to_string() } else { format!("{v}&charset=utf8mb4") } - } else { - let v = value.trim_start_matches('?'); - if v.contains("charset=") { format!("ssl-mode=preferred&{v}") } else { format!("{base}&{v}") } - } - } - DatabaseType::Doris | DatabaseType::StarRocks => { - let v = value.trim_start_matches('?'); - let filtered: Vec<&str> = v.split('&') - .filter(|p| !p.is_empty() && !p.starts_with("charset=") && !p.starts_with("ssl-mode=preferred")) - .collect(); - if filtered.is_empty() { "ssl-mode=disabled".to_string() } else { format!("ssl-mode=disabled&{}", filtered.join("&")) } - } - DatabaseType::Postgres | DatabaseType::Redshift => value.trim_start_matches('?').to_string(), - _ => value.trim_start_matches('?').to_string(), - } - } -} - -fn encode_url_part(value: &str) -> String { - utf8_percent_encode(value, NON_ALPHANUMERIC).to_string() -} - -fn bracket_ipv6(host: &str) -> String { - if host.contains(':') && !host.starts_with('[') { - format!("[{host}]") - } else { - host.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::{ConnectionConfig, DatabaseType}; - - fn mysql_config(username: &str, password: &str, database: Option<&str>) -> ConnectionConfig { - ConnectionConfig { - id: "id".to_string(), - name: "name".to_string(), - db_type: DatabaseType::Mysql, - driver_profile: None, - driver_label: None, - url_params: None, - host: "10.1.2.3".to_string(), - port: 2883, - username: username.to_string(), - password: password.to_string(), - database: database.map(str::to_string), - color: None, - ssh_enabled: false, - ssh_host: String::new(), - ssh_port: 22, - ssh_user: String::new(), - ssh_password: String::new(), - ssh_key_path: String::new(), - ssh_expose_lan: false, - ssl: false, - connection_string: None, - } - } - - #[test] - fn mysql_url_encodes_oceanbase_username() { - let config = mysql_config("user@tenant#cluster", "secret", None); - - assert_eq!( - config.connection_url(), - "mysql://user%40tenant%23cluster:secret@10.1.2.3:2883?ssl-mode=preferred&charset=utf8mb4" - ); - } - - #[test] - fn mysql_url_encodes_password_and_database() { - let config = mysql_config("root", "p@ss:word#1", Some("db/name")); - - assert_eq!( - config.connection_url(), - "mysql://root:p%40ss%3Aword%231@10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4" - ); - } - - #[test] - fn mysql_url_appends_custom_params() { - let mut config = mysql_config("root", "secret", Some("test")); - config.url_params = Some("charset=utf8mb4".to_string()); - - assert_eq!( - config.connection_url(), - "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4" - ); - } - - #[test] - fn postgres_url_appends_custom_params() { - let mut config = mysql_config("postgres", "secret", Some("test")); - config.db_type = DatabaseType::Postgres; - config.url_params = Some("sslmode=disable".to_string()); - - assert_eq!( - config.connection_url(), - "postgres://postgres:secret@10.1.2.3:2883/test?sslmode=disable" - ); - } - - #[test] - fn redacted_mysql_url_omits_credentials() { - let config = mysql_config("user@tenant#cluster", "p@ss:word#1", Some("db/name")); - - let url = config.redacted_connection_url(); - - assert_eq!(url, "mysql://10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4"); - assert!(!url.contains("user")); - assert!(!url.contains("p%40ss")); - assert!(!url.contains("p@ss")); - } - - #[test] - fn redacted_sqlserver_url_omits_credentials() { - let mut config = mysql_config("sa", "super-secret", Some("master")); - config.db_type = DatabaseType::SqlServer; - - let url = config.redacted_connection_url(); - - assert_eq!(url, "server=tcp:10.1.2.3,2883;database=master"); - assert!(!url.contains("sa")); - assert!(!url.contains("super-secret")); - } - - #[test] - fn redacted_redis_url_omits_credentials_and_keeps_tls_scheme() { - let mut config = mysql_config("default", "redis-secret", None); - config.db_type = DatabaseType::Redis; - config.ssl = true; - - let url = config.redacted_connection_url(); - - assert_eq!(url, "rediss://10.1.2.3:2883/"); - assert!(!url.contains("default")); - assert!(!url.contains("redis-secret")); - } -} +pub use dbx_core::models::connection::*;