Compare commits
1 Commits
agent/cto/
...
agent/seni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19aeb7784b |
424
Cargo.lock
generated
424
Cargo.lock
generated
@@ -17,6 +17,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -88,6 +94,21 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[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.2.57"
|
||||
@@ -104,6 +125,20 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -137,6 +172,31 @@ 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",
|
||||
"crossterm_winapi",
|
||||
"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.1.7"
|
||||
@@ -147,6 +207,52 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "db-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db-config",
|
||||
"db-core",
|
||||
"db-drivers",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "db-config"
|
||||
version = "0.1.0"
|
||||
@@ -172,10 +278,24 @@ dependencies = [
|
||||
name = "dbtool-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db-app",
|
||||
"db-config",
|
||||
"db-core",
|
||||
"db-drivers",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbtool-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"db-app",
|
||||
"db-config",
|
||||
"db-core",
|
||||
"ratatui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -211,6 +331,28 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[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 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
@@ -328,6 +470,8 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
@@ -340,6 +484,12 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -430,6 +580,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -451,6 +607,28 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-enum"
|
||||
version = "1.2.1"
|
||||
@@ -460,6 +638,15 @@ dependencies = [
|
||||
"derive_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -502,6 +689,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
@@ -523,6 +716,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
@@ -562,8 +764,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -579,7 +782,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"io-enum",
|
||||
"libc",
|
||||
"lru",
|
||||
"lru 0.16.3",
|
||||
"mysql_common",
|
||||
"named_pipe",
|
||||
"pem",
|
||||
@@ -699,6 +902,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
@@ -860,6 +1069,27 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools",
|
||||
"lru 0.12.5",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -912,12 +1142,31 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.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 = "saturating"
|
||||
version = "0.1.0"
|
||||
@@ -1001,6 +1250,37 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[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 = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
@@ -1026,7 +1306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1035,6 +1315,12 @@ 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 = "stringprep"
|
||||
version = "0.1.5"
|
||||
@@ -1046,6 +1332,34 @@ dependencies = [
|
||||
"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.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -1130,7 +1444,7 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1211,6 +1525,35 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -1380,6 +1723,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -1389,6 +1741,70 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[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_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[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_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[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_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"apps/cli",
|
||||
"apps/tui",
|
||||
"crates/db-app",
|
||||
"crates/db-config",
|
||||
"crates/db-core",
|
||||
"crates/db-drivers",
|
||||
@@ -13,6 +15,7 @@ edition = "2021"
|
||||
rust-version = "1.82"
|
||||
|
||||
[workspace.dependencies]
|
||||
db-app = { path = "crates/db-app" }
|
||||
db-config = { path = "crates/db-config" }
|
||||
db-core = { path = "crates/db-core" }
|
||||
db-drivers = { path = "crates/db-drivers" }
|
||||
|
||||
28
README.md
28
README.md
@@ -36,6 +36,34 @@
|
||||
- 链接器受限的 Linux 环境:使用 `zig cc` 作为 host linker 执行 `cargo check` / `cargo test`
|
||||
|
||||
当前 `connect` / `inspect` / `query` / `export` 在 PostgreSQL、MySQL 与 SQLite 的共享抽象上都已经可用;当前剩余工作主要是 Docker 环境下的跨数据库 smoke 与发布链路。
|
||||
同时,仓库现已提供 `crates/db-app` 作为 CLI / TUI 共享应用层,并为各命令新增 `--result-format json` 以输出结构化结果;当前还补充了 `AppOperation`、`OperationState` 与 `AppEvent<T>` 作为 TUI worker/channel 的稳定执行契约,详见 `TUI_BACKEND_CONTRACT.md`。
|
||||
|
||||
## TUI Shell Baseline
|
||||
|
||||
仓库现已补充 `apps/tui`,用于承载 `dbtool-tui-v1` 的终端工作台骨架,不改变当前 CLI 首发范围。
|
||||
|
||||
当前 TUI baseline 提供:
|
||||
|
||||
- 独立 `dbtool-tui` 入口
|
||||
- 顶部导航、六区主布局和底部快捷键提示
|
||||
- 面板焦点管理与基础键盘导航
|
||||
- `sqlite-local` 的真实 connect / inspect 路径
|
||||
- `sqlite-local` 的真实 query / results / export 工作流
|
||||
- `Ready` / `Loading` / `Error` 基础状态框架
|
||||
|
||||
运行方式:
|
||||
|
||||
```bash
|
||||
cargo run -p dbtool-tui
|
||||
```
|
||||
|
||||
若当前环境缺少 Rust 工具链,可先使用已有二进制:
|
||||
|
||||
```bash
|
||||
./target/debug/dbtool-tui
|
||||
```
|
||||
|
||||
详细范围与验收说明见 `apps/tui/README.md`,共享 UI 方案见 `plans/2026-03-26-dbtool-tui-shell-ui-scope.md`。
|
||||
|
||||
## Workspace 结构
|
||||
|
||||
|
||||
80
TUI_ACCEPTANCE_CHECKLIST.md
Normal file
80
TUI_ACCEPTANCE_CHECKLIST.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# dbtool-tui-v1 验收检查表(阶段 2 分层版)
|
||||
|
||||
## 当前结论
|
||||
|
||||
- 当前仍不能把 `dbtool-tui-v1` 整体判定为阶段 2 live integration 已全部通过;但 `CMP-36` 的 `sqlite-local` 真实 query / results / export 路径已经完成宿主机侧重建与验收回填。
|
||||
- Product / PM 对齐依据:`[CMP-33](/CMP/issues/CMP-33)`、`[CMP-37](/CMP/issues/CMP-37)`、`plans/2026-03-27-dbtool-tui-v1-phase-2-scope.md`。
|
||||
- CTO 优先级对齐依据:第二阶段关键路径仍是 `[CMP-34](/CMP/issues/CMP-34)` → `[CMP-35](/CMP/issues/CMP-35)` → `[CMP-36](/CMP/issues/CMP-36)`,QA 不应越过 shared app 契约提前宣告 live 通过。
|
||||
- 2026-03-27 当前 runner 已回填 shell baseline 证据:`./target/debug/dbtool-tui` 在 `120x40` 与 `90x20` TTY 路径都可复现,退出码均为 `0`。
|
||||
- 当前 runner 不能把“源码可重建”记为通过:系统无 `cargo` / `rustc` / `rustfmt`,无法执行 `cargo check`、`cargo test -p dbtool-tui` 或重新生成二进制。
|
||||
- `dbtool-tui` 是 TTY 程序;`./target/debug/dbtool-tui --help` 在无 TTY 场景下返回 `No such device or address`,这必须被记录为非交互限制,而不是被误记为 smoke 失败或帮助能力已通过。
|
||||
|
||||
## 验收分层
|
||||
|
||||
### A. Shell baseline(当前可验)
|
||||
|
||||
- [x] 预构建二进制 `target/debug/dbtool-tui` 存在
|
||||
- [x] `printf 'q' | script -qec 'stty rows 40 cols 120; ./target/debug/dbtool-tui' /tmp/tui-normal.log` 可稳定启动并退出
|
||||
- [x] 正常尺寸下可见六区布局:`Connections`、`Schema Browser`、`Query Editor`、`Results`、`Inspector`、`Status & Activity`
|
||||
- [x] 正常尺寸下默认状态包含 `Ready`、`Execution Idle`、`Workspace`
|
||||
- [x] 当前 shell 会展示静态连接 / schema / query / results 上下文,但这些仍是本地示例状态,不是 live shared app 数据
|
||||
- [x] `printf 'q' | script -qec 'stty rows 20 cols 90; ./target/debug/dbtool-tui' /tmp/tui-small.log` 可稳定启动并退出
|
||||
- [x] 小终端路径会显示 `Terminal Too Small`
|
||||
- [x] 小终端路径会显示 `Resize the terminal`
|
||||
- [x] shell baseline 与 live integration 的验收边界已在本文档中显式分层
|
||||
- [ ] 当前 runner 上源码重建通过
|
||||
- [ ] 当前 runner 上源码测试通过
|
||||
- [ ] `dbtool-tui` 独立 release artifact smoke 已建立
|
||||
|
||||
### B. 第二阶段 live integration(当前必须保持未通过)
|
||||
|
||||
- [ ] `[CMP-34](/CMP/issues/CMP-34)` 已冻结 TUI live integration 契约
|
||||
- [ ] `[CMP-35](/CMP/issues/CMP-35)` 的 `sqlite-local` 真实 connect / inspect 路径已完成源码重建并回填运行证据
|
||||
- [x] `[CMP-36](/CMP/issues/CMP-36)` 的 `sqlite-local` 真实 query / results / export 路径已完成源码重建并回填运行证据
|
||||
- [ ] 真实连接激活可区分 `loading` / `success` / `failure`
|
||||
- [ ] 真实连接失败后,错误能在 `Inspector` 与 `Status & Activity` 中持续可见
|
||||
- [ ] 真实 schema browser 可展示 schema / object / column 层级
|
||||
- [ ] empty schema 与 restricted schema 在 live 数据下仍有明确文案
|
||||
- [ ] `Query Editor` 可针对真实活动连接执行查询
|
||||
- [x] 真实查询可区分 `running` / `success` / `empty` / `error`
|
||||
- [ ] 真实结果区在常见终端尺寸下仍可浏览
|
||||
- [x] export 结果会给出明确成功 / 失败反馈
|
||||
- [ ] 全路径复用 `crates/db-app`,不通过 CLI 文本解析旁路取数
|
||||
|
||||
### C. 非交互 / 发布前路径(当前必须显式保留限制)
|
||||
|
||||
- [x] QA 已确认 `dbtool-tui` 需要 TTY,不能用 `--help` 替代交互 smoke
|
||||
- [ ] GitHub Actions 已覆盖 `dbtool-tui` smoke
|
||||
- [ ] 非开发者可按 runbook 从 packaged binary 复现 TUI 验收
|
||||
- [ ] TUI 专属 demo / seed / Docker 路径已建立
|
||||
- [ ] release artifact / packaged binary 在目标平台有真实执行证据
|
||||
|
||||
## 当前 blocker
|
||||
|
||||
- [ ] `[CMP-34](/CMP/issues/CMP-34)` 仍在 `in_progress`
|
||||
- [ ] `[CMP-35](/CMP/issues/CMP-35)` 仍在 `in_progress`,且当前 runner 缺少 Rust 工具链,无法回填重建证据
|
||||
- [x] `[CMP-36](/CMP/issues/CMP-36)` 已完成宿主机侧构建与验收回填;当前 agent runner 仍缺 Rust 工具链,但不再阻塞该 issue 关闭
|
||||
- [ ] `.github/workflows/release-smoke.yml` 当前只覆盖 `dbtool`
|
||||
- [ ] 仓库仍无 TUI 专属 demo / seed / Docker live 验证路径
|
||||
- [ ] 当前 runner 缺 `cargo` / `rustc` / `rustfmt`,无法执行源码重建、测试与格式化
|
||||
|
||||
## 2026-03-27 已回填证据
|
||||
|
||||
- 正常尺寸 shell smoke:`printf 'q' | script -qec 'stty rows 40 cols 120; ./target/debug/dbtool-tui' /tmp/tui-normal.log`
|
||||
- 正常尺寸 shell 结果:退出码 `0`;日志包含六区布局、`Ready`、`Execution Idle`、静态 connections/schema/results 文案
|
||||
- 小终端 shell smoke:`printf 'q' | script -qec 'stty rows 20 cols 90; ./target/debug/dbtool-tui' /tmp/tui-small.log`
|
||||
- 小终端 shell 结果:退出码 `0`;日志包含 `Terminal Too Small` 与 `Resize the terminal`
|
||||
- 非交互限制:`./target/debug/dbtool-tui --help` 返回 `Error: Os { code: 6, kind: Uncategorized, message: "No such device or address" }`
|
||||
- 源码状态盘点:`apps/tui/src/main.rs` 已引入 `sqlite-local` 的真实 connect / inspect / query / results / export worker 接线,以及内联测试覆盖
|
||||
- 源码验证限制:当前 runner 无 `cargo` / `rustc`,因此无法在本次回合执行 `cargo check`、`cargo test -p dbtool-tui` 或重新生成 `target/debug/dbtool-tui`
|
||||
|
||||
## 当前不应误判为已通过的能力
|
||||
|
||||
- [ ] 真实 shared app 连接切换
|
||||
- [ ] 真实 inspect / schema browser
|
||||
- [ ] 真实 query 执行
|
||||
- [ ] 真实 empty / error 结果处理
|
||||
- [ ] 真实 export 反馈
|
||||
- [ ] TUI CI smoke
|
||||
- [ ] TUI release artifact smoke
|
||||
- [ ] TUI demo / seed / Docker 路径
|
||||
240
TUI_BACKEND_CONTRACT.md
Normal file
240
TUI_BACKEND_CONTRACT.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# TUI Backend Contract
|
||||
|
||||
日期:2026-03-27
|
||||
作者:Senior Backend Engineer
|
||||
|
||||
## 1. 当前后端边界
|
||||
|
||||
- `crates/db-core`:数据库无关的领域对象与基础校验。
|
||||
- `crates/db-config`:连接 profile、密码环境变量注入与脱敏摘要。
|
||||
- `crates/db-drivers`:PostgreSQL / MySQL / SQLite 驱动实现与差异收敛。
|
||||
- `crates/db-app`:`connect` / `inspect` / `query` / `export` 共享应用编排、结构化结果、统一错误对象,以及 TUI worker 可复用的执行状态。
|
||||
- `apps/cli`:参数解析、stdout/stderr 渲染、退出码。
|
||||
|
||||
## 2. 持久化边界
|
||||
|
||||
- 当前产品仍是单次命令执行模型,没有连接配置持久化、历史记录持久化或后台状态存储。
|
||||
- 当前写路径只有:
|
||||
- `export` 明确指定的输出文件
|
||||
- SQLite 目标数据库本身(由用户选择)
|
||||
- TUI 第二阶段 live integration 仍不引入新的数据库、本地缓存库或后台 daemon 存储契约。
|
||||
|
||||
## 3. 共享请求对象
|
||||
|
||||
TUI worker 应直接构造共享 Rust 对象,不要拼接 CLI flags,更不要解析 CLI 文本。
|
||||
|
||||
### Connect
|
||||
|
||||
- 输入:`db_config::ConnectionProfile`
|
||||
- 说明:
|
||||
- `name` 用于当前会话内的目标标识
|
||||
- `target` 复用 `db_core::ConnectionTarget`
|
||||
- `password_env_var` 只作为脱敏标签保留;secret 仍通过环境变量注入,不进入持久化
|
||||
|
||||
### Inspect
|
||||
|
||||
- 输入:`db_core::InspectRequest`
|
||||
- 字段:
|
||||
- `schema: Option<String>`
|
||||
- `table: Option<String>`
|
||||
- 约束:
|
||||
- `table.is_some()` 时必须同时提供 `schema`
|
||||
|
||||
### Query
|
||||
|
||||
- 输入:`db_core::QueryRequest`
|
||||
- 字段:
|
||||
- `sql`
|
||||
- `source`
|
||||
- `parameters`
|
||||
- `source` 语义:
|
||||
- `QuerySource::Inline`
|
||||
- `QuerySource::File`
|
||||
|
||||
### Export
|
||||
|
||||
- 输入:
|
||||
- `db_core::QueryRequest`
|
||||
- `db_core::ExportRequest`
|
||||
- `ExportRequest` 字段:
|
||||
- `format`
|
||||
- `output_path`
|
||||
- `overwrite`
|
||||
- 约束:
|
||||
- 只允许导出返回结果集的查询
|
||||
- 不静默覆盖已有文件
|
||||
- 不为 TUI 增加新的导出元数据存储
|
||||
|
||||
## 4. 共享结构化结果
|
||||
|
||||
TUI 应优先直接调用 `crates/db-app`,不要解析 CLI 的人类可读文本。
|
||||
|
||||
### Connect
|
||||
|
||||
- 返回 `ConnectResponse`
|
||||
- 字段:
|
||||
- `target.profile_name`
|
||||
- `target.driver`
|
||||
- `target.endpoint`
|
||||
- `target.password_env_var`
|
||||
- `status`
|
||||
- 当前 `status` 固定为 `connected`
|
||||
|
||||
### Inspect
|
||||
|
||||
- 返回 `InspectResponse`
|
||||
- 字段:
|
||||
- `target`
|
||||
- `scope.schema`
|
||||
- `scope.table`
|
||||
- `payload`
|
||||
- `payload.kind` 为:
|
||||
- `schemas`
|
||||
- `tables`
|
||||
- `columns`
|
||||
- `payload.items` 允许为空;空数组不是错误
|
||||
|
||||
### Query
|
||||
|
||||
- 返回 `QueryResponse`
|
||||
- 字段:
|
||||
- `target`
|
||||
- `source.kind`:`inline` / `file`
|
||||
- `source.label`
|
||||
- `source.path`
|
||||
- `result.columns`
|
||||
- `result.rows`
|
||||
- `result.rows_affected`
|
||||
- `result.row_count`
|
||||
- `rows_affected != null` 表示 command-style 执行成功但没有结果集
|
||||
|
||||
### Export
|
||||
|
||||
- 返回 `ExportResponse`
|
||||
- 字段:
|
||||
- `target`
|
||||
- `source`
|
||||
- `export.format`
|
||||
- `export.output_path`
|
||||
- `export.overwrite`
|
||||
- `row_count`
|
||||
|
||||
## 5. 错误模型
|
||||
|
||||
`db-app::AppError.kind` 当前分为:
|
||||
|
||||
- `validation`
|
||||
- `connection`
|
||||
- `authentication`
|
||||
- `inspect`
|
||||
- `query`
|
||||
- `export`
|
||||
|
||||
这组分类是 TUI banner、QA 断言和未来 GUI 状态映射的当前稳定基础。
|
||||
|
||||
`AppError.target` 为可选字段:
|
||||
|
||||
- 有目标上下文时返回脱敏后的 `ConnectionSummary`
|
||||
- 参数校验在目标尚未构成时可为 `null`
|
||||
|
||||
## 6. Worker / Channel 状态契约
|
||||
|
||||
`crates/db-app` 现提供:
|
||||
|
||||
- `AppOperation`:`connect | inspect | query | export`
|
||||
- `OperationState`:`running | success | empty | error`
|
||||
- `AppEvent<T>`:worker / channel 推荐 envelope
|
||||
|
||||
推荐发送顺序:
|
||||
|
||||
1. 先发 `AppEvent::running(operation)`
|
||||
2. 成功后发送 `response.into_event()`
|
||||
3. 失败时发送 `error.into_event()`
|
||||
|
||||
状态语义固定如下:
|
||||
|
||||
- `connect`
|
||||
- `running`:正在测试连接
|
||||
- `success`:连接成功
|
||||
- `error`:连接失败
|
||||
- `inspect`
|
||||
- `success`:返回非空 schema / table / column 集合
|
||||
- `empty`:返回空集合,但请求本身成功
|
||||
- `error`:inspect 失败
|
||||
- `query`
|
||||
- `success`:返回至少一行,或 `rows_affected` 非空
|
||||
- `empty`:返回零行且 `rows_affected == null`
|
||||
- `error`:执行失败
|
||||
- `export`
|
||||
- `success`:文件写入成功;`row_count == 0` 仍算成功
|
||||
- `error`:导出校验、查询或文件写入失败
|
||||
|
||||
示例:
|
||||
|
||||
```rust
|
||||
use db_app::{AppEvent, AppOperation};
|
||||
|
||||
sender.send(AppEvent::<()>::running(AppOperation::Query))?;
|
||||
|
||||
match db_app::query(&profile, &request) {
|
||||
Ok(response) => sender.send(response.into_event())?,
|
||||
Err(error) => sender.send(error.into_event())?,
|
||||
}
|
||||
```
|
||||
|
||||
## 7. CLI 结构化输出
|
||||
|
||||
CLI 继续保留默认文本输出,同时新增:
|
||||
|
||||
- `--result-format text`
|
||||
- `--result-format json`
|
||||
|
||||
JSON 返回 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"operation": "query",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
错误返回 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"operation": "query",
|
||||
"error": {
|
||||
"operation": "query",
|
||||
"kind": "query",
|
||||
"message": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
CLI JSON envelope 继续作为跨入口回归样本,但 TUI live path 不应以调用 CLI 二进制作为主集成方式。
|
||||
|
||||
## 8. Frontend 接入约束
|
||||
|
||||
- TUI 直接依赖 `crates/db-app` 与 `db-core`,不解析 CLI stdout/stderr
|
||||
- UI 状态映射应优先使用 `OperationState`,不要自行发明另一套 success/empty/error 语义
|
||||
- `ConnectionSummary.endpoint` 与 `password_env_var` 仅用于展示脱敏上下文,不应用作 secret 来源
|
||||
- `AppError.kind` 必须原样进入状态区 / inspector,避免把 `validation`、`connection`、`query` 混成统一“失败”
|
||||
- 若未来引入异步 worker 池,仍以共享 response/error 对象作为唯一 UI 数据源
|
||||
|
||||
## 9. 当前回归边界
|
||||
|
||||
- `crates/db-app` 回归需覆盖:
|
||||
- response -> `OperationState` 映射
|
||||
- `AppEvent<T>` / `AppError` 序列化
|
||||
- 空 inspect / 空 query / command query 语义
|
||||
- `apps/cli` 回归继续覆盖:
|
||||
- `--result-format json` envelope
|
||||
- text/json 输出在相同 `db-app` 结果上的一致性
|
||||
|
||||
## 10. 演进规则
|
||||
|
||||
- CLI 文本文案可小幅演进,但 `db-app` 结构化对象字段应保持兼容。
|
||||
- 新增数据库驱动时,优先扩展 `db-drivers` 与 `db-app`,不要在 TUI/CLI 内复制数据库语义。
|
||||
- 若未来引入本地持久化(例如连接历史),应先新增独立文档定义存储边界,再进入实现。
|
||||
96
TUI_REGRESSION_CHECKLIST.md
Normal file
96
TUI_REGRESSION_CHECKLIST.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# dbtool-tui-v1 回归清单(阶段 2 分层版)
|
||||
|
||||
## 0. 记录前提
|
||||
|
||||
- [ ] 记录验证日期、验证人、issue / heartbeat
|
||||
- [ ] 记录二进制路径:`target/debug/dbtool-tui`、源码构建产物或 packaged binary
|
||||
- [ ] 记录是否为“预构建二进制验证”还是“源码重建验证”
|
||||
- [ ] 记录终端尺寸、输入按键、输出日志路径
|
||||
- [ ] 若 live integration 已接线,记录对应 issue、构建版本、demo 环境与 seed 来源
|
||||
- [ ] 若任一步骤未执行,记录原因:未实现、缺环境、被上游 blocker 卡住,或非当前范围
|
||||
|
||||
## 1. Shell baseline 回归(当前 runner 可执行)
|
||||
|
||||
- [ ] `printf 'q' | script -qec 'stty rows 40 cols 120; ./target/debug/dbtool-tui' /tmp/tui-normal.log`
|
||||
- [ ] 确认退出码为 `0`
|
||||
- [ ] 确认日志包含 `Connections`
|
||||
- [ ] 确认日志包含 `Schema Browser`
|
||||
- [ ] 确认日志包含 `Query Editor`
|
||||
- [ ] 确认日志包含 `Results`
|
||||
- [ ] 确认日志包含 `Inspector`
|
||||
- [ ] 确认日志包含 `Status & Activity`
|
||||
- [ ] 确认日志包含 `Ready`
|
||||
- [ ] 确认日志包含 `Execution Idle`
|
||||
- [ ] 确认日志包含静态连接 / schema / result 示例文本
|
||||
- [ ] `printf 'q' | script -qec 'stty rows 20 cols 90; ./target/debug/dbtool-tui' /tmp/tui-small.log`
|
||||
- [ ] 确认退出码为 `0`
|
||||
- [ ] 确认日志包含 `Terminal Too Small`
|
||||
- [ ] 确认日志包含 `Resize the terminal`
|
||||
- [ ] 明确记录:`dbtool-tui --help` 不是有效的非交互 smoke
|
||||
|
||||
## 2. Shell 交互回归(静态 / 示例数据,不等于 live)
|
||||
|
||||
- [ ] `Tab` / `Shift+Tab`:焦点在六个面板之间移动
|
||||
- [ ] `1` / `2` / `3`、`←` / `→`:顶部视图切换
|
||||
- [ ] `Connections` 焦点下 `↑` / `↓`:连接选择移动
|
||||
- [ ] `Connections` 焦点下 `Enter`:激活所选静态连接并同步工作区上下文
|
||||
- [ ] 失败连接激活后仍保持可见错误
|
||||
- [ ] `Schema Browser` 焦点下 `↑` / `↓`:schema / object 选择移动
|
||||
- [ ] `Schema Browser` 焦点下 `Enter`:ready schema 展开 / 折叠;empty / restricted schema 给出明确反馈
|
||||
- [ ] `Query Editor` 焦点下 `[` / `]`:切换静态 query 草稿
|
||||
- [ ] `i`:进入插入模式并可录入字符 / 换行
|
||||
- [ ] `Enter`:触发静态 query 状态轮转或运行占位逻辑
|
||||
- [ ] 结果区可区分 success / empty / error
|
||||
- [ ] `Results` 焦点下 `[` / `]`:宽表列分页
|
||||
- [ ] `Results` 焦点下 `↑` / `↓`:结果行移动
|
||||
- [ ] `l` / `e` / `r`:状态切换
|
||||
- [ ] `Esc`:恢复默认工作台
|
||||
- [ ] `q`:稳定退出并释放 alternate screen
|
||||
|
||||
## 3. 第二阶段 live integration 回归(仅在 `[CMP-34](/CMP/issues/CMP-34)`、`[CMP-35](/CMP/issues/CMP-35)`、`[CMP-36](/CMP/issues/CMP-36)` 就绪后执行)
|
||||
|
||||
- [ ] 真实连接列表可加载
|
||||
- [ ] 真实连接激活可区分 loading / success / failure
|
||||
- [ ] 真实连接失败详情会持续显示在 `Inspector` 与 `Status & Activity`
|
||||
- [ ] 真实 schema / object / column 可浏览
|
||||
- [ ] empty schema 有明确空状态
|
||||
- [ ] restricted schema 有明确失败状态
|
||||
- [ ] `Query Editor` 可对真实活动连接执行查询
|
||||
- [ ] 真实查询会显示 running / success / empty / error
|
||||
- [ ] 真实 success 结果可浏览
|
||||
- [ ] 真实 empty 结果不会误记为错误
|
||||
- [ ] 真实 error 结果带可读定位线索
|
||||
- [ ] export 操作有成功 / 失败反馈
|
||||
- [ ] 工作区上下文在 inspect -> edit -> run -> review -> export 之间保持连续
|
||||
- [ ] 全路径确认未解析 CLI 文本
|
||||
|
||||
## 4. 源码 / CI / 发布前回归
|
||||
|
||||
- [ ] `cargo build -p dbtool-tui`
|
||||
- [ ] `cargo test -p dbtool-tui`
|
||||
- [ ] 内联测试与交互 smoke 都能在同一环境复现
|
||||
- [ ] 仓库存在 TUI 专属 smoke runbook
|
||||
- [ ] GitHub Actions 覆盖 `dbtool-tui`
|
||||
- [ ] packaged binary 在目标平台可执行
|
||||
- [ ] 非开发者按 runbook 可复现最小验收
|
||||
|
||||
## 2026-03-27 当前已回填项
|
||||
|
||||
- [x] 预构建二进制正常尺寸 shell smoke
|
||||
- [x] 正常尺寸退出码 `0`
|
||||
- [x] 正常尺寸日志含六区布局
|
||||
- [x] 正常尺寸日志含 `Ready`
|
||||
- [x] 正常尺寸日志含 `Execution Idle`
|
||||
- [x] 小终端 shell smoke
|
||||
- [x] 小终端退出码 `0`
|
||||
- [x] 小终端日志含 `Terminal Too Small`
|
||||
- [x] 小终端日志含 `Resize the terminal`
|
||||
- [x] 已确认 `dbtool-tui --help` 不能替代 TTY smoke
|
||||
|
||||
## 当前阻塞中的回填项
|
||||
|
||||
- [ ] 源码重建:当前 runner 缺 `cc` / `zig cc`
|
||||
- [ ] 源码测试:当前 runner 缺 `cc` / `zig cc`
|
||||
- [ ] live integration 回归:等待 `[CMP-34](/CMP/issues/CMP-34)`、`[CMP-35](/CMP/issues/CMP-35)`、`[CMP-36](/CMP/issues/CMP-36)`
|
||||
- [ ] CI smoke:仓库尚未覆盖 `dbtool-tui`
|
||||
- [ ] release artifact smoke:仓库尚未为 `dbtool-tui` 建立产物路径
|
||||
121
TUI_TEST_STRATEGY.md
Normal file
121
TUI_TEST_STRATEGY.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# dbtool-tui-v1 测试策略(阶段 2 分层版)
|
||||
|
||||
## 目标
|
||||
|
||||
- 把 `dbtool-tui-v1` 的 shell baseline 验收、第二阶段 live integration 验收、以及非交互 / 发布前验证彻底拆层。
|
||||
- 让 QA 对每个关键用户流都能给出“已执行通过 / 未执行 / 被 blocker 卡住”的可审计判断。
|
||||
- 让 CTO 和 Product Manager 看到真实优先级:先 shared app 契约,再 live 接线,最后才是 live 验收闭环。
|
||||
|
||||
## 当前资产盘点
|
||||
|
||||
### 已存在的共享资产
|
||||
|
||||
- `apps/tui/src/main.rs`:TUI 主实现,当前包含静态连接、schema、query、results 示例状态
|
||||
- `apps/tui/README.md`:当前交互范围与产品/体验说明
|
||||
- `TUI_ACCEPTANCE_CHECKLIST.md`:阶段 2 分层验收矩阵
|
||||
- `TUI_REGRESSION_CHECKLIST.md`:阶段 2 分层回归清单
|
||||
- `plans/2026-03-27-dbtool-tui-v1-phase-2-scope.md`:CTO 第二阶段范围与 gating
|
||||
- `backlog/2026-03-27-dbtool-tui-v1-phase-2-issues.md`:owner-backed issue 分解
|
||||
|
||||
### 已发现的测试资产
|
||||
|
||||
- `apps/tui/src/main.rs` 内已有 5 个内联测试,覆盖:
|
||||
- query 草稿切换
|
||||
- editor `Enter` 触发 running
|
||||
- empty query 完成态
|
||||
- results 宽表列分页
|
||||
- insert mode 录入
|
||||
- 当前存在可执行的预构建二进制:`target/debug/dbtool-tui`
|
||||
|
||||
### 2026-03-27 已执行证据
|
||||
|
||||
- TTY shell smoke:`./target/debug/dbtool-tui` 在 `120x40` 下可启动、渲染并以 `q` 退出,退出码 `0`
|
||||
- 小终端降级:同一二进制在 `90x20` 下显示 `Terminal Too Small` 与 `Resize the terminal`,退出码 `0`
|
||||
- 非交互限制:`./target/debug/dbtool-tui --help` 在当前无 TTY 场景下返回 `No such device or address`
|
||||
|
||||
### 当前缺口
|
||||
|
||||
- 当前 runner 默认无 `cargo` / `rustc` 在 PATH
|
||||
- 虽存在离线 Rust toolchain,但源码重建 / 测试仍被系统缺少 `cc` / `zig cc` 阻塞
|
||||
- 仓库暂无 TUI 专属 smoke runbook
|
||||
- `.github/workflows/release-smoke.yml` 当前只覆盖 `dbtool`
|
||||
- 当前无 TUI 专属 demo / seed / Docker 路径
|
||||
- 第二阶段 live integration 仍未整体完成:`[CMP-34](/CMP/issues/CMP-34)` 仍在 `in_progress`,`[CMP-35](/CMP/issues/CMP-35)` 仍待宿主机回填,而 `[CMP-36](/CMP/issues/CMP-36)` 已完成宿主机侧验收
|
||||
|
||||
## 测试分层
|
||||
|
||||
### 1. 源码层验证
|
||||
|
||||
- 目标:确认实现可重建、内联测试可运行
|
||||
- 当前命令:
|
||||
- `cargo build -p dbtool-tui`
|
||||
- `cargo test -p dbtool-tui`
|
||||
- 当前判断:
|
||||
- 这是必要层,但当前 runner 不能给出通过结论
|
||||
- 缺 `cc` / `zig cc` 时,应记录为环境 blocker,不能伪造成产品失败,也不能反向伪造成已通过
|
||||
|
||||
### 2. TTY shell smoke
|
||||
|
||||
- 目标:最低成本确认 TUI 可启动、可渲染、可退出、可在小终端降级
|
||||
- 当前建议:
|
||||
- 使用 `script` + `stty` 固化 TTY 启动路径
|
||||
- 用预构建二进制先取证,再视环境补做源码重建后的同路径复核
|
||||
- 注意:
|
||||
- `dbtool-tui` 不是普通 CLI;不要用 `--help` 替代交互 smoke
|
||||
|
||||
### 3. Shell 交互回归(静态 / 示例数据)
|
||||
|
||||
- 目标:验证当前 shell 已实现的交互骨架,但不把示例状态误记为 live 能力
|
||||
- 范围:
|
||||
- 视图切换
|
||||
- 焦点切换
|
||||
- 静态连接激活
|
||||
- 静态 schema / object 浏览
|
||||
- 查询草稿切换、插入模式、运行占位、结果分页
|
||||
- 错误 / 空状态 / 恢复路径
|
||||
- 输出要求:
|
||||
- 明确标记“这是 shell 示例状态验证,不是 shared app live 数据验证”
|
||||
|
||||
### 4. 第二阶段 live integration 验收
|
||||
|
||||
- 目标:在真实 shared app 接线落地后,对连接、inspect、query、results、export 给出验收判断
|
||||
- 前置条件:
|
||||
- `[CMP-34](/CMP/issues/CMP-34)` 完成并冻结契约
|
||||
- `[CMP-35](/CMP/issues/CMP-35)` 完成真实连接与 schema browser 接线
|
||||
- `[CMP-36](/CMP/issues/CMP-36)` 完成真实 query / results / export 接线
|
||||
- 必验路径:
|
||||
- 真实连接 `loading` / `success` / `failure`
|
||||
- 真实 inspect ready / empty / restricted
|
||||
- 真实 query running / success / empty / error
|
||||
- export 成功 / 失败反馈
|
||||
- shared app 边界复用,无 CLI 文本解析旁路
|
||||
|
||||
### 5. 发布前 / 非交互验证
|
||||
|
||||
- 目标:确认非开发者与 CI 都能重复最小验收
|
||||
- 当前判断:
|
||||
- 尚未建立
|
||||
- 若后续仍要求 TTY 启动,应在 runbook 与 workflow 中显式写清,不得假装支持普通非交互 `--help` / `--version` smoke
|
||||
|
||||
## 证据规则
|
||||
|
||||
- 每个“通过”项都必须附带命令、终端尺寸、输入按键、输出摘要或错误摘要
|
||||
- 每个“未执行”项都必须标原因:未实现、缺环境、被上游 issue 卡住、或超出当前范围
|
||||
- 代码阅读、README 声明、和真实执行证据必须分开写
|
||||
- 不能用 CLI 已通过来替代 TUI 已通过
|
||||
- 不能把静态示例状态写成真实数据库能力
|
||||
- 不能把 TTY 程序的 `--help` 失败误写成已支持的 smoke 入口
|
||||
|
||||
## 当前优先级
|
||||
|
||||
1. 保住 shell baseline 证据,不让第二阶段重构把它冲掉
|
||||
2. 等待 `[CMP-34](/CMP/issues/CMP-34)` 冻结 shared app 契约
|
||||
3. 在 `[CMP-35](/CMP/issues/CMP-35)` / `[CMP-36](/CMP/issues/CMP-36)` 完成后补 live integration 验收
|
||||
4. 最后再补 TUI 专属 CI / release / runbook
|
||||
|
||||
## 当前验收判断
|
||||
|
||||
- shell baseline:有预构建二进制级别的可重复 smoke,属“有条件通过”
|
||||
- 源码层验证:当前 runner blocked,不能给通过结论
|
||||
- 第二阶段 live integration:当前 blocked,不能给通过结论
|
||||
- 发布前状态:未就绪,因为 `dbtool-tui` 仍缺 CI、release artifact smoke 和 live demo 路径
|
||||
20
apps/tui/Cargo.toml
Normal file
20
apps/tui/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "dbtool-tui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "dbtool-tui"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.28.1"
|
||||
db-app.workspace = true
|
||||
db-config.workspace = true
|
||||
db-core.workspace = true
|
||||
ratatui = "0.29.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
114
apps/tui/README.md
Normal file
114
apps/tui/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# dbtool-tui Shell
|
||||
|
||||
`apps/tui` 提供 `dbtool-tui-v1` 的终端工作台实现。
|
||||
|
||||
## 当前范围
|
||||
|
||||
当前实现包含:
|
||||
|
||||
- 独立 `dbtool-tui` 应用入口
|
||||
- 稳定的六区工作台布局
|
||||
- 顶部视图切换与底部快捷键提示
|
||||
- 连接管理列表、当前连接上下文和失败连接可视化区分
|
||||
- 键盘驱动的连接切换工作流,以及连接 `loading / success / failure` 可视状态
|
||||
- `sqlite-local` 的真实 connect / inspect 路径,schema browser 可展示 live schema / table / column
|
||||
- `sqlite-local` 的真实 query / results / export 工作流,直接复用 `crates/db-app`
|
||||
- 查询草稿切换、基础键入编辑、执行触发与执行状态反馈
|
||||
- 成功 / 空结果 / 错误三类查询结果展示,以及宽表列分页提示
|
||||
- 当前结果集导出到 `/tmp` 的 CSV / JSON 反馈
|
||||
- 面板焦点管理
|
||||
- 列表选择与基础键盘导航
|
||||
- `Ready` / `Loading` / `Error` 三种基础界面状态
|
||||
- 终端尺寸过小的显式降级提示
|
||||
|
||||
当前不包含:
|
||||
|
||||
- 真实网络数据库连接激活
|
||||
- 多连接并发会话
|
||||
- 自定义新增 / 编辑连接表单
|
||||
- 完整 SQL 编辑器能力
|
||||
|
||||
## 运行方式
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
cargo run -p dbtool-tui
|
||||
```
|
||||
|
||||
当前默认 live 路径是 `sqlite-local`,底层使用 `examples/tmp/dbtool-demo.sqlite`。
|
||||
启动后会自动对 `sqlite-local` 执行 connect + inspect;切换连接时会重新触发 live activation。
|
||||
|
||||
如果当前 runner 缺少 Rust 工具链,可先使用已有产物:
|
||||
|
||||
```bash
|
||||
./target/debug/dbtool-tui
|
||||
```
|
||||
|
||||
## 键盘交互
|
||||
|
||||
- `Tab` / `Shift+Tab`:切换焦点面板
|
||||
- `←` / `→` 或 `1` / `2` / `3`:切换顶部视图
|
||||
- `↑` / `↓`:移动连接、schema/object 或结果行选择
|
||||
- `Enter`:在 `Connections` 焦点下激活所选连接;在 `Schema Browser` 焦点下展开 / 折叠 schema 或确认对象;在 `Query Editor` 焦点下执行当前查询
|
||||
- `i`:在 `Query Editor` 中进入基础插入模式
|
||||
- `x`:将当前结果集导出为 CSV 到 `/tmp`
|
||||
- `j`:将当前结果集导出为 JSON 到 `/tmp`
|
||||
- `[` / `]`:在 `Query Editor` 中切换查询草稿;在 `Results` 中横向翻页宽表列
|
||||
- `r`:清空当前 query / export 反馈,回到 `Ready`
|
||||
- `Esc`:回到默认工作台
|
||||
- `q`:退出
|
||||
|
||||
## 快速测试路径
|
||||
|
||||
如果你只是想快速走通一遍当前可见工作流,推荐按下面步骤操作:
|
||||
|
||||
1. 启动 `cargo run -p dbtool-tui`
|
||||
2. 等待默认连接 `sqlite-local` 从 `Loading` 变成可用状态
|
||||
3. 按 `Tab` 把焦点切到 `Schema Browser`
|
||||
4. 用 `↑` / `↓` 选择 `main.accounts` 或 `main.tickets`
|
||||
5. 按 `Tab` 切到 `Query Editor`
|
||||
6. 先不要按 `i`,直接按 `[` / `]` 切换草稿
|
||||
7. 选中 `account_ticket_summary.sql` 后按 `Enter`,确认成功结果
|
||||
8. 切到 `empty_recent_tickets.sql` 后按 `Enter`,确认空结果
|
||||
9. 切到 `bad_syntax.sql` 后按 `Enter`,确认错误结果持续留在工作区
|
||||
10. 切到 `ticket_export_preview.sql` 后按 `Enter`,成功后按 `x` 或 `j`,确认导出反馈和 `/tmp` 路径
|
||||
|
||||
## 常见困惑
|
||||
|
||||
- `Inserted a newline. Press Esc, then Enter to run.`:表示你已经按了 `i` 进入编辑模式,然后按了 `Enter`;这时 `Enter` 不会执行查询,而是插入换行。按 `Esc` 退出编辑模式后,再按 `Enter` 才是执行查询。
|
||||
- 当前不能自定义输入数据库连接:这是当前范围外能力。当前 TUI 只提供内置的 `sqlite-local`、Postgres、MySQL 示例连接,用于验证工作台流程和状态反馈。
|
||||
- 如果觉得界面文字偏多:本轮已先做一轮 Help / Status / Connections 文案收敛;当前版本仍偏向 QA / 契约验证界面,后续再继续收敛文案。
|
||||
|
||||
## 界面验收
|
||||
|
||||
QA 可按以下步骤复核:
|
||||
|
||||
1. 启动后能看到 `Connections`、`Schema Browser`、`Query Editor`、`Results`、`Inspector`、`Status & Activity` 六个区域
|
||||
2. 默认活动连接应为 `sqlite-local`,且 `Connections` 列表中能区分当前连接、可选连接和失败连接
|
||||
3. 使用 `2` 进入 `Connections` 视图后,可用 `↑` / `↓` 移动连接选择
|
||||
4. 在 `Connections` 焦点下按 `Enter` 激活失败连接时,`Status & Activity` 与 `Inspector` 会持续显示可读错误,而不是只给瞬时提示
|
||||
5. 启动后或回到 `sqlite-local` 后,连接状态会先进入 `Loading`,随后在 `Schema Browser` 焦点下可浏览 `main.accounts` 和 `main.tickets`
|
||||
6. 选中对象后,`Results` 会展示列定义,`Inspector` 会展示当前 schema / object / column 上下文
|
||||
7. 在 `Query Editor` 焦点下可用 `[` / `]` 切换查询草稿,`i` 进入基础输入模式,`Enter` 触发执行
|
||||
8. `account_ticket_summary.sql` 应返回真实结果行,`empty_recent_tickets.sql` 应显示空结果,`bad_syntax.sql` 应显示结构化错误
|
||||
9. `ticket_export_preview.sql` 执行成功后,按 `x` 或 `j` 应在工作区内看到导出成功反馈和 `/tmp` 输出路径
|
||||
10. 成功结果在 `Results` 中以表格方式展示,宽表会给出列分页提示,长结果仍可用 `↑` / `↓` 浏览
|
||||
11. 错误状态会保留可读错误信息,不退化成空白或瞬时提示
|
||||
12. 使用 `Tab` / `Shift+Tab` 时,当前焦点区域边框高亮会移动
|
||||
13. 使用 `←` / `→` 或 `1` / `2` / `3` 时,中间内容区会随视图切换
|
||||
14. 使用 `r` 时,query / export 反馈会清空并回到 `Ready`
|
||||
15. 将终端缩小到低于 `100x28` 时,会出现尺寸不足提示而不是错乱布局
|
||||
16. 使用 `Esc` 可恢复默认工作台,使用 `q` 可稳定退出
|
||||
|
||||
## 当前限制
|
||||
|
||||
- `sqlite-local` 是当前唯一可在本地 runner 直接完成 live connect / inspect / query / export 的路径
|
||||
- Postgres / MySQL 连接仍以可读失败态保留,用于验证多目标上下文和错误持续可见性
|
||||
- 当前 runner 若缺少 `cargo`,需要使用已有二进制做交互 smoke;源码重建与测试需在具备 Rust 工具链的环境完成
|
||||
|
||||
共享 QA 文档:
|
||||
|
||||
- `TUI_ACCEPTANCE_CHECKLIST.md`
|
||||
- `TUI_TEST_STRATEGY.md`
|
||||
- `TUI_REGRESSION_CHECKLIST.md`
|
||||
3053
apps/tui/src/main.rs
Normal file
3053
apps/tui/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
19
crates/db-app/Cargo.toml
Normal file
19
crates/db-app/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "db-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
db-config.workspace = true
|
||||
db-core.workspace = true
|
||||
db-drivers.workspace = true
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rusqlite.workspace = true
|
||||
serde_json = "1.0.145"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
812
crates/db-app/src/lib.rs
Normal file
812
crates/db-app/src/lib.rs
Normal file
@@ -0,0 +1,812 @@
|
||||
use db_config::ConnectionProfile;
|
||||
use db_core::{
|
||||
ColumnSummary, CoreError, ExportFormat, ExportRequest, InspectRequest, InspectResult,
|
||||
QueryRequest, QueryResult, QuerySource, SchemaSummary, TableSummary,
|
||||
};
|
||||
use db_drivers::{DriverError, DriverRegistry};
|
||||
use serde::Serialize;
|
||||
use std::{error::Error, fmt, fs};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppOperation {
|
||||
Connect,
|
||||
Inspect,
|
||||
Query,
|
||||
Export,
|
||||
}
|
||||
|
||||
impl AppOperation {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Connect => "connect",
|
||||
Self::Inspect => "inspect",
|
||||
Self::Query => "query",
|
||||
Self::Export => "export",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_name(name: &'static str) -> Self {
|
||||
match name {
|
||||
"connect" => Self::Connect,
|
||||
"inspect" => Self::Inspect,
|
||||
"query" => Self::Query,
|
||||
"export" => Self::Export,
|
||||
other => panic!("unsupported app operation: {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OperationState {
|
||||
Running,
|
||||
Success,
|
||||
Empty,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AppEvent<T> {
|
||||
pub operation: AppOperation,
|
||||
pub state: OperationState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<T>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<AppError>,
|
||||
}
|
||||
|
||||
impl<T> AppEvent<T> {
|
||||
pub fn running(operation: AppOperation) -> Self {
|
||||
Self {
|
||||
operation,
|
||||
state: OperationState::Running,
|
||||
data: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finished(operation: AppOperation, state: OperationState, data: T) -> Self {
|
||||
Self {
|
||||
operation,
|
||||
state,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ConnectionSummary {
|
||||
pub profile_name: String,
|
||||
pub driver: String,
|
||||
pub endpoint: String,
|
||||
pub password_env_var: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&ConnectionProfile> for ConnectionSummary {
|
||||
fn from(profile: &ConnectionProfile) -> Self {
|
||||
Self {
|
||||
profile_name: profile.name.clone(),
|
||||
driver: profile.target.kind.as_str().to_string(),
|
||||
endpoint: profile.target.redacted_endpoint(),
|
||||
password_env_var: profile.password_env_var.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ConnectResponse {
|
||||
pub target: ConnectionSummary,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
impl ConnectResponse {
|
||||
pub fn operation(&self) -> AppOperation {
|
||||
AppOperation::Connect
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OperationState {
|
||||
OperationState::Success
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> AppEvent<Self> {
|
||||
AppEvent::finished(self.operation(), self.state(), self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct InspectScope {
|
||||
pub schema: Option<String>,
|
||||
pub table: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct InspectResponse {
|
||||
pub target: ConnectionSummary,
|
||||
pub scope: InspectScope,
|
||||
pub payload: InspectPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(tag = "kind", content = "items", rename_all = "snake_case")]
|
||||
pub enum InspectPayload {
|
||||
Schemas(Vec<SchemaItem>),
|
||||
Tables(Vec<TableItem>),
|
||||
Columns(Vec<ColumnItem>),
|
||||
}
|
||||
|
||||
impl InspectPayload {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Schemas(items) => items.is_empty(),
|
||||
Self::Tables(items) => items.is_empty(),
|
||||
Self::Columns(items) => items.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectResponse {
|
||||
pub fn operation(&self) -> AppOperation {
|
||||
AppOperation::Inspect
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OperationState {
|
||||
if self.payload.is_empty() {
|
||||
OperationState::Empty
|
||||
} else {
|
||||
OperationState::Success
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> AppEvent<Self> {
|
||||
AppEvent::finished(self.operation(), self.state(), self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct SchemaItem {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<SchemaSummary> for SchemaItem {
|
||||
fn from(value: SchemaSummary) -> Self {
|
||||
Self { name: value.name }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct TableItem {
|
||||
pub schema: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
impl From<TableSummary> for TableItem {
|
||||
fn from(value: TableSummary) -> Self {
|
||||
Self {
|
||||
schema: value.schema,
|
||||
name: value.name,
|
||||
kind: value.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ColumnItem {
|
||||
pub name: String,
|
||||
pub data_type: String,
|
||||
pub nullable: bool,
|
||||
pub primary_key: bool,
|
||||
}
|
||||
|
||||
impl From<ColumnSummary> for ColumnItem {
|
||||
fn from(value: ColumnSummary) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
data_type: value.data_type,
|
||||
nullable: value.nullable,
|
||||
primary_key: value.primary_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InspectResult> for InspectPayload {
|
||||
fn from(value: InspectResult) -> Self {
|
||||
match value {
|
||||
InspectResult::Schemas(items) => {
|
||||
Self::Schemas(items.into_iter().map(SchemaItem::from).collect())
|
||||
}
|
||||
InspectResult::Tables(items) => {
|
||||
Self::Tables(items.into_iter().map(TableItem::from).collect())
|
||||
}
|
||||
InspectResult::Columns(items) => {
|
||||
Self::Columns(items.into_iter().map(ColumnItem::from).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct QuerySourceSummary {
|
||||
pub kind: &'static str,
|
||||
pub label: &'static str,
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&QuerySource> for QuerySourceSummary {
|
||||
fn from(value: &QuerySource) -> Self {
|
||||
match value {
|
||||
QuerySource::Inline => Self {
|
||||
kind: "inline",
|
||||
label: value.label(),
|
||||
path: None,
|
||||
},
|
||||
QuerySource::File(path) => Self {
|
||||
kind: "file",
|
||||
label: value.label(),
|
||||
path: Some(path.display().to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct QueryDataset {
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
pub rows_affected: Option<u64>,
|
||||
pub row_count: usize,
|
||||
}
|
||||
|
||||
impl QueryDataset {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rows_affected.is_none() && self.row_count == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QueryResult> for QueryDataset {
|
||||
fn from(value: QueryResult) -> Self {
|
||||
Self {
|
||||
row_count: value.rows.len(),
|
||||
columns: value.columns,
|
||||
rows: value.rows,
|
||||
rows_affected: value.rows_affected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct QueryResponse {
|
||||
pub target: ConnectionSummary,
|
||||
pub source: QuerySourceSummary,
|
||||
pub result: QueryDataset,
|
||||
}
|
||||
|
||||
impl QueryResponse {
|
||||
pub fn operation(&self) -> AppOperation {
|
||||
AppOperation::Query
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OperationState {
|
||||
if self.result.is_empty() {
|
||||
OperationState::Empty
|
||||
} else {
|
||||
OperationState::Success
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> AppEvent<Self> {
|
||||
AppEvent::finished(self.operation(), self.state(), self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ExportSummary {
|
||||
pub format: &'static str,
|
||||
pub output_path: String,
|
||||
pub overwrite: bool,
|
||||
}
|
||||
|
||||
impl From<&ExportRequest> for ExportSummary {
|
||||
fn from(value: &ExportRequest) -> Self {
|
||||
Self {
|
||||
format: export_format_name(value.format),
|
||||
output_path: value.output_path.display().to_string(),
|
||||
overwrite: value.overwrite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ExportResponse {
|
||||
pub target: ConnectionSummary,
|
||||
pub source: QuerySourceSummary,
|
||||
pub export: ExportSummary,
|
||||
pub row_count: usize,
|
||||
}
|
||||
|
||||
impl ExportResponse {
|
||||
pub fn operation(&self) -> AppOperation {
|
||||
AppOperation::Export
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OperationState {
|
||||
OperationState::Success
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> AppEvent<Self> {
|
||||
AppEvent::finished(self.operation(), self.state(), self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppErrorKind {
|
||||
Validation,
|
||||
Connection,
|
||||
Authentication,
|
||||
Inspect,
|
||||
Query,
|
||||
Export,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AppError {
|
||||
pub operation: &'static str,
|
||||
pub kind: AppErrorKind,
|
||||
pub message: String,
|
||||
pub target: Option<ConnectionSummary>,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn operation_kind(&self) -> AppOperation {
|
||||
AppOperation::from_name(self.operation)
|
||||
}
|
||||
|
||||
pub fn state(&self) -> OperationState {
|
||||
OperationState::Error
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> AppEvent<()> {
|
||||
AppEvent {
|
||||
operation: self.operation_kind(),
|
||||
state: self.state(),
|
||||
data: None,
|
||||
error: Some(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AppError {}
|
||||
|
||||
pub fn connect(profile: &ConnectionProfile) -> Result<ConnectResponse, AppError> {
|
||||
validate_profile("connect", profile)?;
|
||||
DriverRegistry::connect(&profile.target)
|
||||
.map_err(|error| map_driver_error("connect", profile, error))?;
|
||||
|
||||
Ok(ConnectResponse {
|
||||
target: ConnectionSummary::from(profile),
|
||||
status: "connected",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn inspect(
|
||||
profile: &ConnectionProfile,
|
||||
request: &InspectRequest,
|
||||
) -> Result<InspectResponse, AppError> {
|
||||
validate_profile("inspect", profile)?;
|
||||
request
|
||||
.validate()
|
||||
.map_err(|error| map_core_error("inspect", Some(profile), error))?;
|
||||
|
||||
let payload = DriverRegistry::inspect(&profile.target, request)
|
||||
.map(InspectPayload::from)
|
||||
.map_err(|error| map_driver_error("inspect", profile, error))?;
|
||||
|
||||
Ok(InspectResponse {
|
||||
target: ConnectionSummary::from(profile),
|
||||
scope: InspectScope {
|
||||
schema: request.schema.clone(),
|
||||
table: request.table.clone(),
|
||||
},
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query(profile: &ConnectionProfile, request: &QueryRequest) -> Result<QueryResponse, AppError> {
|
||||
validate_profile("query", profile)?;
|
||||
request
|
||||
.validate()
|
||||
.map_err(|error| map_core_error("query", Some(profile), error))?;
|
||||
|
||||
let result = DriverRegistry::query(&profile.target, request)
|
||||
.map(QueryDataset::from)
|
||||
.map_err(|error| map_driver_error("query", profile, error))?;
|
||||
|
||||
Ok(QueryResponse {
|
||||
target: ConnectionSummary::from(profile),
|
||||
source: QuerySourceSummary::from(&request.source),
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn export(
|
||||
profile: &ConnectionProfile,
|
||||
request: &QueryRequest,
|
||||
export: &ExportRequest,
|
||||
) -> Result<ExportResponse, AppError> {
|
||||
validate_profile("export", profile)?;
|
||||
request
|
||||
.validate()
|
||||
.map_err(|error| map_core_error("export", Some(profile), error))?;
|
||||
export
|
||||
.validate()
|
||||
.map_err(|error| map_core_error("export", Some(profile), error))?;
|
||||
validate_export_path(export).map_err(|message| AppError {
|
||||
operation: "export",
|
||||
kind: AppErrorKind::Export,
|
||||
message,
|
||||
target: Some(ConnectionSummary::from(profile)),
|
||||
})?;
|
||||
|
||||
let query_response = query(profile, request)?;
|
||||
validate_export_result(&query_response.result).map_err(|message| AppError {
|
||||
operation: "export",
|
||||
kind: AppErrorKind::Export,
|
||||
message,
|
||||
target: Some(ConnectionSummary::from(profile)),
|
||||
})?;
|
||||
|
||||
let body = render_export_body(export.format, &query_response.result);
|
||||
fs::write(&export.output_path, body).map_err(|error| AppError {
|
||||
operation: "export",
|
||||
kind: AppErrorKind::Export,
|
||||
message: format!(
|
||||
"failed to write export {}: {error}",
|
||||
export.output_path.display()
|
||||
),
|
||||
target: Some(ConnectionSummary::from(profile)),
|
||||
})?;
|
||||
|
||||
Ok(ExportResponse {
|
||||
target: query_response.target,
|
||||
source: query_response.source,
|
||||
export: ExportSummary::from(export),
|
||||
row_count: query_response.result.row_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_export_path(export: &ExportRequest) -> Result<(), String> {
|
||||
if export.output_path.exists() && !export.overwrite {
|
||||
return Err(format!(
|
||||
"export request is invalid: output file {} already exists; pass --overwrite to replace it",
|
||||
export.output_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
if export.output_path.is_dir() {
|
||||
return Err(format!(
|
||||
"export request is invalid: output path {} is a directory; provide a file path",
|
||||
export.output_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(parent) = export.output_path.parent() {
|
||||
if !parent.as_os_str().is_empty() && !parent.exists() {
|
||||
return Err(format!(
|
||||
"export request is invalid: parent directory {} does not exist; create it first or choose another output path",
|
||||
parent.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_export_result(result: &QueryDataset) -> Result<(), String> {
|
||||
if result.rows_affected.is_some() {
|
||||
return Err(String::from(
|
||||
"export requires a row-returning query; the provided SQL completed without a result set",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_export_body(format: ExportFormat, result: &QueryDataset) -> String {
|
||||
match format {
|
||||
ExportFormat::Csv => render_csv(result),
|
||||
ExportFormat::Json => render_json(result),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_profile(operation: &'static str, profile: &ConnectionProfile) -> Result<(), AppError> {
|
||||
profile
|
||||
.validate()
|
||||
.map_err(|error| map_core_error(operation, Some(profile), error))
|
||||
}
|
||||
|
||||
fn map_core_error(
|
||||
operation: &'static str,
|
||||
profile: Option<&ConnectionProfile>,
|
||||
error: CoreError,
|
||||
) -> AppError {
|
||||
AppError {
|
||||
operation,
|
||||
kind: AppErrorKind::Validation,
|
||||
message: error.to_string(),
|
||||
target: profile.map(ConnectionSummary::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_driver_error(
|
||||
operation: &'static str,
|
||||
profile: &ConnectionProfile,
|
||||
error: DriverError,
|
||||
) -> AppError {
|
||||
let kind = match &error {
|
||||
DriverError::NotImplemented(_) => AppErrorKind::Validation,
|
||||
DriverError::Connection(_) => AppErrorKind::Connection,
|
||||
DriverError::Authentication(_) => AppErrorKind::Authentication,
|
||||
DriverError::Inspect(_) => AppErrorKind::Inspect,
|
||||
DriverError::Query(_) => AppErrorKind::Query,
|
||||
};
|
||||
|
||||
AppError {
|
||||
operation,
|
||||
kind,
|
||||
message: error.to_string(),
|
||||
target: Some(ConnectionSummary::from(profile)),
|
||||
}
|
||||
}
|
||||
|
||||
fn export_format_name(format: ExportFormat) -> &'static str {
|
||||
match format {
|
||||
ExportFormat::Csv => "csv",
|
||||
ExportFormat::Json => "json",
|
||||
}
|
||||
}
|
||||
|
||||
fn render_csv(result: &QueryDataset) -> String {
|
||||
let mut lines = Vec::new();
|
||||
lines.push(result.columns.join(","));
|
||||
lines.extend(result.rows.iter().map(|row| {
|
||||
row.iter()
|
||||
.map(|value| escape_csv(value))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}));
|
||||
|
||||
if lines.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{}\n", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_json(result: &QueryDataset) -> String {
|
||||
let rows = result
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let fields = result
|
||||
.columns
|
||||
.iter()
|
||||
.zip(row.iter())
|
||||
.map(|(column, value)| {
|
||||
format!("\"{}\":\"{}\"", escape_json(column), escape_json(value))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!(" {{{fields}}}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",\n");
|
||||
|
||||
if rows.is_empty() {
|
||||
String::from("[]\n")
|
||||
} else {
|
||||
format!("[\n{rows}\n]\n")
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_csv(value: &str) -> String {
|
||||
if value.contains(',') || value.contains('"') || value.contains('\n') {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_json(value: &str) -> String {
|
||||
value
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use db_core::{ConnectionTarget, ConnectionTransport, DatabaseKind};
|
||||
use rusqlite::Connection as SqliteConnection;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env, fs};
|
||||
|
||||
#[test]
|
||||
fn sqlite_query_returns_structured_result() {
|
||||
let path = temp_sqlite_db_path("dbtool-app-query");
|
||||
let connection =
|
||||
SqliteConnection::open(&path).expect("temporary sqlite database should open");
|
||||
connection
|
||||
.execute_batch(include_str!("../../../examples/fixtures/sqlite/init.sql"))
|
||||
.expect("sqlite fixture should seed");
|
||||
drop(connection);
|
||||
|
||||
let target = ConnectionTarget::new(
|
||||
DatabaseKind::Sqlite,
|
||||
ConnectionTransport::File { path: path.clone() },
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("sqlite target should be valid");
|
||||
let profile =
|
||||
ConnectionProfile::new("qa-demo", target, None).expect("profile should be valid");
|
||||
let request = QueryRequest::new(
|
||||
String::from("SELECT COUNT(*) AS count FROM tickets WHERE status = ?1"),
|
||||
QuerySource::Inline,
|
||||
vec![String::from("open")],
|
||||
)
|
||||
.expect("request should be valid");
|
||||
|
||||
let response = query(&profile, &request).expect("query should succeed");
|
||||
|
||||
assert_eq!(response.target.driver, "sqlite");
|
||||
assert_eq!(response.source.kind, "inline");
|
||||
assert_eq!(response.result.columns, vec![String::from("count")]);
|
||||
assert_eq!(response.result.rows, vec![vec![String::from("3")]]);
|
||||
assert_eq!(response.result.row_count, 1);
|
||||
|
||||
fs::remove_file(path).expect("temporary sqlite database should be removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_body_json_escapes_quotes_and_newlines() {
|
||||
let body = render_export_body(
|
||||
ExportFormat::Json,
|
||||
&QueryDataset {
|
||||
columns: vec![String::from("title"), String::from("notes")],
|
||||
rows: vec![vec![
|
||||
String::from("csv export mismatch"),
|
||||
String::from("line one\n\"line two\""),
|
||||
]],
|
||||
rows_affected: None,
|
||||
row_count: 1,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
body,
|
||||
"[\n {\"title\":\"csv export mismatch\",\"notes\":\"line one\\n\\\"line two\\\"\"}\n]\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_response_uses_empty_state_for_empty_payload() {
|
||||
let response = InspectResponse {
|
||||
target: connection_summary(),
|
||||
scope: InspectScope {
|
||||
schema: Some(String::from("main")),
|
||||
table: None,
|
||||
},
|
||||
payload: InspectPayload::Tables(Vec::new()),
|
||||
};
|
||||
|
||||
assert_eq!(response.state(), OperationState::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_response_distinguishes_empty_rows_from_command_results() {
|
||||
let empty = QueryResponse {
|
||||
target: connection_summary(),
|
||||
source: QuerySourceSummary {
|
||||
kind: "inline",
|
||||
label: "inline SQL",
|
||||
path: None,
|
||||
},
|
||||
result: QueryDataset {
|
||||
columns: vec![String::from("count")],
|
||||
rows: Vec::new(),
|
||||
rows_affected: None,
|
||||
row_count: 0,
|
||||
},
|
||||
};
|
||||
let command = QueryResponse {
|
||||
target: connection_summary(),
|
||||
source: QuerySourceSummary {
|
||||
kind: "inline",
|
||||
label: "inline SQL",
|
||||
path: None,
|
||||
},
|
||||
result: QueryDataset {
|
||||
columns: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
rows_affected: Some(2),
|
||||
row_count: 0,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(empty.state(), OperationState::Empty);
|
||||
assert_eq!(command.state(), OperationState::Success);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_response_event_serializes_operation_and_state() {
|
||||
let event = QueryResponse {
|
||||
target: connection_summary(),
|
||||
source: QuerySourceSummary {
|
||||
kind: "file",
|
||||
label: "SQL file",
|
||||
path: Some(String::from("queries/open.sql")),
|
||||
},
|
||||
result: QueryDataset {
|
||||
columns: vec![String::from("count")],
|
||||
rows: vec![vec![String::from("3")]],
|
||||
rows_affected: None,
|
||||
row_count: 1,
|
||||
},
|
||||
}
|
||||
.into_event();
|
||||
|
||||
let json = serde_json::to_value(&event).expect("event should serialize");
|
||||
|
||||
assert_eq!(json["operation"], "query");
|
||||
assert_eq!(json["state"], "success");
|
||||
assert_eq!(json["data"]["source"]["kind"], "file");
|
||||
assert_eq!(json["data"]["result"]["rows"][0][0], "3");
|
||||
assert!(json.get("error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_error_event_serializes_error_state() {
|
||||
let event = AppError {
|
||||
operation: "inspect",
|
||||
kind: AppErrorKind::Inspect,
|
||||
message: String::from("schema lookup failed"),
|
||||
target: Some(connection_summary()),
|
||||
}
|
||||
.into_event();
|
||||
|
||||
let json = serde_json::to_value(&event).expect("error event should serialize");
|
||||
|
||||
assert_eq!(json["operation"], "inspect");
|
||||
assert_eq!(json["state"], "error");
|
||||
assert_eq!(json["error"]["kind"], "inspect");
|
||||
assert_eq!(json["error"]["message"], "schema lookup failed");
|
||||
assert!(json.get("data").is_none());
|
||||
}
|
||||
|
||||
fn connection_summary() -> ConnectionSummary {
|
||||
ConnectionSummary {
|
||||
profile_name: String::from("qa-demo"),
|
||||
driver: String::from("sqlite"),
|
||||
endpoint: String::from("sqlite:///tmp/qa-demo.sqlite"),
|
||||
password_env_var: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_sqlite_db_path(prefix: &str) -> std::path::PathBuf {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos();
|
||||
env::temp_dir().join(format!("{prefix}-{unique}.sqlite"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user