diff --git a/Cargo.lock b/Cargo.lock index 9f8d948..dd4dbf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "arrow-pg" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43a4d328a3f45a159e9b7ee666b7f754eeec4a761a83647780d1a69dd55a1c8" +checksum = "88ce1ffbf30cd0198a53f1f838226337aa136c2eb58530253ed8796b97c05e2e" dependencies = [ "bytes", "chrono", @@ -461,9 +461,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" dependencies = [ "aws-lc-sys", "zeroize", @@ -471,9 +471,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" dependencies = [ "cc", "cmake", @@ -515,9 +515,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bcder" @@ -640,9 +640,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -664,14 +664,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "serde", "wasm-bindgen", "windows-link", ] @@ -778,9 +777,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clio" @@ -860,9 +859,9 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ "crossterm", "unicode-segmentation", @@ -917,6 +916,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constcat" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c" + [[package]] name = "core-foundation" version = "0.10.1" @@ -1029,12 +1034,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1053,11 +1058,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -1078,11 +1082,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.21.3", + "darling_core 0.23.0", "quote", "syn 2.0.114", ] @@ -1103,9 +1107,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba7cb113e9c0bedf9e9765926031e132fa05a1b09ba6e93a6d1a4d7044457b8" +checksum = "f02e9a7e70f214e5282db11c8effba173f4e25a00977e520c6b811817e3a082b" dependencies = [ "arrow", "arrow-schema", @@ -1143,7 +1147,6 @@ dependencies = [ "parking_lot", "rand 0.9.2", "regex", - "rstest", "sqlparser 0.59.0", "tempfile", "tokio", @@ -1153,9 +1156,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a3a799f914a59b1ea343906a0486f17061f39509af74e874a866428951130d" +checksum = "f3e91b2603f906cf8cb8be84ba4e34f9d8fe6dbdfdd6916d55f22317074d1fdf" dependencies = [ "arrow", "async-trait", @@ -1178,9 +1181,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db1b113c80d7a0febcd901476a57aef378e717c54517a163ed51417d87621b0" +checksum = "919d20cdebddee4d8dca651aa0291a44c8104824d1ac288996a325c319ce31ba" dependencies = [ "arrow", "async-trait", @@ -1197,21 +1200,20 @@ dependencies = [ "itertools", "log", "object_store", - "tokio", ] [[package]] name = "datafusion-common" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c10f7659e96127d25e8366be7c8be4109595d6a2c3eac70421f380a7006a1b0" +checksum = "31ff2c4e95be40ad954de93862167b165a6fb49248bb882dea8aef4f888bc767" dependencies = [ "ahash 0.8.12", "arrow", "arrow-ipc", "chrono", "half", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "indexmap", "libc", "log", @@ -1225,9 +1227,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92065bbc6532c6651e2f7dd30b55cba0c7a14f860c7e1d15f165c41a1868d95" +checksum = "0dd9f820fe58c2600b6c33a14432228dbaaf233b96c83a1fd61f16d073d5c3c5" dependencies = [ "futures", "log", @@ -1236,9 +1238,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde13794244bc7581cd82f6fff217068ed79cdc344cafe4ab2c3a1c3510b38d6" +checksum = "86b32b7b12645805d20b70aba6ba846cd262d7b073f7f617640c3294af108d44" dependencies = [ "arrow", "async-trait", @@ -1265,9 +1267,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804fa9b4ecf3157982021770617200ef7c1b2979d57bec9044748314775a9aea" +checksum = "597695c8ebb723ee927b286139d43a3fbed6de7ad9210bd1a9fed5c721ac6fb1" dependencies = [ "arrow", "arrow-ipc", @@ -1289,9 +1291,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a1641a40b259bab38131c5e6f48fac0717bedb7dc93690e604142a849e0568" +checksum = "6bb493d07d8da6d00a89ea9cc3e74a56795076d9faed5ac30284bd9ef37929e9" dependencies = [ "arrow", "async-trait", @@ -1312,9 +1314,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adeacdb00c1d37271176f8fb6a1d8ce096baba16ea7a4b2671840c5c9c64fe85" +checksum = "5e9806521c4d3632f53b9a664041813c267c670232efa1452ef29faee71c3749" dependencies = [ "arrow", "async-trait", @@ -1334,18 +1336,19 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b99e13947667b36ad713549237362afb054b2d8f8cc447751e23ec61202db07" +checksum = "ff69a18418e9878d4840f35e2ad7f2a6386beedf192e9f065e628a7295ff5fbf" [[package]] name = "datafusion-execution" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63695643190679037bc946ad46a263b62016931547bf119859c511f7ff2f5178" +checksum = "ccbc5e469b35d87c0b115327be83d68356ef9154684d32566315b5c071577e23" dependencies = [ "arrow", "async-trait", + "chrono", "dashmap", "datafusion-common", "datafusion-expr", @@ -1360,9 +1363,9 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a4787cbf5feb1ab351f789063398f67654a6df75c4d37d7f637dc96f951a91" +checksum = "81ed3c02a3faf4e09356d5a314471703f440f0a6a14ca6addaf6cfb44ab14de5" dependencies = [ "arrow", "async-trait", @@ -1383,9 +1386,9 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce2fb1b8c15c9ac45b0863c30b268c69dc9ee7a1ee13ecf5d067738338173dc" +checksum = "1567e60d21c372ca766dc9dde98efabe2b06d98f008d988fed00d93546bf5be7" dependencies = [ "arrow", "datafusion-common", @@ -1396,14 +1399,15 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794a9db7f7b96b3346fc007ff25e994f09b8f0511b4cf7dff651fadfe3ebb28f" +checksum = "c4593538abd95c27eeeb2f86b7ad827cce07d0c474eae9b122f4f9675f8c20ad" dependencies = [ "arrow", "arrow-buffer", "base64", "chrono", + "chrono-tz", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -1422,9 +1426,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c25210520a9dcf9c2b2cbbce31ebd4131ef5af7fc60ee92b266dc7d159cb305" +checksum = "f81cdf609f43cd26156934fd81beb7215d60dda40a776c2e1b83d73df69434f2" dependencies = [ "ahash 0.8.12", "arrow", @@ -1443,9 +1447,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f4a66f3b87300bb70f4124b55434d2ae3fe80455f3574701d0348da040b55d" +checksum = "9173f1bcea2ede4a5c23630a48469f06c9db9a408eb5fd140d1ff9a5e0c40ebf" dependencies = [ "ahash 0.8.12", "arrow", @@ -1456,9 +1460,9 @@ dependencies = [ [[package]] name = "datafusion-functions-json" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f427c97cd0d574a2dab3456cbe65695fed700e1136afc09d1ad7093a0ec9fb71" +checksum = "d3ce789cf93834ff0303811ce4080a5c349311fad52e3924ad26f933f59189f3" dependencies = [ "datafusion", "jiter", @@ -1468,9 +1472,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c06eed03918dc7fe7a9f082a284050f0e9ecf95d72f57712d1496da03b8c4" +checksum = "1d0b9f32e7735a3b94ae8b9596d89080dc63dd139029a91133be370da099490d" dependencies = [ "arrow", "arrow-ord", @@ -1491,9 +1495,9 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4fed1d71738fbe22e2712d71396db04c25de4111f1ec252b8f4c6d3b25d7f5" +checksum = "57a29e8a6201b3b9fb2be17d88e287c6d427948d64220cd5ea72ced614a1aee5" dependencies = [ "arrow", "async-trait", @@ -1507,9 +1511,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d92206aa5ae21892f1552b4d61758a862a70956e6fd7a95cb85db1de74bc6d1" +checksum = "cd412754964a31c515e5a814e5ce0edaf30f0ea975f3691e800eff115ee76dfb" dependencies = [ "arrow", "datafusion-common", @@ -1525,9 +1529,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ae9bcc39800820d53a22d758b3b8726ff84a5a3e24cecef04ef4e5fdf1c7cc" +checksum = "d49be73a5ac0797398927a543118bd68e58e80bf95ebdabc77336bcd9c38a711" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1535,9 +1539,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1063ad4c9e094b3f798acee16d9a47bd7372d9699be2de21b05c3bd3f34ab848" +checksum = "439ff5489dcac4d34ed7a49a93310c3345018c4469e34726fa471cdda725346d" dependencies = [ "datafusion-doc", "quote", @@ -1546,9 +1550,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35f9ec5d08b87fd1893a30c2929f2559c2f9806ca072d8fefca5009dc0f06a" +checksum = "a80bb7de8ff5a9948799bc7749c292eac5c629385cdb582893ef2d80b6e718c4" dependencies = [ "arrow", "chrono", @@ -1566,9 +1570,9 @@ dependencies = [ [[package]] name = "datafusion-pg-catalog" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f637c63fabff04818905edcb55f67de7072eba7420a4cffcea98b87dbce87182" +checksum = "daafc06d0478b70b13e8f3d906f2d47c49027efd3718263851137cf6d1d3e0a4" dependencies = [ "async-trait", "datafusion", @@ -1580,9 +1584,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30cc8012e9eedcb48bbe112c6eff4ae5ed19cf3003cb0f505662e88b7014c5d" +checksum = "83480008f66691a0047c5a88990bd76b7c1117dd8a49ca79959e214948b81f0a" dependencies = [ "ahash 0.8.12", "arrow", @@ -1592,19 +1596,21 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-physical-expr-common", "half", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "indexmap", "itertools", "parking_lot", "paste", "petgraph", + "recursive", + "tokio", ] [[package]] name = "datafusion-physical-expr-adapter" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9ff2dbd476221b1f67337699eff432781c4e6e1713d2aefdaa517dfbf79768" +checksum = "6b438306446646b359666a658cc29d5494b1e9873bc7a57707689760666fc82c" dependencies = [ "arrow", "datafusion-common", @@ -1617,23 +1623,26 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90da43e1ec550b172f34c87ec68161986ced70fd05c8d2a2add66eef9c276f03" +checksum = "95b1fbf739038e0b313473588331c5bf79985d1b842b9937c1f10b170665cae1" dependencies = [ "ahash 0.8.12", "arrow", + "chrono", "datafusion-common", "datafusion-expr-common", - "hashbrown 0.14.5", + "hashbrown 0.16.1", + "indexmap", "itertools", + "parking_lot", ] [[package]] name = "datafusion-physical-optimizer" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9804f799acd7daef3be7aaffe77c0033768ed8fdbf5fb82fc4c5f2e6bc14e6" +checksum = "fc4cd3a170faa0f1de04bd4365ccfe309056746dd802ed276e8787ccb8e8a0d4" dependencies = [ "arrow", "datafusion-common", @@ -1650,27 +1659,27 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0acf0ad6b6924c6b1aa7d213b181e012e2d3ec0a64ff5b10ee6282ab0f8532ac" +checksum = "a616a72b4ddf550652b36d5a7c0386eac4accea3ffc6c29a7b16c45f237e9882" dependencies = [ "ahash 0.8.12", "arrow", "arrow-ord", "arrow-schema", "async-trait", - "chrono", "datafusion-common", "datafusion-common-runtime", "datafusion-execution", "datafusion-expr", + "datafusion-functions", "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr", "datafusion-physical-expr-common", "futures", "half", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "indexmap", "itertools", "log", @@ -1681,9 +1690,9 @@ dependencies = [ [[package]] name = "datafusion-postgres" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b2869098db07e7b5e3e609365beba2bfef604e5f22633fbb38ddc462719cb9" +checksum = "12413f19af3af28a49fad42191b45d47941091dfeb5f58bb3791c976d3188be1" dependencies = [ "arrow-pg", "async-trait", @@ -1705,9 +1714,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2c2498a1f134a9e11a9f5ed202a2a7d7e9774bd9249295593053ea3be999db" +checksum = "4bf4b50be3ab65650452993eda4baf81edb245fb039b8714476b0f4c8801a527" dependencies = [ "arrow", "datafusion-common", @@ -1722,9 +1731,9 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f96eebd17555386f459037c65ab73aae8df09f464524c709d6a3134ad4f4776" +checksum = "66e080e2c105284460580c18e751b2133cc306df298181e4349b5b134632811a" dependencies = [ "async-trait", "datafusion-common", @@ -1736,9 +1745,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "51.0.0" +version = "52.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc195fe60634b2c6ccfd131b487de46dc30eccae8a3c35a13f136e7f440414f" +checksum = "3dac502db772ff9bffc2ceae321963091982e8d5f5dfcb877e8dc66fc9a093cc" dependencies = [ "arrow", "bigdecimal", @@ -2050,9 +2059,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -2082,6 +2091,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2183,12 +2198,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.31" @@ -2308,10 +2317,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -2321,7 +2326,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2329,6 +2334,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -2735,6 +2745,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "jiter" version = "0.12.0" @@ -2762,9 +2796,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2784,9 +2818,9 @@ dependencies = [ [[package]] name = "jsonpath-rust" -version = "0.7.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" dependencies = [ "pest", "pest_derive", @@ -2807,10 +2841,11 @@ dependencies = [ [[package]] name = "k8s-metrics" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcd5147ecf729236adaca0ebe19ecd10265592d02541d0b4b467c78ddf1e239d" +checksum = "828ab38ec273f9e8ebaaa36608f73782139b403c0f0e6cad50c47927b8b154a8" dependencies = [ + "constcat", "go-parse-duration", "k8s-openapi", "serde", @@ -2819,12 +2854,12 @@ dependencies = [ [[package]] name = "k8s-openapi" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d9e5e61dd037cdc51da0d7e2b2be10f497478ea7e120d85dad632adb99882b" +checksum = "05a6d6f3611ad1d21732adbd7a2e921f598af6c92d71ae6e2620da4b67ee1f0d" dependencies = [ "base64", - "chrono", + "jiff", "serde", "serde_json", ] @@ -2887,9 +2922,9 @@ dependencies = [ [[package]] name = "kube" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e7bb0b6a46502cc20e4575b6ff401af45cfea150b34ba272a3410b78aa014e" +checksum = "0dae7229247e4215781e5c5104a056e1e2163943e577f9084cf8bba7b5248f7a" dependencies = [ "k8s-openapi", "kube-client", @@ -2900,16 +2935,14 @@ dependencies = [ [[package]] name = "kube-client" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4987d57a184d2b5294fdad3d7fc7f278899469d21a4da39a8f6ca16426567a36" +checksum = "010875e291a9c0a4e076f4f9c35b97d82fd2372cb3bc713252c3d08b7e73ce5b" dependencies = [ "base64", "bytes", - "chrono", "either", "futures", - "home", "http", "http-body", "http-body-util", @@ -2917,6 +2950,7 @@ dependencies = [ "hyper-rustls", "hyper-timeout", "hyper-util", + "jiff", "jsonpath-rust", "k8s-openapi", "kube-core", @@ -2936,14 +2970,14 @@ dependencies = [ [[package]] name = "kube-core" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914bbb770e7bb721a06e3538c0edd2babed46447d128f7c21caa68747060ee73" +checksum = "1ac76281aa698dd34111e25b21f5f6561932a30feabab5357152be273f8a81bb" dependencies = [ - "chrono", "derive_more", "form_urlencoded", "http", + "jiff", "json-patch", "k8s-openapi", "schemars", @@ -2955,11 +2989,11 @@ dependencies = [ [[package]] name = "kube-derive" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03dee8252be137772a6ab3508b81cd797dee62ee771112a2453bc85cbbe150d2" +checksum = "599c09721efcccc0e6a26e93df28c587da60ff5e099c657626fff2af0ae4cbb8" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "serde", @@ -2969,9 +3003,9 @@ dependencies = [ [[package]] name = "kube-runtime" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aea4de4b562c5cc89ab10300bb63474ae1fa57ff5a19275f2e26401a323e3fd" +checksum = "6db43d26700f564baf850f681f3cb0f1195d2699bd379bfa70750ecec4dcb209" dependencies = [ "ahash 0.8.12", "async-broadcast", @@ -2979,7 +3013,7 @@ dependencies = [ "backon", "educe", "futures", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hostname", "json-patch", "k8s-openapi", @@ -3522,9 +3556,9 @@ dependencies = [ [[package]] name = "pgwire" -version = "0.36.3" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a2bcdcc4b20a88e0648778ecf00415bbd5b447742275439c22176835056f99" +checksum = "02d86d57e732d40382ceb9bfea80901d839bae8571aa11c06af9177aed9dfb6c" dependencies = [ "async-trait", "base64", @@ -3543,6 +3577,7 @@ dependencies = [ "ryu", "serde", "serde_json", + "smol_str", "stringprep", "thiserror 2.0.17", "tokio", @@ -3607,11 +3642,20 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postgres-protocol" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" dependencies = [ "base64", "byteorder", @@ -3627,9 +3671,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" dependencies = [ "array-init", "bytes", @@ -3901,7 +3945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3921,7 +3965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3935,9 +3979,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4063,12 +4107,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - [[package]] name = "rend" version = "0.4.2" @@ -4121,40 +4159,11 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rstest" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros", -] - -[[package]] -name = "rstest_macros" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.114", - "unicode-ident", -] - [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ "arrayvec", "borsh", @@ -4169,9 +4178,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc_version" @@ -4234,18 +4243,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -4542,6 +4551,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "socket2" version = "0.6.1" @@ -4792,9 +4811,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -4802,22 +4821,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -4941,9 +4960,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5222,15 +5241,15 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vergen" -version = "9.0.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" dependencies = [ "anyhow", "derive_builder", "rustversion", "time", - "vergen-lib", + "vergen-lib 9.1.0", ] [[package]] @@ -5244,7 +5263,7 @@ dependencies = [ "rustversion", "time", "vergen", - "vergen-lib", + "vergen-lib 0.1.6", ] [[package]] @@ -5258,6 +5277,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + [[package]] name = "version_check" version = "0.9.5" @@ -5291,18 +5321,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -5313,11 +5343,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5326,9 +5357,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5336,9 +5367,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -5349,18 +5380,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5708,9 +5739,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -5871,6 +5902,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml index 941eea6..744b726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["command-line-utilities", "database"] [dependencies] # SQL query engine (Apache Arrow DataFusion) -datafusion = { version = "51", default-features = false, features = [ +datafusion = { version = "52.0.0", default-features = false, features = [ "nested_expressions", "datetime_expressions", "regex_expressions", @@ -21,75 +21,72 @@ datafusion = { version = "51", default-features = false, features = [ "sql", "recursive_protection", ] } -datafusion-functions-json = "0.51" +datafusion-functions-json = "0.52.0" # PRQL compiler (alternative query language) prqlc = "0.13" # Kubernetes client -kube = { version = "2", features = ["runtime", "client", "derive", "rustls-tls", "http-proxy"] } -k8s-openapi = { version = "0.26", features = ["v1_32"] } -k8s-metrics = "0.26" +kube = { version = "3.0.0", features = ["runtime", "client", "derive", "rustls-tls", "http-proxy"] } +k8s-openapi = { version = "0.27.0", features = ["v1_32"] } +k8s-metrics = "0.27.0" # TLS provider for rustls (aws-lc-rs preferred for performance) -rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std", "tls12"] } +rustls = { version = "0.23.36", default-features = false, features = ["aws-lc-rs", "std", "tls12"] } # PostgreSQL wire protocol (for daemon mode) -datafusion-postgres = "0.13" +datafusion-postgres = "0.14.0" # REPL / CLI -rustyline = { version = "17", features = ["derive"] } -indicatif = "0.18" -console = "0.16" -comfy-table = "7" -dirs = "6" +rustyline = { version = "17.0.2", features = ["derive"] } +indicatif = "0.18.3" +console = "0.16.2" +comfy-table = "7.2.2" +dirs = "6.0.0" # Async runtime -tokio = { version = "1", features = ["full"] } +tokio = { version = "1.49.0", features = ["full"] } # Random number generation (for retry jitter) -fastrand = "2" +fastrand = "2.3.0" # CLI argument parsing -clap = { version = "4", features = ["derive"] } +clap = { version = "4.5.54", features = ["derive"] } # Serialization -serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" # Date/time -chrono = "0.4" +chrono = "0.4.43" # Error handling -anyhow = "1" +anyhow = "1.0.100" # Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-rolling-file = { version = "0.1", features = ["non-blocking"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +tracing-rolling-file = { version = "0.1.3", features = ["non-blocking"] } # Futures -futures = "0.3" +futures = "0.3.31" # Async streaming -async-stream = "0.3" +async-stream = "0.3.6" # Async trait support -async-trait = "0.1" +async-trait = "0.1.89" # YAML output -serde_yaml = "0.9" +serde_yaml = "0.9.34" # Regex for LIKE pattern matching -regex = "1" +regex = "1.12.2" -# Pin lazy-regex to 3.4.2 (3.5.0 has a bug with regex::bytes) -lazy-regex = "=3.5.1" +lazy-regex = "3.5.1" # Atomic file operations -tempfile = "3" - -[dev-dependencies] +tempfile = "3.24.0" [profile.release] strip = true @@ -97,4 +94,3 @@ lto = "thin" codegen-units = 1 opt-level = "z" # Optimize for size - diff --git a/src/cli/args.rs b/src/cli/args.rs index 8cce445..2bbc094 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -68,3 +68,171 @@ pub enum OutputFormat { Csv, Yaml, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_output_format() { + let args = Args::parse_from(["k8sql"]); + assert!(matches!(args.output, OutputFormat::Table)); + } + + #[test] + fn test_output_format_json() { + let args = Args::parse_from(["k8sql", "-o", "json"]); + assert!(matches!(args.output, OutputFormat::Json)); + } + + #[test] + fn test_output_format_csv() { + let args = Args::parse_from(["k8sql", "-o", "csv"]); + assert!(matches!(args.output, OutputFormat::Csv)); + } + + #[test] + fn test_output_format_yaml() { + let args = Args::parse_from(["k8sql", "-o", "yaml"]); + assert!(matches!(args.output, OutputFormat::Yaml)); + } + + #[test] + fn test_context_single() { + let args = Args::parse_from(["k8sql", "-c", "prod"]); + assert_eq!(args.context, Some("prod".to_string())); + } + + #[test] + fn test_context_comma_separated() { + let args = Args::parse_from(["k8sql", "-c", "prod,staging,dev"]); + assert_eq!(args.context, Some("prod,staging,dev".to_string())); + } + + #[test] + fn test_context_glob_pattern() { + let args = Args::parse_from(["k8sql", "-c", "prod-*"]); + assert_eq!(args.context, Some("prod-*".to_string())); + } + + #[test] + fn test_context_wildcard() { + let args = Args::parse_from(["k8sql", "-c", "*"]); + assert_eq!(args.context, Some("*".to_string())); + } + + #[test] + fn test_query_simple() { + let args = Args::parse_from(["k8sql", "-q", "SELECT * FROM pods"]); + assert_eq!(args.query, Some("SELECT * FROM pods".to_string())); + } + + #[test] + fn test_query_with_special_characters() { + let args = Args::parse_from([ + "k8sql", + "-q", + "SELECT * FROM pods WHERE labels->>'app' = 'nginx'", + ]); + assert!(args.query.as_ref().unwrap().contains("->>")); + } + + #[test] + fn test_file_flag() { + let args = Args::parse_from(["k8sql", "-f", "/path/to/query.sql"]); + assert_eq!(args.file, Some("/path/to/query.sql".to_string())); + } + + #[test] + fn test_no_headers_flag() { + let args = Args::parse_from(["k8sql", "--no-headers"]); + assert!(args.no_headers); + } + + #[test] + fn test_verbose_flag() { + let args = Args::parse_from(["k8sql", "-v"]); + assert!(args.verbose); + } + + #[test] + fn test_refresh_crds_flag() { + let args = Args::parse_from(["k8sql", "--refresh-crds"]); + assert!(args.refresh_crds); + } + + #[test] + fn test_daemon_subcommand_defaults() { + let args = Args::parse_from(["k8sql", "daemon"]); + match args.command { + Some(Command::Daemon { port, bind }) => { + assert_eq!(port, 15432); + assert_eq!(bind, "127.0.0.1"); + } + _ => panic!("Expected Daemon command"), + } + } + + #[test] + fn test_daemon_custom_port() { + let args = Args::parse_from(["k8sql", "daemon", "--port", "5432"]); + match args.command { + Some(Command::Daemon { port, .. }) => assert_eq!(port, 5432), + _ => panic!("Expected Daemon command"), + } + } + + #[test] + fn test_daemon_custom_bind() { + let args = Args::parse_from(["k8sql", "daemon", "--bind", "0.0.0.0"]); + match args.command { + Some(Command::Daemon { bind, .. }) => assert_eq!(bind, "0.0.0.0"), + _ => panic!("Expected Daemon command"), + } + } + + #[test] + fn test_daemon_both_options() { + let args = Args::parse_from(["k8sql", "daemon", "-p", "5433", "-b", "192.168.1.1"]); + match args.command { + Some(Command::Daemon { port, bind }) => { + assert_eq!(port, 5433); + assert_eq!(bind, "192.168.1.1"); + } + _ => panic!("Expected Daemon command"), + } + } + + #[test] + fn test_interactive_subcommand() { + let args = Args::parse_from(["k8sql", "interactive"]); + assert!(matches!(args.command, Some(Command::Interactive))); + } + + #[test] + fn test_combined_flags() { + let args = Args::parse_from([ + "k8sql", + "-c", + "prod", + "-q", + "SELECT name FROM pods", + "-o", + "json", + "-v", + ]); + assert_eq!(args.context, Some("prod".to_string())); + assert_eq!(args.query, Some("SELECT name FROM pods".to_string())); + assert!(matches!(args.output, OutputFormat::Json)); + assert!(args.verbose); + } + + #[test] + fn test_no_command_no_query() { + // Just k8sql with no arguments (interactive mode by default) + let args = Args::parse_from(["k8sql"]); + assert!(args.command.is_none()); + assert!(args.query.is_none()); + assert!(args.context.is_none()); + } +} diff --git a/src/daemon/pgwire_server.rs b/src/daemon/pgwire_server.rs index 6658961..fbfa5bd 100644 --- a/src/daemon/pgwire_server.rs +++ b/src/daemon/pgwire_server.rs @@ -3,7 +3,6 @@ use std::sync::Arc; -use datafusion_postgres::auth::AuthManager; use datafusion_postgres::datafusion_pg_catalog::pg_catalog::context::EmptyContextProvider; use datafusion_postgres::datafusion_pg_catalog::setup_pg_catalog; use datafusion_postgres::{QueryHook, ServerOptions, serve_with_hooks}; @@ -46,9 +45,6 @@ impl PgWireServer { let ctx = Arc::new(ctx); - // Create default auth manager (accepts "postgres" user with empty password) - let auth_manager = Arc::new(AuthManager::new()); - // Create custom hooks for k8sql-specific commands let hooks: Vec> = vec![ Arc::new(SetConfigHook::new()), // Handle SET commands from PostgreSQL clients @@ -75,7 +71,7 @@ impl PgWireServer { ); // Use datafusion-postgres to serve queries with our custom hooks - serve_with_hooks(ctx, &server_options, auth_manager, hooks) + serve_with_hooks(ctx, &server_options, hooks) .await .map_err(|e| anyhow::anyhow!("{}", e)) } diff --git a/src/datafusion_integration/execution.rs b/src/datafusion_integration/execution.rs index 42adac2..4a4bd3b 100644 --- a/src/datafusion_integration/execution.rs +++ b/src/datafusion_integration/execution.rs @@ -512,12 +512,11 @@ mod tests { #[test] fn test_is_not_found_error_with_404() { - let api_err = kube::Error::Api(kube::error::ErrorResponse { - status: "Failure".to_string(), - message: "namespaces \"missing\" not found".to_string(), - reason: "NotFound".to_string(), - code: 404, - }); + let api_err = kube::Error::Api( + kube::core::Status::failure("namespaces \"missing\" not found", "NotFound") + .with_code(404) + .boxed(), + ); let err = anyhow::Error::new(api_err); assert!(is_not_found_error(&err)); @@ -525,12 +524,11 @@ mod tests { #[test] fn test_is_not_found_error_with_other_code() { - let api_err = kube::Error::Api(kube::error::ErrorResponse { - status: "Failure".to_string(), - message: "Forbidden".to_string(), - reason: "Forbidden".to_string(), - code: 403, - }); + let api_err = kube::Error::Api( + kube::core::Status::failure("Forbidden", "Forbidden") + .with_code(403) + .boxed(), + ); let err = anyhow::Error::new(api_err); assert!(!is_not_found_error(&err)); diff --git a/src/datafusion_integration/filter_extraction.rs b/src/datafusion_integration/filter_extraction.rs index 12cbd99..bb6b4dd 100644 --- a/src/datafusion_integration/filter_extraction.rs +++ b/src/datafusion_integration/filter_extraction.rs @@ -328,4 +328,121 @@ mod tests { let result = extractor.extract(&[expr]); assert_eq!(result, TestFilter::Special); } + + // === Edge case tests for contradictory conditions === + + #[test] + fn test_contradictory_and_uses_first_match() { + // Current behavior: test_col = 'a' AND test_col = 'b' returns first match ('a') + // Note: This is semantically a contradiction that should return no results, + // but the filter extractor returns the first match found. + // DataFusion will handle this at execution time (the query returns 0 rows). + let extractor = FilterExtractor::::new("test_col"); + let expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("test_col").eq(lit("a"))), + op: Operator::And, + right: Box::new(col("test_col").eq(lit("b"))), + }); + + let result = extractor.extract(&[expr]); + // Documents current behavior: returns first match + assert_eq!(result, TestFilter::Single("a".to_string())); + } + + #[test] + fn test_and_with_different_columns() { + // test_col = 'a' AND other_col = 'b' should return 'a' + // (only extracts from the target column) + let extractor = FilterExtractor::::new("test_col"); + let expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("test_col").eq(lit("a"))), + op: Operator::And, + right: Box::new(col("other_col").eq(lit("b"))), + }); + + let result = extractor.extract(&[expr]); + assert_eq!(result, TestFilter::Single("a".to_string())); + } + + #[test] + fn test_and_target_column_on_right() { + // other_col = 'b' AND test_col = 'a' should still find 'a' + let extractor = FilterExtractor::::new("test_col"); + let expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("other_col").eq(lit("b"))), + op: Operator::And, + right: Box::new(col("test_col").eq(lit("a"))), + }); + + let result = extractor.extract(&[expr]); + assert_eq!(result, TestFilter::Single("a".to_string())); + } + + #[test] + fn test_nested_and_with_multiple_values() { + // (test_col = 'a' AND other = 'x') AND (test_col = 'b' AND other = 'y') + // Should return first match from left subtree + let extractor = FilterExtractor::::new("test_col"); + let left_and = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("test_col").eq(lit("a"))), + op: Operator::And, + right: Box::new(col("other").eq(lit("x"))), + }); + let right_and = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("test_col").eq(lit("b"))), + op: Operator::And, + right: Box::new(col("other").eq(lit("y"))), + }); + let expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(left_and), + op: Operator::And, + right: Box::new(right_and), + }); + + let result = extractor.extract(&[expr]); + assert_eq!(result, TestFilter::Single("a".to_string())); + } + + #[test] + fn test_or_inside_and() { + // (test_col = 'a' OR test_col = 'b') AND other = 'x' + // Should extract the OR values + let extractor = FilterExtractor::::new("test_col"); + let or_expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(col("test_col").eq(lit("a"))), + op: Operator::Or, + right: Box::new(col("test_col").eq(lit("b"))), + }); + let expr = Expr::BinaryExpr(BinaryExpr { + left: Box::new(or_expr), + op: Operator::And, + right: Box::new(col("other").eq(lit("x"))), + }); + + let result = extractor.extract(&[expr]); + assert_eq!( + result, + TestFilter::Multiple(vec!["a".to_string(), "b".to_string()]) + ); + } + + #[test] + fn test_empty_filter_list() { + let extractor = FilterExtractor::::new("test_col"); + let result = extractor.extract(&[]); + assert_eq!(result, TestFilter::Default); + } + + #[test] + fn test_multiple_filters_returns_first_match() { + // When given multiple separate filters, returns first match + let extractor = FilterExtractor::::new("test_col"); + let filters = vec![ + col("test_col").eq(lit("first")), + col("test_col").eq(lit("second")), + ]; + + let result = extractor.extract(&filters); + assert_eq!(result, TestFilter::Single("first".to_string())); + } } diff --git a/src/datafusion_integration/hooks.rs b/src/datafusion_integration/hooks.rs index 44fe263..eef386b 100644 --- a/src/datafusion_integration/hooks.rs +++ b/src/datafusion_integration/hooks.rs @@ -192,7 +192,7 @@ impl ShowTablesHook { for table in &tables { let mut encoder = DataRowEncoder::new(Arc::clone(&fields)); encoder.encode_field(&Some(table.as_str()))?; - encoded_rows.push(encoder.finish()); + encoded_rows.push(Ok(encoder.take_row())); } let row_stream = futures::stream::iter(encoded_rows); @@ -309,7 +309,7 @@ impl ShowDatabasesHook { let mut encoder = DataRowEncoder::new(Arc::clone(&fields)); encoder.encode_field(&Some(ctx.as_str()))?; encoder.encode_field(&Some(current_marker))?; - encoded_rows.push(encoder.finish()); + encoded_rows.push(Ok(encoder.take_row())); } let row_stream = futures::stream::iter(encoded_rows); @@ -376,3 +376,105 @@ impl QueryHook for ShowDatabasesHook { } } } + +#[cfg(test)] +mod tests { + use super::*; + use datafusion::sql::sqlparser::dialect::GenericDialect; + use datafusion::sql::sqlparser::parser::Parser; + + fn parse_statement(sql: &str) -> Statement { + let dialect = GenericDialect {}; + Parser::parse_sql(&dialect, sql).unwrap().pop().unwrap() + } + + #[test] + fn test_extract_set_application_name() { + let stmt = parse_statement("SET application_name = 'DBeaver'"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, value) = result.unwrap(); + assert_eq!(name, "application_name"); + assert!(value.contains("DBeaver")); + } + + #[test] + fn test_extract_set_timezone() { + let stmt = parse_statement("SET TIME ZONE 'UTC'"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, _value) = result.unwrap(); + assert_eq!(name, "timezone"); + } + + #[test] + fn test_extract_set_extra_float_digits() { + let stmt = parse_statement("SET extra_float_digits = 3"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, value) = result.unwrap(); + assert_eq!(name, "extra_float_digits"); + assert!(value.contains('3')); + } + + #[test] + fn test_extract_set_client_encoding_utf8() { + let stmt = parse_statement("SET client_encoding TO 'UTF8'"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, _) = result.unwrap(); + assert_eq!(name, "client_encoding"); + } + + #[test] + fn test_extract_set_names() { + let stmt = parse_statement("SET NAMES 'UTF8'"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, value) = result.unwrap(); + assert_eq!(name, "client_encoding"); + assert!(value.contains("UTF8")); + } + + #[test] + fn test_extract_set_statement_timeout() { + let stmt = parse_statement("SET statement_timeout = 30000"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_some()); + let (name, _) = result.unwrap(); + assert_eq!(name, "statement_timeout"); + } + + #[test] + fn test_extract_non_set_returns_none() { + let stmt = parse_statement("SELECT 1"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_none()); + } + + #[test] + fn test_extract_insert_returns_none() { + let stmt = parse_statement("INSERT INTO test VALUES (1)"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_none()); + } + + #[test] + fn test_extract_show_returns_none() { + let stmt = parse_statement("SHOW TABLES"); + let result = SetConfigHook::extract_set_params(&stmt); + assert!(result.is_none()); + } + + #[test] + fn test_set_config_hook_default() { + // Verify SetConfigHook can be created via Default trait + let _hook = SetConfigHook::default(); + } + + #[test] + fn test_show_tables_hook_default() { + // Verify ShowTablesHook can be created via Default trait + let _hook = ShowTablesHook::default(); + } +} diff --git a/src/datafusion_integration/json_path.rs b/src/datafusion_integration/json_path.rs index 8e883e9..8ee4025 100644 --- a/src/datafusion_integration/json_path.rs +++ b/src/datafusion_integration/json_path.rs @@ -62,6 +62,149 @@ fn is_json_column(word: &str, json_columns: &HashSet) -> bool { json_columns.contains(&word.to_lowercase()) } +/// Convert a dot-notation path string to PostgreSQL arrow syntax. +/// +/// This function parses paths like `status.phase` or `spec.containers[0].image` +/// and converts them to arrow operators like `status->>'phase'` or +/// `spec->'containers'->0->>'image'`. +/// +/// # Arguments +/// +/// * `path` - A dot-notation path starting with a JSON column (e.g., "status.phase") +/// * `json_columns` - Optional set of JSON column names. If None, uses DEFAULT_JSON_COLUMNS. +/// +/// # Returns +/// +/// * `Some(arrow_syntax)` if the path starts with a known JSON column and has segments +/// * `None` if the path doesn't start with a JSON column or has no segments +/// +/// # Examples +/// +/// ```ignore +/// assert_eq!( +/// convert_path_to_arrows("status.phase", None), +/// Some("status->>'phase'".to_string()) +/// ); +/// assert_eq!( +/// convert_path_to_arrows("spec.containers[0].image", None), +/// Some("spec->'containers'->0->>'image'".to_string()) +/// ); +/// assert_eq!( +/// convert_path_to_arrows("name", None), // Not a JSON column +/// None +/// ); +/// ``` +pub fn convert_path_to_arrows( + path: &str, + json_columns: Option<&HashSet>, +) -> Option { + let default_columns = build_json_columns_set(&[]); + let json_columns = json_columns.unwrap_or(&default_columns); + + let mut chars = path.chars().peekable(); + + // Extract the JSON column name (first identifier) + let column = consume_identifier(&mut chars)?; + if !is_json_column(&column, json_columns) { + return None; + } + + // Parse path segments: .field, [n], or [] + let segments = parse_path_segments(&mut chars); + if segments.is_empty() { + return None; + } + + let json_path = JsonPath { + alias: None, + column, + segments, + }; + + Some(json_path.to_sql()) +} + +/// Consume an identifier (alphanumeric + underscore) from the character iterator. +fn consume_identifier(chars: &mut std::iter::Peekable) -> Option { + let mut ident = String::new(); + while let Some(&c) = chars.peek() { + if c.is_alphanumeric() || c == '_' { + ident.push(c); + chars.next(); + } else { + break; + } + } + if ident.is_empty() { None } else { Some(ident) } +} + +/// Consume a field name (alphanumeric + underscore + hyphen) from the character iterator. +fn consume_field_name(chars: &mut std::iter::Peekable) -> String { + let mut field = String::new(); + while let Some(&c) = chars.peek() { + if c.is_alphanumeric() || c == '_' || c == '-' { + field.push(c); + chars.next(); + } else { + break; + } + } + field +} + +/// Parse path segments (.field, [n], []) from the character iterator. +fn parse_path_segments(chars: &mut std::iter::Peekable) -> Vec { + let mut segments = Vec::new(); + + while let Some(&c) = chars.peek() { + match c { + '.' => { + chars.next(); + let field = consume_field_name(chars); + if !field.is_empty() { + segments.push(PathSegment::Field(field)); + } + } + '[' => { + chars.next(); + if let Some(segment) = parse_bracket_segment(chars) { + segments.push(segment); + } else { + break; + } + } + _ => break, + } + } + + segments +} + +/// Parse a bracket segment ([n] or []) and consume the closing bracket. +fn parse_bracket_segment(chars: &mut std::iter::Peekable) -> Option { + let mut index_str = String::new(); + while let Some(&c) = chars.peek() { + if c.is_ascii_digit() { + index_str.push(c); + chars.next(); + } else { + break; + } + } + + // Must have closing bracket + if chars.peek() != Some(&']') { + return None; + } + chars.next(); + + if index_str.is_empty() { + Some(PathSegment::Expand) + } else { + index_str.parse::().ok().map(PathSegment::Index) + } +} + /// Parsed segment of a JSON path #[derive(Debug, Clone, PartialEq)] enum PathSegment { @@ -960,4 +1103,207 @@ mod tests { "SELECT \"status.phase\" FROM pods" ); } + + // ========================================================================= + // Edge Case Tests - Array Expansion and Indices + // ========================================================================= + + #[test] + fn test_nested_array_expansion_produces_warning() { + // Nested array expansion spec.containers[].ports[] is not fully supported. + // It should produce output (possibly with a warning) but not crash. + // The second [] should be preserved as literal "[]" in the output. + let result = preprocess_json_paths( + "SELECT spec.containers[].ports[].containerPort FROM pods", + None, + ); + + // Should contain UNNEST for first expansion + assert!(result.contains("UNNEST")); + assert!(result.contains("json_get_array")); + + // The second [] should be preserved as literal (current behavior) + // This documents the current limitation + assert!(result.contains("[]")); + } + + #[test] + fn test_array_expansion_after_index() { + // spec.containers[0].ports[] - index followed by expansion + // This is supported: get first container, then expand its ports + let result = preprocess_json_paths( + "SELECT spec.containers[0].ports[].containerPort FROM pods", + None, + ); + + // Should contain UNNEST for the expansion + assert!(result.contains("UNNEST")); + assert!(result.contains("json_get_array")); + // Should access containers with index 0 first + assert!( + result.contains("'containers', 0, 'ports'") + || result.contains("'containers', '0', 'ports'") + ); + } + + #[test] + fn test_negative_array_index_not_parsed() { + // Negative indices like [-1] are not valid - should not be parsed as array access. + // The tokenizer treats "-1" as a minus operator followed by number, + // so "[" "-" "1" "]" won't match our array index pattern. + let result = preprocess_json_paths("SELECT spec.containers[-1].image FROM pods", None); + + // Should not produce arrow syntax for containers (the path is broken) + // The exact behavior depends on tokenization, but it shouldn't crash + // and shouldn't produce valid array access + assert!(!result.contains("->-1")); + } + + #[test] + fn test_very_large_array_index() { + // Very large indices should still work (they'll return null at runtime) + assert_eq!( + preprocess_json_paths("SELECT spec.containers[999999].image FROM pods", None), + "SELECT spec->'containers'->999999->>'image' FROM pods" + ); + + // Even larger + assert_eq!( + preprocess_json_paths("SELECT spec.containers[2147483647].name FROM pods", None), + "SELECT spec->'containers'->2147483647->>'name' FROM pods" + ); + } + + #[test] + fn test_array_index_zero() { + // Zero index is the most common case + assert_eq!( + preprocess_json_paths("SELECT spec.containers[0].image FROM pods", None), + "SELECT spec->'containers'->0->>'image' FROM pods" + ); + } + + #[test] + fn test_multiple_expansions_at_same_level() { + // Two separate array expansions in same query (different columns) + let result = preprocess_json_paths( + "SELECT spec.containers[].image, spec.volumes[].name FROM pods", + None, + ); + + // Both should produce UNNEST + // Count occurrences of UNNEST + let unnest_count = result.matches("UNNEST").count(); + assert_eq!(unnest_count, 2); + } + + #[test] + fn test_array_expansion_in_where_clause() { + // Array expansion in WHERE clause (complex but valid use case) + let result = preprocess_json_paths( + "SELECT name FROM pods WHERE spec.containers[].image = 'nginx'", + None, + ); + + assert!(result.contains("UNNEST")); + assert!(result.contains("json_get_array")); + } + + #[test] + fn test_decimal_in_brackets_not_parsed() { + // Decimals like [0.5] are not valid array indices + // The tokenizer produces different tokens for "0.5" + let result = preprocess_json_paths("SELECT spec.containers[0.5].image FROM pods", None); + + // Should not produce a valid array index access + // (behavior may vary, but it shouldn't crash) + assert!(!result.contains("->0.5")); + } + + #[test] + fn test_empty_brackets_only() { + // Just empty brackets on a JSON column + let result = preprocess_json_paths("SELECT spec.containers[] FROM pods", None); + + assert!(result.contains("UNNEST")); + assert!(result.contains("json_get_array")); + // No field access after the expansion + assert!(!result.contains("->>")); + } + + // ========================================================================= + // Tests for convert_path_to_arrows (used by PRQL preprocessor) + // ========================================================================= + + #[test] + fn test_convert_path_simple_field() { + assert_eq!( + convert_path_to_arrows("status.phase", None), + Some("status->>'phase'".to_string()) + ); + } + + #[test] + fn test_convert_path_nested_fields() { + assert_eq!( + convert_path_to_arrows("spec.selector.app", None), + Some("spec->'selector'->>'app'".to_string()) + ); + } + + #[test] + fn test_convert_path_array_index() { + assert_eq!( + convert_path_to_arrows("spec.containers[0].image", None), + Some("spec->'containers'->0->>'image'".to_string()) + ); + } + + #[test] + fn test_convert_path_array_expansion() { + let result = convert_path_to_arrows("spec.containers[].image", None); + assert!(result.is_some()); + let sql = result.unwrap(); + assert!(sql.contains("UNNEST")); + assert!(sql.contains("json_get_array")); + } + + #[test] + fn test_convert_path_not_json_column() { + // "name" is not a JSON column + assert_eq!(convert_path_to_arrows("name.something", None), None); + } + + #[test] + fn test_convert_path_json_column_alone() { + // Just "status" with no path segments + assert_eq!(convert_path_to_arrows("status", None), None); + } + + #[test] + fn test_convert_path_all_json_columns() { + assert!(convert_path_to_arrows("labels.app", None).is_some()); + assert!(convert_path_to_arrows("annotations.key", None).is_some()); + assert!(convert_path_to_arrows("spec.field", None).is_some()); + assert!(convert_path_to_arrows("status.field", None).is_some()); + assert!(convert_path_to_arrows("data.key", None).is_some()); + assert!(convert_path_to_arrows("owner_references.name", None).is_some()); + } + + #[test] + fn test_convert_path_deep_nesting() { + assert_eq!( + convert_path_to_arrows("spec.template.spec.containers[0].image", None), + Some("spec->'template'->'spec'->'containers'->0->>'image'".to_string()) + ); + } + + #[test] + fn test_convert_path_field_with_hyphen() { + // Kubernetes often has hyphenated field names + assert_eq!( + convert_path_to_arrows("labels.app-name", None), + Some("labels->>'app-name'".to_string()) + ); + } } diff --git a/src/datafusion_integration/preprocess.rs b/src/datafusion_integration/preprocess.rs index 97da785..cf2f505 100644 --- a/src/datafusion_integration/preprocess.rs +++ b/src/datafusion_integration/preprocess.rs @@ -125,6 +125,8 @@ fn fix_arrow_precedence(sql: &str) -> String { /// This function handles: /// 1. **PRQL detection and compilation**: Queries starting with `from`, `let`, or `prql` /// are automatically compiled to SQL using the prqlc compiler. +/// - For PRQL, JSON paths (e.g., `status.phase`) are first converted to s-strings +/// (e.g., `s"status->>'phase'"`) before compilation. /// 2. **JSON path syntax conversion**: Converts intuitive dot notation like `spec.containers[0].image` /// to PostgreSQL arrow operators like `spec->'containers'->0->>'image'`. /// 3. **JSON arrow precedence fix**: Wraps arrow expressions in parentheses when used @@ -136,6 +138,10 @@ fn fix_arrow_precedence(sql: &str) -> String { /// // PRQL is automatically detected and compiled /// preprocess_sql("from pods | take 5")?; // Returns SQL: SELECT * FROM pods LIMIT 5 /// +/// // PRQL with JSON path syntax works too +/// preprocess_sql("from pods | filter status.phase == \"Running\"")?; +/// // Returns SQL with: WHERE status->>'phase' = 'Running' +/// /// // SQL is processed normally /// preprocess_sql("SELECT * FROM pods")?; // Returns: SELECT * FROM pods /// @@ -150,12 +156,15 @@ fn fix_arrow_precedence(sql: &str) -> String { pub fn preprocess_sql(sql: &str) -> Result { // Step 1: Compile PRQL to SQL if detected let sql = if prql::is_prql(sql) { - prql::compile_prql(sql)? + // Step 1a: Preprocess JSON paths in PRQL before compilation + // This converts status.phase to s"status->>'phase'" etc. + let prql_preprocessed = prql::preprocess_prql_json_paths(sql); + prql::compile_prql(&prql_preprocessed)? } else { sql.to_string() }; - // Step 2: Convert JSON path syntax to arrow operators + // Step 2: Convert JSON path syntax to arrow operators (for SQL) // Uses default JSON columns (spec, status, labels, etc.) // TODO: Accept custom JSON columns from CRD discovery let sql = json_path::preprocess_json_paths(&sql, None); @@ -590,4 +599,114 @@ mod tests { "Chained arrows should work end-to-end without conversion" ); } + + // ========================================================================= + // Edge Case Tests - Arrow Precedence and Idempotency + // ========================================================================= + + #[test] + fn test_already_wrapped_unchanged() { + // Already-wrapped expressions should not be double-wrapped + let sql = "SELECT * FROM pods WHERE (labels->>'app') = 'nginx'"; + let result = preprocess_sql(sql).unwrap(); + // Should NOT contain "((" - no double wrapping + assert!(!result.contains("((labels"), "Should not double-wrap"); + assert_eq!(result, sql); + } + + #[test] + fn test_preprocess_idempotent() { + // Running preprocess twice should produce same result + let sql = "SELECT * FROM pods WHERE labels->>'app' = 'nginx'"; + let once = preprocess_sql(sql).unwrap(); + let twice = preprocess_sql(&once).unwrap(); + assert_eq!(once, twice, "Preprocessing should be idempotent"); + } + + #[test] + fn test_preprocess_idempotent_multiple_arrows() { + // Multiple arrows, run twice + let sql = "SELECT * FROM pods WHERE labels->>'app' = 'nginx' AND labels->>'env' = 'prod'"; + let once = preprocess_sql(sql).unwrap(); + let twice = preprocess_sql(&once).unwrap(); + assert_eq!( + once, twice, + "Preprocessing should be idempotent with multiple arrows" + ); + } + + #[test] + fn test_arrow_comparison_both_sides_idempotent() { + // Arrows on both sides of comparison + let sql = "SELECT * FROM pods p1 JOIN pods p2 ON p1.labels->>'app' = p2.labels->>'app'"; + let once = preprocess_sql(sql).unwrap(); + let twice = preprocess_sql(&once).unwrap(); + assert_eq!( + once, twice, + "Both-side arrow preprocessing should be idempotent" + ); + } + + #[test] + fn test_chained_arrow_with_comparison_idempotent() { + let sql = "SELECT * FROM pods WHERE spec->'selector'->>'app' = 'nginx'"; + let once = preprocess_sql(sql).unwrap(); + let twice = preprocess_sql(&once).unwrap(); + assert_eq!( + once, twice, + "Chained arrow preprocessing should be idempotent" + ); + } + + #[test] + fn test_arrow_with_is_null_no_double_wrap() { + let sql = "SELECT * FROM pods WHERE (labels->>'app') IS NULL"; + let result = preprocess_sql(sql).unwrap(); + // Should remain the same, not double-wrapped + assert!( + !result.contains("((labels"), + "Should not double-wrap IS NULL" + ); + } + + #[test] + fn test_arrow_without_comparison_not_wrapped() { + // Arrows not in comparison context should not be wrapped + let sql = "SELECT labels->>'app', status->>'phase' FROM pods"; + let result = preprocess_sql(sql).unwrap(); + // No parentheses should be added for SELECT list + assert!( + !result.contains("(labels->>'app')"), + "SELECT list arrows should not be wrapped" + ); + assert!( + !result.contains("(status->>'phase')"), + "SELECT list arrows should not be wrapped" + ); + } + + #[test] + fn test_arrow_in_subquery() { + let sql = "SELECT * FROM pods WHERE namespace IN (SELECT namespace FROM pods WHERE labels->>'env' = 'prod')"; + let result = preprocess_sql(sql).unwrap(); + // The inner query's arrow should be wrapped + assert!( + result.contains("(labels->>'env') = 'prod'"), + "Subquery arrow should be wrapped" + ); + } + + #[test] + fn test_empty_sql_unchanged() { + let sql = ""; + let result = preprocess_sql(sql).unwrap(); + assert_eq!(result, sql); + } + + #[test] + fn test_sql_with_no_arrows_unchanged() { + let sql = "SELECT name, namespace FROM pods WHERE namespace = 'default' LIMIT 10"; + let result = preprocess_sql(sql).unwrap(); + assert_eq!(result, sql); + } } diff --git a/src/datafusion_integration/prql.rs b/src/datafusion_integration/prql.rs index 5c87016..11946a7 100644 --- a/src/datafusion_integration/prql.rs +++ b/src/datafusion_integration/prql.rs @@ -13,8 +13,153 @@ //! sort created //! take 10 //! ``` +//! +//! # JSON Path Syntax +//! +//! JSON paths like `status.phase` are automatically converted to s-strings +//! with SQL arrow operators before PRQL compilation: +//! +//! ```prql +//! from pods +//! filter status.phase == "Running" +//! select {name, phase = status.phase} +//! ``` +//! +//! Becomes: +//! +//! ```prql +//! from pods +//! filter s"status->>'phase'" == "Running" +//! select {name, phase = s"status->>'phase'"} +//! ``` +use super::json_path::convert_path_to_arrows; use anyhow::Result; +use regex::Regex; +use std::sync::LazyLock; + +/// Regex pattern to match potential JSON paths in PRQL. +/// +/// Matches patterns like: +/// - `status.phase` +/// - `spec.containers[0].image` +/// - `labels.app` +/// +/// The pattern starts with a word character sequence (the JSON column name) +/// followed by either: +/// - A dot and more word characters/hyphens +/// - A bracket with optional number +/// +/// We're conservative here - we only match at word boundaries and avoid +/// matching inside strings. +static JSON_PATH_PATTERN: LazyLock = LazyLock::new(|| { + // Match: word boundary + json_column + (dot + field or bracket + index)+ + // The JSON column names are: status, spec, labels, annotations, data, owner_references + Regex::new( + r#"(?x) + \b # word boundary + (status|spec|labels|annotations|data|owner_references) # JSON column + ( # followed by: + (?:\.[a-zA-Z_][a-zA-Z0-9_\-]*) # .field (with optional hyphen) + |(?:\[\d*\]) # [index] or [] + )+ # one or more times + "#, + ) + .unwrap() +}); + +/// Preprocess PRQL source to convert JSON path syntax to s-strings. +/// +/// This function finds JSON paths like `status.phase` or `spec.containers[0].image` +/// in PRQL source and converts them to s-strings with SQL arrow operators: +/// `s"status->>'phase'"` or `s"spec->'containers'->0->>'image'"`. +/// +/// This allows PRQL users to use the same intuitive dot notation as SQL users, +/// without having to manually write s-strings. +/// +/// # Arguments +/// +/// * `prql` - The PRQL source string to preprocess +/// +/// # Returns +/// +/// The PRQL source with JSON paths converted to s-strings. +/// +/// # Example +/// +/// ```ignore +/// let prql = "from pods | filter status.phase == \"Running\""; +/// let result = preprocess_prql_json_paths(prql); +/// assert_eq!(result, "from pods | filter s\"status->>'phase'\" == \"Running\""); +/// ``` +pub fn preprocess_prql_json_paths(prql: &str) -> String { + // State machine to avoid converting JSON paths inside strings. + // PRQL uses double quotes for strings ("string") and s-strings (s"raw sql"). + let mut result = String::with_capacity(prql.len()); + let mut chars = prql.chars().peekable(); + let mut code_buffer = String::new(); + + while let Some(c) = chars.next() { + match c { + '"' => { + // Entering a string - flush and convert buffered code first + flush_code_buffer(&mut result, &mut code_buffer); + copy_string_literal(c, &mut chars, &mut result); + } + 's' if chars.peek() == Some(&'"') => { + // s-string: flush code buffer, then copy s"..." verbatim + flush_code_buffer(&mut result, &mut code_buffer); + result.push('s'); + copy_string_literal(chars.next().unwrap(), &mut chars, &mut result); + } + _ => code_buffer.push(c), + } + } + + flush_code_buffer(&mut result, &mut code_buffer); + result +} + +/// Flush the code buffer, converting JSON paths, and clear it. +fn flush_code_buffer(result: &mut String, buffer: &mut String) { + if !buffer.is_empty() { + result.push_str(&convert_json_paths_in_text(buffer)); + buffer.clear(); + } +} + +/// Copy a string literal (starting quote already consumed) to result, handling escapes. +fn copy_string_literal( + opening_quote: char, + chars: &mut std::iter::Peekable, + result: &mut String, +) { + result.push(opening_quote); + while let Some(c) = chars.next() { + result.push(c); + if c == '"' { + break; // End of string + } else if c == '\\' { + // Copy escaped character + if let Some(escaped) = chars.next() { + result.push(escaped); + } + } + } +} + +/// Convert JSON paths in a code fragment (outside of strings) to s-strings. +fn convert_json_paths_in_text(text: &str) -> String { + JSON_PATH_PATTERN + .replace_all(text, |caps: ®ex::Captures| { + let path = &caps[0]; + match convert_path_to_arrows(path, None) { + Some(arrow_sql) => format!("s\"{}\"", arrow_sql), + None => path.to_string(), + } + }) + .into_owned() +} /// Detect if input looks like PRQL (vs SQL). /// @@ -137,4 +282,288 @@ mod tests { let result = compile_prql(prql); assert!(result.is_err()); } + + // ========================================================================= + // Edge Case Tests - PRQL Detection + // ========================================================================= + + #[test] + fn test_from_with_tab_separator() { + // Tab instead of space should work + assert!(is_prql("from\tpods")); + assert!(is_prql("from\t\tpods")); // Multiple tabs + assert!(is_prql("from\t pods")); // Tab then space + } + + #[test] + fn test_from_alone_on_line() { + // "from" alone on its own line (multiline PRQL) + assert!(is_prql("from")); + assert!(is_prql("from\n")); + assert!(is_prql(" from\n pods")); + } + + #[test] + fn test_from_not_detected_when_part_of_word() { + // "from_table" should NOT be detected as PRQL + // This is correctly handled because we check "from " with space + assert!(!is_prql("from_table")); + assert!(!is_prql("fromnow()")); + assert!(!is_prql("fromage")); // French cheese :) + } + + #[test] + fn test_sql_from_keyword_not_prql() { + // SQL FROM in various positions + assert!(!is_prql("SELECT * FROM pods")); + assert!(!is_prql("SELECT name FROM pods WHERE true")); + assert!(!is_prql("DELETE FROM pods")); + assert!(!is_prql("INSERT INTO pods SELECT * FROM other")); + } + + #[test] + fn test_prql_header_variations() { + // prql header with target + assert!(is_prql("prql target:sql.generic\nfrom pods")); + assert!(is_prql("PRQL target:sql.generic\nfrom pods")); // uppercase + assert!(is_prql("prql target:sql.postgres\nfrom pods")); + + // prql with multiple options (space after prql) + assert!(is_prql("prql version:0.9\nfrom pods")); + } + + #[test] + fn test_let_keyword() { + // let statements + assert!(is_prql("let x = 1")); + assert!(is_prql("let my_table = from pods")); + assert!(is_prql("LET x = 1")); // uppercase + + // "let" alone - not detected (needs space after) + // This documents current behavior + assert!(!is_prql("letter")); // "let" as prefix + } + + #[test] + fn test_whitespace_edge_cases() { + // Leading whitespace + assert!(is_prql(" from pods")); + assert!(is_prql("\t\tfrom pods")); + assert!(is_prql("\n\nfrom pods")); + + // Mixed whitespace + assert!(is_prql(" \t \n from pods")); + + // Only whitespace - not PRQL + assert!(!is_prql(" ")); + assert!(!is_prql("\t\t\t")); + assert!(!is_prql("\n\n\n")); + } + + #[test] + fn test_comments_then_prql() { + // Multiple comment styles + assert!(is_prql("# single comment\nfrom pods")); + assert!(is_prql("# comment 1\n# comment 2\n# comment 3\nfrom pods")); + + // Indented comments + assert!(is_prql(" # indented\n from pods")); + + // Comment with PRQL-like content (should still check actual first code line) + assert!(is_prql("# this is not from pods\nfrom actual_table")); + } + + #[test] + fn test_comments_only_not_prql() { + // Comments without any code + assert!(!is_prql("# just a comment")); + assert!(!is_prql("# line 1\n# line 2\n# line 3")); + assert!(!is_prql(" # indented comment only")); + } + + #[test] + fn test_empty_inputs() { + assert!(!is_prql("")); + assert!(!is_prql(" ")); + assert!(!is_prql("\n")); + assert!(!is_prql("\t")); + } + + #[test] + fn test_prql_compile_aggregation() { + // Test compilation of aggregation queries + let prql = "from pods | group namespace (aggregate {count = count this})"; + let result = compile_prql(prql); + assert!(result.is_ok(), "Failed to compile: {:?}", result); + + let sql = result.unwrap(); + assert!(sql.to_lowercase().contains("count")); + assert!(sql.to_lowercase().contains("group by")); + } + + #[test] + fn test_prql_compile_sort() { + // Sort with direction + let prql = "from pods | sort {-created}"; + let result = compile_prql(prql); + assert!(result.is_ok(), "Failed to compile: {:?}", result); + + let sql = result.unwrap(); + assert!(sql.to_lowercase().contains("order by")); + assert!(sql.to_lowercase().contains("desc")); + } + + #[test] + fn test_prql_compile_join() { + // Basic join + let prql = "from pods | join deployments (==namespace)"; + let result = compile_prql(prql); + assert!(result.is_ok(), "Failed to compile: {:?}", result); + + let sql = result.unwrap(); + assert!(sql.to_lowercase().contains("join")); + } + + // ========================================================================= + // PRQL JSON Path Preprocessing Tests + // ========================================================================= + + #[test] + fn test_prql_json_path_simple() { + let prql = "from pods | filter status.phase == \"Running\""; + let result = preprocess_prql_json_paths(prql); + assert!( + result.contains("s\"status->>'phase'\""), + "Expected s-string, got: {}", + result + ); + assert!(result.contains("== \"Running\"")); + } + + #[test] + fn test_prql_json_path_in_select() { + let prql = "from pods | select {name, phase = status.phase}"; + let result = preprocess_prql_json_paths(prql); + assert!( + result.contains("s\"status->>'phase'\""), + "Expected s-string, got: {}", + result + ); + } + + #[test] + fn test_prql_json_path_nested() { + let prql = "from deployments | select {spec.selector.app}"; + let result = preprocess_prql_json_paths(prql); + assert!( + result.contains("s\"spec->'selector'->>'app'\""), + "Expected nested arrow syntax, got: {}", + result + ); + } + + #[test] + fn test_prql_json_path_array_index() { + let prql = "from pods | select {image = spec.containers[0].image}"; + let result = preprocess_prql_json_paths(prql); + assert!( + result.contains("s\"spec->'containers'->0->>'image'\""), + "Expected array index syntax, got: {}", + result + ); + } + + #[test] + fn test_prql_json_path_preserves_strings() { + // JSON paths inside strings should NOT be converted + let prql = "from pods | filter name == \"status.phase\""; + let result = preprocess_prql_json_paths(prql); + // The string "status.phase" should be preserved + assert!( + result.contains("\"status.phase\""), + "String should be preserved, got: {}", + result + ); + } + + #[test] + fn test_prql_json_path_preserves_existing_sstrings() { + // Existing s-strings should not be double-converted + let prql = "from pods | filter s\"status->>'phase'\" == \"Running\""; + let result = preprocess_prql_json_paths(prql); + // Should not have nested s-strings + assert!(!result.contains("s\"s\"")); + assert!(result.contains("s\"status->>'phase'\"")); + } + + #[test] + fn test_prql_json_path_multiple_paths() { + let prql = "from pods | filter status.phase == \"Running\" | select {name, labels.app}"; + let result = preprocess_prql_json_paths(prql); + assert!(result.contains("s\"status->>'phase'\"")); + assert!(result.contains("s\"labels->>'app'\"")); + } + + #[test] + fn test_prql_json_path_non_json_column_unchanged() { + // "name" is not a JSON column, so "name.something" shouldn't be converted + let prql = "from pods | filter name == \"test\""; + let result = preprocess_prql_json_paths(prql); + // Should be unchanged (no s-string wrapping) + assert_eq!(prql, result); + } + + #[test] + fn test_prql_json_path_all_json_columns() { + // Test all JSON columns are recognized + let test_cases = vec![ + ("status.phase", "status->>'phase'"), + ("spec.replicas", "spec->>'replicas'"), + ("labels.app", "labels->>'app'"), + ("annotations.key", "annotations->>'key'"), + ("data.config", "data->>'config'"), + ]; + + for (path, expected_arrow) in test_cases { + let prql = format!("from pods | select {{{}}}", path); + let result = preprocess_prql_json_paths(&prql); + assert!( + result.contains(&format!("s\"{}\"", expected_arrow)), + "For path '{}', expected '{}' in: {}", + path, + expected_arrow, + result + ); + } + } + + #[test] + fn test_prql_json_path_end_to_end() { + // Full end-to-end test: PRQL with JSON path -> SQL + use super::super::preprocess::preprocess_sql; + + let prql = "from pods | filter status.phase == \"Running\" | select {name} | take 5"; + let result = preprocess_sql(prql); + assert!(result.is_ok(), "Failed: {:?}", result); + + let sql = result.unwrap(); + // Should contain the arrow operator + assert!( + sql.contains("status") && sql.contains("phase"), + "SQL should reference status and phase: {}", + sql + ); + // Should be valid SQL with WHERE and LIMIT + assert!(sql.to_lowercase().contains("where")); + assert!(sql.to_lowercase().contains("limit")); + } + + #[test] + fn test_prql_json_path_complex_filter() { + let prql = + "from pods | filter status.phase == \"Running\" && labels.app == \"nginx\" | take 10"; + let result = preprocess_prql_json_paths(prql); + assert!(result.contains("s\"status->>'phase'\"")); + assert!(result.contains("s\"labels->>'app'\"")); + } } diff --git a/tests/integration/lib.sh b/tests/integration/lib.sh index 9fed72b..af3f3c6 100755 --- a/tests/integration/lib.sh +++ b/tests/integration/lib.sh @@ -178,5 +178,27 @@ print_summary() { return 0 } +# Compare SQL and PRQL query results for parity +compare_sql_prql() { + local desc="$1" + local context="$2" + local sql_query="$3" + local prql_query="$4" + + local sql_result + local prql_result + + sql_result=$(run_query "$context" "$sql_query" | jq -S '.' 2>/dev/null) + prql_result=$(run_query "$context" "$prql_query" | jq -S '.' 2>/dev/null) + + if [[ "$sql_result" == "$prql_result" ]]; then + echo -e "${GREEN}✓${NC} $desc" + PASS=$((PASS + 1)) + else + echo -e "${RED}✗${NC} $desc (SQL/PRQL results differ)" + FAIL=$((FAIL + 1)) + fi +} + # Export counters for use in subshells export PASS FAIL diff --git a/tests/integration/tests/23-prql-parity.sh b/tests/integration/tests/23-prql-parity.sh new file mode 100755 index 0000000..3361486 --- /dev/null +++ b/tests/integration/tests/23-prql-parity.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# PRQL/SQL Parity Tests +# Verify that equivalent SQL and PRQL queries produce identical results + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib.sh" + +echo "=== PRQL/SQL Parity Tests ===" + +echo "" +echo "--- Basic SELECT Parity ---" + +# Note: PRQL generates slightly different SQL, so we compare sorted JSON results +# These tests verify semantic equivalence, not exact SQL matching + +compare_sql_prql "SELECT * with LIMIT" "k3d-k8sql-test-1" \ + "SELECT * FROM namespaces ORDER BY name LIMIT 3" \ + "from namespaces | sort name | take 3" + +compare_sql_prql "SELECT specific columns" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces ORDER BY name LIMIT 5" \ + "from namespaces | sort name | select {name} | take 5" + +echo "" +echo "--- Filter Parity ---" + +compare_sql_prql "WHERE equals" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace = 'kube-system' ORDER BY name LIMIT 5" \ + "from pods | filter namespace == \"kube-system\" | sort name | select {name} | take 5" + +compare_sql_prql "WHERE not equals" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name != 'default' ORDER BY name LIMIT 5" \ + "from namespaces | filter name != \"default\" | sort name | select {name} | take 5" + +echo "" +echo "--- Aggregation Parity ---" + +compare_sql_prql "COUNT(*)" "k3d-k8sql-test-1" \ + "SELECT namespace, COUNT(*) AS cnt FROM pods GROUP BY namespace ORDER BY cnt DESC LIMIT 5" \ + "from pods | group namespace (aggregate {cnt = count this}) | sort {-cnt} | take 5" + +echo "" +echo "--- Sort Parity ---" + +compare_sql_prql "ORDER BY ASC" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces ORDER BY name ASC LIMIT 5" \ + "from namespaces | sort name | select {name} | take 5" + +compare_sql_prql "ORDER BY DESC" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces ORDER BY name DESC LIMIT 5" \ + "from namespaces | sort {-name} | select {name} | take 5" + +echo "" +echo "--- JSON Path in PRQL ---" + +# PRQL now supports the same JSON path syntax as SQL! +# status.phase is automatically converted to s"status->>'phase'" before compilation +assert_success "PRQL with JSON path syntax" "k3d-k8sql-test-1" \ + "from pods | select {name, phase = status.phase} | take 5" + +assert_success "PRQL filter with JSON path" "k3d-k8sql-test-1" \ + "from pods | filter status.phase == \"Running\" | select {name} | take 5" + +# Also test nested paths and array indexing +assert_success "PRQL nested JSON path" "k3d-k8sql-test-1" \ + "from deployments | select {name, replicas = spec.replicas} | take 5" + +assert_success "PRQL array index in JSON path" "k3d-k8sql-test-1" \ + "from pods | select {name, image = spec.containers[0].image} | take 5" + +echo "" +echo "--- Complex PRQL Queries ---" + +assert_success "PRQL with multiple transforms" "k3d-k8sql-test-1" \ + "from pods | filter namespace == \"kube-system\" | sort name | select {name, namespace} | take 10" + +assert_success "PRQL with aggregation and sort" "k3d-k8sql-test-1" \ + "from pods | group namespace (aggregate {total = count this}) | sort {-total} | take 5" + +print_summary diff --git a/tests/integration/tests/24-complex-filters.sh b/tests/integration/tests/24-complex-filters.sh new file mode 100755 index 0000000..ba4f01c --- /dev/null +++ b/tests/integration/tests/24-complex-filters.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Complex Filter Tests +# Test edge cases in filter handling including contradictory conditions and complex OR chains + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib.sh" + +echo "=== Complex Filter Tests ===" + +echo "" +echo "--- Contradictory Conditions ---" + +# These conditions are logically impossible - should return 0 rows +# Note: DataFusion handles this at execution time since k8sql pushes the first filter to K8s API +assert_row_count "Contradictory namespace (AND)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace = 'default' AND namespace = 'kube-system'" 0 + +assert_row_count "Contradictory cluster (AND)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE _cluster = 'k3d-k8sql-test-1' AND _cluster = 'k3d-k8sql-test-2'" 0 + +# Contradictory label conditions +assert_row_count "Contradictory label values" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE labels->>'app' = 'nginx' AND labels->>'app' = 'redis'" 0 + +echo "" +echo "--- Complex OR Conditions ---" + +# OR between different filter types +assert_success "OR: namespace OR label" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace = 'kube-system' OR labels->>'app' IS NOT NULL LIMIT 10" + +# OR with same column (IN-like behavior) +assert_success "OR: multiple namespace values" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace = 'default' OR namespace = 'kube-system' LIMIT 10" + +# Nested AND/OR combinations +assert_success "Nested AND/OR" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE (namespace = 'kube-system' AND labels->>'tier' IS NOT NULL) OR namespace = 'default' LIMIT 10" + +echo "" +echo "--- NOT IN and IN Combinations ---" + +# IN list with multiple values +assert_success "IN list" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name IN ('default', 'kube-system', 'kube-public') ORDER BY name" + +# NOT IN list +assert_success "NOT IN list" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name NOT IN ('default', 'kube-system') ORDER BY name LIMIT 5" + +# Combining IN with other filters +assert_success "IN combined with AND" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IN ('default', 'kube-system') AND name IS NOT NULL LIMIT 10" + +echo "" +echo "--- Label Selector Edge Cases ---" + +# Multiple label conditions with AND +assert_success "Multiple labels AND" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE labels->>'component' IS NOT NULL AND labels->>'tier' IS NOT NULL LIMIT 5" + +# Label with special characters in value +assert_success "Label with special chars" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE labels->>'app' != 'test-with-dash' LIMIT 5" + +echo "" +echo "--- Field Selector Edge Cases ---" + +# Field selector on pods (status.phase is pushable) +assert_success "Field selector status.phase" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE status->>'phase' = 'Running' LIMIT 10" + +# Field selector on name (metadata.name is pushable) +assert_success "Field selector on name" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name = 'default'" + +# Combined field selectors +assert_success "Multiple field selectors" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE status->>'phase' = 'Running' AND name IS NOT NULL LIMIT 5" + +echo "" +echo "--- Null Handling ---" + +# IS NULL +assert_success "IS NULL filter" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE labels->>'nonexistent-label' IS NULL LIMIT 5" + +# IS NOT NULL +assert_success "IS NOT NULL filter" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IS NOT NULL LIMIT 5" + +# COALESCE with null +assert_success "COALESCE with null" "k3d-k8sql-test-1" \ + "SELECT name, COALESCE(labels->>'app', 'none') AS app_label FROM pods LIMIT 5" + +echo "" +echo "--- LIKE Pattern Matching ---" + +# LIKE with wildcard +assert_success "LIKE with wildcard" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name LIKE 'kube%' ORDER BY name" + +# NOT LIKE +assert_success "NOT LIKE" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name NOT LIKE 'kube%' ORDER BY name LIMIT 5" + +# ILIKE (case insensitive) +assert_success "ILIKE" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name ILIKE 'DEFAULT'" + +echo "" +echo "--- Empty Result Edge Cases ---" + +# Query that definitely returns no results +assert_row_count "Nonexistent namespace" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace = 'this-namespace-does-not-exist-12345'" 0 + +# Impossible string comparison +assert_row_count "Impossible name pattern" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE name = '' AND name != ''" 0 + +print_summary diff --git a/tests/integration/tests/25-stress.sh b/tests/integration/tests/25-stress.sh new file mode 100755 index 0000000..5d7aa42 --- /dev/null +++ b/tests/integration/tests/25-stress.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Stress Tests +# Test performance edge cases and resource handling + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib.sh" + +echo "=== Stress Tests ===" + +echo "" +echo "--- Large IN Lists ---" + +# Generate a large IN list (50 values) +IN_LIST=$(printf "'ns%d'," {1..50} | sed 's/,$//') +assert_success "Large IN list (50 values)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IN ($IN_LIST, 'default', 'kube-system') LIMIT 5" + +# Even larger IN list (100 values) +IN_LIST_100=$(printf "'ns%d'," {1..100} | sed 's/,$//') +assert_success "Large IN list (100 values)" "k3d-k8sql-test-1" \ + "SELECT name FROM namespaces WHERE name IN ($IN_LIST_100, 'default')" + +echo "" +echo "--- Multi-Cluster Parallel Queries ---" + +# Query all clusters in parallel +assert_success "Multi-cluster wildcard" "k3d-k8sql-test-1" \ + "SELECT name, _cluster FROM pods WHERE _cluster = '*' LIMIT 20" + +# Multi-cluster with aggregation +assert_success "Multi-cluster aggregation" "k3d-k8sql-test-1" \ + "SELECT _cluster, COUNT(*) AS cnt FROM pods WHERE _cluster = '*' GROUP BY _cluster" + +# Multi-cluster with complex filter +assert_success "Multi-cluster complex filter" "k3d-k8sql-test-1" \ + "SELECT name, _cluster FROM pods WHERE _cluster = '*' AND namespace = 'kube-system' LIMIT 10" + +echo "" +echo "--- Projection Pushdown ---" + +# Minimal projection (should be fast) +assert_success "SELECT name only" "k3d-k8sql-test-1" \ + "SELECT name FROM pods LIMIT 100" + +# Full projection (all columns) +assert_success "SELECT * (full projection)" "k3d-k8sql-test-1" \ + "SELECT * FROM pods LIMIT 50" + +# Specific JSON columns +assert_success "SELECT specific JSON columns" "k3d-k8sql-test-1" \ + "SELECT name, spec, status FROM pods LIMIT 50" + +echo "" +echo "--- Large LIMIT/OFFSET ---" + +# Large LIMIT +assert_success "Large LIMIT (1000)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods LIMIT 1000" + +# Very large LIMIT (10000) +assert_success "Very large LIMIT (10000)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods LIMIT 10000" + +# OFFSET with LIMIT +assert_success "OFFSET with LIMIT" "k3d-k8sql-test-1" \ + "SELECT name FROM pods OFFSET 10 LIMIT 5" + +echo "" +echo "--- Many AND Conditions ---" + +# Query with many AND conditions +assert_success "Many AND conditions (6)" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IS NOT NULL AND name IS NOT NULL AND created IS NOT NULL AND api_version IS NOT NULL AND kind IS NOT NULL AND uid IS NOT NULL LIMIT 5" + +# More AND conditions with JSON paths +assert_success "Many AND with JSON" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IS NOT NULL AND status->>'phase' IS NOT NULL AND spec IS NOT NULL LIMIT 5" + +echo "" +echo "--- Complex Aggregations ---" + +# Multiple aggregations +assert_success "Multiple aggregations" "k3d-k8sql-test-1" \ + "SELECT namespace, COUNT(*) AS total, COUNT(DISTINCT name) AS unique_names FROM pods GROUP BY namespace" + +# Aggregation with HAVING +assert_success "Aggregation with HAVING" "k3d-k8sql-test-1" \ + "SELECT namespace, COUNT(*) AS cnt FROM pods GROUP BY namespace HAVING COUNT(*) >= 1" + +echo "" +echo "--- Deep JSON Nesting ---" + +# Deep JSON path access +assert_success "Deep JSON path" "k3d-k8sql-test-1" \ + "SELECT name, spec->'template'->'spec'->>'serviceAccountName' AS sa FROM deployments LIMIT 5" + +# Multiple deep JSON paths +assert_success "Multiple deep JSON paths" "k3d-k8sql-test-1" \ + "SELECT name, spec->>'replicas', status->>'readyReplicas' FROM deployments LIMIT 5" + +echo "" +echo "--- Subquery Performance ---" + +# Simple subquery +assert_success "Simple subquery" "k3d-k8sql-test-1" \ + "SELECT name FROM pods WHERE namespace IN (SELECT name FROM namespaces WHERE name LIKE 'kube%')" + +# Subquery in SELECT (scalar subquery) +assert_success "Scalar subquery" "k3d-k8sql-test-1" \ + "SELECT name, (SELECT COUNT(*) FROM namespaces) AS ns_count FROM pods LIMIT 5" + +echo "" +echo "--- Join Performance ---" + +# Simple join +assert_success "Simple join" "k3d-k8sql-test-1" \ + "SELECT p.name AS pod, d.name AS deploy FROM pods p JOIN deployments d ON p.namespace = d.namespace LIMIT 10" + +# Join with aggregation +assert_success "Join with aggregation" "k3d-k8sql-test-1" \ + "SELECT d.namespace, COUNT(DISTINCT p.name) AS pod_count FROM deployments d LEFT JOIN pods p ON d.namespace = p.namespace GROUP BY d.namespace LIMIT 5" + +print_summary