From c73fa18bf3ae9e7c206fc5b6cf619cd6a535e3af Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Fri, 9 Oct 2026 17:01:53 +0200 Subject: [PATCH] feat: add read-only PostgreSQL inspection TUI --- .gitignore | 1 + Cargo.lock | 1598 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 12 + src/app.rs | 408 +++++++++++++ src/db.rs | 286 +++++++++ src/main.rs | 87 +++ 6 files changed, 2392 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/app.rs create mode 100644 src/db.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fb8d64d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1598 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[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 = "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 = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "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 = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dbdash" +version = "0.1.0" +dependencies = [ + "crossterm", + "native-tls", + "postgres", + "postgres-native-tls", + "ratatui", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[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.119", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[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", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[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.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "line-clipping" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys", +] + +[[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", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "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.119", +] + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + +[[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", + "smallvec", + "windows-link", +] + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "postgres" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ad20e0aa0b24f5a394eab4f78c781d248982b22b25cecc7e3aa46a681605bd" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-native-tls" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef4de47bb81477e0c3deaf153a1b10ae176484713ff1640969f4cb96b653ebc" +dependencies = [ + "native-tls", + "tokio", + "tokio-native-tls", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags", + "compact_str", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "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 = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[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 = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[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-postgres" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..392db14 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dbdash" +version = "0.1.0" +edition = "2024" +description = "Read-only PostgreSQL inspection in your terminal" + +[dependencies] +ratatui = { version = "0.30.2", default-features = false, features = ["crossterm"] } +crossterm = "0.29" +postgres = "0.19" +postgres-native-tls = "0.5" +native-tls = "0.2" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..6e8bdc1 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,408 @@ +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use ratatui::{ + DefaultTerminal, Frame, + layout::{Constraint, Layout}, + style::{Color, Modifier, Style}, + widgets::{Block, Cell, List, ListItem, ListState, Paragraph, Row, Table, TableState}, +}; + +use crate::{ + Result, + db::{Column, Database, PAGE_SIZE, Page, Relation}, + error_text, +}; + +pub struct App { + db: Database, + relations: Vec, + selection: ListState, + active: Option, + columns: Vec, + page: Page, + page_index: usize, + rows_state: TableState, + schema_state: TableState, + column_offset: usize, + sidebar_focus: bool, + show_schema: bool, + error: Option, +} + +impl App { + pub fn new(mut db: Database) -> Result { + let relations = db.relations()?; + let selected = (!relations.is_empty()).then_some(0); + let mut app = Self { + db, + relations, + selection: ListState::default().with_selected(selected), + active: None, + columns: Vec::new(), + page: Page { + rows: Vec::new(), + has_next: false, + }, + page_index: 0, + rows_state: TableState::default(), + schema_state: TableState::default(), + column_offset: 0, + sidebar_focus: true, + show_schema: false, + error: None, + }; + app.open_selection(); + Ok(app) + } + + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; + if let Event::Key(key) = event::read()? + && matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) + && self.handle_key(key) + { + return Ok(()); + } + } + } + + fn handle_key(&mut self, key: KeyEvent) -> bool { + if key.code == KeyCode::Char('q') + || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)) + { + return true; + } + match key.code { + KeyCode::Tab | KeyCode::BackTab => self.sidebar_focus = !self.sidebar_focus, + KeyCode::Enter if self.sidebar_focus => { + self.open_selection(); + self.sidebar_focus = false; + } + KeyCode::Esc => self.sidebar_focus = true, + KeyCode::Down | KeyCode::Char('j') => self.move_selection(1), + KeyCode::Up | KeyCode::Char('k') => self.move_selection(-1), + KeyCode::Home | KeyCode::Char('g') => self.move_selection(isize::MIN), + KeyCode::End | KeyCode::Char('G') => self.move_selection(isize::MAX), + KeyCode::Char('s') => { + self.show_schema = !self.show_schema; + self.column_offset = 0; + } + KeyCode::Left | KeyCode::Char('h') if !self.sidebar_focus => { + self.column_offset = self.column_offset.saturating_sub(1); + } + KeyCode::Right | KeyCode::Char('l') if !self.sidebar_focus => { + let count = if self.show_schema { + 5 + } else { + self.columns.len() + }; + if self.column_offset + 1 < count { + self.column_offset += 1; + } + } + KeyCode::Char('n') | KeyCode::PageDown if !self.show_schema && self.page.has_next => { + self.load_page(self.page_index + 1); + } + KeyCode::Char('p') | KeyCode::PageUp if !self.show_schema && self.page_index > 0 => { + self.load_page(self.page_index - 1); + } + KeyCode::Char('r') => self.refresh(), + _ => {} + } + false + } + + fn move_selection(&mut self, delta: isize) { + if self.sidebar_focus { + let next = moved_index(self.selection.selected(), self.relations.len(), delta); + self.selection.select(next); + } else if self.show_schema { + let next = moved_index(self.schema_state.selected(), self.columns.len(), delta); + self.schema_state.select(next); + } else { + let next = moved_index(self.rows_state.selected(), self.page.rows.len(), delta); + self.rows_state.select(next); + } + } + + fn open_selection(&mut self) { + self.active = self.selection.selected(); + self.columns.clear(); + self.page = Page { + rows: Vec::new(), + has_next: false, + }; + self.page_index = 0; + self.column_offset = 0; + self.rows_state = TableState::default(); + self.schema_state = TableState::default(); + self.error = None; + if let Some(index) = self.active { + match self.db.columns(&self.relations[index]) { + Ok(columns) => { + self.columns = columns; + self.schema_state + .select((!self.columns.is_empty()).then_some(0)); + self.load_page(0); + } + Err(error) => self.error = Some(error_text(error.as_ref())), + } + } + } + + fn load_page(&mut self, index: usize) { + if let Some(active) = self.active { + // ponytail: synchronous reads block input; server timeout is 5s, use a worker if responsiveness matters. + match self.db.page(&self.relations[active], &self.columns, index) { + Ok(page) => { + self.rows_state = + TableState::default().with_selected((!page.rows.is_empty()).then_some(0)); + self.page = page; + self.page_index = index; + self.error = None; + } + Err(error) => self.error = Some(error_text(error.as_ref())), + } + } + } + + fn refresh(&mut self) { + match self.db.relations() { + Ok(relations) => { + let oid = self.active.map(|index| self.relations[index].oid); + let selected = relations + .iter() + .position(|relation| Some(relation.oid) == oid) + .or_else(|| (!relations.is_empty()).then_some(0)); + self.relations = relations; + self.selection = ListState::default().with_selected(selected); + self.open_selection(); + } + Err(error) => self.error = Some(error_text(error.as_ref())), + } + } + + fn draw(&mut self, frame: &mut Frame) { + let area = frame.area(); + if area.width < 70 || area.height < 12 { + frame.render_widget( + Paragraph::new("Enlarge terminal to at least 70×12. q to quit."), + area, + ); + return; + } + let [header, body, status, help] = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(4), + Constraint::Length(2), + Constraint::Length(2), + ]) + .areas(area); + frame.render_widget( + Paragraph::new(" dbdash │ PostgreSQL │ READ ONLY") + .style(Style::default().fg(Color::Cyan)), + header, + ); + let [sidebar, content] = + Layout::horizontal([Constraint::Length(28), Constraint::Min(1)]).areas(body); + let sidebar_title = format!(" Relations ({}) ", self.relations.len()); + let sidebar_block = panel(&sidebar_title, self.sidebar_focus); + if self.relations.is_empty() { + frame.render_widget( + Paragraph::new("No readable tables or views.\nr to refresh.").block(sidebar_block), + sidebar, + ); + } else { + let list = List::new( + self.relations + .iter() + .map(|relation| ListItem::new(relation.label.as_str())), + ) + .block(sidebar_block) + .highlight_style(selected_style()) + .highlight_symbol("› "); + frame.render_stateful_widget(list, sidebar, &mut self.selection); + } + let title = if self.show_schema { + " Schema · s for rows " + } else { + " Rows · s for schema " + }; + let content_block = panel(title, !self.sidebar_focus); + let count = if self.show_schema { + 5 + } else { + self.columns.len() + }; + let visible = usize::from(content.width.saturating_sub(4) / 21) + .max(1) + .min(count.saturating_sub(self.column_offset)); + let widths = (0..visible).map(|index| { + if index + 1 == visible { + Constraint::Min(1) + } else { + Constraint::Length(20) + } + }); + if self.active.is_none() { + frame.render_widget( + Paragraph::new("Choose a relation and press Enter.").block(content_block), + content, + ); + } else if self.show_schema { + let headers = ["Column", "Type", "Nullable", "Key", "Default"]; + let rows = self.columns.iter().map(|column| { + Row::new( + [ + column.label.as_str(), + column.data_type.as_str(), + if column.nullable { "yes" } else { "no" }, + if column.primary_key { "PK" } else { "" }, + column.default.as_str(), + ] + .into_iter() + .skip(self.column_offset) + .take(visible), + ) + }); + let table = Table::new(rows, widths) + .header( + Row::new(headers.into_iter().skip(self.column_offset).take(visible)) + .style(header_style()), + ) + .block(content_block) + .row_highlight_style(selected_style()) + .highlight_symbol("› "); + frame.render_stateful_widget(table, content, &mut self.schema_state); + } else if self.page.rows.is_empty() { + frame.render_widget( + Paragraph::new(if self.error.is_some() { + "Could not load rows. See error below." + } else { + "No rows." + }) + .block(content_block), + content, + ); + } else { + let headers = self + .columns + .iter() + .skip(self.column_offset) + .take(visible) + .map(|column| column.label.as_str()); + let rows = self.page.rows.iter().map(|row| { + Row::new( + row.iter() + .skip(self.column_offset) + .take(visible) + .map(|value| match value { + Some(value) => Cell::from(value.as_str()), + None => Cell::from("NULL").style( + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::ITALIC), + ), + }), + ) + }); + let table = Table::new(rows, widths) + .header(Row::new(headers).style(header_style())) + .block(content_block) + .row_highlight_style(selected_style()) + .highlight_symbol("› "); + frame.render_stateful_widget(table, content, &mut self.rows_state); + } + let description = if let Some(index) = self.active { + let relation = &self.relations[index]; + if self.show_schema { + format!( + " {} [{}] · {} columns · fields {}–{}/5", + relation.label, + relation.kind, + self.columns.len(), + self.column_offset + 1, + self.column_offset + visible + ) + } else { + let start = if self.page.rows.is_empty() { + 0 + } else { + self.page_index * PAGE_SIZE + 1 + }; + let end = self.page_index * PAGE_SIZE + self.page.rows.len(); + format!( + " {} [{}] · rows {start}–{end} · page {}{} · columns {}–{}/{}", + relation.label, + relation.kind, + self.page_index + 1, + if self.page.has_next { " (more)" } else { "" }, + if count > 0 { self.column_offset + 1 } else { 0 }, + self.column_offset + visible, + count + ) + } + } else { + " No readable relations. r to refresh.".into() + }; + let [description_area, error_area] = + Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(status); + frame.render_widget(Paragraph::new(description), description_area); + if let Some(error) = &self.error { + frame.render_widget( + Paragraph::new(error.as_str()).style(Style::default().fg(Color::Red)), + error_area, + ); + } + frame.render_widget(Paragraph::new( + " Tab focus · ↑↓/jk move · Enter open · s schema/rows\n ←→/hl columns · n/p page · r refresh · q/Ctrl-C quit" + ).style(Style::default().fg(Color::DarkGray)), help); + } +} + +fn panel(title: &str, focused: bool) -> Block<'_> { + Block::bordered() + .title(title) + .border_style(Style::default().fg(if focused { + Color::Cyan + } else { + Color::DarkGray + })) +} + +fn selected_style() -> Style { + Style::default().bg(Color::Blue).fg(Color::White) +} + +fn header_style() -> Style { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) +} + +fn moved_index(selected: Option, count: usize, delta: isize) -> Option { + if count == 0 { + return None; + } + Some( + selected + .unwrap_or(0) + .saturating_add_signed(delta) + .min(count - 1), + ) +} + +#[cfg(test)] +mod tests { + use super::moved_index; + + #[test] + fn navigation_clamps_to_available_items() { + assert_eq!(moved_index(None, 0, 1), None); + assert_eq!(moved_index(None, 3, 1), Some(1)); + assert_eq!(moved_index(Some(0), 3, -1), Some(0)); + assert_eq!(moved_index(Some(2), 3, 1), Some(2)); + assert_eq!(moved_index(Some(1), 3, isize::MIN), Some(0)); + assert_eq!(moved_index(Some(1), 3, isize::MAX), Some(2)); + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..838514e --- /dev/null +++ b/src/db.rs @@ -0,0 +1,286 @@ +use std::time::Duration; + +use native_tls::TlsConnector; +use postgres::{Client, Config}; +use postgres_native_tls::MakeTlsConnector; + +use crate::Result; + +pub const PAGE_SIZE: usize = 100; + +pub struct Database { + client: Client, +} + +pub struct Relation { + pub oid: u32, + pub schema: String, + pub name: String, + pub label: String, + pub kind: String, + physical_order: bool, +} + +pub struct Column { + pub name: String, + pub label: String, + pub data_type: String, + pub nullable: bool, + pub primary_key: bool, + pub default: String, +} + +pub struct Page { + pub rows: Vec>>, + pub has_next: bool, +} + +impl Database { + pub fn connect(connection: &str) -> Result { + let mut config: Config = connection.parse()?; + if config.get_connect_timeout().is_none() { + config.connect_timeout(Duration::from_secs(5)); + } + let tls = MakeTlsConnector::new(TlsConnector::new()?); + let mut client = config.connect(tls)?; + // Views can invoke functions: enforce read-only on the server, not just in the UI. + client.batch_execute( + "SET default_transaction_read_only = on; SET statement_timeout = '5s';", + )?; + Ok(Self { client }) + } + + pub fn relations(&mut self) -> Result> { + let rows = self.client.query( + "SELECT c.oid, n.nspname, c.relname, c.relkind::text + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f') + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND n.nspname !~ '^pg_(toast|temp)' + AND pg_catalog.has_schema_privilege(n.oid, 'USAGE') + AND pg_catalog.has_table_privilege(c.oid, 'SELECT') + ORDER BY n.nspname, c.relname", + &[], + )?; + Ok(rows + .into_iter() + .map(|row| { + let schema: String = row.get(1); + let name: String = row.get(2); + let kind: &str = row.get(3); + Relation { + oid: row.get(0), + label: display_text(&format!("{schema}.{name}")), + schema, + name, + physical_order: matches!(kind, "r" | "p" | "m"), + kind: match kind { + "v" => "view", + "m" => "materialized view", + "f" => "foreign table", + "p" => "partitioned table", + _ => "table", + } + .into(), + } + }) + .collect()) + } + + pub fn columns(&mut self, relation: &Relation) -> Result> { + let rows = self.client.query( + "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), + NOT a.attnotnull, + EXISTS (SELECT FROM pg_catalog.pg_index i + WHERE i.indrelid = a.attrelid AND i.indisprimary + AND a.attnum = ANY(i.indkey)), + COALESCE(pg_catalog.pg_get_expr(d.adbin, d.adrelid), '') + FROM pg_catalog.pg_attribute a + LEFT JOIN pg_catalog.pg_attrdef d + ON d.adrelid = a.attrelid AND d.adnum = a.attnum + WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum", + &[&relation.oid], + )?; + Ok(rows + .into_iter() + .map(|row| { + let name: String = row.get(0); + Column { + label: display_text(&name), + name, + data_type: display_text(row.get(1)), + nullable: row.get(2), + primary_key: row.get(3), + default: display_text(row.get(4)), + } + }) + .collect()) + } + + pub fn page(&mut self, relation: &Relation, columns: &[Column], page: usize) -> Result { + let projection = columns + .iter() + .map(|column| format!("t.{}::text", quote_identifier(&column.name))) + .collect::>() + .join(", "); + let primary_key = columns + .iter() + .filter(|column| column.primary_key) + .map(|column| format!("t.{}", quote_identifier(&column.name))) + .collect::>() + .join(", "); + let order = if !primary_key.is_empty() { + format!(" ORDER BY {primary_key}") + } else if relation.physical_order { + " ORDER BY t.tableoid, t.ctid".into() + } else { + // ponytail: keyless views have no stable row identity; use keyset paging if a key is provided. + String::new() + }; + // ponytail: OFFSET is simple but slows on deep pages; switch to keyset paging if that matters. + let sql = format!( + "SELECT {projection} FROM {}.{} AS t{order} LIMIT $1 OFFSET $2", + quote_identifier(&relation.schema), + quote_identifier(&relation.name), + ); + let offset = i64::try_from(page)? + .checked_mul(PAGE_SIZE as i64) + .ok_or("page offset overflow")?; + let result = self + .client + .query(&sql, &[&((PAGE_SIZE + 1) as i64), &offset])?; + let has_next = result.len() > PAGE_SIZE; + let rows = result + .into_iter() + .take(PAGE_SIZE) + .map(|row| { + (0..columns.len()) + .map(|index| row.get::<_, Option<&str>>(index).map(display_text)) + .collect() + }) + .collect(); + Ok(Page { rows, has_next }) + } +} + +fn quote_identifier(value: &str) -> String { + format!("\"{}\"", value.replace('"', "\"\"")) +} + +/// Database text must never emit terminal control sequences. +pub fn display_text(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + for character in value.chars() { + if character.is_control() { + output.extend(character.escape_debug()); + } else { + output.push(character); + } + } + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identifiers_and_terminal_controls_are_safe() { + assert_eq!( + quote_identifier("a\"; DROP TABLE x;--"), + "\"a\"\"; DROP TABLE x;--\"" + ); + assert_eq!(display_text("é🙂\n\t\r\u{1b}[2J"), "é🙂\\n\\t\\r\\u{1b}[2J"); + } + + #[test] + #[ignore = "requires DBDASH_TEST_DATABASE_URL pointing at a disposable PostgreSQL database"] + fn inspect_postgres_read_only() -> Result<()> { + let connection = std::env::var("DBDASH_TEST_DATABASE_URL")?; + let mut fixture = connection + .parse::()? + .connect(MakeTlsConnector::new(TlsConnector::new()?))?; + let schema = format!("dbdash_test_{}", std::process::id()); + fixture.batch_execute(&format!("CREATE SCHEMA {schema}"))?; + struct Cleanup(Client, String); + impl Drop for Cleanup { + fn drop(&mut self) { + let _ = self + .0 + .batch_execute(&format!("DROP SCHEMA {} CASCADE", self.1)); + } + } + let mut cleanup = Cleanup(fixture, schema.clone()); + cleanup.0.batch_execute(&format!( + "CREATE TABLE {schema}.\"odd\"\"table\" ( + id integer PRIMARY KEY, \"odd\"\"column\" text, + amount numeric NOT NULL DEFAULT 1.25, details jsonb, + tags text[], bytes bytea, happened timestamp); + INSERT INTO {schema}.\"odd\"\"table\" + SELECT n, CASE WHEN n = 1 THEN NULL WHEN n = 2 THEN E'line\\nnext' ELSE 'NULL' END, + 1.25, '{{\"ok\": true}}', ARRAY['a', 'b'], '\\x00ff', + TIMESTAMP '2020-01-01 12:00:00' + FROM generate_series(1, 101) n; + CREATE VIEW {schema}.sample_view AS SELECT id FROM {schema}.\"odd\"\"table\"; + CREATE TABLE {schema}.empty_table (id integer);" + ))?; + let mut db = Database::connect(&connection)?; + let relations = db.relations()?; + let table = relations + .iter() + .find(|r| r.schema == schema && r.name == "odd\"table") + .unwrap(); + assert!( + relations + .iter() + .any(|r| r.schema == schema && r.kind == "view") + ); + assert!(!relations.iter().any(|r| r.schema == "pg_catalog")); + let columns = db.columns(table)?; + assert!(columns[0].primary_key); + assert!(!columns[0].nullable); + assert_eq!(columns[1].name, "odd\"column"); + assert!(columns[1].nullable); + assert_eq!(columns[2].data_type, "numeric"); + assert_eq!(columns[2].default, "1.25"); + let first = db.page(table, &columns, 0)?; + assert!(first.has_next); + assert_eq!(first.rows.len(), 100); + assert_eq!( + first.rows[0], + vec![ + Some("1".into()), + None, + Some("1.25".into()), + Some("{\"ok\": true}".into()), + Some("{a,b}".into()), + Some("\\x00ff".into()), + Some("2020-01-01 12:00:00".into()) + ] + ); + assert_eq!(first.rows[1][1].as_deref(), Some("line\\nnext")); + assert_eq!(first.rows[2][1].as_deref(), Some("NULL")); + let second = db.page(table, &columns, 1)?; + assert!(!second.has_next); + assert_eq!(second.rows[0][0].as_deref(), Some("101")); + let empty = relations + .iter() + .find(|r| r.schema == schema && r.name == "empty_table") + .unwrap(); + let columns = db.columns(empty)?; + let page = db.page(empty, &columns, 0)?; + assert!(page.rows.is_empty()); + assert!(!page.has_next); + let error = db + .client + .batch_execute(&format!("DELETE FROM {schema}.empty_table")) + .unwrap_err(); + assert_eq!( + error.code(), + Some(&postgres::error::SqlState::READ_ONLY_SQL_TRANSACTION) + ); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9e79bf2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,87 @@ +mod app; +mod db; + +use std::{error::Error, io::IsTerminal, process::ExitCode}; + +type Result> = std::result::Result; + +const HELP: &str = r#"dbdash — read-only PostgreSQL inspection + +Usage: + dbdash [POSTGRES_CONNECTION_STRING] + dbdash --help + +With no argument, the connection is read from DATABASE_URL. +Prefer the environment variable to keep passwords out of process arguments: + export DATABASE_URL='postgresql://user@localhost:5432/database' + cargo run + +PostgreSQL URLs and libpq-style key=value strings are accepted. +Use sslmode=require for mandatory TLS; certificates and hostnames are verified. +Use sslmode=disable only for trusted local connections. +The terminal must be at least 70 columns wide and 12 lines high. + +Navigation: + Tab / Esc Switch focus / return to relations + Up/Down, j/k Move selection (Enter opens a relation) + Home/End, g/G First / last item + s Toggle rows and column schema + Left/Right Scroll columns (h/l also work) + n/p, PgDn/Up Next / previous 100-row page + r Refresh relations and the active relation + q / Ctrl-C Quit + +Reads are enforced read-only by PostgreSQL and have a 5-second query timeout. +The interface waits for reads to complete. NULL cells are dimmed. +Pages use primary-key order, or physical order for keyless tables. +Keyless views have no guaranteed order; concurrent changes can shift pages. +"#; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("dbdash: {}", error_text(error.as_ref())); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<()> { + let mut args = std::env::args_os().skip(1); + let connection = match args.next() { + Some(argument) if argument == "--help" || argument == "-h" => { + print!("{HELP}"); + return Ok(()); + } + Some(argument) => argument + .into_string() + .map_err(|_| "connection string must be UTF-8")?, + None => std::env::var("DATABASE_URL") + .map_err(|_| "set DATABASE_URL or pass a connection string; see dbdash --help")?, + }; + if args.next().is_some() { + return Err("expected one connection string; see dbdash --help".into()); + } + if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() { + return Err("an interactive terminal is required; see dbdash --help".into()); + } + let mut app = app::App::new(db::Database::connect(&connection)?)?; + let result = (|| { + let mut terminal = ratatui::try_init()?; + app.run(&mut terminal) + })(); + ratatui::restore(); + result +} + +fn error_text(error: &(dyn Error + 'static)) -> String { + let mut text = error.to_string(); + let mut source = error.source(); + while let Some(error) = source { + text.push_str(": "); + text.push_str(&error.to_string()); + source = error.source(); + } + db::display_text(&text) +}