diff --git a/Cargo.lock b/Cargo.lock index 0e39ade0..4025f0c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -52,6 +52,31 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -132,6 +157,184 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "arrow" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378530e55cd479eda3c14eb345310799717e6f76d0c332041e8487022166b471" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ab212d2c1886e802f51c5212d78ebbcbb0bec980fff9dadc1eb8d45cd0b738" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.17.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5aefb56a2c02e9e2b30746241058b85f8983f0fcff2ba0c6d09006e1cded7f" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bffd8fd2579286a5d63bac898159873e5094a79009940bcb42bbfce4f19f1d0" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5994731204603c73ba69267616c50f80780774c6bb0476f1f830625115e0c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "arrow-select" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "58.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29dd7cda3ab9692f43a2e4acc444d760cc17b12bb6d8232ddf64e9bab7c06b42" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax 0.8.5", +] + [[package]] name = "async-compression" version = "0.4.27" @@ -194,6 +397,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -293,6 +505,18 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -302,6 +526,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "bstr" version = "1.12.0" @@ -318,6 +566,28 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.9" @@ -336,12 +606,20 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -363,6 +641,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.41" @@ -489,6 +778,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "crossterm 0.28.1", + "unicode-segmentation", + "unicode-width 0.2.1", +] + [[package]] name = "config" version = "0.3.69" @@ -529,6 +829,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -563,6 +883,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -606,6 +935,19 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -633,6 +975,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto" version = "0.3.69" @@ -848,6 +1196,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -966,6 +1325,24 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "duckdb" +version = "1.10504.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9b997383221efd999a362448a0866c54b9a2cb7d4da8cd4903ead4df9f8eaa" +dependencies = [ + "arrow", + "cast", + "comfy-table", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libduckdb-sys", + "num-integer", + "rust_decimal", + "strum 0.27.2", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -1027,6 +1404,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1063,6 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1072,6 +1462,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1091,6 +1487,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1225,8 +1627,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1237,10 +1641,24 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1304,17 +1722,50 @@ dependencies = [ "walkdir", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.4", +] [[package]] name = "heck" @@ -1657,7 +2108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ "bitflags 2.9.1", - "crossterm", + "crossterm 0.29.0", "dyn-clone", "fuzzy-matcher", "unicode-segmentation", @@ -1743,6 +2194,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1785,12 +2246,86 @@ dependencies = [ "spin", ] -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libduckdb-sys" +version = "1.10504.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e67523a40ee3da30411e00e243990b3760d91877202ab2f39c03ab34f8dbfc" +dependencies = [ + "cc", + "flate2", + "pkg-config", + "reqwest", + "serde", + "serde_json", + "tar", + "vcpkg", + "zip", +] + [[package]] name = "libm" version = "0.2.15" @@ -1808,6 +2343,15 @@ dependencies = [ "redox_syscall 0.5.15", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1850,9 +2394,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "maplit" @@ -2004,6 +2554,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2021,6 +2581,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2267,6 +2836,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "polyval" version = "0.6.2" @@ -2274,7 +2849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -2309,6 +2884,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.5", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2327,6 +2911,26 @@ dependencies = [ "rustyline", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pyo3" version = "0.28.0" @@ -2385,6 +2989,62 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.0", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -2400,6 +3060,18 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radix_trie" version = "0.2.1" @@ -2431,6 +3103,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2469,6 +3152,21 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2568,6 +3266,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.22" @@ -2576,6 +3283,7 @@ checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", "futures-util", "http", @@ -2589,6 +3297,7 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-pki-types", "serde", @@ -2639,6 +3348,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rmcp" version = "0.5.0" @@ -2724,12 +3462,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-demangle" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2785,6 +3546,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -3067,7 +3829,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3126,6 +3888,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.10" @@ -3211,7 +3985,7 @@ checksum = "a0ef947f358b9c238923f764c72a4a9d42f2d637c46e059dbd319d6e7cfb4f82" dependencies = [ "lazy_static", "maplit", - "strum", + "strum 0.24.1", ] [[package]] @@ -3261,7 +4035,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -3277,6 +4060,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3358,6 +4153,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.45" @@ -3506,6 +4307,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -3516,6 +4326,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tmpdir" version = "1.0.0" @@ -3629,8 +4454,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -3642,6 +4467,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3651,9 +4485,30 @@ dependencies = [ "indexmap 2.10.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.12", +] + +[[package]] +name = "toml_edit" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.12", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", ] [[package]] @@ -3716,6 +4571,7 @@ dependencies = [ "crypto", "ctrlc", "cucumber", + "duckdb", "futures", "futures-util", "http", @@ -3739,7 +4595,7 @@ dependencies = [ "tokio-test", "tokio-util", "toml", - "toml_edit", + "toml_edit 0.22.27", "tower-api", "tower-package", "tower-runtime", @@ -4076,6 +4932,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -4125,6 +4987,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", ] @@ -4631,6 +5494,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4646,6 +5515,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "1.5.1" @@ -4759,3 +5637,35 @@ dependencies = [ "quote", "syn 2.0.104", ] + +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.10.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index f89fb323..c17ee29f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ config = { path = "crates/config" } crypto = { path = "crates/crypto" } ctrlc = "3" dirs = "5" +duckdb = { version = "~1.10504.0", features = ["bundled"] } flate2 = "1" fs2 = "0.4" futures = "0.3" diff --git a/crates/tower-cmd/Cargo.toml b/crates/tower-cmd/Cargo.toml index dac7d31b..6d5bcf5e 100644 --- a/crates/tower-cmd/Cargo.toml +++ b/crates/tower-cmd/Cargo.toml @@ -11,6 +11,7 @@ clap = { workspace = true } cli-table = { workspace = true } colored = { workspace = true } config = { workspace = true } +duckdb = { workspace = true } crypto = { workspace = true } ctrlc = { workspace = true } futures-util = { workspace = true } diff --git a/crates/tower-cmd/src/catalogs.rs b/crates/tower-cmd/src/catalogs.rs index 4c1677fb..eba2a6e0 100644 --- a/crates/tower-cmd/src/catalogs.rs +++ b/crates/tower-cmd/src/catalogs.rs @@ -1,6 +1,7 @@ use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use colored::Colorize; use config::Config; +use std::io::{IsTerminal, Read}; use tower_api::models::{ vend_catalog_credentials_body, CatalogCredentials, DescribeCatalogResponse, }; @@ -69,7 +70,7 @@ pub fn catalogs_cmd() -> Command { .help("Environment the catalog belongs to") .action(ArgAction::Set), ) - .about("Show the details of a catalog, including its property names"), + .about("Show the details of a catalog, including its properties and tables"), ) .subcommand( Command::new("credentials") @@ -116,6 +117,44 @@ pub fn catalogs_cmd() -> Command { .short_about("Vend short-lived catalog credentials for external tools"), ), ) + .subcommand( + Command::new("query") + .arg( + Arg::new("catalog_name") + .value_parser(value_parser!(String)) + .index(1) + .required(true) + .help("Name of the catalog to query"), + ) + .arg( + Arg::new("sql") + .short('s') + .long("sql") + .value_parser(value_parser!(String)) + .help("SQL statement to execute; read from stdin when omitted") + .action(ArgAction::Set), + ) + .arg( + Arg::new("environment") + .short('e') + .long("environment") + .default_value("default") + .value_parser(value_parser!(String)) + .help("Environment the catalog belongs to") + .action(ArgAction::Set), + ) + .arg( + Arg::new("write") + .short('w') + .long("write") + .help("Allow write statements by vending read-write credentials; queries are read-only by default") + .action(ArgAction::SetTrue), + ) + .about(beta::STORAGE.short_about("Run a SQL query against a catalog using DuckDB")) + .after_help( + "Reference tables as .., e.g.:\n tower catalogs query default --sql 'SELECT * FROM \"default\".my_namespace.my_table LIMIT 10'", + ), + ) } pub async fn do_list(out: &output::Out, config: Config, args: &ArgMatches) { @@ -197,15 +236,435 @@ pub async fn do_show(out: &output::Out, config: Config, args: &ArgMatches) { .expect("catalog_name is required"); let env = cmd::get_string_flag(args, "environment"); - match api::describe_catalog(&config, name, &env).await { - Ok(response) => { - if is_storage_catalog_type(Some(&response.catalog.r#type)) { - beta::notify_once(out, &beta::STORAGE); - } - let human = catalog_details_text(&response); - out.text(&human, &response); + let response = match api::describe_catalog(&config, name, &env).await { + Ok(response) => response, + Err(err) => out.tower_error_and_die(err, "Fetching catalog details failed"), + }; + + let is_storage = is_storage_catalog_type(Some(&response.catalog.r#type)); + if is_storage { + beta::notify_once(out, &beta::STORAGE); + } + + let tables = if is_storage { + Some(fetch_catalog_tables(out, &config, name, &env).await) + } else { + None + }; + + let mut human = catalog_details_text(&response); + human.push('\n'); + human.push_str(&header_line("Tables")); + match &tables { + None => { + human.push_str(&format!( + " Table listing is not supported for {} catalogs.\n", + response.catalog.r#type + )); + } + Some(Ok(result)) if result.rows.is_empty() => { + human.push_str(" No tables found.\n"); + } + Some(Ok(result)) => { + let headers = vec!["Schema".to_string(), "Table".to_string()]; + let data = result + .rows + .iter() + .map(|row| row.iter().map(json_value_to_cell).collect()) + .collect(); + human.push_str(&output::table_text(headers, data)); + } + Some(Err(err)) => { + human.push_str(&format!(" Unable to list tables: {}\n", err)); + } + } + + // `tables` is an array (possibly empty) on success and null otherwise; + // `tables_error` distinguishes a failed listing (message) from a catalog + // type that doesn't support listing (null). + let (json_tables, tables_error) = match tables { + None => (serde_json::Value::Null, None), + Some(Err(err)) => (serde_json::Value::Null, Some(err)), + Some(Ok(result)) => ( + serde_json::Value::Array( + result + .rows + .iter() + .map(|row| { + serde_json::json!({ + "schema": row.first().cloned().unwrap_or(serde_json::Value::Null), + "table": row.get(1).cloned().unwrap_or(serde_json::Value::Null), + }) + }) + .collect(), + ), + None, + ), + }; + let json_data = serde_json::json!({ + "catalog": response.catalog, + "tables": json_tables, + "tables_error": tables_error, + }); + out.text(&human, &json_data); +} + +/// Lists the tables in a catalog by attaching it in DuckDB. Unlike the query +/// path this never exits: `show` should still render catalog details when the +/// tables can't be fetched. +async fn fetch_catalog_tables( + out: &output::Out, + config: &Config, + name: &str, + env: &str, +) -> Result { + let mut spinner = out.spinner("Listing tables..."); + + let response = match api::vend_catalog_credentials( + config, + name, + env, + vend_catalog_credentials_body::Mode::Read, + ) + .await + { + Ok(response) => response, + Err(err) => { + spinner.failure(out); + return Err(err.to_string()); } + }; + + let token = response.credentials.oauth_token.clone(); + let setup = attach_statements( + name, + &response.credentials, + vend_catalog_credentials_body::Mode::Read, + ); + let db_name = name.to_string(); + + let result = tokio::task::spawn_blocking(move || { + run_duckdb_query( + &setup, + "SELECT \"schema\", name FROM (SHOW ALL TABLES) WHERE database = ? ORDER BY \"schema\", name", + duckdb::params![db_name], + ) + }) + .await; + match result { + Ok(Ok(query_result)) => { + spinner.success(out); + Ok(query_result) + } + Ok(Err(err)) => { + spinner.failure(out); + Err(redact_token(&err.to_string(), &token)) + } + Err(err) => { + spinner.failure(out); + Err(redact_token(&err.to_string(), &token)) + } + } +} + +/// DuckDB errors can echo the failing statement, and the setup batch contains +/// the vended OAuth token — scrub it before the message reaches any output. +fn redact_token(message: &str, token: &str) -> String { + if token.is_empty() { + return message.to_string(); + } + message.replace(token, "[REDACTED]") +} + +pub async fn do_query(out: &output::Out, config: Config, args: &ArgMatches) { + beta::notify_once(out, &beta::STORAGE); + + let name = args + .get_one::("catalog_name") + .expect("catalog_name is required"); + let env = cmd::get_string_flag(args, "environment"); + + let sql = match args.get_one::("sql") { + Some(sql) => sql.clone(), + None => read_sql_from_stdin(out), + }; + let sql = sql.trim().to_string(); + if sql.is_empty() { + out.die("No SQL statement provided. Pass one with --sql or pipe it via stdin."); + } + + let response = match api::describe_catalog(&config, name, &env).await { + Ok(response) => response, Err(err) => out.tower_error_and_die(err, "Fetching catalog details failed"), + }; + if !is_storage_catalog_type(Some(&response.catalog.r#type)) { + out.die(&format!( + "Querying is only supported for {} catalogs; '{}' has type '{}'.", + STORAGE_CATALOG_TYPE, name, response.catalog.r#type + )); + } + + let write = cmd::get_bool_flag(args, "write"); + let query_result = execute_catalog_query(out, &config, name, &env, sql, write).await; + output_query_result(out, &query_result); +} + +/// Vends credentials for the catalog, attaches it in an in-memory DuckDB, and +/// runs `sql` against it. Read-only unless `write` is set, in which case +/// read-write credentials are vended and the attach allows writes. Dies with a +/// user-facing error on failure. +async fn execute_catalog_query( + out: &output::Out, + config: &Config, + name: &str, + env: &str, + sql: String, + write: bool, +) -> QueryResult { + let mode = if write { + vend_catalog_credentials_body::Mode::ReadWrite + } else { + vend_catalog_credentials_body::Mode::Read + }; + + let mut spinner = out.spinner("Running query..."); + + let response = match api::vend_catalog_credentials(config, name, env, mode).await { + Ok(response) => response, + Err(err) => { + spinner.failure(out); + out.tower_error_and_die(err, "Running query failed"); + } + }; + + let token = response.credentials.oauth_token.clone(); + let setup = attach_statements(name, &response.credentials, mode); + let result = tokio::task::spawn_blocking(move || run_duckdb_query(&setup, &sql, [])).await; + + match result { + Ok(Ok(query_result)) => { + spinner.success(out); + query_result + } + Ok(Err(err)) => { + spinner.failure(out); + out.die(&format!( + "Query failed: {}", + redact_token(&err.to_string(), &token) + )); + } + Err(err) => { + spinner.failure(out); + out.die(&format!( + "Query execution panicked: {}", + redact_token(&err.to_string(), &token) + )); + } + } +} + +fn read_sql_from_stdin(out: &output::Out) -> String { + let mut stdin = std::io::stdin(); + if stdin.is_terminal() { + out.die("No SQL statement provided. Pass one with --sql or pipe it via stdin."); + } + let mut sql = String::new(); + if let Err(err) = stdin.read_to_string(&mut sql) { + out.die(&format!("Failed reading SQL from stdin: {}", err)); + } + sql +} + +struct QueryResult { + columns: Vec, + rows: Vec>, +} + +/// Statements that install the Iceberg support and attach the catalog under +/// its Tower name — mirrors `templates/duckdb.sql.tmpl`. The attach is +/// READ_ONLY unless read-write credentials were vended. No `USE`: DuckDB's +/// `USE` needs a `main` schema, which Iceberg catalogs don't have, so queries +/// must qualify tables as ..
. +fn attach_statements( + name: &str, + credentials: &CatalogCredentials, + mode: vend_catalog_credentials_body::Mode, +) -> Vec { + let read_only = match mode { + vend_catalog_credentials_body::Mode::Read => "READ_ONLY, ", + vend_catalog_credentials_body::Mode::ReadWrite => "", + }; + vec![ + "INSTALL httpfs".to_string(), + "LOAD httpfs".to_string(), + "INSTALL iceberg".to_string(), + "LOAD iceberg".to_string(), + "SET s3_region='eu-central-1'".to_string(), + format!( + "CREATE OR REPLACE SECRET tower_cat (TYPE iceberg, TOKEN {})", + SqlLiteral(&credentials.oauth_token), + ), + format!( + "ATTACH {warehouse} AS {name} (TYPE iceberg, {read_only}SECRET tower_cat, ENDPOINT {uri}, DEFAULT_REGION 'eu-central-1')", + warehouse = SqlLiteral(&credentials.warehouse), + name = SqlIdent(name), + uri = SqlLiteral(&credentials.catalog_uri), + ), + ] +} + +/// Runs `setup` statements one at a time, then `query` as a prepared statement +/// with `params` bound. Values that fit a bind position should go through +/// `params` rather than into the query text. +fn run_duckdb_query( + setup: &[String], + query: &str, + params: P, +) -> Result { + let conn = duckdb::Connection::open_in_memory()?; + for statement in setup { + conn.execute_batch(statement)?; + } + + let mut stmt = conn.prepare(query)?; + let mut columns: Vec = Vec::new(); + let mut rows = Vec::new(); + + { + let mut result_rows = stmt.query(params)?; + while let Some(row) = result_rows.next()? { + if columns.is_empty() { + columns = row.as_ref().column_names(); + } + let mut record = Vec::with_capacity(columns.len()); + for idx in 0..columns.len() { + let value: duckdb::types::Value = row.get(idx)?; + record.push(duckdb_value_to_json(value)); + } + rows.push(record); + } + } + + // A query with no result rows never populates columns above. + if columns.is_empty() { + columns = stmt.column_names(); + } + + Ok(QueryResult { columns, rows }) +} + +fn duckdb_value_to_json(value: duckdb::types::Value) -> serde_json::Value { + use duckdb::types::{TimeUnit, Value}; + use serde_json::json; + + match value { + Value::Null => serde_json::Value::Null, + Value::Boolean(v) => json!(v), + Value::TinyInt(v) => json!(v), + Value::SmallInt(v) => json!(v), + Value::Int(v) => json!(v), + Value::BigInt(v) => json!(v), + Value::HugeInt(v) => json!(v.to_string()), + Value::UTinyInt(v) => json!(v), + Value::USmallInt(v) => json!(v), + Value::UInt(v) => json!(v), + Value::UBigInt(v) => json!(v), + Value::Float(v) => json!(v), + Value::Double(v) => json!(v), + Value::Decimal(v) => json!(v.to_string()), + Value::Text(v) => json!(v), + Value::Timestamp(unit, v) => { + let micros = match unit { + TimeUnit::Second => v.checked_mul(1_000_000), + TimeUnit::Millisecond => v.checked_mul(1_000), + TimeUnit::Microsecond => Some(v), + TimeUnit::Nanosecond => Some(v / 1_000), + }; + match micros.and_then(chrono::DateTime::from_timestamp_micros) { + Some(ts) => json!(ts.naive_utc().to_string()), + None => json!(format!("{:?}", Value::Timestamp(unit, v))), + } + } + Value::Date32(days) => { + let date = chrono::DateTime::from_timestamp(i64::from(days) * 86_400, 0); + match date { + Some(d) => json!(d.date_naive().to_string()), + None => json!(format!("{:?}", Value::Date32(days))), + } + } + Value::Time64(unit, v) => { + let micros = match unit { + TimeUnit::Second => v.checked_mul(1_000_000), + TimeUnit::Millisecond => v.checked_mul(1_000), + TimeUnit::Microsecond => Some(v), + TimeUnit::Nanosecond => Some(v / 1_000), + }; + let time = micros.and_then(|m| { + chrono::NaiveTime::from_num_seconds_from_midnight_opt( + (m / 1_000_000) as u32, + ((m % 1_000_000) * 1_000) as u32, + ) + }); + match time { + Some(t) => json!(t.to_string()), + None => json!(format!("{:?}", Value::Time64(unit, v))), + } + } + Value::Enum(v) => json!(v), + Value::List(items) | Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(duckdb_value_to_json).collect()) + } + Value::Struct(fields) => serde_json::Value::Object( + fields + .iter() + .map(|(name, value)| (name.clone(), duckdb_value_to_json(value.clone()))) + .collect(), + ), + Value::Map(entries) => serde_json::Value::Object( + entries + .iter() + .map(|(key, value)| { + ( + json_value_to_cell(&duckdb_value_to_json(key.clone())), + duckdb_value_to_json(value.clone()), + ) + }) + .collect(), + ), + Value::Union(inner) => duckdb_value_to_json(*inner), + other => json!(format!("{:?}", other)), + } +} + +fn output_query_result(out: &output::Out, result: &QueryResult) { + let json_rows: Vec> = result + .rows + .iter() + .map(|row| { + result + .columns + .iter() + .cloned() + .zip(row.iter().cloned()) + .collect() + }) + .collect(); + + let data = result + .rows + .iter() + .map(|row| row.iter().map(json_value_to_cell).collect()) + .collect(); + + out.table(result.columns.clone(), data, Some(&json_rows)); + out.note(&format!("\n{} row(s)\n", result.rows.len())); +} + +fn json_value_to_cell(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Null => String::new(), + serde_json::Value::String(s) => s.clone(), + other => other.to_string(), } } @@ -228,12 +687,24 @@ fn quote(value: &str) -> String { serde_json::to_string(value).expect("serializing a string should not fail") } -fn sql_string(value: &str) -> String { - format!("'{}'", value.replace('\'', "''")) +/// A value embedded in generated SQL as a single-quoted string literal. +/// Escaping lives in the `Display` impl, so a value can only appear in the +/// generated text in its escaped form. +struct SqlLiteral<'a>(&'a str); + +impl std::fmt::Display for SqlLiteral<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "'{}'", self.0.replace('\'', "''")) + } } -fn sql_ident(value: &str) -> String { - format!("\"{}\"", value.replace('"', "\"\"")) +/// A value embedded in generated SQL as a double-quoted identifier. +struct SqlIdent<'a>(&'a str); + +impl std::fmt::Display for SqlIdent<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.0.replace('"', "\"\"")) + } } fn token_export_command(name: &str, environment: &str, mode: &str, tower_url: &str) -> String { @@ -369,7 +840,7 @@ fn snippets( "os.environ[\"TOWER_CATALOG_TOKEN\"]".to_string() }; let sql_token = if show_token { - sql_string(&credentials.oauth_token) + SqlLiteral(&credentials.oauth_token).to_string() } else { "'${TOWER_CATALOG_TOKEN}'".to_string() }; @@ -414,9 +885,15 @@ fn snippets( body: render( DUCKDB_TMPL, &[ - ("__TOWER_NAME__", sql_ident(name)), - ("__TOWER_URI__", sql_string(&credentials.catalog_uri)), - ("__TOWER_WAREHOUSE__", sql_string(&credentials.warehouse)), + ("__TOWER_NAME__", SqlIdent(name).to_string()), + ( + "__TOWER_URI__", + SqlLiteral(&credentials.catalog_uri).to_string(), + ), + ( + "__TOWER_WAREHOUSE__", + SqlLiteral(&credentials.warehouse).to_string(), + ), ("__TOWER_TOKEN__", sql_token.clone()), ], ), @@ -443,7 +920,8 @@ fn snippets( #[cfg(test)] mod tests { use super::{ - catalogs_cmd, is_storage_catalog_type, parse_mode, snippets, token_export_command, + attach_statements, catalogs_cmd, duckdb_value_to_json, is_storage_catalog_type, parse_mode, + run_duckdb_query, snippets, token_export_command, }; use tower_api::models::{vend_catalog_credentials_body, CatalogCredentials}; @@ -637,6 +1115,222 @@ mod tests { ); } + #[test] + fn redact_token_scrubs_secret_from_error_text() { + let msg = "Parser Error near 'CREATE SECRET tower_cat (TYPE iceberg, TOKEN 'sekret-123')'"; + assert_eq!( + super::redact_token(msg, "sekret-123"), + "Parser Error near 'CREATE SECRET tower_cat (TYPE iceberg, TOKEN '[REDACTED]')'" + ); + assert_eq!(super::redact_token(msg, ""), msg); + assert_eq!( + super::redact_token("no secret here", "sekret-123"), + "no secret here" + ); + } + + #[test] + fn show_all_tables_query_lists_tables() { + let setup = vec![ + "CREATE SCHEMA s; CREATE TABLE s.t1 (i INTEGER); CREATE TABLE s.t2 (i INTEGER);" + .to_string(), + ]; + let result = run_duckdb_query( + &setup, + "SELECT \"schema\", name FROM (SHOW ALL TABLES) WHERE database = ? ORDER BY \"schema\", name", + duckdb::params!["memory"], + ) + .expect("query should succeed"); + + assert_eq!(result.columns, vec!["schema", "name"]); + assert_eq!( + result.rows, + vec![ + vec![serde_json::json!("s"), serde_json::json!("t1")], + vec![serde_json::json!("s"), serde_json::json!("t2")], + ] + ); + } + + #[test] + fn query_requires_catalog_name() { + let result = catalogs_cmd().try_get_matches_from(["catalogs", "query"]); + assert!(result.is_err()); + } + + #[test] + fn query_accepts_sql_flag_and_environment() { + let matches = catalogs_cmd() + .try_get_matches_from([ + "catalogs", + "query", + "my-catalog", + "--sql", + "SELECT 1", + "-e", + "production", + ]) + .expect("query with --sql should parse"); + + let (_, query_args) = matches.subcommand().expect("expected query subcommand"); + + assert_eq!( + query_args.get_one::("catalog_name").unwrap(), + "my-catalog" + ); + assert_eq!(query_args.get_one::("sql").unwrap(), "SELECT 1"); + assert_eq!( + query_args.get_one::("environment").unwrap(), + "production" + ); + } + + #[test] + fn query_sql_flag_is_optional() { + let matches = catalogs_cmd() + .try_get_matches_from(["catalogs", "query", "my-catalog"]) + .expect("query without --sql should parse"); + + let (_, query_args) = matches.subcommand().expect("expected query subcommand"); + + assert!(query_args.get_one::("sql").is_none()); + assert_eq!( + query_args.get_one::("environment").unwrap(), + "default" + ); + } + + #[test] + fn attach_statements_escapes_values_and_uses_catalog() { + let credentials = CatalogCredentials::new( + "https://catalog.example.com".to_string(), + "2026-06-26T12:00:00Z".to_string(), + "read".to_string(), + "secret'token".to_string(), + "warehouse-id".to_string(), + ); + + let sql = attach_statements( + "my\"catalog", + &credentials, + vend_catalog_credentials_body::Mode::Read, + ) + .join("\n"); + + assert!(sql.contains("TOKEN 'secret''token'")); + assert!( + sql.contains("ATTACH 'warehouse-id' AS \"my\"\"catalog\" (TYPE iceberg, READ_ONLY,") + ); + assert!(sql.contains("ENDPOINT 'https://catalog.example.com'")); + assert!(!sql.contains("USE ")); + + let write_sql = attach_statements( + "my\"catalog", + &credentials, + vend_catalog_credentials_body::Mode::ReadWrite, + ) + .join("\n"); + assert!(!write_sql.contains("READ_ONLY")); + assert!(write_sql.contains( + "ATTACH 'warehouse-id' AS \"my\"\"catalog\" (TYPE iceberg, SECRET tower_cat," + )); + } + + #[test] + fn query_write_flag_defaults_to_false() { + let matches = catalogs_cmd() + .try_get_matches_from(["catalogs", "query", "my-catalog", "--sql", "SELECT 1"]) + .expect("query should parse"); + let (_, query_args) = matches.subcommand().expect("expected query subcommand"); + assert_eq!(query_args.get_one::("write").copied(), Some(false)); + + let matches = catalogs_cmd() + .try_get_matches_from([ + "catalogs", + "query", + "my-catalog", + "--sql", + "DELETE FROM t", + "--write", + ]) + .expect("query --write should parse"); + let (_, query_args) = matches.subcommand().expect("expected query subcommand"); + assert_eq!(query_args.get_one::("write").copied(), Some(true)); + } + + #[test] + fn duckdb_values_convert_to_json() { + use duckdb::types::{TimeUnit, Value}; + + assert_eq!(duckdb_value_to_json(Value::Null), serde_json::Value::Null); + assert_eq!( + duckdb_value_to_json(Value::BigInt(42)), + serde_json::json!(42) + ); + assert_eq!( + duckdb_value_to_json(Value::Text("hi".to_string())), + serde_json::json!("hi") + ); + assert_eq!( + duckdb_value_to_json(Value::Timestamp(TimeUnit::Microsecond, 0)), + serde_json::json!("1970-01-01 00:00:00") + ); + assert_eq!( + duckdb_value_to_json(Value::Date32(1)), + serde_json::json!("1970-01-02") + ); + } + + #[test] + fn run_duckdb_query_returns_columns_and_rows() { + let setup = vec![ + "CREATE TABLE t (id INTEGER, name VARCHAR); INSERT INTO t VALUES (1, 'a'), (2, NULL);" + .to_string(), + ]; + let result = run_duckdb_query(&setup, "SELECT id, name FROM t ORDER BY id", []) + .expect("query should succeed"); + + assert_eq!(result.columns, vec!["id", "name"]); + assert_eq!(result.rows.len(), 2); + assert_eq!( + result.rows[0], + vec![serde_json::json!(1), serde_json::json!("a")] + ); + assert_eq!( + result.rows[1], + vec![serde_json::json!(2), serde_json::Value::Null] + ); + } + + #[test] + fn nested_duckdb_values_convert_to_json_structures() { + let result = run_duckdb_query( + &[], + "SELECT [1, 2] AS l, {'a': 1, 'b': 'x'} AS s, MAP {'k': 2} AS m", + [], + ) + .expect("query should succeed"); + + assert_eq!(result.columns, vec!["l", "s", "m"]); + assert_eq!( + result.rows[0], + vec![ + serde_json::json!([1, 2]), + serde_json::json!({"a": 1, "b": "x"}), + serde_json::json!({"k": 2}), + ] + ); + } + + #[test] + fn run_duckdb_query_reports_columns_for_empty_results() { + let result = + run_duckdb_query(&[], "SELECT 1 AS x WHERE 1 = 0", []).expect("query should succeed"); + + assert_eq!(result.columns, vec!["x"]); + assert!(result.rows.is_empty()); + } + #[test] fn token_export_command_fetches_token_without_printing_it() { let credentials = CatalogCredentials::new( diff --git a/crates/tower-cmd/src/lib.rs b/crates/tower-cmd/src/lib.rs index 2fb916d1..a4d8b48f 100644 --- a/crates/tower-cmd/src/lib.rs +++ b/crates/tower-cmd/src/lib.rs @@ -151,6 +151,9 @@ impl App { Some(("credentials", args)) => { catalogs::do_credentials(&out, sessionized_config, args).await } + Some(("query", args)) => { + catalogs::do_query(&out, sessionized_config, args).await + } _ => { catalogs::catalogs_cmd().print_help().unwrap(); std::process::exit(2);