diff --git a/Cargo.lock b/Cargo.lock index 1ebbc7b..5c5a6f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "anstream" -version = "1.0.0" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -99,15 +99,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.14" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "1.0.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] @@ -381,7 +381,7 @@ name = "asap_planner" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.6.0", + "clap 4.5.60", "indexmap 2.13.0", "pretty_assertions", "promql-parser", @@ -390,6 +390,8 @@ dependencies = [ "serde_json", "serde_yaml", "sketch_db_common", + "sql_utilities", + "sqlparser 0.59.0", "tempfile", "thiserror 1.0.69", "tracing", @@ -698,9 +700,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.58" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -797,9 +799,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -807,9 +809,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -819,9 +821,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -831,9 +833,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codespan-reporting" @@ -848,9 +850,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" @@ -941,7 +943,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.6.0", + "clap 4.5.60", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1085,7 +1087,7 @@ version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" dependencies = [ - "clap 4.6.0", + "clap 4.5.60", "codespan-reporting", "indexmap 2.13.0", "proc-macro2", @@ -1457,7 +1459,7 @@ dependencies = [ "itertools 0.13.0", "log", "paste", - "petgraph 0.6.5", + "petgraph", ] [[package]] @@ -1709,12 +1711,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "flatbuffers" version = "24.12.23" @@ -2451,9 +2447,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" @@ -2508,12 +2504,10 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ - "cfg-if", - "futures-util", "once_cell", "wasm-bindgen", ] @@ -2611,9 +2605,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags 2.11.0", "libc", @@ -2634,9 +2628,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "libc", @@ -2736,9 +2730,9 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.11.6" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" dependencies = [ "twox-hash 2.1.2", ] @@ -2803,9 +2797,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -2879,9 +2873,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2936,9 +2930,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.6" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -2946,9 +2940,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.6" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2997,9 +2991,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.4" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" @@ -3015,9 +3009,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -3047,9 +3041,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3191,7 +3185,7 @@ checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", - "winnow 0.7.15", + "winnow", ] [[package]] @@ -3227,17 +3221,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.13.0", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "indexmap 2.13.0", ] @@ -3339,9 +3323,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -3491,11 +3475,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.14.0", + "itertools 0.10.5", "log", "multimap", "once_cell", - "petgraph 0.7.1", + "petgraph", "prettyplease", "prost", "prost-types", @@ -3553,7 +3537,7 @@ dependencies = [ "base64 0.21.7", "bincode", "chrono", - "clap 4.6.0", + "clap 4.5.60", "criterion", "ctor", "dashmap 5.5.3", @@ -3930,9 +3914,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] @@ -4110,9 +4094,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -4124,7 +4108,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" name = "sketch-core" version = "0.1.0" dependencies = [ - "clap 4.6.0", + "clap 4.5.60", "ctor", "dsrs", "rmp-serde", @@ -4138,7 +4122,7 @@ name = "sketch_db_common" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.6.0", + "clap 4.5.60", "promql_utilities", "serde", "serde_json", @@ -4152,7 +4136,7 @@ version = "0.1.0" source = "git+https://github.com/ProjectASAP/sketchlib-rust?rev=440427438fdaf3ac2298b53ee148f9e12a64ffcc#440427438fdaf3ac2298b53ee148f9e12a64ffcc" dependencies = [ "bytes", - "clap 4.6.0", + "clap 4.5.60", "pcap", "prost", "prost-build", @@ -4433,9 +4417,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.27.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -4665,32 +4649,32 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] @@ -4829,9 +4813,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.23" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -4884,9 +4868,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" @@ -4944,9 +4928,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5043,9 +5027,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -5056,19 +5040,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ + "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", + "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5076,9 +5064,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -5089,9 +5077,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -5132,9 +5120,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -5439,15 +5427,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -5598,18 +5577,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", diff --git a/asap-planner-rs/Cargo.toml b/asap-planner-rs/Cargo.toml index 0c28bc4..5c74e72 100644 --- a/asap-planner-rs/Cargo.toml +++ b/asap-planner-rs/Cargo.toml @@ -14,6 +14,8 @@ path = "src/main.rs" [dependencies] sketch_db_common.workspace = true promql_utilities.workspace = true +sql_utilities.workspace = true +sqlparser = "0.59.0" serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true diff --git a/asap-planner-rs/src/config/input.rs b/asap-planner-rs/src/config/input.rs index b50d0f3..07226ca 100644 --- a/asap-planner-rs/src/config/input.rs +++ b/asap-planner-rs/src/config/input.rs @@ -70,3 +70,27 @@ pub struct HydraParams { pub col_num: u64, pub k: u64, } + +#[derive(Debug, Clone, Deserialize)] +pub struct SQLControllerConfig { + pub query_groups: Vec, + pub tables: Vec, + pub sketch_parameters: Option, + pub aggregate_cleanup: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SQLQueryGroup { + pub id: Option, + pub queries: Vec, + pub repetition_delay: u64, + pub controller_options: ControllerOptions, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TableDefinition { + pub name: String, + pub time_column: String, + pub value_columns: Vec, + pub metadata_columns: Vec, +} diff --git a/asap-planner-rs/src/error.rs b/asap-planner-rs/src/error.rs index cee5e5c..6806a5e 100644 --- a/asap-planner-rs/src/error.rs +++ b/asap-planner-rs/src/error.rs @@ -14,4 +14,8 @@ pub enum ControllerError { PlannerError(String), #[error("Unknown metric: {0}")] UnknownMetric(String), + #[error("SQL parse error: {0}")] + SqlParse(String), + #[error("Unknown table: {0}")] + UnknownTable(String), } diff --git a/asap-planner-rs/src/lib.rs b/asap-planner-rs/src/lib.rs index 4f3c346..3b8753c 100644 --- a/asap-planner-rs/src/lib.rs +++ b/asap-planner-rs/src/lib.rs @@ -7,8 +7,10 @@ use serde_yaml::Value as YamlValue; use std::path::Path; pub use config::input::ControllerConfig; +pub use config::input::SQLControllerConfig; pub use error::ControllerError; pub use output::generator::{GeneratorOutput, PuntedQuery}; +pub use output::sql_generator::SQLRuntimeOptions; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StreamingEngine { @@ -162,6 +164,117 @@ impl PlannerOutput { pub fn to_inference_yaml_string(&self) -> Result { Ok(serde_yaml::to_string(&self.inference_yaml)?) } + + /// Returns the table_name field of the first aggregation matching agg_type. + pub fn aggregation_table_name(&self, agg_type: &str) -> Option { + if let YamlValue::Mapping(root) = &self.streaming_yaml { + if let Some(YamlValue::Sequence(aggs)) = root.get("aggregations") { + for agg in aggs { + if let YamlValue::Mapping(m) = agg { + if let Some(YamlValue::String(t)) = m.get("aggregationType") { + if t == agg_type { + if let Some(YamlValue::String(name)) = m.get("table_name") { + return Some(name.clone()); + } + } + } + } + } + } + } + None + } + + /// Returns the value_column field of the first aggregation matching agg_type. + pub fn aggregation_value_column(&self, agg_type: &str) -> Option { + if let YamlValue::Mapping(root) = &self.streaming_yaml { + if let Some(YamlValue::Sequence(aggs)) = root.get("aggregations") { + for agg in aggs { + if let YamlValue::Mapping(m) = agg { + if let Some(YamlValue::String(t)) = m.get("aggregationType") { + if t == agg_type { + if let Some(YamlValue::String(col)) = m.get("value_column") { + return Some(col.clone()); + } + } + } + } + } + } + } + None + } + + /// Returns true if any aggregation has the matching type AND sub_type. + pub fn has_aggregation_type_and_sub_type(&self, agg_type: &str, sub_type: &str) -> bool { + if let YamlValue::Mapping(root) = &self.streaming_yaml { + if let Some(YamlValue::Sequence(aggs)) = root.get("aggregations") { + return aggs.iter().any(|agg| { + if let YamlValue::Mapping(m) = agg { + let type_matches = m.get("aggregationType").and_then(|v| { + if let YamlValue::String(s) = v { + Some(s.as_str()) + } else { + None + } + }) == Some(agg_type); + let sub_matches = m.get("aggregationSubType").and_then(|v| { + if let YamlValue::String(s) = v { + Some(s.as_str()) + } else { + None + } + }) == Some(sub_type); + type_matches && sub_matches + } else { + false + } + }); + } + } + false + } +} + +pub struct SQLController { + config: SQLControllerConfig, + options: SQLRuntimeOptions, +} + +impl SQLController { + pub fn from_file(path: &Path, opts: SQLRuntimeOptions) -> Result { + let yaml_str = std::fs::read_to_string(path)?; + Self::from_yaml(&yaml_str, opts) + } + + pub fn from_yaml(yaml: &str, opts: SQLRuntimeOptions) -> Result { + let config: SQLControllerConfig = serde_yaml::from_str(yaml)?; + Ok(Self { + config, + options: opts, + }) + } + + pub fn generate(&self) -> Result { + let output = output::sql_generator::generate_sql_plan(&self.config, &self.options)?; + Ok(PlannerOutput { + punted_queries: output.punted_queries, + streaming_yaml: output.streaming_yaml, + inference_yaml: output.inference_yaml, + aggregation_count: output.aggregation_count, + query_count: output.query_count, + }) + } + + pub fn generate_to_dir(&self, dir: &Path) -> Result { + let output = self.generate()?; + std::fs::create_dir_all(dir)?; + let streaming_str = serde_yaml::to_string(&output.streaming_yaml)?; + let inference_str = serde_yaml::to_string(&output.inference_yaml)?; + std::fs::write(dir.join("streaming_config.yaml"), streaming_str)?; + std::fs::write(dir.join("inference_config.yaml"), inference_str)?; + Ok(output) + } } impl Controller { diff --git a/asap-planner-rs/src/main.rs b/asap-planner-rs/src/main.rs index c1fa0b9..0fca987 100644 --- a/asap-planner-rs/src/main.rs +++ b/asap-planner-rs/src/main.rs @@ -1,5 +1,6 @@ -use asap_planner::{Controller, RuntimeOptions, StreamingEngine}; +use asap_planner::{Controller, RuntimeOptions, SQLController, SQLRuntimeOptions, StreamingEngine}; use clap::Parser; +use sketch_db_common::enums::QueryLanguage; use std::path::PathBuf; #[derive(Parser, Debug)] @@ -11,8 +12,8 @@ struct Args { #[arg(long = "output_dir")] output_dir: PathBuf, - #[arg(long = "prometheus_scrape_interval")] - prometheus_scrape_interval: u64, + #[arg(long = "prometheus_scrape_interval", required = false)] + prometheus_scrape_interval: Option, #[arg(long = "streaming_engine", value_enum)] streaming_engine: EngineArg, @@ -26,6 +27,12 @@ struct Args { #[arg(long = "step", default_value = "0")] step: u64, + #[arg(long = "query-language", value_enum, default_value = "promql")] + query_language: QueryLanguage, + + #[arg(long = "data-ingestion-interval", required = false)] + data_ingestion_interval: Option, + #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, } @@ -52,16 +59,37 @@ fn main() -> anyhow::Result<()> { EngineArg::Flink => StreamingEngine::Flink, }; - let opts = RuntimeOptions { - prometheus_scrape_interval: args.prometheus_scrape_interval, - streaming_engine: engine, - enable_punting: args.enable_punting, - range_duration: args.range_duration, - step: args.step, - }; - - let controller = Controller::from_file(&args.input_config, opts)?; - controller.generate_to_dir(&args.output_dir)?; + match args.query_language { + QueryLanguage::promql => { + let scrape_interval = args.prometheus_scrape_interval.ok_or_else(|| { + anyhow::anyhow!("--prometheus_scrape_interval is required for PromQL mode") + })?; + let opts = RuntimeOptions { + prometheus_scrape_interval: scrape_interval, + streaming_engine: engine, + enable_punting: args.enable_punting, + range_duration: args.range_duration, + step: args.step, + }; + let controller = Controller::from_file(&args.input_config, opts)?; + controller.generate_to_dir(&args.output_dir)?; + } + QueryLanguage::sql | QueryLanguage::elastic_sql => { + let interval = args.data_ingestion_interval.ok_or_else(|| { + anyhow::anyhow!("--data-ingestion-interval is required for SQL mode") + })?; + let opts = SQLRuntimeOptions { + streaming_engine: engine, + query_evaluation_time: None, + data_ingestion_interval: interval, + }; + SQLController::from_file(&args.input_config, opts)? + .generate_to_dir(&args.output_dir)?; + } + QueryLanguage::elastic_querydsl => { + anyhow::bail!("ElasticQueryDSL is not yet supported"); + } + } println!("Generated configs in {}", args.output_dir.display()); Ok(()) diff --git a/asap-planner-rs/src/output/mod.rs b/asap-planner-rs/src/output/mod.rs index 225c968..63c14cb 100644 --- a/asap-planner-rs/src/output/mod.rs +++ b/asap-planner-rs/src/output/mod.rs @@ -1,2 +1,3 @@ pub mod generator; +pub mod sql_generator; pub use generator::*; diff --git a/asap-planner-rs/src/output/sql_generator.rs b/asap-planner-rs/src/output/sql_generator.rs new file mode 100644 index 0000000..21d1bda --- /dev/null +++ b/asap-planner-rs/src/output/sql_generator.rs @@ -0,0 +1,202 @@ +use indexmap::IndexMap; +use serde_yaml::Value as YamlValue; +use sketch_db_common::enums::CleanupPolicy; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::config::input::SQLControllerConfig; +use crate::error::ControllerError; +use crate::output::generator::{ + build_aggregation_entry, build_queries_yaml, parse_cleanup_policy, GeneratorOutput, +}; +use crate::planner::single_query::IntermediateAggConfig; +use crate::planner::sql_single_query::SQLSingleQueryProcessor; +use crate::StreamingEngine; + +pub struct SQLRuntimeOptions { + pub streaming_engine: StreamingEngine, + pub query_evaluation_time: Option, + pub data_ingestion_interval: u64, +} + +pub fn generate_sql_plan( + config: &SQLControllerConfig, + opts: &SQLRuntimeOptions, +) -> Result { + let eval_time: f64 = opts.query_evaluation_time.unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs_f64() + }); + + let cleanup_policy_str = config + .aggregate_cleanup + .as_ref() + .and_then(|c| c.policy.as_deref()) + .unwrap_or("read_based"); + let cleanup_policy = parse_cleanup_policy(cleanup_policy_str)?; + + // Validate T % data_ingestion_interval == 0 + for qg in &config.query_groups { + if qg.repetition_delay % opts.data_ingestion_interval != 0 { + return Err(ControllerError::PlannerError(format!( + "repetition_delay {} is not a multiple of data_ingestion_interval {}", + qg.repetition_delay, opts.data_ingestion_interval + ))); + } + } + + // Check for duplicate queries + let mut seen_queries = std::collections::HashSet::new(); + for qg in &config.query_groups { + for q in &qg.queries { + if !seen_queries.insert(q.clone()) { + return Err(ControllerError::DuplicateQuery(q.clone())); + } + } + } + + // Dedup map: identifying_key -> IntermediateAggConfig + let mut dedup_map: IndexMap = IndexMap::new(); + // query_string -> Vec<(key, cleanup_param)> + let mut query_keys_map: IndexMap)>> = IndexMap::new(); + + for qg in &config.query_groups { + for query_string in &qg.queries { + let processor = SQLSingleQueryProcessor::new( + query_string.clone(), + qg.repetition_delay, + opts.data_ingestion_interval, + config.tables.clone(), + opts.streaming_engine, + config.sketch_parameters.clone(), + cleanup_policy, + ); + + let (configs, cleanup_param) = + processor.get_streaming_aggregation_configs(eval_time)?; + + let mut keys_for_query = Vec::new(); + for config_item in configs { + let key = config_item.identifying_key(); + keys_for_query.push((key.clone(), cleanup_param)); + dedup_map.entry(key).or_insert(config_item); + } + query_keys_map.insert(query_string.clone(), keys_for_query); + } + } + + // Assign sequential IDs + let mut id_map: HashMap = HashMap::new(); + for (idx, key) in dedup_map.keys().enumerate() { + id_map.insert(key.clone(), idx as u32 + 1); + } + + let streaming_yaml = build_sql_streaming_yaml(config, &dedup_map, &id_map)?; + let inference_yaml = build_sql_inference_yaml( + config, + cleanup_policy, + cleanup_policy_str, + &query_keys_map, + &id_map, + )?; + + Ok(GeneratorOutput { + punted_queries: Vec::new(), + streaming_yaml, + inference_yaml, + aggregation_count: dedup_map.len(), + query_count: query_keys_map.len(), + }) +} + +fn build_tables_yaml(config: &SQLControllerConfig) -> Vec { + config + .tables + .iter() + .map(|t| { + let mut map = serde_yaml::Mapping::new(); + map.insert( + YamlValue::String("name".to_string()), + YamlValue::String(t.name.clone()), + ); + map.insert( + YamlValue::String("time_column".to_string()), + YamlValue::String(t.time_column.clone()), + ); + map.insert( + YamlValue::String("value_columns".to_string()), + YamlValue::Sequence( + t.value_columns + .iter() + .map(|c| YamlValue::String(c.clone())) + .collect(), + ), + ); + map.insert( + YamlValue::String("metadata_columns".to_string()), + YamlValue::Sequence( + t.metadata_columns + .iter() + .map(|c| YamlValue::String(c.clone())) + .collect(), + ), + ); + YamlValue::Mapping(map) + }) + .collect() +} + +fn build_sql_streaming_yaml( + config: &SQLControllerConfig, + dedup_map: &IndexMap, + id_map: &HashMap, +) -> Result { + let aggregations: Vec = dedup_map + .iter() + .map(|(key, cfg)| build_aggregation_entry(id_map[key], cfg)) + .collect(); + + let mut root = serde_yaml::Mapping::new(); + root.insert( + YamlValue::String("aggregations".to_string()), + YamlValue::Sequence(aggregations), + ); + root.insert( + YamlValue::String("tables".to_string()), + YamlValue::Sequence(build_tables_yaml(config)), + ); + + Ok(YamlValue::Mapping(root)) +} + +fn build_sql_inference_yaml( + config: &SQLControllerConfig, + cleanup_policy: CleanupPolicy, + cleanup_policy_str: &str, + query_keys_map: &IndexMap)>>, + id_map: &HashMap, +) -> Result { + let mut cleanup_map = serde_yaml::Mapping::new(); + cleanup_map.insert( + YamlValue::String("name".to_string()), + YamlValue::String(cleanup_policy_str.to_string()), + ); + + let mut root = serde_yaml::Mapping::new(); + root.insert( + YamlValue::String("cleanup_policy".to_string()), + YamlValue::Mapping(cleanup_map), + ); + root.insert( + YamlValue::String("queries".to_string()), + YamlValue::Sequence(build_queries_yaml(cleanup_policy, query_keys_map, id_map)), + ); + root.insert( + YamlValue::String("tables".to_string()), + YamlValue::Sequence(build_tables_yaml(config)), + ); + + Ok(YamlValue::Mapping(root)) +} diff --git a/asap-planner-rs/src/planner/mod.rs b/asap-planner-rs/src/planner/mod.rs index 427dbdd..44475be 100644 --- a/asap-planner-rs/src/planner/mod.rs +++ b/asap-planner-rs/src/planner/mod.rs @@ -1,4 +1,5 @@ pub mod logics; pub mod patterns; pub mod single_query; +pub mod sql_single_query; pub use single_query::*; diff --git a/asap-planner-rs/src/planner/sql_single_query.rs b/asap-planner-rs/src/planner/sql_single_query.rs new file mode 100644 index 0000000..308cf58 --- /dev/null +++ b/asap-planner-rs/src/planner/sql_single_query.rs @@ -0,0 +1,214 @@ +use std::collections::HashSet; + +use promql_utilities::data_model::KeyByLabelNames; +use promql_utilities::query_logics::enums::{QueryTreatmentType, Statistic}; +use sketch_db_common::enums::CleanupPolicy; +use sql_utilities::ast_matching::sqlhelper::Table; +use sql_utilities::ast_matching::sqlpattern_matcher::{QueryType, SQLPatternMatcher}; +use sql_utilities::ast_matching::sqlpattern_parser::SQLPatternParser; +use sql_utilities::ast_matching::SQLSchema; +use sqlparser::dialect::ClickHouseDialect; +use sqlparser::parser::Parser as SqlParser; + +use crate::config::input::{SketchParameterOverrides, TableDefinition}; +use crate::error::ControllerError; +use crate::planner::logics::{ + build_sketch_parameters, get_sql_cleanup_param, IntermediateWindowConfig, +}; +use crate::planner::single_query::{build_agg_configs_for_statistics, IntermediateAggConfig}; +use crate::StreamingEngine; + +pub struct SQLSingleQueryProcessor { + query_string: String, + t_repeat: u64, + data_ingestion_interval: u64, + table_definitions: Vec, + #[allow(dead_code)] + streaming_engine: StreamingEngine, + sketch_parameters: Option, + cleanup_policy: CleanupPolicy, +} + +impl SQLSingleQueryProcessor { + #[allow(clippy::too_many_arguments)] + pub fn new( + query_string: String, + t_repeat: u64, + data_ingestion_interval: u64, + table_definitions: Vec, + streaming_engine: StreamingEngine, + sketch_parameters: Option, + cleanup_policy: CleanupPolicy, + ) -> Self { + Self { + query_string, + t_repeat, + data_ingestion_interval, + table_definitions, + streaming_engine, + sketch_parameters, + cleanup_policy, + } + } + + pub fn get_streaming_aggregation_configs( + &self, + query_evaluation_time: f64, + ) -> Result<(Vec, Option), ControllerError> { + let schema = build_sql_schema(&self.table_definitions); + + // Parse SQL + let stmts = SqlParser::parse_sql(&ClickHouseDialect {}, &self.query_string) + .map_err(|e| ControllerError::SqlParse(e.to_string()))?; + + // Parse query into SQLQueryData + let qdata = SQLPatternParser::new(&schema, query_evaluation_time) + .parse_query(&stmts) + .ok_or_else(|| { + ControllerError::SqlParse(format!( + "Failed to parse SQL query: {}", + self.query_string + )) + })?; + + // Match query to pattern + let sql_query = SQLPatternMatcher::new(schema.clone(), self.data_ingestion_interval as f64) + .query_info_to_pattern(&qdata); + + if !sql_query.is_valid() { + return Err(ControllerError::SqlParse(sql_query.msg.unwrap_or_default())); + } + + let n = sql_query.query_data.len(); + + if n != 1 { + return Err(ControllerError::SqlParse(format!( + "Nested SQL queries (n={}) are not supported", + n + ))); + } + + // Determine fields from query vecs + let agg_info = &sql_query.query_data[0].aggregation_info; + let query_type = &sql_query.query_type[0]; + let labels = &sql_query.query_data[0].labels; + let table_name = &sql_query.query_data[0].metric; + + let value_column = agg_info.get_value_column_name().to_string(); + + // Compute window + let window_cfg = + compute_sql_window(query_type, self.data_ingestion_interval, self.t_repeat); + + // Get all metadata columns for the table + let all_metadata = get_all_metadata_columns(&self.table_definitions, table_name)?; + + // Label routing + let spatial_output = KeyByLabelNames::new(labels.iter().cloned().collect::>()); + let rollup = all_metadata.difference(&spatial_output); + + let treatment_type = get_sql_treatment_type(agg_info.get_name()); + let statistics = get_sql_statistics(agg_info.get_name())?; + + let configs = build_agg_configs_for_statistics( + &statistics, + treatment_type, + &spatial_output, + &rollup, + &window_cfg, + table_name, + Some(table_name), + Some(&value_column), + "", + |agg_type, agg_sub_type| { + build_sketch_parameters( + agg_type, + agg_sub_type, + None, + self.sketch_parameters.as_ref(), + ) + }, + ) + .map_err(ControllerError::SqlParse)?; + + let t_lookback = match query_type { + QueryType::Spatial => self.data_ingestion_interval, + _ => sql_query.query_data[0].time_info.get_duration() as u64, + }; + + let cleanup_param = if self.cleanup_policy == CleanupPolicy::NoCleanup { + None + } else { + Some( + get_sql_cleanup_param(self.cleanup_policy, t_lookback, self.t_repeat) + .map_err(ControllerError::PlannerError)?, + ) + }; + + Ok((configs, cleanup_param)) + } +} + +fn build_sql_schema(tables: &[TableDefinition]) -> SQLSchema { + let table_vec: Vec = tables + .iter() + .map(|t| { + Table::new( + t.name.clone(), + t.time_column.clone(), + t.value_columns.iter().cloned().collect::>(), + t.metadata_columns.iter().cloned().collect::>(), + ) + }) + .collect(); + SQLSchema::new(table_vec) +} + +fn get_sql_treatment_type(name: &str) -> QueryTreatmentType { + match name.to_uppercase().as_str() { + "MIN" | "MAX" => QueryTreatmentType::Exact, + _ => QueryTreatmentType::Approximate, + } +} + +fn get_sql_statistics(name: &str) -> Result, ControllerError> { + match name.to_uppercase().as_str() { + "QUANTILE" => Ok(vec![Statistic::Quantile]), + "SUM" => Ok(vec![Statistic::Sum]), + "COUNT" => Ok(vec![Statistic::Count]), + "AVG" => Ok(vec![Statistic::Sum, Statistic::Count]), + "MIN" => Ok(vec![Statistic::Min]), + "MAX" => Ok(vec![Statistic::Max]), + other => Err(ControllerError::SqlParse(format!( + "Unsupported aggregation: {}", + other + ))), + } +} + +fn compute_sql_window( + query_type: &QueryType, + data_ingestion_interval: u64, + t_repeat: u64, +) -> IntermediateWindowConfig { + let window_size = match query_type { + QueryType::Spatial => data_ingestion_interval, + _ => t_repeat, + }; + IntermediateWindowConfig { + window_size, + slide_interval: window_size, + window_type: "tumbling".to_string(), + } +} + +fn get_all_metadata_columns( + table_definitions: &[TableDefinition], + table_name: &str, +) -> Result { + let table = table_definitions + .iter() + .find(|t| t.name == table_name) + .ok_or_else(|| ControllerError::UnknownTable(table_name.to_string()))?; + Ok(KeyByLabelNames::new(table.metadata_columns.clone())) +} diff --git a/asap-planner-rs/tests/sql_integration.rs b/asap-planner-rs/tests/sql_integration.rs new file mode 100644 index 0000000..485a307 --- /dev/null +++ b/asap-planner-rs/tests/sql_integration.rs @@ -0,0 +1,765 @@ +use asap_planner::{ControllerError, SQLController, SQLRuntimeOptions, StreamingEngine}; + +// ── helpers ────────────────────────────────────────────────────────────────── + +fn sql_opts() -> SQLRuntimeOptions { + SQLRuntimeOptions { + streaming_engine: StreamingEngine::Arroyo, + // Fixed evaluation time so NOW()-relative timestamps are deterministic. + query_evaluation_time: Some(1_000_000.0), + data_ingestion_interval: 15, + } +} + +/// Single-query config with a 3-column metadata schema. +/// +/// Schema: metrics_table +/// time_column : time +/// value_columns : [cpu_usage] +/// metadata_columns : [hostname, datacenter, region] +/// data_ingestion_interval = 15 s +fn one_query_config(query: &str, t_repeat: u64) -> String { + format!( + r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: {t_repeat} + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + {query} +aggregate_cleanup: + policy: read_based +"# + ) +} + +// ── single-query happy-path tests ───────────────────────────────────────────── +// +// All queries: SELECT (cpu_usage) FROM metrics_table +// WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() +// GROUP BY datacenter +// +// With 3 metadata columns [hostname, datacenter, region] and GROUP BY datacenter: +// rollup = [hostname, region] (the two NOT in GROUP BY) +// grouping = [datacenter] +// aggregated = [] (no label-level aggregation dimension in SQL) +// +// Each test checks ALL observable plan properties: +// - streaming_aggregation_count +// - inference_query_count (always 1) +// - aggregation types present +// - tumbling window size +// - table_name and value_column +// - label routing: rollup / grouping / aggregated +// - inference cleanup param + +// ── aggregate-type coverage (T = 300 s, range = 300 s) ─────────────────────── + +/// SUM is Approximate → CountMinSketch + DeltaSetAggregator. +/// window = range = 300 s, cleanup = ceil(300 / 300) = 1. +#[test] +fn temporal_sum() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("CountMinSketch")); + assert!(out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("CountMinSketch"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("CountMinSketch"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("CountMinSketch", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("CountMinSketch", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("CountMinSketch", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// COUNT is Approximate → CountMinSketch + DeltaSetAggregator (identical plan to SUM). +/// window = 300 s, cleanup = 1. +#[test] +fn temporal_count() { + let q = "SELECT COUNT(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("CountMinSketch")); + assert!(out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("CountMinSketch"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("CountMinSketch"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("CountMinSketch", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("CountMinSketch", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("CountMinSketch", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// MIN is Exact → MultipleMinMax only (no DeltaSetAggregator). +/// window = 300 s, cleanup = 1. +#[test] +fn temporal_min() { + let q = "SELECT MIN(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("MultipleMinMax")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("MultipleMinMax"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("MultipleMinMax"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("MultipleMinMax", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("MultipleMinMax", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("MultipleMinMax", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// MAX is Exact → MultipleMinMax only (identical plan to MIN). +/// window = 300 s, cleanup = 1. +#[test] +fn temporal_max() { + let q = "SELECT MAX(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("MultipleMinMax")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("MultipleMinMax"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("MultipleMinMax"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("MultipleMinMax", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("MultipleMinMax", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("MultipleMinMax", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// AVG decomposes into SUM + COUNT → 2 CountMinSketch + 1 DeltaSetAggregator (deduped) = 3 total. +/// window = 300 s, cleanup = 1. +#[test] +fn temporal_avg() { + let q = "SELECT AVG(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 3); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("CountMinSketch")); + assert!(out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("CountMinSketch"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("CountMinSketch"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("CountMinSketch", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("CountMinSketch", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("CountMinSketch", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// QUANTILE (Clickhouse parametric syntax) → DatasketchesKLL only (no DeltaSetAggregator). +/// window = 300 s, cleanup = 1. +#[test] +fn temporal_quantile() { + let q = "SELECT quantile(0.95)(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("DatasketchesKLL")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("DatasketchesKLL"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("DatasketchesKLL"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("DatasketchesKLL", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "grouping"), + vec!["datacenter".to_string()] + ); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "aggregated"), + Vec::::new() + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +// ── Elastic SQL syntax variants ─────────────────────────────────────────────── +// +// These three tests exercise syntactic forms used by Elastic: +// 1. PERCENTILE(col, integer_percentile) — integer 0–100, col-first arg order +// 2. DATEADD('s', …) — quoted unit string +// 3. CAST('…' AS DATETIME) — absolute timestamp bounds +// +// All produce the same plan as temporal_quantile: DatasketchesKLL, no +// DeltaSetAggregator, one streaming config, window = T = 300 s, cleanup = 1. + +/// PERCENTILE(col, 95) is the Elastic aggregation syntax. +/// The parser normalises it to QUANTILE internally, so the plan is identical +/// to temporal_quantile (DatasketchesKLL, grouping = [datacenter], rollup = [hostname, region]). +#[test] +fn temporal_quantile_percentile_syntax() { + let q = "SELECT PERCENTILE(cpu_usage, 95) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("DatasketchesKLL")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("DatasketchesKLL"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("DatasketchesKLL"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("DatasketchesKLL", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "grouping"), + vec!["datacenter".to_string()] + ); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "aggregated"), + Vec::::new() + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// DATEADD('s', …) — quoted unit string (Elastic style) vs unquoted `s`. +/// The parser accepts both forms; the plan must be identical to temporal_quantile. +#[test] +fn temporal_quantile_quoted_dateadd_unit() { + let q = "SELECT PERCENTILE(cpu_usage, 95) FROM metrics_table WHERE time BETWEEN DATEADD('s', -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("DatasketchesKLL")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("DatasketchesKLL"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("DatasketchesKLL"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("DatasketchesKLL", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "grouping"), + vec!["datacenter".to_string()] + ); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "aggregated"), + Vec::::new() + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +/// Full Elastic syntax: PERCENTILE(col, N) + DATEADD('s', …) + CAST('…' AS DATETIME). +/// Absolute timestamp bounds replace NOW(); the 300 s range still yields cleanup = 1. +#[test] +fn temporal_quantile_cast_datetime_bounds() { + let q = "SELECT PERCENTILE(cpu_usage, 95) FROM metrics_table WHERE time BETWEEN DATEADD('s', -300, CAST('2024-01-01T00:05:00Z' AS DATETIME)) AND CAST('2024-01-01T00:05:00Z' AS DATETIME) GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 1); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("DatasketchesKLL")); + assert!(!out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(300)); + assert_eq!( + out.aggregation_table_name("DatasketchesKLL"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("DatasketchesKLL"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("DatasketchesKLL", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "grouping"), + vec!["datacenter".to_string()] + ); + assert_eq!( + out.aggregation_labels("DatasketchesKLL", "aggregated"), + Vec::::new() + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +// ── T-value variants for SUM (range = 300 s fixed) ─────────────────────────── +// +// These three tests use the same query and differ only in repetition_delay (T). +// Window size = T (the repetition delay), not the query range. +// They verify cleanup scales correctly with T and that T > range is valid. + +/// T = 30 s < range: window = 30, cleanup = ceil(300 / 30) = 10. +#[test] +fn temporal_sum_t30() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 30), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("CountMinSketch")); + assert!(out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(30)); + assert_eq!( + out.aggregation_table_name("CountMinSketch"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("CountMinSketch"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("CountMinSketch", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("CountMinSketch", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("CountMinSketch", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(10)); +} + +/// T = 300 s == range: covered by temporal_sum above (cleanup = 1). +/// T = 600 s > range: window = 600, cleanup = ceil(300 / 600) = 1. +/// T larger than the query range is valid; the result is still 1 retained window. +#[test] +fn temporal_sum_t600() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let out = SQLController::from_yaml(&one_query_config(q, 600), sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 1); + assert!(out.has_aggregation_type("CountMinSketch")); + assert!(out.has_aggregation_type("DeltaSetAggregator")); + assert!(out.all_tumbling_window_sizes_eq(600)); + assert_eq!( + out.aggregation_table_name("CountMinSketch"), + Some("metrics_table".to_string()) + ); + assert_eq!( + out.aggregation_value_column("CountMinSketch"), + Some("cpu_usage".to_string()) + ); + let mut rollup = out.aggregation_labels("CountMinSketch", "rollup"); + rollup.sort(); + assert_eq!(rollup, vec!["hostname".to_string(), "region".to_string()]); + assert_eq!( + out.aggregation_labels("CountMinSketch", "grouping"), + Vec::::new() + ); + assert_eq!( + out.aggregation_labels("CountMinSketch", "aggregated"), + vec!["datacenter".to_string()] + ); + assert_eq!(out.inference_cleanup_param(q), Some(1)); +} + +// ── multi-query tests ───────────────────────────────────────────────────────── + +/// MIN and MAX: distinct sub_types → 2 streaming configs (one "min", one "max"), 2 inference entries. +#[test] +fn two_queries_min_and_max_produce_separate_streaming_configs() { + let yaml = r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + SELECT MIN(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter + - id: 2 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + SELECT MAX(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter +aggregate_cleanup: + policy: read_based +"#; + let out = SQLController::from_yaml(yaml, sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 2); + assert!(out.has_aggregation_type_and_sub_type("MultipleMinMax", "min")); + assert!(out.has_aggregation_type_and_sub_type("MultipleMinMax", "max")); +} + +/// Two SUM queries on different value columns: neither pair deduped (different value_column). +/// 2 × (CMS + DeltaSet) = 4 configs, 2 inference entries. +#[test] +fn two_queries_different_value_columns_produce_four_configs() { + let yaml = r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage, memory_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter + - >- + SELECT SUM(memory_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter +aggregate_cleanup: + policy: read_based +"#; + let out = SQLController::from_yaml(yaml, sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 4); + assert_eq!(out.inference_query_count(), 2); +} + +/// Identical SQL queries in separate groups are rejected as duplicates, same as PromQL. +#[test] +fn identical_queries_across_groups_return_duplicate_error() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let yaml = format!( + r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + {q} + - id: 2 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + {q} +aggregate_cleanup: + policy: read_based +"# + ); + let result = SQLController::from_yaml(&yaml, sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::DuplicateQuery(_)))); +} + +/// SUM queries with same table/column but different window sizes +/// Since they will both produce tumnbling windows of 300, we will only get 2 configs total +#[test] +fn two_queries_different_windows_are_deduped() { + let yaml = r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter + - id: 2 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -600, NOW()) AND NOW() GROUP BY datacenter +aggregate_cleanup: + policy: read_based +"#; + let out = SQLController::from_yaml(yaml, sql_opts()) + .unwrap() + .generate() + .unwrap(); + + assert_eq!(out.streaming_aggregation_count(), 2); + assert_eq!(out.inference_query_count(), 2); +} + +// ── output-file test ────────────────────────────────────────────────────────── + +/// generate_to_dir writes both streaming_config.yaml and inference_config.yaml. +#[test] +fn generate_to_dir_writes_both_yaml_files() { + let dir = tempfile::tempdir().unwrap(); + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate_to_dir(dir.path()) + .unwrap(); + assert!(dir.path().join("streaming_config.yaml").exists()); + assert!(dir.path().join("inference_config.yaml").exists()); +} + +// ── error-path tests ────────────────────────────────────────────────────────── + +#[test] +fn malformed_yaml_returns_parse_error() { + let result = SQLController::from_yaml("{ invalid yaml :", sql_opts()); + assert!(matches!(result, Err(ControllerError::YamlParse(_)))); +} + +#[test] +fn malformed_sql_returns_sql_parse_error() { + let q = "NOT VALID SQL AT ALL %%%"; + let result = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::SqlParse(_)))); +} + +#[test] +fn query_referencing_unknown_table_returns_error() { + let q = "SELECT SUM(cpu_usage) FROM nonexistent_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let result = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate(); + assert!(matches!( + result, + Err(ControllerError::UnknownTable(_)) | Err(ControllerError::SqlParse(_)) + )); +} + +#[test] +fn query_referencing_unknown_value_column_returns_sql_parse_error() { + let q = "SELECT SUM(nonexistent_col) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let result = SQLController::from_yaml(&one_query_config(q, 300), sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::SqlParse(_)))); +} + +#[test] +fn duplicate_queries_in_same_group_return_error() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let yaml = format!( + r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + {q} + - >- + {q} +aggregate_cleanup: + policy: read_based +"# + ); + let result = SQLController::from_yaml(&yaml, sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::DuplicateQuery(_)))); +} + +#[test] +fn unknown_cleanup_policy_returns_planner_error() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -300, NOW()) AND NOW() GROUP BY datacenter"; + let yaml = format!( + r#" +tables: + - name: metrics_table + time_column: time + value_columns: [cpu_usage] + metadata_columns: [hostname, datacenter, region] +query_groups: + - id: 1 + repetition_delay: 300 + controller_options: + accuracy_sla: 0.95 + latency_sla: 100.0 + queries: + - >- + {q} +aggregate_cleanup: + policy: not_a_real_policy +"# + ); + let result = SQLController::from_yaml(&yaml, sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::PlannerError(_)))); +} + +/// T that is not a multiple of data_ingestion_interval is invalid: sketch windows +/// must align with the ingestion cadence. +/// data_ingestion_interval = 15 s, T = 200 s → 200 mod 15 ≠ 0 → PlannerError. +#[test] +fn t_not_multiple_of_data_ingestion_interval_returns_planner_error() { + let q = "SELECT SUM(cpu_usage) FROM metrics_table WHERE time BETWEEN DATEADD(s, -200, NOW()) AND NOW() GROUP BY datacenter"; + let result = SQLController::from_yaml(&one_query_config(q, 200), sql_opts()) + .unwrap() + .generate(); + assert!(matches!(result, Err(ControllerError::PlannerError(_)))); +}