diff --git a/CHANGELOG.md b/CHANGELOG.md index a246c2e7..3b0a8bfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ This changelog documents the changes between release versions. +## [Unreleased] + +### Added + +- You can now group documents for aggregation according to multiple grouping criteria ([#144](https://github.com/hasura/ndc-mongodb/pull/144), [#145](https://github.com/hasura/ndc-mongodb/pull/145)) + +### Changed + +- **BREAKING:** Update to ndc-spec v0.2 ([#139](https://github.com/hasura/ndc-mongodb/pull/139)) +- **BREAKING:** Remove custom count aggregation - use standard count instead ([#144](https://github.com/hasura/ndc-mongodb/pull/144)) +- Results for `avg` and `sum` aggregations are coerced to consistent result types ([#144](https://github.com/hasura/ndc-mongodb/pull/144)) + +#### ndc-spec v0.2 + +This database connector communicates with the GraphQL Engine using an IR +described by [ndc-spec](https://hasura.github.io/ndc-spec/). Version 0.2 makes +a number of improvements to the spec, and enables features that were previously +not possible. Highlights of those new features include: + +- relationships can use a nested object field on the target side as a join key +- grouping result documents, and aggregating on groups of documents +- queries on fields of nested collections (document fields that are arrays of objects) +- filtering on scalar values inside array document fields - previously it was possible to filter on fields of objects inside arrays, but not on scalars + +For more details on what has changed in the spec see [the +changelog](https://hasura.github.io/ndc-spec/specification/changelog.html#020). + +Use of the new spec requires a version of GraphQL Engine that supports ndc-spec +v0.2, and there are required metadata changes. + +#### Removed custom count aggregation + +Previously there were two options for getting document counts named `count` and +`_count`. These did the same thing. `count` has been removed - use `_count` +instead. + +#### Results for `avg` and `sum` aggregations are coerced to consistent result types + +This change is required for compliance with ndc-spec. + +Results for `avg` are always coerced to `double`. + +Results for `sum` are coerced to `double` if the summed inputs use a fractional +numeric type, or to `long` if inputs use an integral numeric type. + ## [1.7.0] - 2025-03-10 ### Added diff --git a/Cargo.lock b/Cargo.lock index 83fe43e6..452c15f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -24,10 +24,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -71,43 +71,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arrayvec" @@ -128,9 +129,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -139,13 +140,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -159,13 +160,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -176,9 +177,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -194,7 +195,7 @@ dependencies = [ "headers", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.32", "itoa", "matchit", "memchr", @@ -206,9 +207,9 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] @@ -246,24 +247,24 @@ dependencies = [ "pin-project-lite", "serde", "tokio", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -286,18 +287,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -307,9 +308,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bitvec" @@ -332,6 +333,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "bson" version = "2.13.0" @@ -342,7 +352,7 @@ dependencies = [ "base64 0.13.1", "bitvec", "hex", - "indexmap 2.2.6", + "indexmap 2.8.0", "js-sys", "once_cell", "rand", @@ -355,15 +365,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" @@ -380,24 +390,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.5", + "windows-link", ] [[package]] name = "clap" -version = "4.5.7" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" dependencies = [ "clap_builder", "clap_derive", @@ -405,9 +421,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" dependencies = [ "anstream", "anstyle", @@ -417,21 +433,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codespan-reporting" @@ -440,14 +456,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colorful" @@ -463,7 +479,7 @@ dependencies = [ "async-tempfile", "futures", "googletest 0.12.0", - "itertools", + "itertools 0.13.0", "mongodb", "mongodb-support", "ndc-models", @@ -480,14 +496,14 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -505,7 +521,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -528,15 +544,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -552,18 +568,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -583,9 +599,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -593,34 +609,34 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" [[package]] name = "deranged" @@ -651,7 +667,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -662,20 +678,20 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] @@ -703,13 +719,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -720,27 +736,27 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "either" -version = "1.12.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -754,7 +770,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -774,36 +790,36 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", @@ -853,9 +869,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -884,9 +900,9 @@ checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -907,7 +923,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -958,20 +974,32 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "googletest" @@ -1004,7 +1032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171deab504ad43a9ea80324a3686a0cbe9436220d9d0b48ae4d7f7bd303b48a9" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1015,7 +1043,7 @@ checksum = "f5070fa86976044fe2b004d874c10af5d1aed6d8f6a72ff93a6eb29cc87048bc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1030,7 +1058,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1039,17 +1067,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.1.0", - "indexmap 2.2.6", + "http 1.3.1", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1064,9 +1092,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "headers" @@ -1098,12 +1126,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -1112,9 +1134,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad3d6d98c648ed628df039541a5577bee1a7c83e9e16fe3dbedeea4cdfeb971" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" dependencies = [ "async-trait", "cfg-if", @@ -1136,9 +1158,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.2" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", @@ -1188,9 +1210,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1210,24 +1232,24 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1239,9 +1261,9 @@ checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1251,9 +1273,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1275,16 +1297,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2 0.4.8", + "http 1.3.1", + "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", @@ -1293,13 +1315,30 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.23", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.32", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1312,7 +1351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", @@ -1326,7 +1365,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.3.1", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -1336,29 +1375,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1448,9 +1486,9 @@ checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", @@ -1492,7 +1530,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1503,16 +1541,25 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "icu_normalizer", - "icu_properties", + "idna_adapter", "smallvec", "utf8_iter", ] +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indent" version = "0.1.1" @@ -1532,24 +1579,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] [[package]] name = "insta" -version = "1.39.0" +version = "1.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" dependencies = [ "console", - "lazy_static", "linked-hash-map", + "once_cell", + "pin-project", "serde", "similar", ] @@ -1563,7 +1611,7 @@ dependencies = [ "insta", "ndc-models", "ndc-test-helpers", - "reqwest 0.12.4", + "reqwest 0.12.13", "serde", "serde_json", "tokio", @@ -1579,20 +1627,20 @@ dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -1603,18 +1651,28 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1631,21 +1689,15 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "linked-hash-map" @@ -1655,15 +1707,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1677,9 +1729,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lru-cache" @@ -1699,7 +1751,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1713,7 +1765,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1724,7 +1776,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1735,7 +1787,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1771,9 +1823,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d8b92cd8358e8d229c11df9358decae64d137c5be540952c5ca7b25aea768" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1783,9 +1835,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -1799,22 +1851,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -1840,7 +1892,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -1908,8 +1960,8 @@ dependencies = [ "futures-util", "http 0.2.12", "indent", - "indexmap 2.2.6", - "itertools", + "indexmap 2.8.0", + "itertools 0.13.0", "lazy_static", "mockall", "mongodb", @@ -1918,6 +1970,7 @@ dependencies = [ "ndc-models", "ndc-query-plan", "ndc-test-helpers", + "nonempty", "once_cell", "pretty_assertions", "proptest", @@ -1944,8 +1997,8 @@ dependencies = [ "enum-iterator", "futures-util", "googletest 0.13.0", - "indexmap 2.2.6", - "itertools", + "indexmap 2.8.0", + "itertools 0.13.0", "json-structural-diff", "mongodb", "mongodb-agent-common", @@ -1977,8 +2030,8 @@ dependencies = [ "enum-iterator", "futures", "http 0.2.12", - "indexmap 2.2.6", - "itertools", + "indexmap 2.8.0", + "itertools 0.13.0", "mongodb", "mongodb-agent-common", "mongodb-support", @@ -2003,7 +2056,7 @@ dependencies = [ "macro_magic", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -2012,7 +2065,7 @@ version = "1.7.0" dependencies = [ "anyhow", "enum-iterator", - "indexmap 2.2.6", + "indexmap 2.8.0", "mongodb", "schemars", "serde", @@ -2022,9 +2075,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -2039,16 +2092,16 @@ dependencies = [ [[package]] name = "ndc-models" -version = "0.1.6" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" +version = "0.2.0" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.2.0-rc.2#2fad1c699df79890dbb3877d1035ffd8bd0abfc2" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.8.0", "ref-cast", "schemars", "serde", "serde_json", "serde_with", - "smol_str", + "smol_str 0.1.24", ] [[package]] @@ -2059,8 +2112,8 @@ dependencies = [ "derivative", "enum-iterator", "indent", - "indexmap 2.2.6", - "itertools", + "indexmap 2.8.0", + "itertools 0.13.0", "lazy_static", "ndc-models", "ndc-test-helpers", @@ -2073,17 +2126,16 @@ dependencies = [ [[package]] name = "ndc-sdk" -version = "0.4.0" -source = "git+https://github.com/hasura/ndc-sdk-rs.git?tag=v0.4.0#665509f7d3b47ce4f014fc23f817a3599ba13933" +version = "0.5.0" +source = "git+https://github.com/hasura/ndc-sdk-rs.git?rev=643b96b8ee4c8b372b44433167ce2ac4de193332#643b96b8ee4c8b372b44433167ce2ac4de193332" dependencies = [ "async-trait", "axum", "axum-extra", - "bytes", "clap", "http 0.2.12", - "mime", "ndc-models", + "ndc-sdk-core", "ndc-test", "opentelemetry", "opentelemetry-http", @@ -2093,7 +2145,7 @@ dependencies = [ "opentelemetry_sdk", "prometheus", "reqwest 0.11.27", - "serde", + "semver", "serde_json", "thiserror", "tokio", @@ -2104,36 +2156,54 @@ dependencies = [ "url", ] +[[package]] +name = "ndc-sdk-core" +version = "0.5.0" +source = "git+https://github.com/hasura/ndc-sdk-rs.git?rev=643b96b8ee4c8b372b44433167ce2ac4de193332#643b96b8ee4c8b372b44433167ce2ac4de193332" +dependencies = [ + "async-trait", + "axum", + "bytes", + "http 0.2.12", + "mime", + "ndc-models", + "ndc-test", + "prometheus", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "ndc-test" -version = "0.1.6" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" +version = "0.2.0" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.2.0-rc.2#2fad1c699df79890dbb3877d1035ffd8bd0abfc2" dependencies = [ "async-trait", "clap", "colorful", - "indexmap 2.2.6", + "indexmap 2.8.0", "ndc-models", "rand", - "reqwest 0.11.27", + "reqwest 0.12.13", "semver", "serde", "serde_json", - "smol_str", "thiserror", "tokio", - "url", ] [[package]] name = "ndc-test-helpers" version = "1.7.0" dependencies = [ - "indexmap 2.2.6", - "itertools", + "indexmap 2.8.0", + "itertools 0.13.0", "ndc-models", "serde_json", - "smol_str", + "smol_str 0.3.2", ] [[package]] @@ -2148,9 +2218,9 @@ dependencies = [ [[package]] name = "nonempty" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" [[package]] name = "nu-ansi-term" @@ -2175,33 +2245,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", ] [[package]] name = "object" -version = "0.36.0" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" [[package]] name = "openssl" @@ -2209,7 +2268,7 @@ version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -2226,14 +2285,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -2359,9 +2418,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ "num-traits", ] @@ -2392,7 +2451,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2412,29 +2471,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2444,9 +2503,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "powerfmt" @@ -2456,15 +2515,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "predicates-core", @@ -2472,15 +2534,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -2495,14 +2557,14 @@ dependencies = [ "arrayvec", "termcolor", "typed-arena", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -2510,9 +2572,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2534,13 +2596,13 @@ dependencies = [ [[package]] name = "proptest" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.9.0", "lazy_static", "num-traits", "rand", @@ -2569,10 +2631,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -2589,9 +2651,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -2629,7 +2691,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2643,31 +2705,31 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.0", ] [[package]] name = "ref-cast" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -2678,7 +2740,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -2693,9 +2755,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2728,13 +2790,12 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.32", "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", - "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -2743,8 +2804,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -2752,49 +2813,52 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "389a89e494bbc88bebf30e23da98742c843863a16a352647716116aa71fae80a" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2 0.4.8", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.6.0", + "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.2.0", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.52.0", + "windows-registry", ] [[package]] @@ -2809,13 +2873,13 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.12" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9b823fa29b721a59671b41d6b06e66b29e0628e207e8b1c3ceeda701ec928d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "untrusted", "windows-sys 0.52.0", @@ -2829,9 +2893,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -2848,15 +2912,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2880,19 +2944,32 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -2909,19 +2986,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -2935,9 +3011,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2946,9 +3022,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -2964,44 +3040,44 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.8.0", "schemars_derive", "serde", "serde_json", - "smol_str", + "smol_str 0.1.24", ] [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3022,11 +3098,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -3035,9 +3111,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3045,37 +3121,37 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3086,16 +3162,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.8.0", "itoa", "memchr", "ryu", @@ -3104,9 +3180,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", @@ -3126,15 +3202,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.8.0", "serde", "serde_derive", "serde_json", @@ -3144,14 +3220,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3160,7 +3236,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.8.0", "itoa", "ryu", "serde", @@ -3226,9 +3302,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" @@ -3241,9 +3317,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "smawk" @@ -3260,11 +3336,21 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3295,9 +3381,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -3312,9 +3398,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -3327,6 +3413,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -3335,7 +3430,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3346,7 +3441,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3359,6 +3465,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -3373,14 +3489,16 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3394,9 +3512,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-helpers" @@ -3414,33 +3532,33 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3455,9 +3573,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -3470,15 +3588,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -3505,9 +3623,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3520,21 +3638,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3549,13 +3666,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] @@ -3589,11 +3706,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.23", + "tokio", +] + [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3602,9 +3729,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3629,18 +3756,18 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.32", "hyper-timeout", "percent-encoding", "pin-project", "prost", "rustls-native-certs", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -3666,13 +3793,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.0", "bytes", "futures-core", "futures-util", @@ -3688,21 +3830,21 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3712,20 +3854,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3762,9 +3904,9 @@ dependencies = [ [[package]] name = "tracing-serde" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" dependencies = [ "serde", "tracing-core", @@ -3772,9 +3914,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -3829,14 +3971,14 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unarray" @@ -3846,24 +3988,21 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -3873,24 +4012,30 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unsafe-libyaml" @@ -3906,9 +4051,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3941,19 +4086,19 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" dependencies = [ - "getrandom", + "getrandom 0.3.1", "serde", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3963,15 +4108,15 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -3991,48 +4136,59 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4040,28 +4196,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -4107,11 +4266,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4126,7 +4285,42 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", ] [[package]] @@ -4144,7 +4338,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4164,18 +4367,34 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -4186,9 +4405,15 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" @@ -4198,9 +4423,15 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" @@ -4210,15 +4441,27 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" @@ -4228,9 +4471,15 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" @@ -4240,9 +4489,15 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" @@ -4252,9 +4507,15 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" @@ -4264,9 +4525,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winreg" @@ -4279,13 +4546,12 @@ dependencies = [ ] [[package]] -name = "winreg" -version = "0.52.0" +name = "wit-bindgen-rt" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "bitflags 2.9.0", ] [[package]] @@ -4311,15 +4577,15 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4329,54 +4595,74 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.23", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", "synstructure", ] @@ -4405,5 +4691,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index c3456df7..fbe829e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,15 @@ resolver = "2" # The tag or rev of ndc-models must match the locked tag or rev of the # ndc-models dependency of ndc-sdk [workspace.dependencies] -ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs.git", tag = "v0.4.0" } -ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } +ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs.git", rev = "643b96b8ee4c8b372b44433167ce2ac4de193332" } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.2.0-rc.2" } indexmap = { version = "2", features = [ "serde", ] } # should match the version that ndc-models uses -itertools = "^0.12.1" -mongodb = { version = "^3.1.0", features = ["tracing-unstable"] } +itertools = "^0.13.0" +mongodb = { version = "^3.2.2", features = ["tracing-unstable"] } +nonempty = "^0.11.0" schemars = "^0.8.12" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } diff --git a/arion-compose/services/engine.nix b/arion-compose/services/engine.nix index 1d30bc2f..6924506f 100644 --- a/arion-compose/services/engine.nix +++ b/arion-compose/services/engine.nix @@ -85,6 +85,7 @@ in useHostStore = true; command = [ "engine" + "--unstable-feature=enable-ndc-v02-support" "--port=${port}" "--metadata-path=${metadata}" "--authn-config-path=${auth-config}" diff --git a/connector-definition/connector-metadata.yaml b/connector-definition/connector-metadata.yaml index d3334163..02fa44d7 100644 --- a/connector-definition/connector-metadata.yaml +++ b/connector-definition/connector-metadata.yaml @@ -24,11 +24,20 @@ nativeToolchainDefinition: powershell: | $ErrorActionPreference = "Stop" & "$env:HASURA_DDN_NATIVE_CONNECTOR_PLUGIN_DIR\hasura-ndc-mongodb.exe" update + watch: + type: ShellScript + bash: | + #!/usr/bin/env bash + echo "Watch is not supported for this connector" + exit 1 + powershell: | + Write-Output "Watch is not supported for this connector" + exit 1 commands: update: hasura-ndc-mongodb update cliPlugin: name: ndc-mongodb - version: + version: dockerComposeWatch: - path: ./ target: /etc/connector diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bbe736ce..0324f5f2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -22,7 +22,7 @@ itertools = { workspace = true } json-structural-diff = "^0.2.0" ndc-models = { workspace = true } nom = { version = "^7.1.3", optional = true } -nonempty = "^0.10.0" +nonempty = { workspace = true } pretty = { version = "^0.12.3", features = ["termcolor"], optional = true } ref-cast = { workspace = true } regex = "^1.11.1" diff --git a/crates/cli/src/native_query/infer_result_type.rs b/crates/cli/src/native_query/infer_result_type.rs deleted file mode 100644 index eb5c8b02..00000000 --- a/crates/cli/src/native_query/infer_result_type.rs +++ /dev/null @@ -1,475 +0,0 @@ -use std::{collections::BTreeMap, iter::once}; - -use configuration::{ - schema::{ObjectField, ObjectType, Type}, - Configuration, -}; -use mongodb::bson::{Bson, Document}; -use mongodb_support::{ - aggregate::{Accumulator, Pipeline, Stage}, - BsonScalarType, -}; -use ndc_models::{CollectionName, FieldName, ObjectTypeName}; - -use crate::introspection::{sampling::make_object_type, type_unification::unify_object_types}; - -use super::{ - aggregation_expression::{ - self, infer_type_from_aggregation_expression, infer_type_from_reference_shorthand, - }, - error::{Error, Result}, - helpers::find_collection_object_type, - pipeline_type_context::{PipelineTypeContext, PipelineTypes}, - reference_shorthand::{parse_reference_shorthand, Reference}, -}; - -type ObjectTypes = BTreeMap; - -pub fn infer_result_type( - configuration: &Configuration, - // If we have to define a new object type, use this name - desired_object_type_name: &str, - input_collection: Option<&CollectionName>, - pipeline: &Pipeline, -) -> Result { - let collection_doc_type = input_collection - .map(|collection_name| find_collection_object_type(configuration, collection_name)) - .transpose()?; - let mut stages = pipeline.iter().enumerate(); - let mut context = PipelineTypeContext::new(configuration, collection_doc_type); - match stages.next() { - Some((stage_index, stage)) => infer_result_type_helper( - &mut context, - desired_object_type_name, - stage_index, - stage, - stages, - ), - None => Err(Error::EmptyPipeline), - }?; - context.try_into() -} - -pub fn infer_result_type_helper<'a, 'b>( - context: &mut PipelineTypeContext<'a>, - desired_object_type_name: &str, - stage_index: usize, - stage: &Stage, - mut rest: impl Iterator, -) -> Result<()> { - match stage { - Stage::Documents(docs) => { - let document_type_name = - context.unique_type_name(&format!("{desired_object_type_name}_documents")); - let new_object_types = infer_type_from_documents(&document_type_name, docs); - context.set_stage_doc_type(document_type_name, new_object_types); - } - Stage::Match(_) => (), - Stage::Sort(_) => (), - Stage::Limit(_) => (), - Stage::Lookup { .. } => todo!("lookup stage"), - Stage::Skip(_) => (), - Stage::Group { - key_expression, - accumulators, - } => { - let object_type_name = infer_type_from_group_stage( - context, - desired_object_type_name, - key_expression, - accumulators, - )?; - context.set_stage_doc_type(object_type_name, Default::default()) - } - Stage::Facet(_) => todo!("facet stage"), - Stage::Count(_) => todo!("count stage"), - Stage::ReplaceWith(selection) => { - let selection: &Document = selection.into(); - let result_type = aggregation_expression::infer_type_from_aggregation_expression( - context, - desired_object_type_name, - selection.clone().into(), - )?; - match result_type { - Type::Object(object_type_name) => { - context.set_stage_doc_type(object_type_name.into(), Default::default()); - } - t => Err(Error::ExpectedObject { actual_type: t })?, - } - } - Stage::Unwind { - path, - include_array_index, - preserve_null_and_empty_arrays, - } => { - let result_type = infer_type_from_unwind_stage( - context, - desired_object_type_name, - path, - include_array_index.as_deref(), - *preserve_null_and_empty_arrays, - )?; - context.set_stage_doc_type(result_type, Default::default()) - } - Stage::Other(doc) => { - let warning = Error::UnknownAggregationStage { - stage_index, - stage: doc.clone(), - }; - context.set_unknown_stage_doc_type(warning); - } - }; - match rest.next() { - Some((next_stage_index, next_stage)) => infer_result_type_helper( - context, - desired_object_type_name, - next_stage_index, - next_stage, - rest, - ), - None => Ok(()), - } -} - -pub fn infer_type_from_documents( - object_type_name: &ObjectTypeName, - documents: &[Document], -) -> ObjectTypes { - let mut collected_object_types = vec![]; - for document in documents { - let object_types = make_object_type(object_type_name, document, false, false); - collected_object_types = if collected_object_types.is_empty() { - object_types - } else { - unify_object_types(collected_object_types, object_types) - }; - } - collected_object_types - .into_iter() - .map(|type_with_name| (type_with_name.name, type_with_name.value)) - .collect() -} - -fn infer_type_from_group_stage( - context: &mut PipelineTypeContext<'_>, - desired_object_type_name: &str, - key_expression: &Bson, - accumulators: &BTreeMap, -) -> Result { - let group_key_expression_type = infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_id"), - key_expression.clone(), - )?; - - let group_expression_field: (FieldName, ObjectField) = ( - "_id".into(), - ObjectField { - r#type: group_key_expression_type.clone(), - description: None, - }, - ); - let accumulator_fields = accumulators.iter().map(|(key, accumulator)| { - let accumulator_type = match accumulator { - Accumulator::Count => Type::Scalar(BsonScalarType::Int), - Accumulator::Min(expr) => infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_min"), - expr.clone(), - )?, - Accumulator::Max(expr) => infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_min"), - expr.clone(), - )?, - Accumulator::Push(expr) => { - let t = infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_push"), - expr.clone(), - )?; - Type::ArrayOf(Box::new(t)) - } - Accumulator::Avg(expr) => { - let t = infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_avg"), - expr.clone(), - )?; - match t { - Type::ExtendedJSON => t, - Type::Scalar(scalar_type) if scalar_type.is_numeric() => t, - _ => Type::Nullable(Box::new(Type::Scalar(BsonScalarType::Int))), - } - } - Accumulator::Sum(expr) => { - let t = infer_type_from_aggregation_expression( - context, - &format!("{desired_object_type_name}_push"), - expr.clone(), - )?; - match t { - Type::ExtendedJSON => t, - Type::Scalar(scalar_type) if scalar_type.is_numeric() => t, - _ => Type::Scalar(BsonScalarType::Int), - } - } - }; - Ok::<_, Error>(( - key.clone().into(), - ObjectField { - r#type: accumulator_type, - description: None, - }, - )) - }); - let fields = once(Ok(group_expression_field)) - .chain(accumulator_fields) - .collect::>()?; - - let object_type = ObjectType { - fields, - description: None, - }; - let object_type_name = context.unique_type_name(desired_object_type_name); - context.insert_object_type(object_type_name.clone(), object_type); - Ok(object_type_name) -} - -fn infer_type_from_unwind_stage( - context: &mut PipelineTypeContext<'_>, - desired_object_type_name: &str, - path: &str, - include_array_index: Option<&str>, - _preserve_null_and_empty_arrays: Option, -) -> Result { - let field_to_unwind = parse_reference_shorthand(path)?; - let Reference::InputDocumentField { name, nested_path } = field_to_unwind else { - return Err(Error::ExpectedStringPath(path.into())); - }; - - let field_type = infer_type_from_reference_shorthand(context, path)?; - let Type::ArrayOf(field_element_type) = field_type else { - return Err(Error::ExpectedArrayReference { - reference: path.into(), - referenced_type: field_type, - }); - }; - - let nested_path_iter = nested_path.into_iter(); - - let mut doc_type = context.get_input_document_type()?.into_owned(); - if let Some(index_field_name) = include_array_index { - doc_type.fields.insert( - index_field_name.into(), - ObjectField { - r#type: Type::Scalar(BsonScalarType::Long), - description: Some(format!("index of unwound array elements in {name}")), - }, - ); - } - - // If `path` includes a nested_path then the type for the unwound field will be nested - // objects - fn build_nested_types( - context: &mut PipelineTypeContext<'_>, - ultimate_field_type: Type, - parent_object_type: &mut ObjectType, - desired_object_type_name: &str, - field_name: FieldName, - mut rest: impl Iterator, - ) { - match rest.next() { - Some(next_field_name) => { - let object_type_name = context.unique_type_name(desired_object_type_name); - let mut object_type = ObjectType { - fields: Default::default(), - description: None, - }; - build_nested_types( - context, - ultimate_field_type, - &mut object_type, - &format!("{desired_object_type_name}_{next_field_name}"), - next_field_name, - rest, - ); - context.insert_object_type(object_type_name.clone(), object_type); - parent_object_type.fields.insert( - field_name, - ObjectField { - r#type: Type::Object(object_type_name.into()), - description: None, - }, - ); - } - None => { - parent_object_type.fields.insert( - field_name, - ObjectField { - r#type: ultimate_field_type, - description: None, - }, - ); - } - } - } - build_nested_types( - context, - *field_element_type, - &mut doc_type, - desired_object_type_name, - name, - nested_path_iter, - ); - - let object_type_name = context.unique_type_name(desired_object_type_name); - context.insert_object_type(object_type_name.clone(), doc_type); - - Ok(object_type_name) -} - -#[cfg(test)] -mod tests { - use configuration::schema::{ObjectField, ObjectType, Type}; - use mongodb::bson::doc; - use mongodb_support::{ - aggregate::{Pipeline, Selection, Stage}, - BsonScalarType, - }; - use pretty_assertions::assert_eq; - use test_helpers::configuration::mflix_config; - - use crate::native_query::pipeline_type_context::PipelineTypeContext; - - use super::{infer_result_type, infer_type_from_unwind_stage}; - - type Result = anyhow::Result; - - #[test] - fn infers_type_from_documents_stage() -> Result<()> { - let pipeline = Pipeline::new(vec![Stage::Documents(vec![ - doc! { "foo": 1 }, - doc! { "bar": 2 }, - ])]); - let config = mflix_config(); - let pipeline_types = infer_result_type(&config, "documents", None, &pipeline).unwrap(); - let expected = [( - "documents_documents".into(), - ObjectType { - fields: [ - ( - "foo".into(), - ObjectField { - r#type: Type::Nullable(Box::new(Type::Scalar(BsonScalarType::Int))), - description: None, - }, - ), - ( - "bar".into(), - ObjectField { - r#type: Type::Nullable(Box::new(Type::Scalar(BsonScalarType::Int))), - description: None, - }, - ), - ] - .into(), - description: None, - }, - )] - .into(); - let actual = pipeline_types.object_types; - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn infers_type_from_replace_with_stage() -> Result<()> { - let pipeline = Pipeline::new(vec![Stage::ReplaceWith(Selection::new(doc! { - "selected_title": "$title" - }))]); - let config = mflix_config(); - let pipeline_types = infer_result_type( - &config, - "movies_selection", - Some(&("movies".into())), - &pipeline, - ) - .unwrap(); - let expected = [( - "movies_selection".into(), - ObjectType { - fields: [( - "selected_title".into(), - ObjectField { - r#type: Type::Scalar(BsonScalarType::String), - description: None, - }, - )] - .into(), - description: None, - }, - )] - .into(); - let actual = pipeline_types.object_types; - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn infers_type_from_unwind_stage() -> Result<()> { - let config = mflix_config(); - let mut context = PipelineTypeContext::new(&config, None); - context.insert_object_type( - "words_doc".into(), - ObjectType { - fields: [( - "words".into(), - ObjectField { - r#type: Type::ArrayOf(Box::new(Type::Scalar(BsonScalarType::String))), - description: None, - }, - )] - .into(), - description: None, - }, - ); - context.set_stage_doc_type("words_doc".into(), Default::default()); - - let inferred_type_name = infer_type_from_unwind_stage( - &mut context, - "unwind_stage", - "$words", - Some("idx"), - Some(false), - )?; - - assert_eq!( - context - .get_object_type(&inferred_type_name) - .unwrap() - .into_owned(), - ObjectType { - fields: [ - ( - "words".into(), - ObjectField { - r#type: Type::Scalar(BsonScalarType::String), - description: None, - } - ), - ( - "idx".into(), - ObjectField { - r#type: Type::Scalar(BsonScalarType::Long), - description: Some("index of unwound array elements in words".into()), - } - ), - ] - .into(), - description: None, - } - ); - Ok(()) - } -} diff --git a/crates/cli/src/native_query/pipeline/mod.rs b/crates/cli/src/native_query/pipeline/mod.rs index acc80046..9f14d085 100644 --- a/crates/cli/src/native_query/pipeline/mod.rs +++ b/crates/cli/src/native_query/pipeline/mod.rs @@ -211,7 +211,7 @@ fn infer_type_from_group_stage( None, expr.clone(), )?, - Accumulator::Push(expr) => { + Accumulator::AddToSet(expr) | Accumulator::Push(expr) => { let t = infer_type_from_aggregation_expression( context, &format!("{desired_object_type_name}_push"), @@ -341,7 +341,7 @@ mod tests { aggregate::{Pipeline, Selection, Stage}, BsonScalarType, }; - use nonempty::nonempty; + use nonempty::NonEmpty; use pretty_assertions::assert_eq; use test_helpers::configuration::mflix_config; @@ -462,7 +462,7 @@ mod tests { Some(TypeConstraint::ElementOf(Box::new( TypeConstraint::FieldOf { target_type: Box::new(TypeConstraint::Variable(input_doc_variable)), - path: nonempty!["words".into()], + path: NonEmpty::singleton("words".into()), } ))) ) diff --git a/crates/cli/src/native_query/pipeline/project_stage.rs b/crates/cli/src/native_query/pipeline/project_stage.rs index 05bdea41..427d9c55 100644 --- a/crates/cli/src/native_query/pipeline/project_stage.rs +++ b/crates/cli/src/native_query/pipeline/project_stage.rs @@ -7,7 +7,7 @@ use itertools::Itertools as _; use mongodb::bson::{Bson, Decimal128, Document}; use mongodb_support::BsonScalarType; use ndc_models::{FieldName, ObjectTypeName}; -use nonempty::{nonempty, NonEmpty}; +use nonempty::NonEmpty; use crate::native_query::{ aggregation_expression::infer_type_from_aggregation_expression, @@ -89,7 +89,7 @@ fn projection_tree_into_field_overrides( ProjectionTree::Object(sub_specs) => { let original_field_type = TypeConstraint::FieldOf { target_type: Box::new(input_type.clone()), - path: nonempty![name.clone()], + path: NonEmpty::singleton(name.clone()), }; Some(projection_tree_into_field_overrides( original_field_type, @@ -265,7 +265,7 @@ fn path_collision_error(path: impl IntoIterator) mod tests { use mongodb::bson::doc; use mongodb_support::BsonScalarType; - use nonempty::nonempty; + use nonempty::{nonempty, NonEmpty}; use pretty_assertions::assert_eq; use test_helpers::configuration::mflix_config; @@ -310,7 +310,7 @@ mod tests { "title".into(), TypeConstraint::FieldOf { target_type: Box::new(input_type.clone()), - path: nonempty!["title".into()], + path: NonEmpty::singleton("title".into()), }, ), ( @@ -321,7 +321,7 @@ mod tests { "releaseDate".into(), TypeConstraint::FieldOf { target_type: Box::new(input_type.clone()), - path: nonempty!["released".into()], + path: NonEmpty::singleton("released".into()), }, ), ] @@ -410,7 +410,7 @@ mod tests { augmented_object_type_name: "Movie_project_tomatoes".into(), target_type: Box::new(TypeConstraint::FieldOf { target_type: Box::new(input_type.clone()), - path: nonempty!["tomatoes".into()], + path: NonEmpty::singleton("tomatoes".into()), }), fields: [ ("lastUpdated".into(), None), @@ -422,9 +422,9 @@ mod tests { target_type: Box::new(TypeConstraint::FieldOf { target_type: Box::new(TypeConstraint::FieldOf { target_type: Box::new(input_type.clone()), - path: nonempty!["tomatoes".into()], + path: NonEmpty::singleton("tomatoes".into()), }), - path: nonempty!["critic".into()], + path: NonEmpty::singleton("critic".into()), }), fields: [("rating".into(), None), ("meter".into(), None),] .into(), diff --git a/crates/cli/src/native_query/type_solver/mod.rs b/crates/cli/src/native_query/type_solver/mod.rs index bc7a8f38..5c40a9cc 100644 --- a/crates/cli/src/native_query/type_solver/mod.rs +++ b/crates/cli/src/native_query/type_solver/mod.rs @@ -147,7 +147,7 @@ mod tests { use anyhow::Result; use configuration::schema::{ObjectField, ObjectType, Type}; use mongodb_support::BsonScalarType; - use nonempty::nonempty; + use nonempty::NonEmpty; use pretty_assertions::assert_eq; use test_helpers::configuration::mflix_config; @@ -252,7 +252,7 @@ mod tests { "selected_title".into(), TypeConstraint::FieldOf { target_type: Box::new(TypeConstraint::Variable(var0)), - path: nonempty!["title".into()], + path: NonEmpty::singleton("title".into()), }, )] .into(), diff --git a/crates/cli/src/native_query/type_solver/simplify.rs b/crates/cli/src/native_query/type_solver/simplify.rs index f007c554..dad0e829 100644 --- a/crates/cli/src/native_query/type_solver/simplify.rs +++ b/crates/cli/src/native_query/type_solver/simplify.rs @@ -522,7 +522,7 @@ mod tests { use googletest::prelude::*; use mongodb_support::BsonScalarType; - use nonempty::nonempty; + use nonempty::NonEmpty; use test_helpers::configuration::mflix_config; use crate::native_query::{ @@ -584,7 +584,7 @@ mod tests { Some(TypeVariable::new(1, Variance::Covariant)), [TypeConstraint::FieldOf { target_type: Box::new(TypeConstraint::Object("movies".into())), - path: nonempty!["title".into()], + path: NonEmpty::singleton("title".into()), }], ); expect_that!( diff --git a/crates/configuration/src/configuration.rs b/crates/configuration/src/configuration.rs index 729b680b..ffb93863 100644 --- a/crates/configuration/src/configuration.rs +++ b/crates/configuration/src/configuration.rs @@ -276,7 +276,6 @@ fn collection_to_collection_info( collection_type: collection.r#type, description: collection.description, arguments: Default::default(), - foreign_keys: Default::default(), uniqueness_constraints: BTreeMap::from_iter(pk_constraint), } } @@ -298,7 +297,6 @@ fn native_query_to_collection_info( collection_type: native_query.result_document_type.clone(), description: native_query.description.clone(), arguments: arguments_to_ndc_arguments(native_query.arguments.clone()), - foreign_keys: Default::default(), uniqueness_constraints: BTreeMap::from_iter(pk_constraint), } } diff --git a/crates/configuration/src/mongo_scalar_type.rs b/crates/configuration/src/mongo_scalar_type.rs index 9641ce9f..38c3532f 100644 --- a/crates/configuration/src/mongo_scalar_type.rs +++ b/crates/configuration/src/mongo_scalar_type.rs @@ -1,7 +1,9 @@ +use std::fmt::Display; + use mongodb_support::{BsonScalarType, EXTENDED_JSON_TYPE_NAME}; use ndc_query_plan::QueryPlanError; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum MongoScalarType { /// One of the predefined BSON scalar types Bson(BsonScalarType), @@ -20,6 +22,12 @@ impl MongoScalarType { } } +impl From for MongoScalarType { + fn from(value: BsonScalarType) -> Self { + Self::Bson(value) + } +} + impl TryFrom<&ndc_models::ScalarTypeName> for MongoScalarType { type Error = QueryPlanError; @@ -34,3 +42,14 @@ impl TryFrom<&ndc_models::ScalarTypeName> for MongoScalarType { } } } + +impl Display for MongoScalarType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MongoScalarType::ExtendedJSON => write!(f, "extendedJSON"), + MongoScalarType::Bson(bson_scalar_type) => { + write!(f, "{}", bson_scalar_type.bson_name()) + } + } + } +} diff --git a/crates/configuration/src/schema/mod.rs b/crates/configuration/src/schema/mod.rs index cba2a589..e3a4f821 100644 --- a/crates/configuration/src/schema/mod.rs +++ b/crates/configuration/src/schema/mod.rs @@ -192,6 +192,7 @@ impl From for ndc_models::ObjectType { .into_iter() .map(|(name, field)| (name, field.into())) .collect(), + foreign_keys: Default::default(), } } } diff --git a/crates/integration-tests/src/tests/aggregation.rs b/crates/integration-tests/src/tests/aggregation.rs index dedfad6a..86d6a180 100644 --- a/crates/integration-tests/src/tests/aggregation.rs +++ b/crates/integration-tests/src/tests/aggregation.rs @@ -131,7 +131,7 @@ async fn returns_zero_when_counting_empty_result_set() -> anyhow::Result<()> { moviesAggregate(filter_input: {where: {title: {_eq: "no such movie"}}}) { _count title { - count + _count } } } @@ -152,7 +152,6 @@ async fn returns_zero_when_counting_nested_fields_in_empty_result_set() -> anyho moviesAggregate(filter_input: {where: {title: {_eq: "no such movie"}}}) { awards { nominations { - count _count } } diff --git a/crates/integration-tests/src/tests/expressions.rs b/crates/integration-tests/src/tests/expressions.rs index ff527bd3..584cbd69 100644 --- a/crates/integration-tests/src/tests/expressions.rs +++ b/crates/integration-tests/src/tests/expressions.rs @@ -61,6 +61,7 @@ async fn evaluates_exists_with_predicate() -> anyhow::Result<()> { query() .predicate(exists( ExistsInCollection::Related { + field_path: Default::default(), relationship: "albums".into(), arguments: Default::default(), }, @@ -74,7 +75,10 @@ async fn evaluates_exists_with_predicate() -> anyhow::Result<()> { ]).order_by([asc!("Title")])) ]), ) - .relationships([("albums", relationship("Album", [("ArtistId", "ArtistId")]))]) + .relationships([( + "albums", + relationship("Album", [("ArtistId", &["ArtistId"])]) + )]) ) .await? ); diff --git a/crates/integration-tests/src/tests/filtering.rs b/crates/integration-tests/src/tests/filtering.rs index 2d8fba81..fb435af3 100644 --- a/crates/integration-tests/src/tests/filtering.rs +++ b/crates/integration-tests/src/tests/filtering.rs @@ -1,5 +1,7 @@ use insta::assert_yaml_snapshot; -use ndc_test_helpers::{binop, field, query, query_request, target, value, variable}; +use ndc_test_helpers::{ + array_contains, binop, field, is_empty, query, query_request, target, value, variable, +}; use crate::{connector::Connector, graphql_query, run_connector_query}; @@ -67,18 +69,17 @@ async fn filters_by_comparisons_on_elements_of_array_field() -> anyhow::Result<( } #[tokio::test] -async fn filters_by_comparisons_on_elements_of_array_of_scalars_against_variable( -) -> anyhow::Result<()> { +async fn filters_by_comparison_with_a_variable() -> anyhow::Result<()> { assert_yaml_snapshot!( run_connector_query( Connector::SampleMflix, query_request() - .variables([[("cast_member", "Albert Austin")]]) + .variables([[("title", "The Blue Bird")]]) .collection("movies") .query( query() - .predicate(binop("_eq", target!("cast"), variable!(cast_member))) - .fields([field!("title"), field!("cast")]), + .predicate(binop("_eq", target!("title"), variable!(title))) + .fields([field!("title")]), ) ) .await? @@ -86,6 +87,39 @@ async fn filters_by_comparisons_on_elements_of_array_of_scalars_against_variable Ok(()) } +#[tokio::test] +async fn filters_by_array_comparison_contains() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + .predicate(array_contains(target!("cast"), value!("Albert Austin"))) + .fields([field!("title"), field!("cast")]), + ) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn filters_by_array_comparison_is_empty() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + .predicate(is_empty(target!("writers"))) + .fields([field!("writers")]) + .limit(1), + ) + ) + .await? + ); + Ok(()) +} + #[tokio::test] async fn filters_by_uuid() -> anyhow::Result<()> { assert_yaml_snapshot!( diff --git a/crates/integration-tests/src/tests/grouping.rs b/crates/integration-tests/src/tests/grouping.rs new file mode 100644 index 00000000..135faa19 --- /dev/null +++ b/crates/integration-tests/src/tests/grouping.rs @@ -0,0 +1,162 @@ +use insta::assert_yaml_snapshot; +use ndc_test_helpers::{ + and, asc, binop, column_aggregate, column_count_aggregate, dimension_column, field, grouping, or, ordered_dimensions, query, query_request, star_count_aggregate, target, value +}; + +use crate::{connector::Connector, run_connector_query}; + +#[tokio::test] +async fn runs_single_column_aggregate_on_groups() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + // The predicate avoids an error when encountering documents where `year` is + // a string instead of a number. + .predicate(or([ + binop("_gt", target!("year"), value!(0)), + binop("_lte", target!("year"), value!(0)), + ])) + .order_by([asc!("_id")]) + .limit(10) + .groups( + grouping() + .dimensions([dimension_column("year")]) + .aggregates([ + ( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg"), + ), + ("max_runtime", column_aggregate("runtime", "max")), + ]) + .order_by(ordered_dimensions()), + ), + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn counts_column_values_in_groups() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + .predicate(and([ + binop("_gt", target!("year"), value!(1920)), + binop("_lte", target!("year"), value!(1923)), + ])) + .groups( + grouping() + .dimensions([dimension_column("rated")]) + .aggregates([ + // The distinct count should be 3 or less because we filtered to only 3 years + column_count_aggregate!("year_distinct_count" => "year", distinct: true), + column_count_aggregate!("year_count" => "year", distinct: false), + star_count_aggregate!("count"), + ]) + .order_by(ordered_dimensions()), + ), + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn groups_by_multiple_dimensions() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + .predicate(binop("_lt", target!("year"), value!(1950))) + .order_by([asc!("_id")]) + .limit(10) + .groups( + grouping() + .dimensions([ + dimension_column("year"), + dimension_column("languages"), + dimension_column("rated"), + ]) + .aggregates([( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg"), + )]) + .order_by(ordered_dimensions()), + ), + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn combines_aggregates_and_groups_in_one_query() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + .predicate(binop("_gte", target!("year"), value!(2000))) + .order_by([asc!("_id")]) + .limit(10) + .aggregates([( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg") + )]) + .groups( + grouping() + .dimensions([dimension_column("year"),]) + .aggregates([( + "average_viewer_rating_by_year", + column_aggregate("tomatoes.viewer.rating", "avg"), + )]) + .order_by(ordered_dimensions()), + ), + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn combines_fields_and_groups_in_one_query() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request().collection("movies").query( + query() + // The predicate avoids an error when encountering documents where `year` is + // a string instead of a number. + .predicate(or([ + binop("_gt", target!("year"), value!(0)), + binop("_lte", target!("year"), value!(0)), + ])) + .order_by([asc!("_id")]) + .limit(3) + .fields([field!("title"), field!("year")]) + .order_by([asc!("_id")]) + .groups( + grouping() + .dimensions([dimension_column("year")]) + .aggregates([( + "average_viewer_rating_by_year", + column_aggregate("tomatoes.viewer.rating", "avg"), + )]) + .order_by(ordered_dimensions()), + ) + ), + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/local_relationship.rs b/crates/integration-tests/src/tests/local_relationship.rs index a9997d04..2031028b 100644 --- a/crates/integration-tests/src/tests/local_relationship.rs +++ b/crates/integration-tests/src/tests/local_relationship.rs @@ -1,6 +1,11 @@ use crate::{connector::Connector, graphql_query, run_connector_query}; use insta::assert_yaml_snapshot; -use ndc_test_helpers::{asc, field, query, query_request, relation_field, relationship}; +use ndc_test_helpers::{ + asc, binop, column, column_aggregate, column_count_aggregate, dimension_column, exists, field, + grouping, is_in, ordered_dimensions, query, query_request, related, relation_field, + relationship, star_count_aggregate, target, value, +}; +use serde_json::json; #[tokio::test] async fn joins_local_relationships() -> anyhow::Result<()> { @@ -203,10 +208,211 @@ async fn joins_on_field_names_that_require_escaping() -> anyhow::Result<()> { ) .relationships([( "join", - relationship("weird_field_names", [("$invalid.name", "$invalid.name")]) + relationship("weird_field_names", [("$invalid.name", &["$invalid.name"])]) )]) ) .await? ); Ok(()) } + +#[tokio::test] +async fn joins_relationships_on_nested_key() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::TestCases, + query_request() + .collection("departments") + .query( + query() + .predicate(exists( + related!("schools_departments"), + binop("_eq", target!("name"), value!("West Valley")) + )) + .fields([ + relation_field!("departments" => "schools_departments", query().fields([ + field!("name") + ])) + ]) + .order_by([asc!("_id")]) + ) + .relationships([( + "schools_departments", + relationship("schools", [("_id", &["departments", "math_department_id"])]) + )]) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn aggregates_over_related_collection() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::Chinook, + query_request() + .collection("Album") + .query( + query() + // avoid albums that are modified in mutation tests + .predicate(is_in( + target!("AlbumId"), + [json!(15), json!(91), json!(227)] + )) + .fields([relation_field!("tracks" => "tracks", query().aggregates([ + star_count_aggregate!("count"), + ("average_price", column_aggregate("UnitPrice", "avg").into()), + ]))]) + .order_by([asc!("_id")]) + ) + .relationships([("tracks", relationship("Track", [("AlbumId", &["AlbumId"])]))]) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn aggregates_over_empty_subset_of_related_collection() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::Chinook, + query_request() + .collection("Album") + .query( + query() + // avoid albums that are modified in mutation tests + .predicate(is_in( + target!("AlbumId"), + [json!(15), json!(91), json!(227)] + )) + .fields([relation_field!("tracks" => "tracks", query() + .predicate(binop("_eq", target!("Name"), value!("non-existent name"))) + .aggregates([ + star_count_aggregate!("count"), + column_count_aggregate!("composer_count" => "Composer", distinct: true), + ("average_price", column_aggregate("UnitPrice", "avg").into()), + ]))]) + .order_by([asc!("_id")]) + ) + .relationships([("tracks", relationship("Track", [("AlbumId", &["AlbumId"])]))]) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn groups_by_related_field() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::Chinook, + query_request() + .collection("Track") + .query( + query() + // avoid albums that are modified in mutation tests + .predicate(is_in( + target!("AlbumId"), + [json!(15), json!(91), json!(227)] + )) + .groups( + grouping() + .dimensions([dimension_column( + column("Name").from_relationship("track_genre") + )]) + .aggregates([( + "average_price", + column_aggregate("UnitPrice", "avg") + )]) + .order_by(ordered_dimensions()) + ) + ) + .relationships([( + "track_genre", + relationship("Genre", [("GenreId", &["GenreId"])]).object_type() + )]) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn gets_groups_through_relationship() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::Chinook, + query_request() + .collection("Album") + .query( + query() + // avoid albums that are modified in mutation tests + .predicate(is_in(target!("AlbumId"), [json!(15), json!(91), json!(227)])) + .order_by([asc!("_id")]) + .fields([field!("AlbumId"), relation_field!("tracks" => "album_tracks", query() + .groups(grouping() + .dimensions([dimension_column(column("Name").from_relationship("track_genre"))]) + .aggregates([ + ("AlbumId", column_aggregate("AlbumId", "avg")), + ("average_price", column_aggregate("UnitPrice", "avg")), + ]) + .order_by(ordered_dimensions()), + ) + )]) + ) + .relationships([ + ( + "album_tracks", + relationship("Track", [("AlbumId", &["AlbumId"])]) + ), + ( + "track_genre", + relationship("Genre", [("GenreId", &["GenreId"])]).object_type() + ) + ]) + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn gets_fields_and_groups_through_relationship() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::Chinook, + query_request() + .collection("Album") + .query( + query() + .predicate(is_in(target!("AlbumId"), [json!(15), json!(91), json!(227)])) + .order_by([asc!("_id")]) + .fields([field!("AlbumId"), relation_field!("tracks" => "album_tracks", query() + .order_by([asc!("_id")]) + .fields([field!("AlbumId"), field!("Name"), field!("UnitPrice")]) + .groups(grouping() + .dimensions([dimension_column(column("Name").from_relationship("track_genre"))]) + .aggregates([( + "average_price", column_aggregate("UnitPrice", "avg") + )]) + .order_by(ordered_dimensions()), + ) + )]) + ) + .relationships([ + ( + "album_tracks", + relationship("Track", [("AlbumId", &["AlbumId"])]) + ), + ( + "track_genre", + relationship("Genre", [("GenreId", &["GenreId"])]).object_type() + ) + ]) + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/mod.rs b/crates/integration-tests/src/tests/mod.rs index 1956d231..6533de72 100644 --- a/crates/integration-tests/src/tests/mod.rs +++ b/crates/integration-tests/src/tests/mod.rs @@ -11,9 +11,11 @@ mod aggregation; mod basic; mod expressions; mod filtering; +mod grouping; mod local_relationship; mod native_mutation; mod native_query; +mod nested_collection; mod permissions; mod remote_relationship; mod sorting; diff --git a/crates/integration-tests/src/tests/nested_collection.rs b/crates/integration-tests/src/tests/nested_collection.rs new file mode 100644 index 00000000..eee65140 --- /dev/null +++ b/crates/integration-tests/src/tests/nested_collection.rs @@ -0,0 +1,28 @@ +use crate::{connector::Connector, run_connector_query}; +use insta::assert_yaml_snapshot; +use ndc_test_helpers::{ + array, asc, binop, exists, exists_in_nested, field, object, query, query_request, target, value, +}; + +#[tokio::test] +async fn exists_in_nested_collection() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::TestCases, + query_request().collection("nested_collection").query( + query() + .predicate(exists( + exists_in_nested("staff"), + binop("_eq", target!("name"), value!("Alyx")) + )) + .fields([ + field!("institution"), + field!("staff" => "staff", array!(object!([field!("name")]))), + ]) + .order_by([asc!("_id")]) + ) + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/remote_relationship.rs b/crates/integration-tests/src/tests/remote_relationship.rs index c607b30b..20837657 100644 --- a/crates/integration-tests/src/tests/remote_relationship.rs +++ b/crates/integration-tests/src/tests/remote_relationship.rs @@ -1,6 +1,9 @@ use crate::{connector::Connector, graphql_query, run_connector_query}; use insta::assert_yaml_snapshot; -use ndc_test_helpers::{and, asc, binop, field, query, query_request, target, variable}; +use ndc_test_helpers::{ + and, asc, binop, column_aggregate, column_count_aggregate, dimension_column, field, grouping, + ordered_dimensions, query, query_request, star_count_aggregate, target, value, variable, +}; use serde_json::json; #[tokio::test] @@ -74,3 +77,116 @@ async fn variable_used_in_multiple_type_contexts() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn aggregates_request_with_variable_sets() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .collection("movies") + .variables([[("year", json!(2014))]]) + .query( + query() + .predicate(binop("_eq", target!("year"), variable!(year))) + .aggregates([ + ( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg").into(), + ), + column_count_aggregate!("rated_count" => "rated", distinct: true), + star_count_aggregate!("count"), + ]) + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn aggregates_request_with_variable_sets_over_empty_collection_subset() -> anyhow::Result<()> +{ + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .collection("movies") + .variables([[("year", json!(2014))]]) + .query( + query() + .predicate(and([ + binop("_eq", target!("year"), variable!(year)), + binop("_eq", target!("title"), value!("non-existent title")), + ])) + .aggregates([ + ( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg").into(), + ), + column_count_aggregate!("rated_count" => "rated", distinct: true), + star_count_aggregate!("count"), + ]) + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn provides_groups_for_variable_set() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .collection("movies") + .variables([[("year", json!(2014))]]) + .query( + query() + .predicate(binop("_eq", target!("year"), variable!(year))) + .groups( + grouping() + .dimensions([dimension_column("rated")]) + .aggregates([( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg"), + ),]) + .order_by(ordered_dimensions()), + ), + ), + ) + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn provides_fields_combined_with_groups_for_variable_set() -> anyhow::Result<()> { + assert_yaml_snapshot!( + run_connector_query( + Connector::SampleMflix, + query_request() + .collection("movies") + .variables([[("year", json!(2014))]]) + .query( + query() + .predicate(binop("_eq", target!("year"), variable!(year))) + .fields([field!("title"), field!("rated")]) + .order_by([asc!("_id")]) + .groups( + grouping() + .dimensions([dimension_column("rated")]) + .aggregates([( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg"), + ),]) + .order_by(ordered_dimensions()), + ) + .limit(3), + ), + ) + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_extended_json_representing_mixture_of_numeric_types.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_extended_json_representing_mixture_of_numeric_types.snap index c4a039c5..bcaa082a 100644 --- a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_extended_json_representing_mixture_of_numeric_types.snap +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_extended_json_representing_mixture_of_numeric_types.snap @@ -6,14 +6,14 @@ data: extendedJsonTestDataAggregate: value: avg: - $numberDecimal: "4.5" + $numberDouble: "4.5" _count: 8 max: $numberLong: "8" min: $numberDecimal: "1" sum: - $numberDecimal: "36" + $numberDouble: "36.0" _count_distinct: 8 extendedJsonTestData: - type: decimal diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_empty_result_set.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_empty_result_set.snap index 61d3c939..f436ce34 100644 --- a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_empty_result_set.snap +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_empty_result_set.snap @@ -1,10 +1,10 @@ --- source: crates/integration-tests/src/tests/aggregation.rs -expression: "graphql_query(r#\"\n query {\n moviesAggregate(filter_input: {where: {title: {_eq: \"no such movie\"}}}) {\n _count\n title {\n count\n }\n }\n }\n \"#).run().await?" +expression: "graphql_query(r#\"\n query {\n moviesAggregate(filter_input: {where: {title: {_eq: \"no such movie\"}}}) {\n _count\n title {\n _count\n }\n }\n }\n \"#).run().await?" --- data: moviesAggregate: _count: 0 title: - count: 0 + _count: 0 errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_nested_fields_in_empty_result_set.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_nested_fields_in_empty_result_set.snap index c621c020..f7d33a3c 100644 --- a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_nested_fields_in_empty_result_set.snap +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_nested_fields_in_empty_result_set.snap @@ -1,11 +1,10 @@ --- source: crates/integration-tests/src/tests/aggregation.rs -expression: "graphql_query(r#\"\n query {\n moviesAggregate(filter_input: {where: {title: {_eq: \"no such movie\"}}}) {\n awards {\n nominations {\n count\n _count\n }\n }\n }\n }\n \"#).run().await?" +expression: "graphql_query(r#\"\n query {\n moviesAggregate(filter_input: {where: {title: {_eq: \"no such movie\"}}}) {\n awards {\n nominations {\n _count\n }\n }\n }\n }\n \"#).run().await?" --- data: moviesAggregate: awards: nominations: - count: 0 _count: 0 errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__runs_aggregation_over_top_level_fields.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__runs_aggregation_over_top_level_fields.snap index b3a603b1..3fb73855 100644 --- a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__runs_aggregation_over_top_level_fields.snap +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__runs_aggregation_over_top_level_fields.snap @@ -26,7 +26,7 @@ data: avg: 333925.875 max: 436453 min: 221701 - sum: 2671407 + sum: "2671407" unitPrice: _count: 8 _count_distinct: 1 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_contains.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_contains.snap new file mode 100644 index 00000000..43711a77 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_contains.snap @@ -0,0 +1,11 @@ +--- +source: crates/integration-tests/src/tests/filtering.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(array_contains(target!(\"cast\"),\nvalue!(\"Albert Austin\"))).fields([field!(\"title\"), field!(\"cast\")]),)).await?" +--- +- rows: + - cast: + - Charles Chaplin + - Edna Purviance + - Eric Campbell + - Albert Austin + title: The Immigrant diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_is_empty.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_is_empty.snap new file mode 100644 index 00000000..5285af75 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_array_comparison_is_empty.snap @@ -0,0 +1,6 @@ +--- +source: crates/integration-tests/src/tests/filtering.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(is_empty(target!(\"writers\"))).fields([field!(\"writers\")]).limit(1),)).await?" +--- +- rows: + - writers: [] diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparison_with_a_variable.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparison_with_a_variable.snap new file mode 100644 index 00000000..d2b39ddc --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparison_with_a_variable.snap @@ -0,0 +1,6 @@ +--- +source: crates/integration-tests/src/tests/filtering.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().variables([[(\"title\",\n\"The Blue Bird\")]]).collection(\"movies\").query(query().predicate(binop(\"_eq\",\ntarget!(\"title\"), variable!(title))).fields([field!(\"title\")]),)).await?" +--- +- rows: + - title: The Blue Bird diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_of_scalars_against_variable.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_of_scalars_against_variable.snap deleted file mode 100644 index 46425908..00000000 --- a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_comparisons_on_elements_of_array_of_scalars_against_variable.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/integration-tests/src/tests/filtering.rs -expression: "run_connector_query(Connector::SampleMflix,\n query_request().variables([[(\"cast_member\",\n \"Albert Austin\")]]).collection(\"movies\").query(query().predicate(binop(\"_eq\",\n target!(\"cast\"),\n variable!(cast_member))).fields([field!(\"title\"),\n field!(\"cast\")]))).await?" ---- -- rows: - - cast: - - Charles Chaplin - - Edna Purviance - - Eric Campbell - - Albert Austin - title: The Immigrant diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_aggregates_and_groups_in_one_query.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_aggregates_and_groups_in_one_query.snap new file mode 100644 index 00000000..efff0c4f --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_aggregates_and_groups_in_one_query.snap @@ -0,0 +1,27 @@ +--- +source: crates/integration-tests/src/tests/grouping.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(binop(\"_gte\",\ntarget!(\"year\"),\nvalue!(2000))).limit(10).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"))]).groups(grouping().dimensions([dimension_column(\"year\"),]).aggregates([(\"average_viewer_rating_by_year\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"),)]).order_by(ordered_dimensions()),),),).await?" +--- +- aggregates: + average_viewer_rating: 3.05 + groups: + - dimensions: + - 2000 + aggregates: + average_viewer_rating_by_year: 3.825 + - dimensions: + - 2001 + aggregates: + average_viewer_rating_by_year: 2.55 + - dimensions: + - 2002 + aggregates: + average_viewer_rating_by_year: 1.8 + - dimensions: + - 2003 + aggregates: + average_viewer_rating_by_year: 3 + - dimensions: + - 2005 + aggregates: + average_viewer_rating_by_year: 3.5 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_fields_and_groups_in_one_query.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_fields_and_groups_in_one_query.snap new file mode 100644 index 00000000..236aadae --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__combines_fields_and_groups_in_one_query.snap @@ -0,0 +1,24 @@ +--- +source: crates/integration-tests/src/tests/grouping.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(or([binop(\"_gt\",\ntarget!(\"year\"), value!(0)),\nbinop(\"_lte\", target!(\"year\"),\nvalue!(0)),])).fields([field!(\"title\"),\nfield!(\"year\")]).order_by([asc!(\"_id\")]).groups(grouping().dimensions([dimension_column(\"year\")]).aggregates([(\"average_viewer_rating_by_year\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"),)]).order_by(ordered_dimensions()),).limit(3),),).await?" +--- +- rows: + - title: Blacksmith Scene + year: 1893 + - title: The Great Train Robbery + year: 1903 + - title: The Land Beyond the Sunset + year: 1912 + groups: + - dimensions: + - 1893 + aggregates: + average_viewer_rating_by_year: 3 + - dimensions: + - 1903 + aggregates: + average_viewer_rating_by_year: 3.7 + - dimensions: + - 1912 + aggregates: + average_viewer_rating_by_year: 3.7 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__counts_column_values_in_groups.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__counts_column_values_in_groups.snap new file mode 100644 index 00000000..d8542d2b --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__counts_column_values_in_groups.snap @@ -0,0 +1,35 @@ +--- +source: crates/integration-tests/src/tests/grouping.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(and([binop(\"_gt\",\ntarget!(\"year\"), value!(1920)),\nbinop(\"_lte\", target!(\"year\"),\nvalue!(1923)),])).groups(grouping().dimensions([dimension_column(\"rated\")]).aggregates([column_count_aggregate!(\"year_distinct_count\"\n=> \"year\", distinct: true),\ncolumn_count_aggregate!(\"year_count\" => \"year\", distinct: false),\nstar_count_aggregate!(\"count\"),]).order_by(ordered_dimensions()),),),).await?" +--- +- groups: + - dimensions: + - ~ + aggregates: + year_distinct_count: 3 + year_count: 6 + count: 6 + - dimensions: + - NOT RATED + aggregates: + year_distinct_count: 3 + year_count: 4 + count: 4 + - dimensions: + - PASSED + aggregates: + year_distinct_count: 1 + year_count: 3 + count: 3 + - dimensions: + - TV-PG + aggregates: + year_distinct_count: 1 + year_count: 1 + count: 1 + - dimensions: + - UNRATED + aggregates: + year_distinct_count: 2 + year_count: 5 + count: 5 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__groups_by_multiple_dimensions.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__groups_by_multiple_dimensions.snap new file mode 100644 index 00000000..f2f0d486 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__groups_by_multiple_dimensions.snap @@ -0,0 +1,53 @@ +--- +source: crates/integration-tests/src/tests/grouping.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(binop(\"_lt\",\ntarget!(\"year\"),\nvalue!(1950))).order_by([asc!(\"_id\")]).limit(10).groups(grouping().dimensions([dimension_column(\"year\"),\ndimension_column(\"languages\"),\ndimension_column(\"rated\"),]).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"),)]).order_by(ordered_dimensions()),),),).await?" +--- +- groups: + - dimensions: + - 1893 + - ~ + - UNRATED + aggregates: + average_viewer_rating: 3 + - dimensions: + - 1903 + - - English + - TV-G + aggregates: + average_viewer_rating: 3.7 + - dimensions: + - 1909 + - - English + - G + aggregates: + average_viewer_rating: 3.6 + - dimensions: + - 1911 + - - English + - ~ + aggregates: + average_viewer_rating: 3.4 + - dimensions: + - 1912 + - - English + - UNRATED + aggregates: + average_viewer_rating: 3.7 + - dimensions: + - 1913 + - - English + - TV-PG + aggregates: + average_viewer_rating: 3 + - dimensions: + - 1914 + - - English + - ~ + aggregates: + average_viewer_rating: 3.0666666666666664 + - dimensions: + - 1915 + - ~ + - NOT RATED + aggregates: + average_viewer_rating: 3.2 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__runs_single_column_aggregate_on_groups.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__runs_single_column_aggregate_on_groups.snap new file mode 100644 index 00000000..4b3177a1 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__grouping__runs_single_column_aggregate_on_groups.snap @@ -0,0 +1,45 @@ +--- +source: crates/integration-tests/src/tests/grouping.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").query(query().predicate(or([binop(\"_gt\",\ntarget!(\"year\"), value!(0)),\nbinop(\"_lte\", target!(\"year\"),\nvalue!(0)),])).order_by([asc!(\"_id\")]).limit(10).groups(grouping().dimensions([dimension_column(\"year\")]).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\", \"avg\"),),\n(\"max_runtime\",\ncolumn_aggregate(\"runtime\",\n\"max\")),]).order_by(ordered_dimensions()),),),).await?" +--- +- groups: + - dimensions: + - 1893 + aggregates: + average_viewer_rating: 3 + max_runtime: 1 + - dimensions: + - 1903 + aggregates: + average_viewer_rating: 3.7 + max_runtime: 11 + - dimensions: + - 1909 + aggregates: + average_viewer_rating: 3.6 + max_runtime: 14 + - dimensions: + - 1911 + aggregates: + average_viewer_rating: 3.4 + max_runtime: 7 + - dimensions: + - 1912 + aggregates: + average_viewer_rating: 3.7 + max_runtime: 14 + - dimensions: + - 1913 + aggregates: + average_viewer_rating: 3 + max_runtime: 88 + - dimensions: + - 1914 + aggregates: + average_viewer_rating: 3.0666666666666664 + max_runtime: 199 + - dimensions: + - 1915 + aggregates: + average_viewer_rating: 3.2 + max_runtime: 165 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_empty_subset_of_related_collection.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_empty_subset_of_related_collection.snap new file mode 100644 index 00000000..398d5674 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_empty_subset_of_related_collection.snap @@ -0,0 +1,20 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::Chinook,\nquery_request().collection(\"Album\").query(query().predicate(is_in(target!(\"AlbumId\"),\n[json!(15), json!(91),\njson!(227)])).fields([relation_field!(\"tracks\" => \"tracks\",\nquery().predicate(binop(\"_eq\", target!(\"Name\"),\nvalue!(\"non-existent name\"))).aggregates([star_count_aggregate!(\"count\"),\ncolumn_count_aggregate!(\"composer_count\" => \"Composer\", distinct: true),\n(\"average_price\",\ncolumn_aggregate(\"UnitPrice\",\n\"avg\").into()),]))]).order_by([asc!(\"_id\")])).relationships([(\"tracks\",\nrelationship(\"Track\", [(\"AlbumId\", &[\"AlbumId\"])]))])).await?" +--- +- rows: + - tracks: + aggregates: + average_price: ~ + composer_count: 0 + count: 0 + - tracks: + aggregates: + average_price: ~ + composer_count: 0 + count: 0 + - tracks: + aggregates: + average_price: ~ + composer_count: 0 + count: 0 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_related_collection.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_related_collection.snap new file mode 100644 index 00000000..03f0e861 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__aggregates_over_related_collection.snap @@ -0,0 +1,17 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::Chinook,\nquery_request().collection(\"Album\").query(query().predicate(is_in(target!(\"AlbumId\"),\n[json!(15), json!(91),\njson!(227)])).fields([relation_field!(\"tracks\" => \"tracks\",\nquery().aggregates([star_count_aggregate!(\"count\"),\n(\"average_price\",\ncolumn_aggregate(\"UnitPrice\",\n\"avg\").into()),]))]).order_by([asc!(\"_id\")])).relationships([(\"tracks\",\nrelationship(\"Track\", [(\"AlbumId\", &[\"AlbumId\"])]))])).await?" +--- +- rows: + - tracks: + aggregates: + average_price: 0.99 + count: 5 + - tracks: + aggregates: + average_price: 0.99 + count: 16 + - tracks: + aggregates: + average_price: 1.99 + count: 19 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_fields_and_groups_through_relationship.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_fields_and_groups_through_relationship.snap new file mode 100644 index 00000000..f3aaa8ea --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_fields_and_groups_through_relationship.snap @@ -0,0 +1,152 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::Chinook,\nquery_request().collection(\"Album\").query(query().predicate(is_in(target!(\"AlbumId\"),\n[json!(15), json!(91),\njson!(227)])).order_by([asc!(\"_id\")]).fields([field!(\"AlbumId\"),\nrelation_field!(\"tracks\" => \"album_tracks\",\nquery().order_by([asc!(\"_id\")]).fields([field!(\"AlbumId\"), field!(\"Name\"),\nfield!(\"UnitPrice\")]).groups(grouping().dimensions([dimension_column(column(\"Name\").from_relationship(\"track_genre\"))]).aggregates([(\"average_price\",\ncolumn_aggregate(\"UnitPrice\",\n\"avg\"))]).order_by(ordered_dimensions()),))])).relationships([(\"album_tracks\",\nrelationship(\"Track\", [(\"AlbumId\", &[\"AlbumId\"])])),\n(\"track_genre\",\nrelationship(\"Genre\", [(\"GenreId\", &[\"GenreId\"])]).object_type())])).await?" +--- +- rows: + - AlbumId: 15 + tracks: + groups: + - average_price: 0.99 + dimensions: + - - Metal + rows: + - AlbumId: 15 + Name: Heart Of Gold + UnitPrice: "0.99" + - AlbumId: 15 + Name: Snowblind + UnitPrice: "0.99" + - AlbumId: 15 + Name: Like A Bird + UnitPrice: "0.99" + - AlbumId: 15 + Name: Blood In The Wall + UnitPrice: "0.99" + - AlbumId: 15 + Name: The Beginning...At Last + UnitPrice: "0.99" + - AlbumId: 91 + tracks: + groups: + - average_price: 0.99 + dimensions: + - - Rock + rows: + - AlbumId: 91 + Name: Right Next Door to Hell + UnitPrice: "0.99" + - AlbumId: 91 + Name: "Dust N' Bones" + UnitPrice: "0.99" + - AlbumId: 91 + Name: Live and Let Die + UnitPrice: "0.99" + - AlbumId: 91 + Name: "Don't Cry (Original)" + UnitPrice: "0.99" + - AlbumId: 91 + Name: Perfect Crime + UnitPrice: "0.99" + - AlbumId: 91 + Name: "You Ain't the First" + UnitPrice: "0.99" + - AlbumId: 91 + Name: Bad Obsession + UnitPrice: "0.99" + - AlbumId: 91 + Name: Back off Bitch + UnitPrice: "0.99" + - AlbumId: 91 + Name: "Double Talkin' Jive" + UnitPrice: "0.99" + - AlbumId: 91 + Name: November Rain + UnitPrice: "0.99" + - AlbumId: 91 + Name: The Garden + UnitPrice: "0.99" + - AlbumId: 91 + Name: Garden of Eden + UnitPrice: "0.99" + - AlbumId: 91 + Name: "Don't Damn Me" + UnitPrice: "0.99" + - AlbumId: 91 + Name: Bad Apples + UnitPrice: "0.99" + - AlbumId: 91 + Name: Dead Horse + UnitPrice: "0.99" + - AlbumId: 91 + Name: Coma + UnitPrice: "0.99" + - AlbumId: 227 + tracks: + groups: + - average_price: 1.99 + dimensions: + - - Sci Fi & Fantasy + - average_price: 1.99 + dimensions: + - - Science Fiction + - average_price: 1.99 + dimensions: + - - TV Shows + rows: + - AlbumId: 227 + Name: Occupation / Precipice + UnitPrice: "1.99" + - AlbumId: 227 + Name: "Exodus, Pt. 1" + UnitPrice: "1.99" + - AlbumId: 227 + Name: "Exodus, Pt. 2" + UnitPrice: "1.99" + - AlbumId: 227 + Name: Collaborators + UnitPrice: "1.99" + - AlbumId: 227 + Name: Torn + UnitPrice: "1.99" + - AlbumId: 227 + Name: A Measure of Salvation + UnitPrice: "1.99" + - AlbumId: 227 + Name: Hero + UnitPrice: "1.99" + - AlbumId: 227 + Name: Unfinished Business + UnitPrice: "1.99" + - AlbumId: 227 + Name: The Passage + UnitPrice: "1.99" + - AlbumId: 227 + Name: The Eye of Jupiter + UnitPrice: "1.99" + - AlbumId: 227 + Name: Rapture + UnitPrice: "1.99" + - AlbumId: 227 + Name: Taking a Break from All Your Worries + UnitPrice: "1.99" + - AlbumId: 227 + Name: The Woman King + UnitPrice: "1.99" + - AlbumId: 227 + Name: A Day In the Life + UnitPrice: "1.99" + - AlbumId: 227 + Name: Dirty Hands + UnitPrice: "1.99" + - AlbumId: 227 + Name: Maelstrom + UnitPrice: "1.99" + - AlbumId: 227 + Name: The Son Also Rises + UnitPrice: "1.99" + - AlbumId: 227 + Name: "Crossroads, Pt. 1" + UnitPrice: "1.99" + - AlbumId: 227 + Name: "Crossroads, Pt. 2" + UnitPrice: "1.99" diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_groups_through_relationship.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_groups_through_relationship.snap new file mode 100644 index 00000000..9d6719e1 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__gets_groups_through_relationship.snap @@ -0,0 +1,34 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::Chinook,\nquery_request().collection(\"Album\").query(query().predicate(is_in(target!(\"AlbumId\"),\n[json!(15), json!(91),\njson!(227)])).order_by([asc!(\"_id\")]).fields([field!(\"AlbumId\"),\nrelation_field!(\"tracks\" => \"album_tracks\",\nquery().groups(grouping().dimensions([dimension_column(column(\"Name\").from_relationship(\"track_genre\"))]).aggregates([(\"AlbumId\",\ncolumn_aggregate(\"AlbumId\", \"avg\")),\n(\"average_price\",\ncolumn_aggregate(\"UnitPrice\",\n\"avg\")),]).order_by(ordered_dimensions()),))])).relationships([(\"album_tracks\",\nrelationship(\"Track\", [(\"AlbumId\", &[\"AlbumId\"])])),\n(\"track_genre\",\nrelationship(\"Genre\", [(\"GenreId\", &[\"GenreId\"])]).object_type())])).await?" +--- +- rows: + - AlbumId: 15 + tracks: + groups: + - AlbumId: 15 + average_price: 0.99 + dimensions: + - - Metal + - AlbumId: 91 + tracks: + groups: + - AlbumId: 91 + average_price: 0.99 + dimensions: + - - Rock + - AlbumId: 227 + tracks: + groups: + - AlbumId: 227 + average_price: 1.99 + dimensions: + - - Sci Fi & Fantasy + - AlbumId: 227 + average_price: 1.99 + dimensions: + - - Science Fiction + - AlbumId: 227 + average_price: 1.99 + dimensions: + - - TV Shows diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__groups_by_related_field.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__groups_by_related_field.snap new file mode 100644 index 00000000..5e960c98 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__groups_by_related_field.snap @@ -0,0 +1,25 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::Chinook,\nquery_request().collection(\"Track\").query(query().predicate(is_in(target!(\"AlbumId\"),\n[json!(15), json!(91),\njson!(227)])).groups(grouping().dimensions([dimension_column(column(\"Name\").from_relationship(\"track_genre\"))]).aggregates([(\"average_price\",\ncolumn_aggregate(\"UnitPrice\",\n\"avg\"))]).order_by(ordered_dimensions()))).relationships([(\"track_genre\",\nrelationship(\"Genre\", [(\"GenreId\", &[\"GenreId\"])]).object_type())])).await?" +--- +- groups: + - dimensions: + - - Metal + aggregates: + average_price: 0.99 + - dimensions: + - - Rock + aggregates: + average_price: 0.99 + - dimensions: + - - Sci Fi & Fantasy + aggregates: + average_price: 1.99 + - dimensions: + - - Science Fiction + aggregates: + average_price: 1.99 + - dimensions: + - - TV Shows + aggregates: + average_price: 1.99 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__joins_relationships_on_nested_key.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__joins_relationships_on_nested_key.snap new file mode 100644 index 00000000..2200e9e1 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__local_relationship__joins_relationships_on_nested_key.snap @@ -0,0 +1,8 @@ +--- +source: crates/integration-tests/src/tests/local_relationship.rs +expression: "run_connector_query(Connector::TestCases,\nquery_request().collection(\"departments\").query(query().predicate(exists(related!(\"schools_departments\"),\nbinop(\"_eq\", target!(\"name\"),\nvalue!(\"West Valley\")))).fields([relation_field!(\"departments\" =>\n\"schools_departments\",\nquery().fields([field!(\"name\")]))]).order_by([asc!(\"_id\")])).relationships([(\"schools_departments\",\nrelationship(\"schools\",\n[(\"_id\", &[\"departments\", \"math_department_id\"])]))])).await?" +--- +- rows: + - departments: + rows: + - name: West Valley diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__nested_collection__exists_in_nested_collection.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__nested_collection__exists_in_nested_collection.snap new file mode 100644 index 00000000..5283509a --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__nested_collection__exists_in_nested_collection.snap @@ -0,0 +1,10 @@ +--- +source: crates/integration-tests/src/tests/nested_collection.rs +expression: "run_connector_query(Connector::TestCases,\nquery_request().collection(\"nested_collection\").query(query().predicate(exists(nested(\"staff\"),\nbinop(\"_eq\", target!(\"name\"),\nvalue!(\"Alyx\")))).fields([field!(\"institution\"),\nfield!(\"staff\" => \"staff\",\narray!(object!([field!(\"name\")]))),]).order_by([asc!(\"_id\")]))).await?" +--- +- rows: + - institution: City 17 + staff: + - name: Alyx + - name: Freeman + - name: Breen diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets.snap new file mode 100644 index 00000000..8e61071d --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets.snap @@ -0,0 +1,8 @@ +--- +source: crates/integration-tests/src/tests/remote_relationship.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").variables([[(\"year\",\njson!(2014))]]).query(query().predicate(binop(\"_eq\", target!(\"year\"),\nvariable!(year))).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\", \"avg\").into(),),\ncolumn_count_aggregate!(\"rated_count\" => \"rated\", distinct: true),\nstar_count_aggregate!(\"count\"),])),).await?" +--- +- aggregates: + average_viewer_rating: 3.2435114503816793 + rated_count: 10 + count: 1147 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets_over_empty_collection_subset.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets_over_empty_collection_subset.snap new file mode 100644 index 00000000..d86d4497 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__aggregates_request_with_variable_sets_over_empty_collection_subset.snap @@ -0,0 +1,8 @@ +--- +source: crates/integration-tests/src/tests/remote_relationship.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").variables([[(\"year\",\njson!(2014))]]).query(query().predicate(and([binop(\"_eq\", target!(\"year\"),\nvariable!(year)),\nbinop(\"_eq\", target!(\"title\"),\nvalue!(\"non-existent title\")),])).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\", \"avg\").into(),),\ncolumn_count_aggregate!(\"rated_count\" => \"rated\", distinct: true),\nstar_count_aggregate!(\"count\"),])),).await?" +--- +- aggregates: + average_viewer_rating: ~ + rated_count: 0 + count: 0 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_fields_combined_with_groups_for_variable_set.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_fields_combined_with_groups_for_variable_set.snap new file mode 100644 index 00000000..37d2867c --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_fields_combined_with_groups_for_variable_set.snap @@ -0,0 +1,24 @@ +--- +source: crates/integration-tests/src/tests/remote_relationship.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").variables([[(\"year\",\njson!(2014))]]).query(query().predicate(binop(\"_eq\", target!(\"year\"),\nvariable!(year))).fields([field!(\"title\"),\nfield!(\"rated\")]).order_by([asc!(\"_id\")]).groups(grouping().dimensions([dimension_column(\"rated\")]).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"),),]).order_by(ordered_dimensions()),).limit(3),),).await?" +--- +- rows: + - rated: ~ + title: Action Jackson + - rated: PG-13 + title: The Giver + - rated: R + title: The Equalizer + groups: + - dimensions: + - ~ + aggregates: + average_viewer_rating: 2.3 + - dimensions: + - PG-13 + aggregates: + average_viewer_rating: 3.4 + - dimensions: + - R + aggregates: + average_viewer_rating: 3.9 diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_groups_for_variable_set.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_groups_for_variable_set.snap new file mode 100644 index 00000000..fad8a471 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__remote_relationship__provides_groups_for_variable_set.snap @@ -0,0 +1,49 @@ +--- +source: crates/integration-tests/src/tests/remote_relationship.rs +expression: "run_connector_query(Connector::SampleMflix,\nquery_request().collection(\"movies\").variables([[(\"year\",\njson!(2014))]]).query(query().predicate(binop(\"_eq\", target!(\"year\"),\nvariable!(year))).groups(grouping().dimensions([dimension_column(\"rated\")]).aggregates([(\"average_viewer_rating\",\ncolumn_aggregate(\"tomatoes.viewer.rating\",\n\"avg\"),),]).order_by(ordered_dimensions()),),),).await?" +--- +- groups: + - dimensions: + - ~ + aggregates: + average_viewer_rating: 3.1320754716981134 + - dimensions: + - G + aggregates: + average_viewer_rating: 3.8 + - dimensions: + - NOT RATED + aggregates: + average_viewer_rating: 2.824242424242424 + - dimensions: + - PG + aggregates: + average_viewer_rating: 3.7096774193548385 + - dimensions: + - PG-13 + aggregates: + average_viewer_rating: 3.470707070707071 + - dimensions: + - R + aggregates: + average_viewer_rating: 3.3283783783783787 + - dimensions: + - TV-14 + aggregates: + average_viewer_rating: 3.233333333333333 + - dimensions: + - TV-G + aggregates: + average_viewer_rating: ~ + - dimensions: + - TV-MA + aggregates: + average_viewer_rating: 4.2 + - dimensions: + - TV-PG + aggregates: + average_viewer_rating: ~ + - dimensions: + - UNRATED + aggregates: + average_viewer_rating: 3.06875 diff --git a/crates/mongodb-agent-common/Cargo.toml b/crates/mongodb-agent-common/Cargo.toml index 52511d7e..639d00ef 100644 --- a/crates/mongodb-agent-common/Cargo.toml +++ b/crates/mongodb-agent-common/Cargo.toml @@ -28,6 +28,7 @@ lazy_static = "^1.4.0" mockall = { version = "^0.13.1", optional = true } mongodb = { workspace = true } ndc-models = { workspace = true } +nonempty = { workspace = true } once_cell = "1" pretty_assertions = { version = "1", optional = true } regex = "1" diff --git a/crates/mongodb-agent-common/src/aggregation_function.rs b/crates/mongodb-agent-common/src/aggregation_function.rs index 54cb0c0f..9c637dd6 100644 --- a/crates/mongodb-agent-common/src/aggregation_function.rs +++ b/crates/mongodb-agent-common/src/aggregation_function.rs @@ -1,23 +1,24 @@ +use configuration::MongoScalarType; use enum_iterator::{all, Sequence}; -// TODO: How can we unify this with the Accumulator type in the mongodb module? -#[derive(Copy, Clone, Debug, PartialEq, Eq, Sequence)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Sequence)] pub enum AggregationFunction { Avg, - Count, Min, Max, Sum, } +use mongodb_support::BsonScalarType; use ndc_query_plan::QueryPlanError; use AggregationFunction as A; +use crate::mongo_query_plan::Type; + impl AggregationFunction { pub fn graphql_name(self) -> &'static str { match self { A::Avg => "avg", - A::Count => "count", A::Min => "min", A::Max => "max", A::Sum => "sum", @@ -32,13 +33,28 @@ impl AggregationFunction { }) } - pub fn is_count(self) -> bool { + /// Returns the result type that is declared for this function in the schema. + pub fn expected_result_type(self, argument_type: &Type) -> Option { match self { - A::Avg => false, - A::Count => true, - A::Min => false, - A::Max => false, - A::Sum => false, + A::Avg => Some(BsonScalarType::Double), + A::Min => None, + A::Max => None, + A::Sum => Some(if is_fractional(argument_type) { + BsonScalarType::Double + } else { + BsonScalarType::Long + }), } } } + +fn is_fractional(t: &Type) -> bool { + match t { + Type::Scalar(MongoScalarType::Bson(s)) => s.is_fractional(), + Type::Scalar(MongoScalarType::ExtendedJSON) => true, + Type::Object(_) => false, + Type::ArrayOf(_) => false, + Type::Tuple(ts) => ts.iter().all(is_fractional), + Type::Nullable(t) => is_fractional(t), + } +} diff --git a/crates/mongodb-agent-common/src/comparison_function.rs b/crates/mongodb-agent-common/src/comparison_function.rs index 842df44e..f6357687 100644 --- a/crates/mongodb-agent-common/src/comparison_function.rs +++ b/crates/mongodb-agent-common/src/comparison_function.rs @@ -1,14 +1,12 @@ use enum_iterator::{all, Sequence}; use mongodb::bson::{doc, Bson, Document}; +use ndc_models as ndc; /// Supported binary comparison operators. This type provides GraphQL names, MongoDB operator /// names, and aggregation pipeline code for each operator. Argument types are defined in /// mongodb-agent-common/src/scalar_types_capabilities.rs. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Sequence)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Sequence)] pub enum ComparisonFunction { - // Equality and inequality operators (except for `NotEqual`) are built into the v2 spec, but - // the only built-in operator in v3 is `Equal`. So we need at minimum definitions for - // inequality operators here. LessThan, LessThanOrEqual, GreaterThan, @@ -58,6 +56,33 @@ impl ComparisonFunction { } } + pub fn ndc_definition( + self, + argument_type: impl FnOnce(Self) -> ndc::Type, + ) -> ndc::ComparisonOperatorDefinition { + use ndc::ComparisonOperatorDefinition as NDC; + match self { + C::Equal => NDC::Equal, + C::In => NDC::In, + C::LessThan => NDC::LessThan, + C::LessThanOrEqual => NDC::LessThanOrEqual, + C::GreaterThan => NDC::GreaterThan, + C::GreaterThanOrEqual => NDC::GreaterThanOrEqual, + C::NotEqual => NDC::Custom { + argument_type: argument_type(self), + }, + C::NotIn => NDC::Custom { + argument_type: argument_type(self), + }, + C::Regex => NDC::Custom { + argument_type: argument_type(self), + }, + C::IRegex => NDC::Custom { + argument_type: argument_type(self), + }, + } + } + pub fn from_graphql_name(s: &str) -> Result { all::() .find(|variant| variant.graphql_name() == s) diff --git a/crates/mongodb-agent-common/src/constants.rs b/crates/mongodb-agent-common/src/constants.rs new file mode 100644 index 00000000..91745adb --- /dev/null +++ b/crates/mongodb-agent-common/src/constants.rs @@ -0,0 +1,24 @@ +use mongodb::bson; +use serde::Deserialize; + +/// Value must match the field name in [BsonRowSet] +pub const ROW_SET_AGGREGATES_KEY: &str = "aggregates"; + +/// Value must match the field name in [BsonRowSet] +pub const ROW_SET_GROUPS_KEY: &str = "groups"; + +/// Value must match the field name in [BsonRowSet] +pub const ROW_SET_ROWS_KEY: &str = "rows"; + +#[derive(Debug, Deserialize)] +pub struct BsonRowSet { + #[serde(default)] + pub aggregates: Option, // name matches ROW_SET_AGGREGATES_KEY + #[serde(default)] + pub groups: Vec, // name matches ROW_SET_GROUPS_KEY + #[serde(default)] + pub rows: Vec, // name matches ROW_SET_ROWS_KEY +} + +/// Value must match the field name in [ndc_models::Group] +pub const GROUP_DIMENSIONS_KEY: &str = "dimensions"; diff --git a/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs b/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs index fe285960..ede7be2c 100644 --- a/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs +++ b/crates/mongodb-agent-common/src/interface_types/mongo_agent_error.rs @@ -8,7 +8,7 @@ use mongodb::bson; use ndc_query_plan::QueryPlanError; use thiserror::Error; -use crate::{procedure::ProcedureError, query::QueryResponseError}; +use crate::{mongo_query_plan::Dimension, procedure::ProcedureError, query::QueryResponseError}; /// A superset of the DC-API `AgentError` type. This enum adds error cases specific to the MongoDB /// agent. @@ -16,6 +16,7 @@ use crate::{procedure::ProcedureError, query::QueryResponseError}; pub enum MongoAgentError { BadCollectionSchema(Box<(String, bson::Bson, bson::de::Error)>), // boxed to avoid an excessively-large stack value BadQuery(anyhow::Error), + InvalidGroupDimension(Dimension), InvalidVariableName(String), InvalidScalarTypeName(String), MongoDB(#[from] mongodb::error::Error), @@ -66,6 +67,9 @@ impl MongoAgentError { ) }, BadQuery(err) => (StatusCode::BAD_REQUEST, ErrorResponse::new(&err)), + InvalidGroupDimension(dimension) => ( + StatusCode::BAD_REQUEST, ErrorResponse::new(&format!("Cannot express grouping dimension as a MongoDB query document expression: {dimension:?}")) + ), InvalidVariableName(name) => ( StatusCode::BAD_REQUEST, ErrorResponse::new(&format!("Column identifier includes characters that are not permitted in a MongoDB variable name: {name}")) diff --git a/crates/mongodb-agent-common/src/lib.rs b/crates/mongodb-agent-common/src/lib.rs index ff8e8132..02819e93 100644 --- a/crates/mongodb-agent-common/src/lib.rs +++ b/crates/mongodb-agent-common/src/lib.rs @@ -1,5 +1,6 @@ pub mod aggregation_function; pub mod comparison_function; +mod constants; pub mod explain; pub mod interface_types; pub mod mongo_query_plan; diff --git a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs index f3312356..2ce94cf6 100644 --- a/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs +++ b/crates/mongodb-agent-common/src/mongo_query_plan/mod.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use configuration::{ native_mutation::NativeMutation, native_query::NativeQuery, Configuration, MongoScalarType, }; -use mongodb_support::{ExtendedJsonMode, EXTENDED_JSON_TYPE_NAME}; +use mongodb_support::{BsonScalarType, ExtendedJsonMode, EXTENDED_JSON_TYPE_NAME}; use ndc_models as ndc; use ndc_query_plan::{ConnectorTypes, QueryContext, QueryPlanError}; @@ -32,6 +32,14 @@ impl ConnectorTypes for MongoConfiguration { type AggregateFunction = AggregationFunction; type ComparisonOperator = ComparisonFunction; type ScalarType = MongoScalarType; + + fn count_aggregate_type() -> ndc_query_plan::Type { + ndc_query_plan::Type::scalar(BsonScalarType::Int) + } + + fn string_type() -> ndc_query_plan::Type { + ndc_query_plan::Type::scalar(BsonScalarType::String) + } } impl QueryContext for MongoConfiguration { @@ -102,17 +110,23 @@ fn scalar_type_name(t: &Type) -> Option<&'static str> { pub type Aggregate = ndc_query_plan::Aggregate; pub type Argument = ndc_query_plan::Argument; pub type Arguments = ndc_query_plan::Arguments; +pub type ArrayComparison = ndc_query_plan::ArrayComparison; pub type ComparisonTarget = ndc_query_plan::ComparisonTarget; pub type ComparisonValue = ndc_query_plan::ComparisonValue; pub type ExistsInCollection = ndc_query_plan::ExistsInCollection; pub type Expression = ndc_query_plan::Expression; pub type Field = ndc_query_plan::Field; +pub type Dimension = ndc_query_plan::Dimension; +pub type Grouping = ndc_query_plan::Grouping; +pub type GroupOrderBy = ndc_query_plan::GroupOrderBy; +pub type GroupOrderByTarget = ndc_query_plan::GroupOrderByTarget; pub type MutationOperation = ndc_query_plan::MutationOperation; pub type MutationPlan = ndc_query_plan::MutationPlan; pub type MutationProcedureArgument = ndc_query_plan::MutationProcedureArgument; pub type NestedField = ndc_query_plan::NestedField; pub type NestedArray = ndc_query_plan::NestedArray; pub type NestedObject = ndc_query_plan::NestedObject; +pub type ObjectField = ndc_query_plan::ObjectField; pub type ObjectType = ndc_query_plan::ObjectType; pub type OrderBy = ndc_query_plan::OrderBy; pub type OrderByTarget = ndc_query_plan::OrderByTarget; diff --git a/crates/mongodb-agent-common/src/mongodb/mod.rs b/crates/mongodb-agent-common/src/mongodb/mod.rs index 48f16304..2e489234 100644 --- a/crates/mongodb-agent-common/src/mongodb/mod.rs +++ b/crates/mongodb-agent-common/src/mongodb/mod.rs @@ -1,14 +1,11 @@ mod collection; mod database; pub mod sanitize; -mod selection; #[cfg(any(test, feature = "test-helpers"))] pub mod test_helpers; -pub use self::{ - collection::CollectionTrait, database::DatabaseTrait, selection::selection_from_query_request, -}; +pub use self::{collection::CollectionTrait, database::DatabaseTrait}; // MockCollectionTrait is generated by automock when the test flag is active. #[cfg(any(test, feature = "test-helpers"))] diff --git a/crates/mongodb-agent-common/src/mongodb/sanitize.rs b/crates/mongodb-agent-common/src/mongodb/sanitize.rs index d9ef90d6..fc1cea2a 100644 --- a/crates/mongodb-agent-common/src/mongodb/sanitize.rs +++ b/crates/mongodb-agent-common/src/mongodb/sanitize.rs @@ -1,15 +1,5 @@ use std::borrow::Cow; -use mongodb::bson::{doc, Document}; - -/// Produces a MongoDB expression that references a field by name in a way that is safe from code -/// injection. -/// -/// TODO: equivalent to ColumnRef::Expression -pub fn get_field(name: &str) -> Document { - doc! { "$getField": { "$literal": name } } -} - /// Given a name returns a valid variable name for use in MongoDB aggregation expressions. Outputs /// are guaranteed to be distinct for distinct inputs. Consistently returns the same output for the /// same input string. diff --git a/crates/mongodb-agent-common/src/procedure/interpolated_command.rs b/crates/mongodb-agent-common/src/procedure/interpolated_command.rs index ac6775a3..131cee38 100644 --- a/crates/mongodb-agent-common/src/procedure/interpolated_command.rs +++ b/crates/mongodb-agent-common/src/procedure/interpolated_command.rs @@ -159,7 +159,7 @@ mod tests { use serde_json::json; use crate::{ - mongo_query_plan::{ObjectType, Type}, + mongo_query_plan::{ObjectField, ObjectType, Type}, procedure::arguments_to_mongodb_expressions::arguments_to_mongodb_expressions, }; @@ -170,7 +170,11 @@ mod tests { let native_mutation = NativeMutation { result_type: Type::Object(ObjectType { name: Some("InsertArtist".into()), - fields: [("ok".into(), Type::Scalar(MongoScalarType::Bson(S::Bool)))].into(), + fields: [( + "ok".into(), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::Bool))), + )] + .into(), }), command: doc! { "insert": "Artist", @@ -224,11 +228,11 @@ mod tests { fields: [ ( "ArtistId".into(), - Type::Scalar(MongoScalarType::Bson(S::Int)), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::Int))), ), ( "Name".into(), - Type::Scalar(MongoScalarType::Bson(S::String)), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::String))), ), ] .into(), @@ -237,7 +241,11 @@ mod tests { let native_mutation = NativeMutation { result_type: Type::Object(ObjectType { name: Some("InsertArtist".into()), - fields: [("ok".into(), Type::Scalar(MongoScalarType::Bson(S::Bool)))].into(), + fields: [( + "ok".into(), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::Bool))), + )] + .into(), }), command: doc! { "insert": "Artist", @@ -287,7 +295,11 @@ mod tests { let native_mutation = NativeMutation { result_type: Type::Object(ObjectType { name: Some("Insert".into()), - fields: [("ok".into(), Type::Scalar(MongoScalarType::Bson(S::Bool)))].into(), + fields: [( + "ok".into(), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::Bool))), + )] + .into(), }), command: doc! { "insert": "{{prefix}}-{{basename}}", @@ -334,7 +346,11 @@ mod tests { let native_mutation = NativeMutation { result_type: Type::Object(ObjectType { name: Some("InsertArtist".into()), - fields: [("ok".into(), Type::Scalar(MongoScalarType::Bson(S::Bool)))].into(), + fields: [( + "ok".into(), + ObjectField::new(Type::Scalar(MongoScalarType::Bson(S::Bool))), + )] + .into(), }), command: doc! { "insert": "Artist", diff --git a/crates/mongodb-agent-common/src/query/aggregates.rs b/crates/mongodb-agent-common/src/query/aggregates.rs new file mode 100644 index 00000000..86abf948 --- /dev/null +++ b/crates/mongodb-agent-common/src/query/aggregates.rs @@ -0,0 +1,406 @@ +use std::collections::BTreeMap; + +use indexmap::IndexMap; +use mongodb::bson::{bson, Bson}; +use mongodb_support::aggregate::{Accumulator, Pipeline, Selection, Stage}; +use ndc_models::FieldName; + +use crate::{aggregation_function::AggregationFunction, mongo_query_plan::Aggregate}; + +use super::column_ref::ColumnRef; + +pub fn pipeline_for_aggregates(aggregates: &IndexMap) -> Pipeline { + let group_stage = Stage::Group { + key_expression: Bson::Null, + accumulators: accumulators_for_aggregates(aggregates), + }; + let replace_with_stage = Stage::ReplaceWith(selection_for_aggregates(aggregates)); + Pipeline::new(vec![group_stage, replace_with_stage]) +} + +pub fn accumulators_for_aggregates( + aggregates: &IndexMap, +) -> BTreeMap { + aggregates + .into_iter() + .map(|(name, aggregate)| (name.to_string(), aggregate_to_accumulator(aggregate))) + .collect() +} + +fn aggregate_to_accumulator(aggregate: &Aggregate) -> Accumulator { + use Aggregate as A; + match aggregate { + A::ColumnCount { + column, + field_path, + distinct, + .. + } => { + let field_ref = ColumnRef::from_column_and_field_path(column, field_path.as_ref()) + .into_aggregate_expression() + .into_bson(); + if *distinct { + Accumulator::AddToSet(field_ref) + } else { + Accumulator::Sum(bson!({ + "$cond": { + "if": { "$eq": [field_ref, null] }, // count non-null, non-missing values + "then": 0, + "else": 1, + } + })) + } + } + A::SingleColumn { + column, + field_path, + function, + .. + } => { + use AggregationFunction as A; + + let field_ref = ColumnRef::from_column_and_field_path(column, field_path.as_ref()) + .into_aggregate_expression() + .into_bson(); + + match function { + A::Avg => Accumulator::Avg(field_ref), + A::Min => Accumulator::Min(field_ref), + A::Max => Accumulator::Max(field_ref), + A::Sum => Accumulator::Sum(field_ref), + } + } + A::StarCount => Accumulator::Sum(bson!(1)), + } +} + +fn selection_for_aggregates(aggregates: &IndexMap) -> Selection { + let selected_aggregates = aggregates + .iter() + .map(|(key, aggregate)| selection_for_aggregate(key, aggregate)) + .collect(); + Selection::new(selected_aggregates) +} + +pub fn selection_for_aggregate(key: &FieldName, aggregate: &Aggregate) -> (String, Bson) { + let column_ref = ColumnRef::from_field(key.as_ref()).into_aggregate_expression(); + + // Selecting distinct counts requires some post-processing since the $group stage produces + // an array of unique values. We need to count the non-null values in that array. + let value_expression = match aggregate { + Aggregate::ColumnCount { distinct, .. } if *distinct => bson!({ + "$reduce": { + "input": column_ref, + "initialValue": 0, + "in": { + "$cond": { + "if": { "$eq": ["$$this", null] }, + "then": "$$value", + "else": { "$sum": ["$$value", 1] }, + } + }, + } + }), + _ => column_ref.into(), + }; + + // Fill in null or zero values for missing fields. If we skip this we get errors on missing + // data down the line. + let value_expression = replace_missing_aggregate_value(value_expression, aggregate.is_count()); + + // Convert types to match what the engine expects for each aggregation result + let value_expression = convert_aggregate_result_type(value_expression, aggregate); + + (key.to_string(), value_expression) +} + +pub fn replace_missing_aggregate_value(expression: Bson, is_count: bool) -> Bson { + bson!({ + "$ifNull": [ + expression, + if is_count { bson!(0) } else { bson!(null) } + ] + }) +} + +/// The system expects specific return types for specific aggregates. That means we may need +/// to do a numeric type conversion here. The conversion applies to the aggregated result, +/// not to input values. +fn convert_aggregate_result_type(column_ref: impl Into, aggregate: &Aggregate) -> Bson { + let convert_to = match aggregate { + Aggregate::ColumnCount { .. } => None, + Aggregate::SingleColumn { + column_type, + function, + .. + } => function.expected_result_type(column_type), + Aggregate::StarCount => None, + }; + match convert_to { + // $convert implicitly fills `null` if input value is missing + Some(scalar_type) => bson!({ + "$convert": { + "input": column_ref, + "to": scalar_type.bson_name(), + } + }), + None => column_ref.into(), + } +} + +#[cfg(test)] +mod tests { + use configuration::Configuration; + use mongodb::bson::bson; + use ndc_test_helpers::{ + binop, collection, column_aggregate, column_count_aggregate, dimension_column, field, + group, grouping, named_type, object_type, query, query_request, row_set, target, value, + }; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::{ + mongo_query_plan::MongoConfiguration, + mongodb::test_helpers::mock_collection_aggregate_response_for_pipeline, + query::execute_query_request::execute_query_request, test_helpers::mflix_config, + }; + + #[tokio::test] + async fn executes_aggregation() -> Result<(), anyhow::Error> { + let query_request = query_request() + .collection("students") + .query(query().aggregates([ + column_count_aggregate!("count" => "gpa", distinct: true), + ("avg", column_aggregate("gpa", "avg").into()), + ])) + .into(); + + let expected_response = row_set() + .aggregates([("count", json!(11)), ("avg", json!(3))]) + .into_response(); + + let expected_pipeline = bson!([ + { + "$group": { + "_id": null, + "avg": { "$avg": "$gpa" }, + "count": { "$addToSet": "$gpa" }, + }, + }, + { + "$replaceWith": { + "avg": { + "$convert": { + "to": "double", + "input": { "$ifNull": ["$avg", null] }, + } + }, + "count": { + "$ifNull": [ + { + "$reduce": { + "input": "$count", + "initialValue": 0, + "in": { + "$cond": { + "if": { "$eq": ["$$this", null] }, + "then": "$$value", + "else": { "$sum": ["$$value", 1] } + } + } + } + }, + 0 + ] + }, + }, + }, + ]); + + let db = mock_collection_aggregate_response_for_pipeline( + "students", + expected_pipeline, + bson!([{ + "count": 11, + "avg": 3, + }]), + ); + + let result = execute_query_request(db, &students_config(), query_request).await?; + assert_eq!(result, expected_response); + Ok(()) + } + + #[tokio::test] + async fn executes_aggregation_with_fields() -> Result<(), anyhow::Error> { + let query_request = query_request() + .collection("students") + .query( + query() + .aggregates([("avg", column_aggregate("gpa", "avg"))]) + .fields([field!("student_gpa" => "gpa")]) + .predicate(binop("_lt", target!("gpa"), value!(4.0))), + ) + .into(); + + let expected_response = row_set() + .aggregates([("avg", json!(3.1))]) + .row([("student_gpa", 3.1)]) + .into_response(); + + let expected_pipeline = bson!([ + { "$match": { "gpa": { "$lt": 4.0 } } }, + { + "$facet": { + "__AGGREGATES__": [ + { "$group": { "_id": null, "avg": { "$avg": "$gpa" } } }, + { + "$replaceWith": { + "avg": { + "$convert": { + "to": "double", + "input": { "$ifNull": ["$avg", null] }, + } + }, + }, + }, + ], + "__ROWS__": [{ + "$replaceWith": { + "student_gpa": { "$ifNull": ["$gpa", null] }, + }, + }], + }, + }, + { + "$replaceWith": { + "aggregates": { "$first": "$__AGGREGATES__" }, + "rows": "$__ROWS__", + }, + }, + ]); + + let db = mock_collection_aggregate_response_for_pipeline( + "students", + expected_pipeline, + bson!([{ + "aggregates": { + "avg": 3.1, + }, + "rows": [{ + "student_gpa": 3.1, + }], + }]), + ); + + let result = execute_query_request(db, &students_config(), query_request).await?; + assert_eq!(result, expected_response); + Ok(()) + } + + #[tokio::test] + async fn executes_query_with_groups_with_single_column_aggregates() -> Result<(), anyhow::Error> + { + let query_request = query_request() + .collection("movies") + .query( + query().groups( + grouping() + .dimensions([dimension_column("year")]) + .aggregates([ + ( + "average_viewer_rating", + column_aggregate("tomatoes.viewer.rating", "avg"), + ), + ("max.runtime", column_aggregate("runtime", "max")), + ]), + ), + ) + .into(); + + let expected_response = row_set() + .groups([ + group( + [2007], + [ + ("average_viewer_rating", json!(7.5)), + ("max.runtime", json!(207)), + ], + ), + group( + [2015], + [ + ("average_viewer_rating", json!(6.9)), + ("max.runtime", json!(412)), + ], + ), + ]) + .into_response(); + + let expected_pipeline = bson!([ + { + "$group": { + "_id": ["$year"], + "average_viewer_rating": { "$avg": "$tomatoes.viewer.rating" }, + "max.runtime": { "$max": "$runtime" }, + } + }, + { + "$replaceWith": { + "dimensions": "$_id", + "average_viewer_rating": { + "$convert": { + "to": "double", + "input": { "$ifNull": ["$average_viewer_rating", null] }, + } + }, + "max.runtime": { "$ifNull": [{ "$getField": { "$literal": "max.runtime" } }, null] }, + } + }, + ]); + + let db = mock_collection_aggregate_response_for_pipeline( + "movies", + expected_pipeline, + bson!([ + { + "dimensions": [2007], + "average_viewer_rating": 7.5, + "max.runtime": 207, + }, + { + "dimensions": [2015], + "average_viewer_rating": 6.9, + "max.runtime": 412, + }, + ]), + ); + + let result = execute_query_request(db, &mflix_config(), query_request).await?; + assert_eq!(result, expected_response); + Ok(()) + } + + // TODO: Test: + // - fields & group by + // - group by & aggregates + // - various counts on groups + // - groups and variables + // - groups and relationships + + fn students_config() -> MongoConfiguration { + MongoConfiguration(Configuration { + collections: [collection("students")].into(), + object_types: [( + "students".into(), + object_type([("gpa", named_type("Double"))]), + )] + .into(), + functions: Default::default(), + procedures: Default::default(), + native_mutations: Default::default(), + native_queries: Default::default(), + options: Default::default(), + }) + } +} diff --git a/crates/mongodb-agent-common/src/query/column_ref.rs b/crates/mongodb-agent-common/src/query/column_ref.rs index fc95f652..1522e95f 100644 --- a/crates/mongodb-agent-common/src/query/column_ref.rs +++ b/crates/mongodb-agent-common/src/query/column_ref.rs @@ -5,7 +5,9 @@ use std::{borrow::Cow, iter::once}; use mongodb::bson::{doc, Bson}; +use ndc_models::FieldName; use ndc_query_plan::Scope; +use nonempty::NonEmpty; use crate::{ interface_types::MongoAgentError, @@ -13,6 +15,8 @@ use crate::{ mongodb::sanitize::is_name_safe, }; +use super::make_selector::AggregationExpression; + /// Reference to a document field, or a nested property of a document field. There are two contexts /// where we reference columns: /// @@ -44,8 +48,7 @@ pub enum ColumnRef<'a> { impl<'a> ColumnRef<'a> { /// Given a column target returns a string that can be used in a MongoDB match query that /// references the corresponding field, either in the target collection of a query request, or - /// in the related collection. Resolves nested fields and root collection references, but does - /// not traverse relationships. + /// in the related collection. /// /// If the given target cannot be represented as a match query key, falls back to providing an /// aggregation expression referencing the column. @@ -53,25 +56,38 @@ impl<'a> ColumnRef<'a> { from_comparison_target(column) } + pub fn from_column_and_field_path<'b>( + name: &'b FieldName, + field_path: Option<&'b Vec>, + ) -> ColumnRef<'b> { + from_column_and_field_path(&[], name, field_path) + } + + pub fn from_relationship_path_column_and_field_path<'b>( + relationship_path: &'b [ndc_models::RelationshipName], + name: &'b FieldName, + field_path: Option<&'b Vec>, + ) -> ColumnRef<'b> { + from_column_and_field_path(relationship_path, name, field_path) + } + /// TODO: This will hopefully become infallible once ENG-1011 & ENG-1010 are implemented. pub fn from_order_by_target(target: &OrderByTarget) -> Result, MongoAgentError> { from_order_by_target(target) } - pub fn from_field_path<'b>( - field_path: impl IntoIterator, - ) -> ColumnRef<'b> { + pub fn from_field_path(field_path: NonEmpty<&ndc_models::FieldName>) -> ColumnRef<'_> { from_path( None, field_path .into_iter() .map(|field_name| field_name.as_ref() as &str), ) - .unwrap() + .expect("field_path is not empty") // safety: NonEmpty cannot be empty } - pub fn from_field(field_name: &ndc_models::FieldName) -> ColumnRef<'_> { - fold_path_element(None, field_name.as_ref()) + pub fn from_field(field_name: &str) -> ColumnRef<'_> { + fold_path_element(None, field_name) } pub fn from_relationship(relationship_name: &ndc_models::RelationshipName) -> ColumnRef<'_> { @@ -87,69 +103,63 @@ impl<'a> ColumnRef<'a> { Self::ExpressionStringShorthand(format!("$${variable_name}").into()) } - pub fn into_nested_field<'b: 'a>(self, field_name: &'b ndc_models::FieldName) -> ColumnRef<'b> { - fold_path_element(Some(self), field_name.as_ref()) + pub fn into_nested_field<'b: 'a>(self, field_name: &'b str) -> ColumnRef<'b> { + fold_path_element(Some(self), field_name) } - pub fn into_aggregate_expression(self) -> Bson { - match self { + pub fn into_aggregate_expression(self) -> AggregationExpression { + let bson = match self { ColumnRef::MatchKey(key) => format!("${key}").into(), ColumnRef::ExpressionStringShorthand(key) => key.to_string().into(), ColumnRef::Expression(expr) => expr, + }; + AggregationExpression(bson) + } + + pub fn into_match_key(self) -> Option> { + match self { + ColumnRef::MatchKey(key) => Some(key), + _ => None, } } } fn from_comparison_target(column: &ComparisonTarget) -> ColumnRef<'_> { match column { - // We exclude `path` (the relationship path) from the resulting ColumnRef because MongoDB - // field references are not relationship-aware. Traversing relationship references is - // handled upstream. ComparisonTarget::Column { name, field_path, .. - } => { - let name_and_path = once(name.as_ref() as &str).chain( - field_path - .iter() - .flatten() - .map(|field_name| field_name.as_ref() as &str), - ); - // The None case won't come up if the input to [from_target_helper] has at least - // one element, and we know it does because we start the iterable with `name` - from_path(None, name_and_path).unwrap() - } - ComparisonTarget::ColumnInScope { - name, - field_path, - scope, - .. - } => { - // "$$ROOT" is not actually a valid match key, but cheating here makes the - // implementation much simpler. This match branch produces a ColumnRef::Expression - // in all cases. - let init = ColumnRef::variable(name_from_scope(scope)); - from_path( - Some(init), - once(name.as_ref() as &str).chain( - field_path - .iter() - .flatten() - .map(|field_name| field_name.as_ref() as &str), - ), - ) - // The None case won't come up if the input to [from_target_helper] has at least - // one element, and we know it does because we start the iterable with `name` - .unwrap() - } + } => from_column_and_field_path(&[], name, field_path.as_ref()), } } +fn from_column_and_field_path<'a>( + relationship_path: &'a [ndc_models::RelationshipName], + name: &'a FieldName, + field_path: Option<&'a Vec>, +) -> ColumnRef<'a> { + let name_and_path = relationship_path + .iter() + .map(|r| r.as_ref() as &str) + .chain(once(name.as_ref() as &str)) + .chain( + field_path + .iter() + .copied() + .flatten() + .map(|field_name| field_name.as_ref() as &str), + ); + // The None case won't come up if the input to [from_target_helper] has at least + // one element, and we know it does because we start the iterable with `name` + from_path(None, name_and_path).unwrap() +} + fn from_order_by_target(target: &OrderByTarget) -> Result, MongoAgentError> { match target { OrderByTarget::Column { + path, name, field_path, - path, + .. } => { let name_and_path = path .iter() @@ -165,17 +175,9 @@ fn from_order_by_target(target: &OrderByTarget) -> Result, MongoAg // one element, and we know it does because we start the iterable with `name` Ok(from_path(None, name_and_path).unwrap()) } - OrderByTarget::SingleColumnAggregate { .. } => { + OrderByTarget::Aggregate { .. } => { // TODO: ENG-1011 - Err(MongoAgentError::NotImplemented( - "ordering by single column aggregate".into(), - )) - } - OrderByTarget::StarCountAggregate { .. } => { - // TODO: ENG-1010 - Err(MongoAgentError::NotImplemented( - "ordering by star count aggregate".into(), - )) + Err(MongoAgentError::NotImplemented("order by aggregate".into())) } } } @@ -232,7 +234,9 @@ fn fold_path_element<'a>( /// Unlike `column_ref` this expression cannot be used as a match query key - it can only be used /// as an expression. pub fn column_expression(column: &ComparisonTarget) -> Bson { - ColumnRef::from_comparison_target(column).into_aggregate_expression() + ColumnRef::from_comparison_target(column) + .into_aggregate_expression() + .into_bson() } #[cfg(test)] @@ -240,7 +244,6 @@ mod tests { use configuration::MongoScalarType; use mongodb::bson::doc; use mongodb_support::BsonScalarType; - use ndc_query_plan::Scope; use pretty_assertions::assert_eq; use crate::mongo_query_plan::{ComparisonTarget, Type}; @@ -251,9 +254,9 @@ mod tests { fn produces_match_query_key() -> anyhow::Result<()> { let target = ComparisonTarget::Column { name: "imdb".into(), + arguments: Default::default(), field_path: Some(vec!["rating".into()]), field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::Double)), - path: Default::default(), }; let actual = ColumnRef::from_comparison_target(&target); let expected = ColumnRef::MatchKey("imdb.rating".into()); @@ -265,9 +268,9 @@ mod tests { fn escapes_nested_field_name_with_dots() -> anyhow::Result<()> { let target = ComparisonTarget::Column { name: "subtitles".into(), + arguments: Default::default(), field_path: Some(vec!["english.us".into()]), field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), }; let actual = ColumnRef::from_comparison_target(&target); let expected = ColumnRef::Expression( @@ -287,9 +290,9 @@ mod tests { fn escapes_top_level_field_name_with_dots() -> anyhow::Result<()> { let target = ComparisonTarget::Column { name: "meta.subtitles".into(), + arguments: Default::default(), field_path: Some(vec!["english_us".into()]), field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), }; let actual = ColumnRef::from_comparison_target(&target); let expected = ColumnRef::Expression( @@ -309,9 +312,9 @@ mod tests { fn escapes_multiple_unsafe_nested_field_names() -> anyhow::Result<()> { let target = ComparisonTarget::Column { name: "meta".into(), + arguments: Default::default(), field_path: Some(vec!["$unsafe".into(), "$also_unsafe".into()]), field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), }; let actual = ColumnRef::from_comparison_target(&target); let expected = ColumnRef::Expression( @@ -336,9 +339,9 @@ mod tests { fn traverses_multiple_field_names_before_escaping() -> anyhow::Result<()> { let target = ComparisonTarget::Column { name: "valid_key".into(), + arguments: Default::default(), field_path: Some(vec!["also_valid".into(), "$not_valid".into()]), field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), }; let actual = ColumnRef::from_comparison_target(&target); let expected = ColumnRef::Expression( @@ -354,117 +357,121 @@ mod tests { Ok(()) } - #[test] - fn produces_dot_separated_root_column_reference() -> anyhow::Result<()> { - let target = ComparisonTarget::ColumnInScope { - name: "field".into(), - field_path: Some(vec!["prop1".into(), "prop2".into()]), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Root, - }; - let actual = ColumnRef::from_comparison_target(&target); - let expected = - ColumnRef::ExpressionStringShorthand("$$scope_root.field.prop1.prop2".into()); - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn escapes_unsafe_field_name_in_root_column_reference() -> anyhow::Result<()> { - let target = ComparisonTarget::ColumnInScope { - name: "$field".into(), - field_path: Default::default(), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Named("scope_0".into()), - }; - let actual = ColumnRef::from_comparison_target(&target); - let expected = ColumnRef::Expression( - doc! { - "$getField": { - "input": "$$scope_0", - "field": { "$literal": "$field" }, - } - } - .into(), - ); - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn escapes_unsafe_nested_property_name_in_root_column_reference() -> anyhow::Result<()> { - let target = ComparisonTarget::ColumnInScope { - name: "field".into(), - field_path: Some(vec!["$unsafe_name".into()]), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Root, - }; - let actual = ColumnRef::from_comparison_target(&target); - let expected = ColumnRef::Expression( - doc! { - "$getField": { - "input": "$$scope_root.field", - "field": { "$literal": "$unsafe_name" }, - } - } - .into(), - ); - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn escapes_multiple_layers_of_nested_property_names_in_root_column_reference( - ) -> anyhow::Result<()> { - let target = ComparisonTarget::ColumnInScope { - name: "$field".into(), - field_path: Some(vec!["$unsafe_name1".into(), "$unsafe_name2".into()]), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Root, - }; - let actual = ColumnRef::from_comparison_target(&target); - let expected = ColumnRef::Expression( - doc! { - "$getField": { - "input": { - "$getField": { - "input": { - "$getField": { - "input": "$$scope_root", - "field": { "$literal": "$field" }, - } - }, - "field": { "$literal": "$unsafe_name1" }, - } - }, - "field": { "$literal": "$unsafe_name2" }, - } - } - .into(), - ); - assert_eq!(actual, expected); - Ok(()) - } - - #[test] - fn escapes_unsafe_deeply_nested_property_name_in_root_column_reference() -> anyhow::Result<()> { - let target = ComparisonTarget::ColumnInScope { - name: "field".into(), - field_path: Some(vec!["prop1".into(), "$unsafe_name".into()]), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Root, - }; - let actual = ColumnRef::from_comparison_target(&target); - let expected = ColumnRef::Expression( - doc! { - "$getField": { - "input": "$$scope_root.field.prop1", - "field": { "$literal": "$unsafe_name" }, - } - } - .into(), - ); - assert_eq!(actual, expected); - Ok(()) - } + // TODO: ENG-1487 `ComparisonTarget::ColumnInScope` is gone, but there is new, similar + // functionality in the form of named scopes. It will be useful to modify these tests when + // named scopes are supported in this connector. + + // #[test] + // fn produces_dot_separated_root_column_reference() -> anyhow::Result<()> { + // let target = ComparisonTarget::ColumnInScope { + // name: "field".into(), + // field_path: Some(vec!["prop1".into(), "prop2".into()]), + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Root, + // }; + // let actual = ColumnRef::from_comparison_target(&target); + // let expected = + // ColumnRef::ExpressionStringShorthand("$$scope_root.field.prop1.prop2".into()); + // assert_eq!(actual, expected); + // Ok(()) + // } + + // #[test] + // fn escapes_unsafe_field_name_in_root_column_reference() -> anyhow::Result<()> { + // let target = ComparisonTarget::ColumnInScope { + // name: "$field".into(), + // field_path: Default::default(), + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Named("scope_0".into()), + // }; + // let actual = ColumnRef::from_comparison_target(&target); + // let expected = ColumnRef::Expression( + // doc! { + // "$getField": { + // "input": "$$scope_0", + // "field": { "$literal": "$field" }, + // } + // } + // .into(), + // ); + // assert_eq!(actual, expected); + // Ok(()) + // } + + // #[test] + // fn escapes_unsafe_nested_property_name_in_root_column_reference() -> anyhow::Result<()> { + // let target = ComparisonTarget::ColumnInScope { + // name: "field".into(), + // field_path: Some(vec!["$unsafe_name".into()]), + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Root, + // }; + // let actual = ColumnRef::from_comparison_target(&target); + // let expected = ColumnRef::Expression( + // doc! { + // "$getField": { + // "input": "$$scope_root.field", + // "field": { "$literal": "$unsafe_name" }, + // } + // } + // .into(), + // ); + // assert_eq!(actual, expected); + // Ok(()) + // } + + // #[test] + // fn escapes_multiple_layers_of_nested_property_names_in_root_column_reference( + // ) -> anyhow::Result<()> { + // let target = ComparisonTarget::ColumnInScope { + // name: "$field".into(), + // field_path: Some(vec!["$unsafe_name1".into(), "$unsafe_name2".into()]), + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Root, + // }; + // let actual = ColumnRef::from_comparison_target(&target); + // let expected = ColumnRef::Expression( + // doc! { + // "$getField": { + // "input": { + // "$getField": { + // "input": { + // "$getField": { + // "input": "$$scope_root", + // "field": { "$literal": "$field" }, + // } + // }, + // "field": { "$literal": "$unsafe_name1" }, + // } + // }, + // "field": { "$literal": "$unsafe_name2" }, + // } + // } + // .into(), + // ); + // assert_eq!(actual, expected); + // Ok(()) + // } + + // #[test] + // fn escapes_unsafe_deeply_nested_property_name_in_root_column_reference() -> anyhow::Result<()> { + // let target = ComparisonTarget::ColumnInScope { + // name: "field".into(), + // field_path: Some(vec!["prop1".into(), "$unsafe_name".into()]), + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Root, + // }; + // let actual = ColumnRef::from_comparison_target(&target); + // let expected = ColumnRef::Expression( + // doc! { + // "$getField": { + // "input": "$$scope_root.field.prop1", + // "field": { "$literal": "$unsafe_name" }, + // } + // } + // .into(), + // ); + // assert_eq!(actual, expected); + // Ok(()) + // } } diff --git a/crates/mongodb-agent-common/src/query/constants.rs b/crates/mongodb-agent-common/src/query/constants.rs deleted file mode 100644 index a8569fc0..00000000 --- a/crates/mongodb-agent-common/src/query/constants.rs +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: check for collision with aggregation field names -pub const ROWS_FIELD: &str = "__ROWS__"; -pub const RESULT_FIELD: &str = "result"; diff --git a/crates/mongodb-agent-common/src/query/foreach.rs b/crates/mongodb-agent-common/src/query/foreach.rs index 4995eb40..e62fc5bb 100644 --- a/crates/mongodb-agent-common/src/query/foreach.rs +++ b/crates/mongodb-agent-common/src/query/foreach.rs @@ -1,14 +1,16 @@ use anyhow::anyhow; use itertools::Itertools as _; -use mongodb::bson::{self, doc, Bson}; +use mongodb::bson::{self, bson, doc, Bson}; use mongodb_support::aggregate::{Pipeline, Selection, Stage}; use ndc_query_plan::VariableSet; +use super::is_response_faceted::ResponseFacets; use super::pipeline::pipeline_for_non_foreach; use super::query_level::QueryLevel; use super::query_variable_name::query_variable_name; use super::serialization::json_to_bson; use super::QueryTarget; +use crate::constants::{ROW_SET_AGGREGATES_KEY, ROW_SET_GROUPS_KEY, ROW_SET_ROWS_KEY}; use crate::interface_types::MongoAgentError; use crate::mongo_query_plan::{MongoConfiguration, QueryPlan, Type, VariableTypes}; @@ -45,18 +47,39 @@ pub fn pipeline_for_foreach( r#as: "query".to_string(), }; - let selection = if query_request.query.has_aggregates() && query_request.query.has_fields() { - doc! { - "aggregates": { "$getField": { "input": { "$first": "$query" }, "field": "aggregates" } }, - "rows": { "$getField": { "input": { "$first": "$query" }, "field": "rows" } }, + let selection = match ResponseFacets::from_query(&query_request.query) { + ResponseFacets::Combination { + aggregates, + fields, + groups, + } => { + let mut keys = vec![]; + if aggregates.is_some() { + keys.push(ROW_SET_AGGREGATES_KEY); + } + if fields.is_some() { + keys.push(ROW_SET_ROWS_KEY); + } + if groups.is_some() { + keys.push(ROW_SET_GROUPS_KEY) + } + keys.into_iter() + .map(|key| { + ( + key.to_string(), + bson!({ "$getField": { "input": { "$first": "$query" }, "field": key } }), + ) + }) + .collect() + } + ResponseFacets::AggregatesOnly(_) => { + doc! { ROW_SET_AGGREGATES_KEY: { "$first": "$query" } } } - } else if query_request.query.has_aggregates() { - doc! { - "aggregates": { "$getField": { "input": { "$first": "$query" }, "field": "aggregates" } }, + ResponseFacets::FieldsOnly(_) => { + doc! { ROW_SET_ROWS_KEY: "$query" } } - } else { - doc! { - "rows": "$query" + ResponseFacets::GroupsOnly(_) => { + doc! { ROW_SET_GROUPS_KEY: "$query" } } }; let selection_stage = Stage::ReplaceWith(Selection::new(selection)); @@ -224,28 +247,30 @@ mod tests { "pipeline": [ { "$match": { "$expr": { "$eq": ["$artistId", "$$artistId_int"] } }}, { "$facet": { + "__AGGREGATES__": [ + { + "$group": { + "_id": null, + "count": { "$sum": 1 }, + } + }, + { + "$replaceWith": { + "count": { "$ifNull": ["$count", 0] }, + } + }, + ], "__ROWS__": [{ "$replaceWith": { "albumId": { "$ifNull": ["$albumId", null] }, "title": { "$ifNull": ["$title", null] } }}], - "count": [{ "$count": "result" }], - } }, - { "$replaceWith": { - "aggregates": { - "count": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "count" } } } - } - }, - 0, - ] - }, - }, - "rows": "$__ROWS__", } }, + { + "$replaceWith": { + "aggregates": { "$first": "$__AGGREGATES__" }, + "rows": "$__ROWS__", + } + }, ] } }, @@ -330,30 +355,23 @@ mod tests { "as": "query", "pipeline": [ { "$match": { "$expr": { "$eq": ["$artistId", "$$artistId_int"] } }}, - { "$facet": { - "count": [{ "$count": "result" }], - } }, - { "$replaceWith": { - "aggregates": { - "count": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "count" } } } - } - }, - 0, - ] - }, - }, - } }, + { + "$group": { + "_id": null, + "count": { "$sum": 1 } + } + }, + { + "$replaceWith": { + "count": { "$ifNull": ["$count", 0] }, + } + }, ] } }, { "$replaceWith": { - "aggregates": { "$getField": { "input": { "$first": "$query" }, "field": "aggregates" } }, + "aggregates": { "$first": "$query" }, } }, ]); diff --git a/crates/mongodb-agent-common/src/query/groups.rs b/crates/mongodb-agent-common/src/query/groups.rs new file mode 100644 index 00000000..85017dd7 --- /dev/null +++ b/crates/mongodb-agent-common/src/query/groups.rs @@ -0,0 +1,113 @@ +use std::borrow::Cow; + +use mongodb::bson::{self, bson}; +use mongodb_support::aggregate::{Pipeline, Selection, SortDocument, Stage}; +use ndc_models::OrderDirection; + +use crate::{ + constants::GROUP_DIMENSIONS_KEY, + interface_types::MongoAgentError, + mongo_query_plan::{Dimension, GroupOrderBy, GroupOrderByTarget, Grouping}, +}; + +use super::{ + aggregates::{accumulators_for_aggregates, selection_for_aggregate}, + column_ref::ColumnRef, +}; + +type Result = std::result::Result; + +// TODO: This function can be infallible once ENG-1562 is implemented. +pub fn pipeline_for_groups(grouping: &Grouping) -> Result { + let group_stage = Stage::Group { + key_expression: dimensions_to_expression(&grouping.dimensions).into(), + accumulators: accumulators_for_aggregates(&grouping.aggregates), + }; + + // TODO: ENG-1562 This implementation does not fully implement the + // 'query.aggregates.group_by.order' capability! This only orders by dimensions. Before + // enabling the capability we also need to be able to order by aggregates. We need partial + // support for order by to get consistent integration test snapshots. + let sort_groups_stage = grouping + .order_by + .as_ref() + .map(sort_stage_for_grouping) + .transpose()?; + + // TODO: ENG-1563 to implement 'query.aggregates.group_by.paginate' apply grouping.limit and + // grouping.offset **after** group stage because those options count groups, not documents + + let replace_with_stage = Stage::ReplaceWith(selection_for_grouping(grouping, "_id")); + + Ok(Pipeline::new( + [ + Some(group_stage), + sort_groups_stage, + Some(replace_with_stage), + ] + .into_iter() + .flatten() + .collect(), + )) +} + +/// Converts each dimension to a MongoDB aggregate expression that evaluates to the appropriate +/// value when applied to each input document. The array of expressions can be used directly as the +/// group stage key expression. +fn dimensions_to_expression(dimensions: &[Dimension]) -> bson::Array { + dimensions + .iter() + .map(|dimension| { + let column_ref = match dimension { + Dimension::Column { + path, + column_name, + field_path, + .. + } => ColumnRef::from_relationship_path_column_and_field_path( + path, + column_name, + field_path.as_ref(), + ), + }; + column_ref.into_aggregate_expression().into_bson() + }) + .collect() +} + +fn selection_for_grouping(grouping: &Grouping, dimensions_field_name: &str) -> Selection { + let dimensions = ( + GROUP_DIMENSIONS_KEY.to_string(), + bson!(format!("${dimensions_field_name}")), + ); + let selected_aggregates = grouping + .aggregates + .iter() + .map(|(key, aggregate)| selection_for_aggregate(key, aggregate)); + let selection_doc = std::iter::once(dimensions) + .chain(selected_aggregates) + .collect(); + Selection::new(selection_doc) +} + +// TODO: ENG-1562 This is where we need to implement sorting by aggregates +fn sort_stage_for_grouping(order_by: &GroupOrderBy) -> Result { + let sort_doc = order_by + .elements + .iter() + .map(|element| match element.target { + GroupOrderByTarget::Dimension { index } => { + let key = format!("_id.{index}"); + let direction = match element.order_direction { + OrderDirection::Asc => bson!(1), + OrderDirection::Desc => bson!(-1), + }; + Ok((key, direction)) + } + GroupOrderByTarget::Aggregate { .. } => Err(MongoAgentError::NotImplemented( + Cow::Borrowed("sorting groups by aggregate"), + )), + }) + .collect::>()?; + Ok(Stage::Sort(SortDocument::from_doc(sort_doc))) +} diff --git a/crates/mongodb-agent-common/src/query/is_response_faceted.rs b/crates/mongodb-agent-common/src/query/is_response_faceted.rs new file mode 100644 index 00000000..f53b23d0 --- /dev/null +++ b/crates/mongodb-agent-common/src/query/is_response_faceted.rs @@ -0,0 +1,97 @@ +//! Centralized logic for query response packing. + +use indexmap::IndexMap; +use lazy_static::lazy_static; +use ndc_models::FieldName; + +use crate::mongo_query_plan::{Aggregate, Field, Grouping, Query}; + +lazy_static! { + static ref DEFAULT_FIELDS: IndexMap = IndexMap::new(); +} + +/// In some queries we may need to "fork" the query to provide data that requires incompatible +/// pipelines. For example queries that combine two or more of row, group, and aggregates, or +/// queries that use multiple aggregates that use different buckets. In these cases we use the +/// `$facet` aggregation stage which runs multiple sub-pipelines, and stores the results of +/// each in fields of the output pipeline document with array values. +/// +/// In other queries we don't need to fork - instead of providing data in a nested array the stream +/// of pipeline output documents is itself the requested data. +/// +/// Depending on whether or not a pipeline needs to use `$facet` to fork response processing needs +/// to be done differently. +pub enum ResponseFacets<'a> { + /// When matching on the Combination variant assume that requested data has already been checked to make sure that maps are not empty. + Combination { + aggregates: Option<&'a IndexMap>, + fields: Option<&'a IndexMap>, + groups: Option<&'a Grouping>, + }, + AggregatesOnly(&'a IndexMap), + FieldsOnly(&'a IndexMap), + GroupsOnly(&'a Grouping), +} + +impl ResponseFacets<'_> { + pub fn from_parameters<'a>( + aggregates: Option<&'a IndexMap>, + fields: Option<&'a IndexMap>, + groups: Option<&'a Grouping>, + ) -> ResponseFacets<'a> { + let facet_score = [ + get_aggregates(aggregates).map(|_| ()), + get_fields(fields).map(|_| ()), + get_groups(groups).map(|_| ()), + ] + .into_iter() + .flatten() + .count(); + + if facet_score > 1 { + ResponseFacets::Combination { + aggregates: get_aggregates(aggregates), + fields: get_fields(fields), + groups: get_groups(groups), + } + } else if let Some(aggregates) = aggregates { + ResponseFacets::AggregatesOnly(aggregates) + } else if let Some(grouping) = groups { + ResponseFacets::GroupsOnly(grouping) + } else { + ResponseFacets::FieldsOnly(fields.unwrap_or(&DEFAULT_FIELDS)) + } + } + + pub fn from_query(query: &Query) -> ResponseFacets<'_> { + Self::from_parameters( + query.aggregates.as_ref(), + query.fields.as_ref(), + query.groups.as_ref(), + ) + } +} + +fn get_aggregates( + aggregates: Option<&IndexMap>, +) -> Option<&IndexMap> { + if let Some(aggregates) = aggregates { + if !aggregates.is_empty() { + return Some(aggregates); + } + } + None +} + +fn get_fields(fields: Option<&IndexMap>) -> Option<&IndexMap> { + if let Some(fields) = fields { + if !fields.is_empty() { + return Some(fields); + } + } + None +} + +fn get_groups(groups: Option<&Grouping>) -> Option<&Grouping> { + groups +} diff --git a/crates/mongodb-agent-common/src/query/make_selector/make_aggregation_expression.rs b/crates/mongodb-agent-common/src/query/make_selector/make_aggregation_expression.rs index 7ea14c76..4f17d6cd 100644 --- a/crates/mongodb-agent-common/src/query/make_selector/make_aggregation_expression.rs +++ b/crates/mongodb-agent-common/src/query/make_selector/make_aggregation_expression.rs @@ -1,5 +1,3 @@ -use std::iter::once; - use anyhow::anyhow; use itertools::Itertools as _; use mongodb::bson::{self, doc, Bson}; @@ -8,7 +6,9 @@ use ndc_models::UnaryComparisonOperator; use crate::{ comparison_function::ComparisonFunction, interface_types::MongoAgentError, - mongo_query_plan::{ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type}, + mongo_query_plan::{ + ArrayComparison, ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type, + }, query::{ column_ref::{column_expression, ColumnRef}, query_variable_name::query_variable_name, @@ -22,11 +22,21 @@ use super::Result; pub struct AggregationExpression(pub Bson); impl AggregationExpression { - fn into_bson(self) -> Bson { + pub fn new(expression: impl Into) -> Self { + Self(expression.into()) + } + + pub fn into_bson(self) -> Bson { self.0 } } +impl From for Bson { + fn from(value: AggregationExpression) -> Self { + value.into_bson() + } +} + pub fn make_aggregation_expression(expr: &Expression) -> Result { match expr { Expression::And { expressions } => { @@ -71,8 +81,11 @@ pub fn make_aggregation_expression(expr: &Expression) -> Result make_binary_comparison_selector(column, operator, value), + Expression::ArrayComparison { column, comparison } => { + make_array_comparison_selector(column, comparison) + } Expression::UnaryComparisonOperator { column, operator } => { - make_unary_comparison_selector(column, *operator) + Ok(make_unary_comparison_selector(column, *operator)) } } } @@ -118,7 +131,7 @@ pub fn make_aggregation_expression_for_exists( }, Some(predicate), ) => { - let column_ref = ColumnRef::from_field_path(field_path.iter().chain(once(column_name))); + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); exists_in_array(column_ref, predicate)? } ( @@ -129,7 +142,29 @@ pub fn make_aggregation_expression_for_exists( }, None, ) => { - let column_ref = ColumnRef::from_field_path(field_path.iter().chain(once(column_name))); + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); + exists_in_array_no_predicate(column_ref) + } + ( + ExistsInCollection::NestedScalarCollection { + column_name, + field_path, + .. + }, + Some(predicate), + ) => { + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); + exists_in_array(column_ref, predicate)? // TODO: ENG-1488 predicate expects objects with a __value field + } + ( + ExistsInCollection::NestedScalarCollection { + column_name, + field_path, + .. + }, + None, + ) => { + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); exists_in_array_no_predicate(column_ref) } }; @@ -146,7 +181,7 @@ fn exists_in_array( "$anyElementTrue": { "$map": { "input": array_ref.into_aggregate_expression(), - "as": "CURRENT", // implicitly changes the document root in `exp` to be the array element + "as": "CURRENT", // implicitly changes the document root in `sub_expression` to be the array element "in": sub_expression, } } @@ -156,14 +191,9 @@ fn exists_in_array( } fn exists_in_array_no_predicate(array_ref: ColumnRef<'_>) -> AggregationExpression { - let index_zero = "0".into(); - let first_element_ref = array_ref.into_nested_field(&index_zero); - AggregationExpression( - doc! { - "$ne": [first_element_ref.into_aggregate_expression(), null] - } - .into(), - ) + AggregationExpression::new(doc! { + "$gt": [{ "$size": array_ref.into_aggregate_expression() }, 0] + }) } fn make_binary_comparison_selector( @@ -171,102 +201,78 @@ fn make_binary_comparison_selector( operator: &ComparisonFunction, value: &ComparisonValue, ) -> Result { - let aggregation_expression = match value { + let left_operand = ColumnRef::from_comparison_target(target_column).into_aggregate_expression(); + let right_operand = value_expression(value)?; + let expr = AggregationExpression( + operator + .mongodb_aggregation_expression(left_operand, right_operand) + .into(), + ); + Ok(expr) +} + +fn make_unary_comparison_selector( + target_column: &ndc_query_plan::ComparisonTarget, + operator: UnaryComparisonOperator, +) -> AggregationExpression { + match operator { + UnaryComparisonOperator::IsNull => AggregationExpression( + doc! { + "$eq": [column_expression(target_column), null] + } + .into(), + ), + } +} + +fn make_array_comparison_selector( + column: &ComparisonTarget, + comparison: &ArrayComparison, +) -> Result { + let doc = match comparison { + ArrayComparison::Contains { value } => doc! { + "$in": [value_expression(value)?, column_expression(column)] + }, + ArrayComparison::IsEmpty => doc! { + "$eq": [{ "$size": column_expression(column) }, 0] + }, + }; + Ok(AggregationExpression(doc.into())) +} + +fn value_expression(value: &ComparisonValue) -> Result { + match value { ComparisonValue::Column { - column: value_column, + path, + name, + field_path, + scope: _, // We'll need to reference scope for ENG-1153 + .. } => { // TODO: ENG-1153 Do we want an implicit exists in the value relationship? If both // target and value reference relationships do we want an exists in a Cartesian product // of the two? - if !value_column.relationship_path().is_empty() { + if !path.is_empty() { return Err(MongoAgentError::NotImplemented("binary comparisons where the right-side of the comparison references a relationship".into())); } - let left_operand = ColumnRef::from_comparison_target(target_column); - let right_operand = ColumnRef::from_comparison_target(value_column); - AggregationExpression( - operator - .mongodb_aggregation_expression( - left_operand.into_aggregate_expression(), - right_operand.into_aggregate_expression(), - ) - .into(), - ) + let value_ref = ColumnRef::from_column_and_field_path(name, field_path.as_ref()); + Ok(value_ref.into_aggregate_expression()) } ComparisonValue::Scalar { value, value_type } => { let comparison_value = bson_from_scalar_value(value, value_type)?; - - // Special case for array-to-scalar comparisons - this is required because implicit - // existential quantification over arrays for scalar comparisons does not work in - // aggregation expressions. - let expression_doc = if target_column.get_field_type().is_array() - && !value_type.is_array() - { - doc! { - "$reduce": { - "input": column_expression(target_column), - "initialValue": false, - "in": operator.mongodb_aggregation_expression("$$this", comparison_value) - }, - } - } else { - operator.mongodb_aggregation_expression( - column_expression(target_column), - comparison_value, - ) - }; - AggregationExpression(expression_doc.into()) + Ok(AggregationExpression::new(doc! { + "$literal": comparison_value + })) } ComparisonValue::Variable { name, variable_type, } => { let comparison_value = variable_to_mongo_expression(name, variable_type); - let expression_doc = - // Special case for array-to-scalar comparisons - this is required because implicit - // existential quantification over arrays for scalar comparisons does not work in - // aggregation expressions. - if target_column.get_field_type().is_array() && !variable_type.is_array() { - doc! { - "$reduce": { - "input": column_expression(target_column), - "initialValue": false, - "in": operator.mongodb_aggregation_expression("$$this", comparison_value.into_aggregate_expression()) - }, - } - } else { - operator.mongodb_aggregation_expression( - column_expression(target_column), - comparison_value.into_aggregate_expression() - ) - }; - AggregationExpression(expression_doc.into()) + Ok(comparison_value.into_aggregate_expression()) } - }; - - let implicit_exists_over_relationship = - traverse_relationship_path(target_column.relationship_path(), aggregation_expression); - - Ok(implicit_exists_over_relationship) -} - -fn make_unary_comparison_selector( - target_column: &ndc_query_plan::ComparisonTarget, - operator: UnaryComparisonOperator, -) -> std::result::Result { - let aggregation_expression = match operator { - UnaryComparisonOperator::IsNull => AggregationExpression( - doc! { - "$eq": [column_expression(target_column), null] - } - .into(), - ), - }; - - let implicit_exists_over_relationship = - traverse_relationship_path(target_column.relationship_path(), aggregation_expression); - - Ok(implicit_exists_over_relationship) + } } /// Convert a JSON Value into BSON using the provided type information. @@ -275,26 +281,6 @@ fn bson_from_scalar_value(value: &serde_json::Value, value_type: &Type) -> Resul json_to_bson(value_type, value.clone()).map_err(|e| MongoAgentError::BadQuery(anyhow!(e))) } -fn traverse_relationship_path( - relationship_path: &[ndc_models::RelationshipName], - AggregationExpression(mut expression): AggregationExpression, -) -> AggregationExpression { - for path_element in relationship_path.iter().rev() { - let path_element_ref = ColumnRef::from_relationship(path_element); - expression = doc! { - "$anyElementTrue": { - "$map": { - "input": path_element_ref.into_aggregate_expression(), - "as": "CURRENT", // implicitly changes the document root in `exp` to be the array element - "in": expression, - } - } - } - .into() - } - AggregationExpression(expression) -} - fn variable_to_mongo_expression( variable: &ndc_models::VariableName, value_type: &Type, diff --git a/crates/mongodb-agent-common/src/query/make_selector/make_query_document.rs b/crates/mongodb-agent-common/src/query/make_selector/make_query_document.rs index 916c586f..df766662 100644 --- a/crates/mongodb-agent-common/src/query/make_selector/make_query_document.rs +++ b/crates/mongodb-agent-common/src/query/make_selector/make_query_document.rs @@ -1,14 +1,14 @@ -use std::iter::once; - use anyhow::anyhow; use itertools::Itertools as _; -use mongodb::bson::{self, doc}; +use mongodb::bson::{self, doc, Bson}; use ndc_models::UnaryComparisonOperator; use crate::{ comparison_function::ComparisonFunction, interface_types::MongoAgentError, - mongo_query_plan::{ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type}, + mongo_query_plan::{ + ArrayComparison, ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type, + }, query::{column_ref::ColumnRef, serialization::json_to_bson}, }; @@ -73,6 +73,9 @@ pub fn make_query_document(expr: &Expression) -> Result> { Expression::UnaryComparisonOperator { column, operator } => { make_unary_comparison_selector(column, operator) } + Expression::ArrayComparison { column, comparison } => { + make_array_comparison_selector(column, comparison) + } } } @@ -102,7 +105,7 @@ fn make_query_document_for_exists( }, Some(predicate), ) => { - let column_ref = ColumnRef::from_field_path(field_path.iter().chain(once(column_name))); + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); exists_in_array(column_ref, predicate)? } ( @@ -113,7 +116,29 @@ fn make_query_document_for_exists( }, None, ) => { - let column_ref = ColumnRef::from_field_path(field_path.iter().chain(once(column_name))); + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); + exists_in_array_no_predicate(column_ref) + } + ( + ExistsInCollection::NestedScalarCollection { + column_name, + field_path, + .. + }, + Some(predicate), + ) => { + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); + exists_in_array(column_ref, predicate)? // TODO: predicate expects objects with a __value field + } + ( + ExistsInCollection::NestedScalarCollection { + column_name, + field_path, + .. + }, + None, + ) => { + let column_ref = ColumnRef::from_column_and_field_path(column_name, Some(field_path)); exists_in_array_no_predicate(column_ref) } }; @@ -151,25 +176,16 @@ fn make_binary_comparison_selector( operator: &ComparisonFunction, value: &ComparisonValue, ) -> Result> { - let query_doc = match value { - ComparisonValue::Scalar { value, value_type } => { - let comparison_value = bson_from_scalar_value(value, value_type)?; + let selector = + value_expression(value)?.and_then(|value| { match ColumnRef::from_comparison_target(target_column) { - ColumnRef::MatchKey(key) => Some(QueryDocument( - operator.mongodb_match_query(key, comparison_value), - )), + ColumnRef::MatchKey(key) => { + Some(QueryDocument(operator.mongodb_match_query(key, value))) + } _ => None, } - } - ComparisonValue::Column { .. } => None, - // Variables cannot be referenced in match documents - ComparisonValue::Variable { .. } => None, - }; - - let implicit_exists_over_relationship = - query_doc.and_then(|d| traverse_relationship_path(target_column.relationship_path(), d)); - - Ok(implicit_exists_over_relationship) + }); + Ok(selector) } fn make_unary_comparison_selector( @@ -184,35 +200,43 @@ fn make_unary_comparison_selector( _ => None, }, }; + Ok(query_doc) +} - let implicit_exists_over_relationship = - query_doc.and_then(|d| traverse_relationship_path(target_column.relationship_path(), d)); - - Ok(implicit_exists_over_relationship) +fn make_array_comparison_selector( + column: &ComparisonTarget, + comparison: &ArrayComparison, +) -> Result> { + let column_ref = ColumnRef::from_comparison_target(column); + let ColumnRef::MatchKey(key) = column_ref else { + return Ok(None); + }; + let doc = match comparison { + ArrayComparison::Contains { value } => value_expression(value)?.map(|value| { + doc! { + key: { "$elemMatch": { "$eq": value } } + } + }), + ArrayComparison::IsEmpty => Some(doc! { + key: { "$size": 0 } + }), + }; + Ok(doc.map(QueryDocument)) } -/// For simple cases the target of an expression is a field reference. But if the target is -/// a column of a related collection then we're implicitly making an array comparison (because -/// related documents always come as an array, even for object relationships), so we have to wrap -/// the starting expression with an `$elemMatch` for each relationship that is traversed to reach -/// the target column. -fn traverse_relationship_path( - path: &[ndc_models::RelationshipName], - QueryDocument(expression): QueryDocument, -) -> Option { - let mut expression = Some(expression); - for path_element in path.iter().rev() { - let path_element_ref = ColumnRef::from_relationship(path_element); - expression = expression.and_then(|expr| match path_element_ref { - ColumnRef::MatchKey(key) => Some(doc! { - key: { - "$elemMatch": expr - } - }), - _ => None, - }); - } - expression.map(QueryDocument) +/// Only scalar comparison values can be represented in query documents. This function returns such +/// a representation if there is a legal way to do so. +fn value_expression(value: &ComparisonValue) -> Result> { + let expression = match value { + ComparisonValue::Scalar { value, value_type } => { + let bson_value = bson_from_scalar_value(value, value_type)?; + Some(bson_value) + } + ComparisonValue::Column { .. } => None, + // Variables cannot be referenced in match documents + ComparisonValue::Variable { .. } => None, + }; + Ok(expression) } /// Convert a JSON Value into BSON using the provided type information. diff --git a/crates/mongodb-agent-common/src/query/make_selector/mod.rs b/crates/mongodb-agent-common/src/query/make_selector/mod.rs index 2f28b1d0..4dcf9d00 100644 --- a/crates/mongodb-agent-common/src/query/make_selector/mod.rs +++ b/crates/mongodb-agent-common/src/query/make_selector/mod.rs @@ -32,14 +32,9 @@ pub fn make_selector(expr: &Expression) -> Result { #[cfg(test)] mod tests { use configuration::MongoScalarType; - use mongodb::bson::{self, bson, doc}; + use mongodb::bson::doc; use mongodb_support::BsonScalarType; use ndc_models::UnaryComparisonOperator; - use ndc_query_plan::{plan_for_query_request, Scope}; - use ndc_test_helpers::{ - binop, column_value, path_element, query, query_request, relation_field, root, target, - value, - }; use pretty_assertions::assert_eq; use crate::{ @@ -47,8 +42,6 @@ mod tests { mongo_query_plan::{ ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Type, }, - query::pipeline_for_query_request, - test_helpers::{chinook_config, chinook_relationships}, }; use super::make_selector; @@ -56,18 +49,26 @@ mod tests { #[test] fn compares_fields_of_related_documents_using_elem_match_in_binary_comparison( ) -> anyhow::Result<()> { - let selector = make_selector(&Expression::BinaryComparisonOperator { - column: ComparisonTarget::Column { - name: "Name".into(), - field_path: None, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: vec!["Albums".into(), "Tracks".into()], - }, - operator: ComparisonFunction::Equal, - value: ComparisonValue::Scalar { - value: "Helter Skelter".into(), - value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + let selector = make_selector(&Expression::Exists { + in_collection: ExistsInCollection::Related { + relationship: "Albums".into(), }, + predicate: Some(Box::new(Expression::Exists { + in_collection: ExistsInCollection::Related { + relationship: "Tracks".into(), + }, + predicate: Some(Box::new(Expression::BinaryComparisonOperator { + column: ComparisonTarget::column( + "Name", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), + operator: ComparisonFunction::Equal, + value: ComparisonValue::Scalar { + value: "Helter Skelter".into(), + value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + }, + })), + })), })?; let expected = doc! { @@ -89,14 +90,22 @@ mod tests { #[test] fn compares_fields_of_related_documents_using_elem_match_in_unary_comparison( ) -> anyhow::Result<()> { - let selector = make_selector(&Expression::UnaryComparisonOperator { - column: ComparisonTarget::Column { - name: "Name".into(), - field_path: None, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: vec!["Albums".into(), "Tracks".into()], + let selector = make_selector(&Expression::Exists { + in_collection: ExistsInCollection::Related { + relationship: "Albums".into(), }, - operator: UnaryComparisonOperator::IsNull, + predicate: Some(Box::new(Expression::Exists { + in_collection: ExistsInCollection::Related { + relationship: "Tracks".into(), + }, + predicate: Some(Box::new(Expression::UnaryComparisonOperator { + column: ComparisonTarget::column( + "Name", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), + operator: UnaryComparisonOperator::IsNull, + })), + })), })?; let expected = doc! { @@ -118,21 +127,15 @@ mod tests { #[test] fn compares_two_columns() -> anyhow::Result<()> { let selector = make_selector(&Expression::BinaryComparisonOperator { - column: ComparisonTarget::Column { - name: "Name".into(), - field_path: None, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), - }, + column: ComparisonTarget::column( + "Name", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), operator: ComparisonFunction::Equal, - value: ComparisonValue::Column { - column: ComparisonTarget::Column { - name: "Title".into(), - field_path: None, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - path: Default::default(), - }, - }, + value: ComparisonValue::column( + "Title", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), })?; let expected = doc! { @@ -145,119 +148,120 @@ mod tests { Ok(()) } - #[test] - fn compares_root_collection_column_to_scalar() -> anyhow::Result<()> { - let selector = make_selector(&Expression::BinaryComparisonOperator { - column: ComparisonTarget::ColumnInScope { - name: "Name".into(), - field_path: None, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - scope: Scope::Named("scope_0".to_string()), - }, - operator: ComparisonFunction::Equal, - value: ComparisonValue::Scalar { - value: "Lady Gaga".into(), - value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - }, - })?; + // TODO: ENG-1487 modify this test for the new named scopes feature + // #[test] + // fn compares_root_collection_column_to_scalar() -> anyhow::Result<()> { + // let selector = make_selector(&Expression::BinaryComparisonOperator { + // column: ComparisonTarget::ColumnInScope { + // name: "Name".into(), + // field_path: None, + // field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // scope: Scope::Named("scope_0".to_string()), + // }, + // operator: ComparisonFunction::Equal, + // value: ComparisonValue::Scalar { + // value: "Lady Gaga".into(), + // value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + // }, + // })?; + // + // let expected = doc! { + // "$expr": { + // "$eq": ["$$scope_0.Name", "Lady Gaga"] + // } + // }; + // + // assert_eq!(selector, expected); + // Ok(()) + // } - let expected = doc! { - "$expr": { - "$eq": ["$$scope_0.Name", "Lady Gaga"] - } - }; - - assert_eq!(selector, expected); - Ok(()) - } - - #[test] - fn root_column_reference_refereces_column_of_nearest_query() -> anyhow::Result<()> { - let request = query_request() - .collection("Artist") - .query( - query().fields([relation_field!("Albums" => "Albums", query().predicate( - binop( - "_gt", - target!("Milliseconds", relations: [ - path_element("Tracks".into()).predicate( - binop("_eq", target!("Name"), column_value!(root("Title"))) - ), - ]), - value!(30_000), - ) - ))]), - ) - .relationships(chinook_relationships()) - .into(); - - let config = chinook_config(); - let plan = plan_for_query_request(&config, request)?; - let pipeline = pipeline_for_query_request(&config, &plan)?; - - let expected_pipeline = bson!([ - { - "$lookup": { - "from": "Album", - "localField": "ArtistId", - "foreignField": "ArtistId", - "as": "Albums", - "let": { - "scope_root": "$$ROOT", - }, - "pipeline": [ - { - "$lookup": { - "from": "Track", - "localField": "AlbumId", - "foreignField": "AlbumId", - "as": "Tracks", - "let": { - "scope_0": "$$ROOT", - }, - "pipeline": [ - { - "$match": { - "$expr": { "$eq": ["$Name", "$$scope_0.Title"] }, - }, - }, - { - "$replaceWith": { - "Milliseconds": { "$ifNull": ["$Milliseconds", null] } - } - }, - ] - } - }, - { - "$match": { - "Tracks": { - "$elemMatch": { - "Milliseconds": { "$gt": 30_000 } - } - } - } - }, - { - "$replaceWith": { - "Tracks": { "$getField": { "$literal": "Tracks" } } - } - }, - ], - }, - }, - { - "$replaceWith": { - "Albums": { - "rows": [] - } - } - }, - ]); - - assert_eq!(bson::to_bson(&pipeline).unwrap(), expected_pipeline); - Ok(()) - } + // #[test] + // fn root_column_reference_refereces_column_of_nearest_query() -> anyhow::Result<()> { + // let request = query_request() + // .collection("Artist") + // .query( + // query().fields([relation_field!("Albums" => "Albums", query().predicate( + // binop( + // "_gt", + // target!("Milliseconds", relations: [ + // path_element("Tracks".into()).predicate( + // binop("_eq", target!("Name"), column_value!(root("Title"))) + // ), + // ]), + // value!(30_000), + // ) + // ))]), + // ) + // .relationships(chinook_relationships()) + // .into(); + // + // let config = chinook_config(); + // let plan = plan_for_query_request(&config, request)?; + // let pipeline = pipeline_for_query_request(&config, &plan)?; + // + // let expected_pipeline = bson!([ + // { + // "$lookup": { + // "from": "Album", + // "localField": "ArtistId", + // "foreignField": "ArtistId", + // "as": "Albums", + // "let": { + // "scope_root": "$$ROOT", + // }, + // "pipeline": [ + // { + // "$lookup": { + // "from": "Track", + // "localField": "AlbumId", + // "foreignField": "AlbumId", + // "as": "Tracks", + // "let": { + // "scope_0": "$$ROOT", + // }, + // "pipeline": [ + // { + // "$match": { + // "$expr": { "$eq": ["$Name", "$$scope_0.Title"] }, + // }, + // }, + // { + // "$replaceWith": { + // "Milliseconds": { "$ifNull": ["$Milliseconds", null] } + // } + // }, + // ] + // } + // }, + // { + // "$match": { + // "Tracks": { + // "$elemMatch": { + // "Milliseconds": { "$gt": 30_000 } + // } + // } + // } + // }, + // { + // "$replaceWith": { + // "Tracks": { "$getField": { "$literal": "Tracks" } } + // } + // }, + // ], + // }, + // }, + // { + // "$replaceWith": { + // "Albums": { + // "rows": [] + // } + // } + // }, + // ]); + // + // assert_eq!(bson::to_bson(&pipeline).unwrap(), expected_pipeline); + // Ok(()) + // } #[test] fn compares_value_to_elements_of_array_field() -> anyhow::Result<()> { @@ -268,12 +272,10 @@ mod tests { field_path: Default::default(), }, predicate: Some(Box::new(Expression::BinaryComparisonOperator { - column: ComparisonTarget::Column { - name: "last_name".into(), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - field_path: Default::default(), - path: Default::default(), - }, + column: ComparisonTarget::column( + "last_name", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), operator: ComparisonFunction::Equal, value: ComparisonValue::Scalar { value: "Hughes".into(), @@ -303,12 +305,10 @@ mod tests { field_path: vec!["site_info".into()], }, predicate: Some(Box::new(Expression::BinaryComparisonOperator { - column: ComparisonTarget::Column { - name: "last_name".into(), - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), - field_path: Default::default(), - path: Default::default(), - }, + column: ComparisonTarget::column( + "last_name", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), operator: ComparisonFunction::Equal, value: ComparisonValue::Scalar { value: "Hughes".into(), @@ -318,7 +318,7 @@ mod tests { })?; let expected = doc! { - "site_info.staff": { + "staff.site_info": { "$elemMatch": { "last_name": { "$eq": "Hughes" } } diff --git a/crates/mongodb-agent-common/src/query/make_sort.rs b/crates/mongodb-agent-common/src/query/make_sort.rs index 7adad5a8..5046ea6b 100644 --- a/crates/mongodb-agent-common/src/query/make_sort.rs +++ b/crates/mongodb-agent-common/src/query/make_sort.rs @@ -27,7 +27,7 @@ pub fn make_sort_stages(order_by: &OrderBy) -> Result> { if !required_aliases.is_empty() { let fields = required_aliases .into_iter() - .map(|(alias, expression)| (alias, expression.into_aggregate_expression())) + .map(|(alias, expression)| (alias, expression.into_aggregate_expression().into_bson())) .collect(); let stage = Stage::AddFields(fields); stages.push(stage); @@ -80,6 +80,7 @@ fn safe_alias(target: &OrderByTarget) -> Result { name, field_path, path, + .. } => { let name_and_path = once("__sort_key_") .chain(path.iter().map(|n| n.as_str())) @@ -95,17 +96,9 @@ fn safe_alias(target: &OrderByTarget) -> Result { &combine_all_elements_into_one_name, )) } - ndc_query_plan::OrderByTarget::SingleColumnAggregate { .. } => { - // TODO: ENG-1011 - Err(MongoAgentError::NotImplemented( - "ordering by single column aggregate".into(), - )) - } - ndc_query_plan::OrderByTarget::StarCountAggregate { .. } => { - // TODO: ENG-1010 - Err(MongoAgentError::NotImplemented( - "ordering by star count aggregate".into(), - )) + ndc_query_plan::OrderByTarget::Aggregate { .. } => { + // TODO: ENG-1010, ENG-1011 + Err(MongoAgentError::NotImplemented("order by aggregate".into())) } } } @@ -116,6 +109,7 @@ mod tests { use mongodb_support::aggregate::SortDocument; use ndc_models::{FieldName, OrderDirection}; use ndc_query_plan::OrderByElement; + use nonempty::{nonempty, NonEmpty}; use pretty_assertions::assert_eq; use crate::{mongo_query_plan::OrderBy, query::column_ref::ColumnRef}; @@ -131,10 +125,11 @@ mod tests { name: "$schema".into(), field_path: Default::default(), path: Default::default(), + arguments: Default::default(), }, }], }; - let path: [FieldName; 1] = ["$schema".into()]; + let path: NonEmpty = NonEmpty::singleton("$schema".into()); let actual = make_sort(&order_by)?; let expected_sort_doc = SortDocument(doc! { @@ -142,7 +137,7 @@ mod tests { }); let expected_aliases = [( "__sort_key__·24schema".into(), - ColumnRef::from_field_path(path.iter()), + ColumnRef::from_field_path(path.as_ref()), )] .into(); assert_eq!(actual, (expected_sort_doc, expected_aliases)); @@ -158,10 +153,11 @@ mod tests { name: "configuration".into(), field_path: Some(vec!["$schema".into()]), path: Default::default(), + arguments: Default::default(), }, }], }; - let path: [FieldName; 2] = ["configuration".into(), "$schema".into()]; + let path: NonEmpty = nonempty!["configuration".into(), "$schema".into()]; let actual = make_sort(&order_by)?; let expected_sort_doc = SortDocument(doc! { @@ -169,7 +165,7 @@ mod tests { }); let expected_aliases = [( "__sort_key__configuration_·24schema".into(), - ColumnRef::from_field_path(path.iter()), + ColumnRef::from_field_path(path.as_ref()), )] .into(); assert_eq!(actual, (expected_sort_doc, expected_aliases)); diff --git a/crates/mongodb-agent-common/src/query/mod.rs b/crates/mongodb-agent-common/src/query/mod.rs index d6094ca6..6bc505af 100644 --- a/crates/mongodb-agent-common/src/query/mod.rs +++ b/crates/mongodb-agent-common/src/query/mod.rs @@ -1,7 +1,9 @@ +mod aggregates; pub mod column_ref; -mod constants; mod execute_query_request; mod foreach; +mod groups; +mod is_response_faceted; mod make_selector; mod make_sort; mod native_query; @@ -11,6 +13,7 @@ mod query_target; mod query_variable_name; mod relations; pub mod response; +mod selection; pub mod serialization; use ndc_models::{QueryRequest, QueryResponse}; @@ -19,7 +22,7 @@ use self::execute_query_request::execute_query_request; pub use self::{ make_selector::make_selector, make_sort::make_sort_stages, - pipeline::{is_response_faceted, pipeline_for_non_foreach, pipeline_for_query_request}, + pipeline::{pipeline_for_non_foreach, pipeline_for_query_request}, query_target::QueryTarget, response::QueryResponseError, }; @@ -44,11 +47,10 @@ mod tests { use mongodb::bson::{self, bson}; use ndc_models::{QueryResponse, RowSet}; use ndc_test_helpers::{ - binop, collection, column_aggregate, column_count_aggregate, field, named_type, - object_type, query, query_request, row_set, target, value, + binop, collection, field, named_type, object_type, query, query_request, row_set, target, + value, }; use pretty_assertions::assert_eq; - use serde_json::json; use super::execute_query_request; use crate::{ @@ -92,150 +94,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn executes_aggregation() -> Result<(), anyhow::Error> { - let query_request = query_request() - .collection("students") - .query(query().aggregates([ - column_count_aggregate!("count" => "gpa", distinct: true), - column_aggregate!("avg" => "gpa", "avg"), - ])) - .into(); - - let expected_response = row_set() - .aggregates([("count", json!(11)), ("avg", json!(3))]) - .into_response(); - - let expected_pipeline = bson!([ - { - "$facet": { - "avg": [ - { "$match": { "gpa": { "$ne": null } } }, - { "$group": { "_id": null, "result": { "$avg": "$gpa" } } }, - ], - "count": [ - { "$match": { "gpa": { "$ne": null } } }, - { "$group": { "_id": "$gpa" } }, - { "$count": "result" }, - ], - }, - }, - { - "$replaceWith": { - "aggregates": { - "avg": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "avg" } } }, - } - }, - null - ] - }, - "count": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "count" } } }, - } - }, - 0, - ] - }, - }, - }, - }, - ]); - - let db = mock_collection_aggregate_response_for_pipeline( - "students", - expected_pipeline, - bson!([{ - "aggregates": { - "count": 11, - "avg": 3, - }, - }]), - ); - - let result = execute_query_request(db, &students_config(), query_request).await?; - assert_eq!(result, expected_response); - Ok(()) - } - - #[tokio::test] - async fn executes_aggregation_with_fields() -> Result<(), anyhow::Error> { - let query_request = query_request() - .collection("students") - .query( - query() - .aggregates([column_aggregate!("avg" => "gpa", "avg")]) - .fields([field!("student_gpa" => "gpa")]) - .predicate(binop("_lt", target!("gpa"), value!(4.0))), - ) - .into(); - - let expected_response = row_set() - .aggregates([("avg", json!(3.1))]) - .row([("student_gpa", 3.1)]) - .into_response(); - - let expected_pipeline = bson!([ - { "$match": { "gpa": { "$lt": 4.0 } } }, - { - "$facet": { - "__ROWS__": [{ - "$replaceWith": { - "student_gpa": { "$ifNull": ["$gpa", null] }, - }, - }], - "avg": [ - { "$match": { "gpa": { "$ne": null } } }, - { "$group": { "_id": null, "result": { "$avg": "$gpa" } } }, - ], - }, - }, - { - "$replaceWith": { - "aggregates": { - "avg": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "avg" } } }, - } - }, - null - ] - }, - }, - "rows": "$__ROWS__", - }, - }, - ]); - - let db = mock_collection_aggregate_response_for_pipeline( - "students", - expected_pipeline, - bson!([{ - "aggregates": { - "avg": 3.1, - }, - "rows": [{ - "student_gpa": 3.1, - }], - }]), - ); - - let result = execute_query_request(db, &students_config(), query_request).await?; - assert_eq!(result, expected_response); - Ok(()) - } - #[tokio::test] async fn converts_date_inputs_to_bson() -> Result<(), anyhow::Error> { let query_request = query_request() @@ -287,6 +145,7 @@ mod tests { let expected_response = QueryResponse(vec![RowSet { aggregates: None, rows: Some(vec![]), + groups: Default::default(), }]); let db = mock_collection_aggregate_response("comments", bson!([])); diff --git a/crates/mongodb-agent-common/src/query/pipeline.rs b/crates/mongodb-agent-common/src/query/pipeline.rs index f89d2c8f..5bfe3290 100644 --- a/crates/mongodb-agent-common/src/query/pipeline.rs +++ b/crates/mongodb-agent-common/src/query/pipeline.rs @@ -1,53 +1,31 @@ use std::collections::BTreeMap; -use configuration::MongoScalarType; use itertools::Itertools; -use mongodb::bson::{self, doc, Bson}; -use mongodb_support::{ - aggregate::{Accumulator, Pipeline, Selection, Stage}, - BsonScalarType, -}; -use ndc_models::FieldName; +use mongodb::bson::{bson, Bson}; +use mongodb_support::aggregate::{Pipeline, Selection, Stage}; use tracing::instrument; use crate::{ - aggregation_function::AggregationFunction, - comparison_function::ComparisonFunction, + constants::{ROW_SET_AGGREGATES_KEY, ROW_SET_GROUPS_KEY, ROW_SET_ROWS_KEY}, interface_types::MongoAgentError, - mongo_query_plan::{ - Aggregate, ComparisonTarget, ComparisonValue, Expression, MongoConfiguration, Query, - QueryPlan, Type, - }, - mongodb::{sanitize::get_field, selection_from_query_request}, + mongo_query_plan::{MongoConfiguration, Query, QueryPlan}, }; use super::{ - column_ref::ColumnRef, - constants::{RESULT_FIELD, ROWS_FIELD}, - foreach::pipeline_for_foreach, - make_selector, - make_sort::make_sort_stages, - native_query::pipeline_for_native_query, - query_level::QueryLevel, - relations::pipeline_for_relations, + aggregates::pipeline_for_aggregates, column_ref::ColumnRef, foreach::pipeline_for_foreach, + groups::pipeline_for_groups, is_response_faceted::ResponseFacets, make_selector, + make_sort::make_sort_stages, native_query::pipeline_for_native_query, query_level::QueryLevel, + relations::pipeline_for_relations, selection::selection_for_fields, }; -/// A query that includes aggregates will be run using a $facet pipeline stage, while a query -/// without aggregates will not. The choice affects how result rows are mapped to a QueryResponse. -/// -/// If we have aggregate pipelines they should be combined with the fields pipeline (if there is -/// one) in a single facet stage. If we have fields, and no aggregates then the fields pipeline -/// can instead be appended to `pipeline`. -pub fn is_response_faceted(query: &Query) -> bool { - query.has_aggregates() -} +type Result = std::result::Result; /// Shared logic to produce a MongoDB aggregation pipeline for a query request. #[instrument(name = "Build Query Pipeline" skip_all, fields(internal.visibility = "user"))] pub fn pipeline_for_query_request( config: &MongoConfiguration, query_plan: &QueryPlan, -) -> Result { +) -> Result { if let Some(variable_sets) = &query_plan.variables { pipeline_for_foreach(variable_sets, config, query_plan) } else { @@ -62,9 +40,10 @@ pub fn pipeline_for_non_foreach( config: &MongoConfiguration, query_plan: &QueryPlan, query_level: QueryLevel, -) -> Result { +) -> Result { let query = &query_plan.query; let Query { + limit, offset, order_by, predicate, @@ -87,148 +66,105 @@ pub fn pipeline_for_non_foreach( .iter() .map(make_sort_stages) .flatten_ok() - .collect::, _>>()?; + .collect::>>()?; + let limit_stage = limit.map(Into::into).map(Stage::Limit); let skip_stage = offset.map(Into::into).map(Stage::Skip); match_stage .into_iter() .chain(sort_stages) .chain(skip_stage) + .chain(limit_stage) .for_each(|stage| pipeline.push(stage)); - // `diverging_stages` includes either a $facet stage if the query includes aggregates, or the - // sort and limit stages if we are requesting rows only. In both cases the last stage is - // a $replaceWith. - let diverging_stages = if is_response_faceted(query) { - let (facet_pipelines, select_facet_results) = - facet_pipelines_for_query(query_plan, query_level)?; - let aggregation_stages = Stage::Facet(facet_pipelines); - let replace_with_stage = Stage::ReplaceWith(select_facet_results); - Pipeline::from_iter([aggregation_stages, replace_with_stage]) - } else { - pipeline_for_fields_facet(query_plan, query_level)? + let diverging_stages = match ResponseFacets::from_query(query) { + ResponseFacets::Combination { .. } => { + let (facet_pipelines, select_facet_results) = + facet_pipelines_for_query(query_plan, query_level)?; + let facet_stage = Stage::Facet(facet_pipelines); + let replace_with_stage = Stage::ReplaceWith(select_facet_results); + Pipeline::new(vec![facet_stage, replace_with_stage]) + } + ResponseFacets::AggregatesOnly(aggregates) => pipeline_for_aggregates(aggregates), + ResponseFacets::FieldsOnly(_) => pipeline_for_fields_facet(query_plan, query_level)?, + ResponseFacets::GroupsOnly(grouping) => pipeline_for_groups(grouping)?, }; pipeline.append(diverging_stages); Ok(pipeline) } -/// Generate a pipeline to select fields requested by the given query. This is intended to be used -/// within a $facet stage. We assume that the query's `where`, `order_by`, `offset` criteria (which -/// are shared with aggregates) have already been applied, and that we have already joined -/// relations. -pub fn pipeline_for_fields_facet( - query_plan: &QueryPlan, - query_level: QueryLevel, -) -> Result { - let Query { - limit, - relationships, - .. - } = &query_plan.query; - - let mut selection = selection_from_query_request(query_plan)?; - if query_level != QueryLevel::Top { - // Queries higher up the chain might need to reference relationships from this query. So we - // forward relationship arrays if this is not the top-level query. - for relationship_key in relationships.keys() { - selection = selection.try_map_document(|mut doc| { - doc.insert( - relationship_key.to_owned(), - get_field(relationship_key.as_str()), - ); - doc - })?; - } - } - - let limit_stage = limit.map(Into::into).map(Stage::Limit); - let replace_with_stage: Stage = Stage::ReplaceWith(selection); - - Ok(Pipeline::from_iter( - [limit_stage, replace_with_stage.into()] - .into_iter() - .flatten(), - )) -} - /// Returns a map of pipelines for evaluating each aggregate independently, paired with /// a `Selection` that converts results of each pipeline to a format compatible with /// `QueryResponse`. fn facet_pipelines_for_query( query_plan: &QueryPlan, query_level: QueryLevel, -) -> Result<(BTreeMap, Selection), MongoAgentError> { +) -> Result<(BTreeMap, Selection)> { let query = &query_plan.query; let Query { aggregates, - aggregates_limit, fields, + groups, .. } = query; - let mut facet_pipelines = aggregates - .iter() - .flatten() - .map(|(key, aggregate)| { - Ok(( - key.to_string(), - pipeline_for_aggregate(aggregate.clone(), *aggregates_limit)?, - )) - }) - .collect::, MongoAgentError>>()?; - - if fields.is_some() { - let fields_pipeline = pipeline_for_fields_facet(query_plan, query_level)?; - facet_pipelines.insert(ROWS_FIELD.to_owned(), fields_pipeline); - } - - // This builds a map that feeds into a `$replaceWith` pipeline stage to build a map of - // aggregation results. - let aggregate_selections: bson::Document = aggregates - .iter() - .flatten() - .map(|(key, aggregate)| { - // The facet result for each aggregate is an array containing a single document which - // has a field called `result`. This code selects each facet result by name, and pulls - // out the `result` value. - let value_expr = doc! { - "$getField": { - "field": RESULT_FIELD, // evaluates to the value of this field - "input": { "$first": get_field(key.as_str()) }, // field is accessed from this document - }, - }; - - // Matching SQL semantics, if a **count** aggregation does not match any rows we want - // to return zero. Other aggregations should return null. - let value_expr = if is_count(aggregate) { - doc! { - "$ifNull": [value_expr, 0], - } - // Otherwise if the aggregate value is missing because the aggregation applied to an - // empty document set then provide an explicit `null` value. - } else { - doc! { - "$ifNull": [value_expr, null] - } - }; - - (key.to_string(), value_expr.into()) - }) - .collect(); + let mut facet_pipelines = BTreeMap::new(); + + let (aggregates_pipeline_facet, select_aggregates) = match aggregates { + Some(aggregates) => { + let internal_key = "__AGGREGATES__"; + let aggregates_pipeline = pipeline_for_aggregates(aggregates); + let facet = (internal_key.to_string(), aggregates_pipeline); + let selection = ( + ROW_SET_AGGREGATES_KEY.to_string(), + bson!({ "$first": format!("${internal_key}") }), + ); + (Some(facet), Some(selection)) + } + None => (None, None), + }; - let select_aggregates = if !aggregate_selections.is_empty() { - Some(("aggregates".to_owned(), aggregate_selections.into())) - } else { - None + let (groups_pipeline_facet, select_groups) = match groups { + Some(grouping) => { + let internal_key = "__GROUPS__"; + let groups_pipeline = pipeline_for_groups(grouping)?; + let facet = (internal_key.to_string(), groups_pipeline); + let selection = ( + ROW_SET_GROUPS_KEY.to_string(), + Bson::String(format!("${internal_key}")), + ); + (Some(facet), Some(selection)) + } + None => (None, None), }; - let select_rows = match fields { - Some(_) => Some(("rows".to_owned(), Bson::String(format!("${ROWS_FIELD}")))), - _ => None, + let (rows_pipeline_facet, select_rows) = match fields { + Some(_) => { + let internal_key = "__ROWS__"; + let rows_pipeline = pipeline_for_fields_facet(query_plan, query_level)?; + let facet = (internal_key.to_string(), rows_pipeline); + let selection = ( + ROW_SET_ROWS_KEY.to_string().to_string(), + Bson::String(format!("${internal_key}")), + ); + (Some(facet), Some(selection)) + } + None => (None, None), }; + for (key, pipeline) in [ + aggregates_pipeline_facet, + groups_pipeline_facet, + rows_pipeline_facet, + ] + .into_iter() + .flatten() + { + facet_pipelines.insert(key, pipeline); + } + let selection = Selection::new( - [select_aggregates, select_rows] + [select_aggregates, select_groups, select_rows] .into_iter() .flatten() .collect(), @@ -237,127 +173,31 @@ fn facet_pipelines_for_query( Ok((facet_pipelines, selection)) } -fn is_count(aggregate: &Aggregate) -> bool { - match aggregate { - Aggregate::ColumnCount { .. } => true, - Aggregate::StarCount { .. } => true, - Aggregate::SingleColumn { function, .. } => function.is_count(), - } -} +/// Generate a pipeline to select fields requested by the given query. This is intended to be used +/// within a $facet stage. We assume that the query's `where`, `order_by`, `offset`, `limit` +/// criteria (which are shared with aggregates) have already been applied, and that we have already +/// joined relations. +pub fn pipeline_for_fields_facet( + query_plan: &QueryPlan, + query_level: QueryLevel, +) -> Result { + let Query { relationships, .. } = &query_plan.query; -fn pipeline_for_aggregate( - aggregate: Aggregate, - limit: Option, -) -> Result { - fn mk_target_field(name: FieldName, field_path: Option>) -> ComparisonTarget { - ComparisonTarget::Column { - name, - field_path, - field_type: Type::Scalar(MongoScalarType::ExtendedJSON), // type does not matter here - path: Default::default(), + let mut selection = selection_for_fields(query_plan.query.fields.as_ref())?; + if query_level != QueryLevel::Top { + // Queries higher up the chain might need to reference relationships from this query. So we + // forward relationship arrays if this is not the top-level query. + for relationship_key in relationships.keys() { + selection = selection.try_map_document(|mut doc| { + doc.insert( + relationship_key.to_owned(), + ColumnRef::from_field(relationship_key.as_str()).into_aggregate_expression(), + ); + doc + })?; } } - fn filter_to_documents_with_value( - target_field: ComparisonTarget, - ) -> Result { - Ok(Stage::Match(make_selector( - &Expression::BinaryComparisonOperator { - column: target_field, - operator: ComparisonFunction::NotEqual, - value: ComparisonValue::Scalar { - value: serde_json::Value::Null, - value_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::Null)), - }, - }, - )?)) - } - - let pipeline = match aggregate { - Aggregate::ColumnCount { - column, - field_path, - distinct, - } if distinct => { - let target_field = mk_target_field(column, field_path); - Pipeline::from_iter( - [ - Some(filter_to_documents_with_value(target_field.clone())?), - limit.map(Into::into).map(Stage::Limit), - Some(Stage::Group { - key_expression: ColumnRef::from_comparison_target(&target_field) - .into_aggregate_expression(), - accumulators: [].into(), - }), - Some(Stage::Count(RESULT_FIELD.to_string())), - ] - .into_iter() - .flatten(), - ) - } - - Aggregate::ColumnCount { - column, - field_path, - distinct: _, - } => Pipeline::from_iter( - [ - Some(filter_to_documents_with_value(mk_target_field( - column, field_path, - ))?), - limit.map(Into::into).map(Stage::Limit), - Some(Stage::Count(RESULT_FIELD.to_string())), - ] - .into_iter() - .flatten(), - ), - - Aggregate::SingleColumn { - column, - field_path, - function, - result_type: _, - } => { - use AggregationFunction::*; - - let target_field = ComparisonTarget::Column { - name: column.clone(), - field_path, - field_type: Type::Scalar(MongoScalarType::Bson(BsonScalarType::Null)), // type does not matter here - path: Default::default(), - }; - let field_ref = - ColumnRef::from_comparison_target(&target_field).into_aggregate_expression(); - - let accumulator = match function { - Avg => Accumulator::Avg(field_ref), - Count => Accumulator::Count, - Min => Accumulator::Min(field_ref), - Max => Accumulator::Max(field_ref), - Sum => Accumulator::Sum(field_ref), - }; - Pipeline::from_iter( - [ - Some(filter_to_documents_with_value(target_field)?), - limit.map(Into::into).map(Stage::Limit), - Some(Stage::Group { - key_expression: Bson::Null, - accumulators: [(RESULT_FIELD.to_string(), accumulator)].into(), - }), - ] - .into_iter() - .flatten(), - ) - } - - Aggregate::StarCount {} => Pipeline::from_iter( - [ - limit.map(Into::into).map(Stage::Limit), - Some(Stage::Count(RESULT_FIELD.to_string())), - ] - .into_iter() - .flatten(), - ), - }; - Ok(pipeline) + let replace_with_stage: Stage = Stage::ReplaceWith(selection); + Ok(Pipeline::new(vec![replace_with_stage])) } diff --git a/crates/mongodb-agent-common/src/query/query_variable_name.rs b/crates/mongodb-agent-common/src/query/query_variable_name.rs index bacaccbe..66589962 100644 --- a/crates/mongodb-agent-common/src/query/query_variable_name.rs +++ b/crates/mongodb-agent-common/src/query/query_variable_name.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use configuration::MongoScalarType; +use itertools::Itertools; use crate::{ mongo_query_plan::{ObjectType, Type}, @@ -28,13 +29,14 @@ fn type_name(input_type: &Type) -> Cow<'static, str> { Type::Object(obj) => object_type_name(obj).into(), Type::ArrayOf(t) => format!("[{}]", type_name(t)).into(), Type::Nullable(t) => format!("nullable({})", type_name(t)).into(), + Type::Tuple(ts) => format!("({})", ts.iter().map(type_name).join(", ")).into(), } } fn object_type_name(obj: &ObjectType) -> String { let mut output = "{".to_string(); for (key, t) in &obj.fields { - output.push_str(&format!("{key}:{}", type_name(t))); + output.push_str(&format!("{key}:{}", type_name(&t.r#type))); } output.push('}'); output diff --git a/crates/mongodb-agent-common/src/query/relations.rs b/crates/mongodb-agent-common/src/query/relations.rs index 44efcc6f..089b3caa 100644 --- a/crates/mongodb-agent-common/src/query/relations.rs +++ b/crates/mongodb-agent-common/src/query/relations.rs @@ -4,6 +4,7 @@ use itertools::Itertools as _; use mongodb::bson::{doc, Document}; use mongodb_support::aggregate::{Pipeline, Stage}; use ndc_query_plan::Scope; +use nonempty::NonEmpty; use crate::mongo_query_plan::{MongoConfiguration, Query, QueryPlan}; use crate::query::column_ref::name_from_scope; @@ -59,7 +60,7 @@ pub fn pipeline_for_relations( fn make_lookup_stage( from: ndc_models::CollectionName, - column_mapping: &BTreeMap, + column_mapping: &BTreeMap>, r#as: ndc_models::RelationshipName, lookup_pipeline: Pipeline, scope: Option<&Scope>, @@ -67,41 +68,30 @@ fn make_lookup_stage( // If there is a single column mapping, and the source and target field references can be // expressed as match keys (we don't need to escape field names), then we can use a concise // correlated subquery. Otherwise we need to fall back to an uncorrelated subquery. - let safe_single_column_mapping = if column_mapping.len() == 1 { - // Safe to unwrap because we just checked the hashmap size - let (source_selector, target_selector) = column_mapping.iter().next().unwrap(); - - let source_ref = ColumnRef::from_field(source_selector); - let target_ref = ColumnRef::from_field(target_selector); - - match (source_ref, target_ref) { - (ColumnRef::MatchKey(source_key), ColumnRef::MatchKey(target_key)) => { - Some((source_key.to_string(), target_key.to_string())) - } - - // If the source and target refs cannot be expressed in required syntax then we need to - // fall back to a lookup pipeline that con compare arbitrary expressions. - // [multiple_column_mapping_lookup] does this. - _ => None, - } + let single_mapping = if column_mapping.len() == 1 { + column_mapping.iter().next() } else { None }; - - match safe_single_column_mapping { - Some((source_selector_key, target_selector_key)) => { - lookup_with_concise_correlated_subquery( - from, - source_selector_key, - target_selector_key, - r#as, - lookup_pipeline, - scope, - ) - } - None => { - lookup_with_uncorrelated_subquery(from, column_mapping, r#as, lookup_pipeline, scope) - } + let source_selector = single_mapping.map(|(field_name, _)| field_name); + let target_selector = single_mapping.map(|(_, target_path)| target_path); + + let source_key = + source_selector.and_then(|f| ColumnRef::from_field(f.as_ref()).into_match_key()); + let target_key = + target_selector.and_then(|path| ColumnRef::from_field_path(path.as_ref()).into_match_key()); + + match (source_key, target_key) { + (Some(source_key), Some(target_key)) => lookup_with_concise_correlated_subquery( + from, + source_key.into_owned(), + target_key.into_owned(), + r#as, + lookup_pipeline, + scope, + ), + + _ => lookup_with_uncorrelated_subquery(from, column_mapping, r#as, lookup_pipeline, scope), } } @@ -138,7 +128,7 @@ fn lookup_with_concise_correlated_subquery( /// cases like joining on field names that require escaping. fn lookup_with_uncorrelated_subquery( from: ndc_models::CollectionName, - column_mapping: &BTreeMap, + column_mapping: &BTreeMap>, r#as: ndc_models::RelationshipName, lookup_pipeline: Pipeline, scope: Option<&Scope>, @@ -148,7 +138,9 @@ fn lookup_with_uncorrelated_subquery( .map(|local_field| { ( variable(local_field.as_str()), - ColumnRef::from_field(local_field).into_aggregate_expression(), + ColumnRef::from_field(local_field.as_ref()) + .into_aggregate_expression() + .into_bson(), ) }) .collect(); @@ -160,16 +152,16 @@ fn lookup_with_uncorrelated_subquery( // Creating an intermediate Vec and sorting it is done just to help with testing. // A stable order for matchers makes it easier to assert equality between actual // and expected pipelines. - let mut column_pairs: Vec<(&ndc_models::FieldName, &ndc_models::FieldName)> = + let mut column_pairs: Vec<(&ndc_models::FieldName, &NonEmpty)> = column_mapping.iter().collect(); column_pairs.sort(); let matchers: Vec = column_pairs .into_iter() - .map(|(local_field, remote_field)| { + .map(|(local_field, remote_field_path)| { doc! { "$eq": [ ColumnRef::variable(variable(local_field.as_str())).into_aggregate_expression(), - ColumnRef::from_field(remote_field).into_aggregate_expression(), + ColumnRef::from_field_path(remote_field_path.as_ref()).into_aggregate_expression(), ] } }) .collect(); @@ -223,7 +215,7 @@ mod tests { ])) .relationships([( "class_students", - relationship("students", [("_id", "classId")]), + relationship("students", [("_id", &["classId"])]), )]) .into(); @@ -265,7 +257,7 @@ mod tests { "students": { "rows": { "$map": { - "input": { "$getField": { "$literal": "class_students" } }, + "input": "$class_students", "in": { "student_name": "$$this.student_name" } @@ -306,7 +298,7 @@ mod tests { ])) .relationships([( "student_class", - relationship("classes", [("classId", "_id")]), + relationship("classes", [("classId", &["_id"])]), )]) .into(); @@ -354,7 +346,7 @@ mod tests { "class": { "rows": { "$map": { - "input": { "$getField": { "$literal": "student_class" } }, + "input": "$student_class", "in": { "class_title": "$$this.class_title" } @@ -398,7 +390,10 @@ mod tests { ])) .relationships([( "students", - relationship("students", [("title", "class_title"), ("year", "year")]), + relationship( + "students", + [("title", &["class_title"]), ("year", &["year"])], + ), )]) .into(); @@ -448,7 +443,7 @@ mod tests { "students": { "rows": { "$map": { - "input": { "$getField": { "$literal": "students" } }, + "input": "$students", "in": { "student_name": "$$this.student_name" } @@ -489,7 +484,7 @@ mod tests { ])) .relationships([( "join", - relationship("weird_field_names", [("$invalid.name", "$invalid.name")]), + relationship("weird_field_names", [("$invalid.name", &["$invalid.name"])]), )]) .into(); @@ -525,7 +520,7 @@ mod tests { "join": { "rows": { "$map": { - "input": { "$getField": { "$literal": "join" } }, + "input": "$join", "in": { "invalid_name": "$$this.invalid_name", } @@ -562,10 +557,13 @@ mod tests { ])), ])) .relationships([ - ("students", relationship("students", [("_id", "class_id")])), + ( + "students", + relationship("students", [("_id", &["class_id"])]), + ), ( "assignments", - relationship("assignments", [("_id", "student_id")]), + relationship("assignments", [("_id", &["student_id"])]), ), ]) .into(); @@ -624,7 +622,7 @@ mod tests { }, { "$replaceWith": { - "assignments": { "$getField": { "$literal": "assignments" } }, + "assignments": "$assignments", "student_name": { "$ifNull": ["$name", null] }, }, }, @@ -638,7 +636,7 @@ mod tests { "students": { "rows": { "$map": { - "input": { "$getField": { "$literal": "students" } }, + "input": "$students", "in": { "assignments": "$$this.assignments", "student_name": "$$this.student_name", @@ -694,7 +692,10 @@ mod tests { star_count_aggregate!("aggregate_count") ])), ])) - .relationships([("students", relationship("students", [("_id", "classId")]))]) + .relationships([( + "students", + relationship("students", [("_id", &["classId"])]), + )]) .into(); let expected_response = row_set() @@ -719,27 +720,14 @@ mod tests { }, "pipeline": [ { - "$facet": { - "aggregate_count": [ - { "$count": "result" }, - ], + "$group": { + "_id": null, + "aggregate_count": { "$sum": 1 }, } }, { "$replaceWith": { - "aggregates": { - "aggregate_count": { - "$ifNull": [ - { - "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "aggregate_count" } } }, - }, - }, - 0, - ] - }, - }, + "aggregate_count": { "$ifNull": ["$aggregate_count", 0] }, }, } ], @@ -749,16 +737,16 @@ mod tests { { "$replaceWith": { "students_aggregate": { - "$let": { - "vars": { - "row_set": { "$first": { "$getField": { "$literal": "students" } } } - }, - "in": { - "aggregates": { - "aggregate_count": "$$row_set.aggregates.aggregate_count" + "aggregates": { + "$let": { + "vars": { + "aggregates": { "$first": "$students" } + }, + "in": { + "aggregate_count": { "$ifNull": ["$$aggregates.aggregate_count", 0] } } } - } + }, } }, }, @@ -800,6 +788,7 @@ mod tests { ndc_models::ExistsInCollection::Related { relationship: "movie".into(), arguments: Default::default(), + field_path: Default::default(), }, binop( "_eq", @@ -810,7 +799,7 @@ mod tests { ) .relationships([( "movie", - relationship("movies", [("movie_id", "_id")]).object_type(), + relationship("movies", [("movie_id", &["_id"])]).object_type(), )]) .into(); @@ -862,7 +851,7 @@ mod tests { "movie": { "rows": { "$map": { - "input": { "$getField": { "$literal": "movie" } }, + "input": "$movie", "in": { "year": "$$this.year", "title": "$$this.title", @@ -913,6 +902,7 @@ mod tests { ndc_models::ExistsInCollection::Related { relationship: "movie".into(), arguments: Default::default(), + field_path: Default::default(), }, binop( "_eq", @@ -921,7 +911,7 @@ mod tests { ), )), ) - .relationships([("movie", relationship("movies", [("movie_id", "_id")]))]) + .relationships([("movie", relationship("movies", [("movie_id", &["_id"])]))]) .into(); let expected_response: QueryResponse = row_set() @@ -983,7 +973,7 @@ mod tests { "movie": { "rows": { "$map": { - "input": { "$getField": { "$literal": "movie" } }, + "input": "$movie", "in": { "credits": "$$this.credits", } diff --git a/crates/mongodb-agent-common/src/query/response.rs b/crates/mongodb-agent-common/src/query/response.rs index cec6f1b8..8ed67a47 100644 --- a/crates/mongodb-agent-common/src/query/response.rs +++ b/crates/mongodb-agent-common/src/query/response.rs @@ -1,21 +1,28 @@ -use std::collections::BTreeMap; +use std::{borrow::Cow, collections::BTreeMap}; use configuration::MongoScalarType; use indexmap::IndexMap; use itertools::Itertools; -use mongodb::bson::{self, Bson}; +use mongodb::bson::{self, doc, Bson}; use mongodb_support::ExtendedJsonMode; -use ndc_models::{QueryResponse, RowFieldValue, RowSet}; -use serde::Deserialize; +use ndc_models::{FieldName, Group, QueryResponse, RowFieldValue, RowSet}; +use serde_json::json; use thiserror::Error; use tracing::instrument; use crate::{ + constants::{ + BsonRowSet, GROUP_DIMENSIONS_KEY, ROW_SET_AGGREGATES_KEY, ROW_SET_GROUPS_KEY, + ROW_SET_ROWS_KEY, + }, mongo_query_plan::{ - Aggregate, Field, NestedArray, NestedField, NestedObject, ObjectType, Query, QueryPlan, - Type, + Aggregate, Dimension, Field, Grouping, NestedArray, NestedField, NestedObject, ObjectField, + ObjectType, Query, QueryPlan, Type, + }, + query::{ + is_response_faceted::ResponseFacets, + serialization::{bson_to_json, BsonToJsonError}, }, - query::serialization::{bson_to_json, BsonToJsonError}, }; use super::serialization::is_nullable; @@ -31,6 +38,9 @@ pub enum QueryResponseError { #[error("{0}")] BsonToJson(#[from] BsonToJsonError), + #[error("a group response is missing its '{GROUP_DIMENSIONS_KEY}' field")] + GroupMissingDimensions { path: Vec }, + #[error("expected a single response document from MongoDB, but did not get one")] ExpectedSingleDocument, @@ -40,14 +50,6 @@ pub enum QueryResponseError { type Result = std::result::Result; -#[derive(Debug, Deserialize)] -struct BsonRowSet { - #[serde(default)] - aggregates: Bson, - #[serde(default)] - rows: Vec, -} - #[instrument(name = "Serialize Query Response", skip_all, fields(internal.visibility = "user"))] pub fn serialize_query_response( mode: ExtendedJsonMode, @@ -61,7 +63,7 @@ pub fn serialize_query_response( .into_iter() .map(|document| { let row_set = bson::from_document(document)?; - serialize_row_set_with_aggregates( + serialize_row_set( mode, &[collection_name.as_str()], &query_plan.query, @@ -69,28 +71,45 @@ pub fn serialize_query_response( ) }) .try_collect() - } else if query_plan.query.has_aggregates() { - let row_set = parse_single_document(response_documents)?; - Ok(vec![serialize_row_set_with_aggregates( - mode, - &[], - &query_plan.query, - row_set, - )?]) } else { - Ok(vec![serialize_row_set_rows_only( - mode, - &[], - &query_plan.query, - response_documents, - )?]) + match ResponseFacets::from_query(&query_plan.query) { + ResponseFacets::Combination { .. } => { + let row_set = parse_single_document(response_documents)?; + Ok(vec![serialize_row_set( + mode, + &[], + &query_plan.query, + row_set, + )?]) + } + ResponseFacets::AggregatesOnly(aggregates) => { + Ok(vec![serialize_row_set_aggregates_only( + mode, + &[], + aggregates, + response_documents, + )?]) + } + ResponseFacets::FieldsOnly(_) => Ok(vec![serialize_row_set_rows_only( + mode, + &[], + &query_plan.query, + response_documents, + )?]), + ResponseFacets::GroupsOnly(grouping) => Ok(vec![serialize_row_set_groups_only( + mode, + &[], + grouping, + response_documents, + )?]), + } }?; let response = QueryResponse(row_sets); tracing::debug!(query_response = %serde_json::to_string(&response).unwrap()); Ok(response) } -// When there are no aggregates we expect a list of rows +// When there are no aggregates or groups we expect a list of rows fn serialize_row_set_rows_only( mode: ExtendedJsonMode, path: &[&str], @@ -106,12 +125,40 @@ fn serialize_row_set_rows_only( Ok(RowSet { aggregates: None, rows, + groups: None, + }) +} + +fn serialize_row_set_aggregates_only( + mode: ExtendedJsonMode, + path: &[&str], + aggregates: &IndexMap, + docs: Vec, +) -> Result { + let doc = docs.first().cloned().unwrap_or(doc! {}); + Ok(RowSet { + aggregates: Some(serialize_aggregates(mode, path, aggregates, doc)?), + rows: None, + groups: None, + }) +} + +fn serialize_row_set_groups_only( + mode: ExtendedJsonMode, + path: &[&str], + grouping: &Grouping, + docs: Vec, +) -> Result { + Ok(RowSet { + aggregates: None, + rows: None, + groups: Some(serialize_groups(mode, path, grouping, docs)?), }) } -// When there are aggregates we expect a single document with `rows` and `aggregates` -// fields -fn serialize_row_set_with_aggregates( +// When a query includes some combination of aggregates, rows, or groups then the response is +// "faceted" to give us a single document with `rows`, `aggregates`, and `groups` fields. +fn serialize_row_set( mode: ExtendedJsonMode, path: &[&str], query: &Query, @@ -120,7 +167,16 @@ fn serialize_row_set_with_aggregates( let aggregates = query .aggregates .as_ref() - .map(|aggregates| serialize_aggregates(mode, path, aggregates, row_set.aggregates)) + .map(|aggregates| { + let aggregate_values = row_set.aggregates.unwrap_or_else(|| doc! {}); + serialize_aggregates(mode, path, aggregates, aggregate_values) + }) + .transpose()?; + + let groups = query + .groups + .as_ref() + .map(|grouping| serialize_groups(mode, path, grouping, row_set.groups)) .transpose()?; let rows = query @@ -129,26 +185,41 @@ fn serialize_row_set_with_aggregates( .map(|fields| serialize_rows(mode, path, fields, row_set.rows)) .transpose()?; - Ok(RowSet { aggregates, rows }) + Ok(RowSet { + aggregates, + rows, + groups, + }) } fn serialize_aggregates( mode: ExtendedJsonMode, - path: &[&str], + _path: &[&str], query_aggregates: &IndexMap, - value: Bson, + value: bson::Document, ) -> Result> { - let aggregates_type = type_for_aggregates(query_aggregates); - let json = bson_to_json(mode, &aggregates_type, value)?; - - // The NDC type uses an IndexMap for aggregate values; we need to convert the map - // underlying the Value::Object value to an IndexMap - let aggregate_values = match json { - serde_json::Value::Object(obj) => obj.into_iter().map(|(k, v)| (k.into(), v)).collect(), - _ => Err(QueryResponseError::AggregatesNotObject { - path: path_to_owned(path), - })?, - }; + // The NDC type uses an IndexMap for aggregate values; we need to convert the map underlying + // the Value::Object value to an IndexMap. + // + // We also need to fill in missing aggregate values. This can be an issue in a query that does + // not match any documents. In that case instead of an object with null aggregate values + // MongoDB does not return any documents, so this function gets an empty document. + let aggregate_values = query_aggregates + .iter() + .map(|(key, aggregate)| { + let json_value = match value.get(key.as_str()).cloned() { + Some(bson_value) => bson_to_json(mode, &type_for_aggregate(aggregate), bson_value)?, + None => { + if aggregate.is_count() { + json!(0) + } else { + json!(null) + } + } + }; + Ok((key.clone(), json_value)) + }) + .collect::>()?; Ok(aggregate_values) } @@ -177,47 +248,140 @@ fn serialize_rows( .try_collect() } +fn serialize_groups( + mode: ExtendedJsonMode, + path: &[&str], + grouping: &Grouping, + docs: Vec, +) -> Result> { + docs.into_iter() + .map(|doc| { + let dimensions_field_value = doc.get(GROUP_DIMENSIONS_KEY).ok_or_else(|| { + QueryResponseError::GroupMissingDimensions { + path: path_to_owned(path), + } + })?; + + let dimensions_array = match dimensions_field_value { + Bson::Array(vec) => Cow::Borrowed(vec), + other_bson_value => Cow::Owned(vec![other_bson_value.clone()]), + }; + + let dimensions = grouping + .dimensions + .iter() + .zip(dimensions_array.iter()) + .map(|(dimension_definition, dimension_value)| { + Ok(bson_to_json( + mode, + dimension_definition.value_type(), + dimension_value.clone(), + )?) + }) + .collect::>()?; + + let aggregates = serialize_aggregates(mode, path, &grouping.aggregates, doc)?; + + // TODO: This conversion step can be removed when the aggregates map key type is + // changed from String to FieldName + let aggregates = aggregates + .into_iter() + .map(|(key, value)| (key.to_string(), value)) + .collect(); + + Ok(Group { + dimensions, + aggregates, + }) + }) + .try_collect() +} + fn type_for_row_set( path: &[&str], aggregates: &Option>, fields: &Option>, + groups: &Option, ) -> Result { - let mut type_fields = BTreeMap::new(); + let mut object_fields = BTreeMap::new(); if let Some(aggregates) = aggregates { - type_fields.insert("aggregates".into(), type_for_aggregates(aggregates)); + object_fields.insert( + ROW_SET_AGGREGATES_KEY.into(), + ObjectField { + r#type: Type::Object(type_for_aggregates(aggregates)), + parameters: Default::default(), + }, + ); } if let Some(query_fields) = fields { let row_type = type_for_row(path, query_fields)?; - type_fields.insert("rows".into(), Type::ArrayOf(Box::new(row_type))); + object_fields.insert( + ROW_SET_ROWS_KEY.into(), + ObjectField { + r#type: Type::ArrayOf(Box::new(row_type)), + parameters: Default::default(), + }, + ); + } + + if let Some(grouping) = groups { + let dimension_types = grouping + .dimensions + .iter() + .map(Dimension::value_type) + .cloned() + .collect(); + let dimension_tuple_type = Type::Tuple(dimension_types); + let mut group_object_type = type_for_aggregates(&grouping.aggregates); + group_object_type + .fields + .insert(GROUP_DIMENSIONS_KEY.into(), dimension_tuple_type.into()); + object_fields.insert( + ROW_SET_GROUPS_KEY.into(), + ObjectField { + r#type: Type::array_of(Type::Object(group_object_type)), + parameters: Default::default(), + }, + ); } Ok(Type::Object(ObjectType { - fields: type_fields, + fields: object_fields, name: None, })) } -fn type_for_aggregates(query_aggregates: &IndexMap) -> Type { +fn type_for_aggregates( + query_aggregates: &IndexMap, +) -> ObjectType { let fields = query_aggregates .iter() .map(|(field_name, aggregate)| { + let result_type = type_for_aggregate(aggregate); ( field_name.to_string().into(), - match aggregate { - Aggregate::ColumnCount { .. } => { - Type::Scalar(MongoScalarType::Bson(mongodb_support::BsonScalarType::Int)) - } - Aggregate::StarCount => { - Type::Scalar(MongoScalarType::Bson(mongodb_support::BsonScalarType::Int)) - } - Aggregate::SingleColumn { result_type, .. } => result_type.clone(), + ObjectField { + r#type: result_type, + parameters: Default::default(), }, ) }) .collect(); - Type::Object(ObjectType { fields, name: None }) + ObjectType { fields, name: None } +} + +fn type_for_aggregate(aggregate: &Aggregate) -> Type { + match aggregate { + Aggregate::ColumnCount { .. } => { + Type::Scalar(MongoScalarType::Bson(mongodb_support::BsonScalarType::Int)) + } + Aggregate::StarCount => { + Type::Scalar(MongoScalarType::Bson(mongodb_support::BsonScalarType::Int)) + } + Aggregate::SingleColumn { result_type, .. } => result_type.clone(), + } } fn type_for_row( @@ -231,7 +395,11 @@ fn type_for_row( &append_to_path(path, [field_name.as_str()]), field_definition, )?; - Ok((field_name.clone(), field_type)) + let object_field = ObjectField { + r#type: field_type, + parameters: Default::default(), + }; + Ok((field_name.clone(), object_field)) }) .try_collect::<_, _, QueryResponseError>()?; Ok(Type::Object(ObjectType { fields, name: None })) @@ -250,8 +418,11 @@ fn type_for_field(path: &[&str], field_definition: &Field) -> Result { .. } => type_for_nested_field(path, column_type, nested_field)?, Field::Relationship { - aggregates, fields, .. - } => type_for_row_set(path, aggregates, fields)?, + aggregates, + fields, + groups, + .. + } => type_for_row_set(path, aggregates, fields, groups)?, }; Ok(field_type) } @@ -379,6 +550,7 @@ mod tests { })) )] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -417,6 +589,7 @@ mod tests { ])) )] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -473,6 +646,7 @@ mod tests { ) ] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -525,6 +699,7 @@ mod tests { ), ] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -588,6 +763,7 @@ mod tests { })) )] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -651,6 +827,7 @@ mod tests { })) )] .into()]), + groups: Default::default(), }]) ); Ok(()) @@ -661,7 +838,7 @@ mod tests { let collection_name = "appearances"; let request: QueryRequest = query_request() .collection(collection_name) - .relationships([("author", relationship("authors", [("authorId", "id")]))]) + .relationships([("author", relationship("authors", [("authorId", &["id"])]))]) .query( query().fields([relation_field!("presenter" => "author", query().fields([ field!("addr" => "address", object!([ @@ -684,47 +861,53 @@ mod tests { &path, &query_plan.query.aggregates, &query_plan.query.fields, + &query_plan.query.groups, )?; - let expected = Type::Object(ObjectType { - name: None, - fields: [ - ("rows".into(), Type::ArrayOf(Box::new(Type::Object(ObjectType { - name: None, - fields: [ - ("presenter".into(), Type::Object(ObjectType { - name: None, - fields: [ - ("rows".into(), Type::ArrayOf(Box::new(Type::Object(ObjectType { - name: None, - fields: [ - ("addr".into(), Type::Object(ObjectType { - name: None, - fields: [ - ("geocode".into(), Type::Nullable(Box::new(Type::Object(ObjectType { - name: None, - fields: [ - ("latitude".into(), Type::Scalar(MongoScalarType::Bson(BsonScalarType::Double))), - ("long".into(), Type::Scalar(MongoScalarType::Bson(BsonScalarType::Double))), - ].into(), - })))), - ("street".into(), Type::Scalar(MongoScalarType::Bson(BsonScalarType::String))), - ].into(), - })), - ("articles".into(), Type::ArrayOf(Box::new(Type::Object(ObjectType { - name: None, - fields: [ - ("article_title".into(), Type::Scalar(MongoScalarType::Bson(BsonScalarType::String))), - ].into(), - })))), - ].into(), - })))) - ].into(), - })) - ].into() - })))) - ].into(), - }); + let expected = Type::object([( + "rows", + Type::array_of(Type::Object(ObjectType::new([( + "presenter", + Type::object([( + "rows", + Type::array_of(Type::object([ + ( + "addr", + Type::object([ + ( + "geocode", + Type::nullable(Type::object([ + ( + "latitude", + Type::Scalar(MongoScalarType::Bson( + BsonScalarType::Double, + )), + ), + ( + "long", + Type::Scalar(MongoScalarType::Bson( + BsonScalarType::Double, + )), + ), + ])), + ), + ( + "street", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + ), + ]), + ), + ( + "articles", + Type::array_of(Type::object([( + "article_title", + Type::Scalar(MongoScalarType::Bson(BsonScalarType::String)), + )])), + ), + ])), + )]), + )]))), + )]); assert_eq!(row_set_type, expected); Ok(()) diff --git a/crates/mongodb-agent-common/src/mongodb/selection.rs b/crates/mongodb-agent-common/src/query/selection.rs similarity index 62% rename from crates/mongodb-agent-common/src/mongodb/selection.rs rename to crates/mongodb-agent-common/src/query/selection.rs index 614594c1..e65f8c78 100644 --- a/crates/mongodb-agent-common/src/mongodb/selection.rs +++ b/crates/mongodb-agent-common/src/query/selection.rs @@ -2,29 +2,35 @@ use indexmap::IndexMap; use mongodb::bson::{doc, Bson, Document}; use mongodb_support::aggregate::Selection; use ndc_models::FieldName; +use nonempty::NonEmpty; use crate::{ + constants::{ + GROUP_DIMENSIONS_KEY, ROW_SET_AGGREGATES_KEY, ROW_SET_GROUPS_KEY, ROW_SET_ROWS_KEY, + }, interface_types::MongoAgentError, - mongo_query_plan::{Field, NestedArray, NestedField, NestedObject, QueryPlan}, - mongodb::sanitize::get_field, + mongo_query_plan::{Aggregate, Field, Grouping, NestedArray, NestedField, NestedObject}, query::column_ref::ColumnRef, }; -pub fn selection_from_query_request( - query_request: &QueryPlan, +use super::{aggregates::replace_missing_aggregate_value, is_response_faceted::ResponseFacets}; + +/// Creates a document to use in a $replaceWith stage to limit query results to the specific fields +/// requested. Assumes that only fields are requested. +pub fn selection_for_fields( + fields: Option<&IndexMap>, ) -> Result { - // let fields = (&query_request.query.fields).flatten().unwrap_or_default(); let empty_map = IndexMap::new(); - let fields = if let Some(fs) = &query_request.query.fields { + let fields = if let Some(fs) = fields { fs } else { &empty_map }; - let doc = from_query_request_helper(None, fields)?; + let doc = for_fields_helper(None, fields)?; Ok(Selection::new(doc)) } -fn from_query_request_helper( +fn for_fields_helper( parent: Option>, field_selection: &IndexMap, ) -> Result { @@ -52,7 +58,7 @@ fn selection_for_field( .. } => { let col_ref = nested_column_reference(parent, column); - let col_ref_or_null = value_or_null(col_ref.into_aggregate_expression()); + let col_ref_or_null = value_or_null(col_ref.into_aggregate_expression().into_bson()); Ok(col_ref_or_null) } Field::Column { @@ -61,7 +67,7 @@ fn selection_for_field( .. } => { let col_ref = nested_column_reference(parent, column); - let nested_selection = from_query_request_helper(Some(col_ref.clone()), fields)?; + let nested_selection = for_fields_helper(Some(col_ref.clone()), fields)?; Ok(doc! {"$cond": {"if": col_ref.into_aggregate_expression(), "then": nested_selection, "else": Bson::Null}}.into()) } Field::Column { @@ -76,70 +82,155 @@ fn selection_for_field( relationship, aggregates, fields, + groups, .. } => { + // TODO: ENG-1569 If we get a unification of two relationship references where one + // selects only fields, and the other selects only groups, we may end up in a broken + // state where the response should be faceted but is not. Data will be populated + // correctly - the issue is only here where we need to figure out whether to write + // a selection for faceted data or not. Instead of referencing the + // [Field::Relationship] value to determine faceting we need to reference the + // [Relationship] attached to the [Query] that populated it. + // The pipeline for the relationship has already selected the requested fields with the // appropriate aliases. At this point all we need to do is to prune the selection down // to requested fields, omitting fields of the relationship that were selected for // filtering and sorting. - let field_selection: Option = fields.as_ref().map(|fields| { + fn field_selection(fields: &IndexMap) -> Document { fields .iter() .map(|(field_name, _)| { ( field_name.to_string(), ColumnRef::variable("this") - .into_nested_field(field_name) - .into_aggregate_expression(), + .into_nested_field(field_name.as_ref()) + .into_aggregate_expression() + .into_bson(), ) }) .collect() - }); + } - if let Some(aggregates) = aggregates { - let aggregate_selecion: Document = aggregates - .iter() - .map(|(aggregate_name, _)| { - ( - aggregate_name.to_string(), - format!("$$row_set.aggregates.{aggregate_name}").into(), - ) + fn aggregates_selection( + from: ColumnRef<'_>, + aggregates: &IndexMap, + check_for_null: bool, + ) -> Document { + aggregates + .into_iter() + .map(|(aggregate_name, aggregate)| { + let value_ref = from + .clone() + .into_nested_field(aggregate_name.as_ref()) + .into_aggregate_expression() + .into_bson(); + let value_ref = if check_for_null { + replace_missing_aggregate_value(value_ref, aggregate.is_count()) + } else { + value_ref + }; + (aggregate_name.to_string(), value_ref) }) - .collect(); - let mut new_row_set = doc! { "aggregates": aggregate_selecion }; + .collect() + } - if let Some(field_selection) = field_selection { - new_row_set.insert( - "rows", - doc! { - "$map": { - "input": "$$row_set.rows", - "in": field_selection, - } - }, - ); - } + fn group_selection(from: ColumnRef<'_>, grouping: &Grouping) -> Document { + let mut selection = aggregates_selection(from, &grouping.aggregates, false); + selection.insert( + GROUP_DIMENSIONS_KEY, + ColumnRef::variable("this") + .into_nested_field(GROUP_DIMENSIONS_KEY) + .into_aggregate_expression(), + ); + selection + } + + // Field of the incoming pipeline document that contains data fetched for the + // relationship. + let relationship_field = ColumnRef::from_field(relationship.as_ref()); + + let doc = match ResponseFacets::from_parameters( + aggregates.as_ref(), + fields.as_ref(), + groups.as_ref(), + ) { + ResponseFacets::Combination { + aggregates, + fields, + groups, + } => { + let mut new_row_set = Document::new(); + + if let Some(aggregates) = aggregates { + new_row_set.insert( + ROW_SET_AGGREGATES_KEY, + aggregates_selection( + ColumnRef::variable("row_set") + .into_nested_field(ROW_SET_AGGREGATES_KEY), + aggregates, + false, + ), + ); + } + + if let Some(fields) = fields { + new_row_set.insert( + ROW_SET_ROWS_KEY, + doc! { + "$map": { + "input": ColumnRef::variable("row_set").into_nested_field(ROW_SET_ROWS_KEY).into_aggregate_expression(), + "in": field_selection(fields), + } + }, + ); + } + + if let Some(grouping) = groups { + new_row_set.insert( + ROW_SET_GROUPS_KEY, + doc! { + "$map": { + "input": ColumnRef::variable("row_set").into_nested_field(ROW_SET_GROUPS_KEY).into_aggregate_expression(), + "in": group_selection(ColumnRef::variable("this"), grouping), + } + }, + ); + } - Ok(doc! { - "$let": { - "vars": { "row_set": { "$first": get_field(relationship.as_str()) } }, - "in": new_row_set, + doc! { + "$let": { + "vars": { "row_set": { "$first": relationship_field.into_aggregate_expression() } }, + "in": new_row_set, + } } } - .into()) - } else if let Some(field_selection) = field_selection { - Ok(doc! { - "rows": { + ResponseFacets::AggregatesOnly(aggregates) => doc! { + ROW_SET_AGGREGATES_KEY: { + "$let": { + "vars": { "aggregates": { "$first": relationship_field.into_aggregate_expression() } }, + "in": aggregates_selection(ColumnRef::variable("aggregates"), aggregates, true), + } + } + }, + ResponseFacets::FieldsOnly(fields) => doc! { + ROW_SET_ROWS_KEY: { "$map": { - "input": get_field(relationship.as_str()), - "in": field_selection, + "input": relationship_field.into_aggregate_expression(), + "in": field_selection(fields), } } - } - .into()) - } else { - Ok(doc! { "rows": [] }.into()) - } + }, + ResponseFacets::GroupsOnly(grouping) => doc! { + ROW_SET_GROUPS_KEY: { + "$map": { + "input": relationship_field.into_aggregate_expression(), + "in": group_selection(ColumnRef::variable("this"), grouping), + } + } + }, + }; + Ok(doc.into()) } } } @@ -152,7 +243,7 @@ fn selection_for_array( match field { NestedField::Object(NestedObject { fields }) => { let mut nested_selection = - from_query_request_helper(Some(ColumnRef::variable("this")), fields)?; + for_fields_helper(Some(ColumnRef::variable("this")), fields)?; for _ in 0..array_nesting_level { nested_selection = doc! {"$map": {"input": "$$this", "in": nested_selection}} } @@ -170,8 +261,8 @@ fn nested_column_reference<'a>( column: &'a FieldName, ) -> ColumnRef<'a> { match parent { - Some(parent) => parent.into_nested_field(column), - None => ColumnRef::from_field_path([column]), + Some(parent) => parent.into_nested_field(column.as_ref()), + None => ColumnRef::from_field_path(NonEmpty::singleton(column)), } } @@ -186,7 +277,9 @@ mod tests { }; use pretty_assertions::assert_eq; - use crate::{mongo_query_plan::MongoConfiguration, mongodb::selection_from_query_request}; + use crate::mongo_query_plan::MongoConfiguration; + + use super::*; #[test] fn calculates_selection_for_query_request() -> Result<(), anyhow::Error> { @@ -214,7 +307,7 @@ mod tests { let query_plan = plan_for_query_request(&foo_config(), query_request)?; - let selection = selection_from_query_request(&query_plan)?; + let selection = selection_for_fields(query_plan.query.fields.as_ref())?; assert_eq!( Into::::into(selection), doc! { @@ -296,7 +389,7 @@ mod tests { ])) .relationships([( "class_students", - relationship("students", [("_id", "classId")]), + relationship("students", [("_id", &["classId"])]), )]) .into(); @@ -306,14 +399,14 @@ mod tests { // twice (once with the key `class_students`, and then with the key `class_students_0`). // This is because the queries on the two relationships have different scope names. The // query would work with just one lookup. Can we do that optimization? - let selection = selection_from_query_request(&query_plan)?; + let selection = selection_for_fields(query_plan.query.fields.as_ref())?; assert_eq!( Into::::into(selection), doc! { "class_students": { "rows": { "$map": { - "input": { "$getField": { "$literal": "class_students" } }, + "input": "$class_students", "in": { "name": "$$this.name" }, @@ -323,7 +416,7 @@ mod tests { "students": { "rows": { "$map": { - "input": { "$getField": { "$literal": "class_students_0" } }, + "input": "$class_students_0", "in": { "student_name": "$$this.student_name" }, diff --git a/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs b/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs index a03d50e0..7cc80e02 100644 --- a/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs +++ b/crates/mongodb-agent-common/src/query/serialization/bson_to_json.rs @@ -21,14 +21,14 @@ pub enum BsonToJsonError { #[error("error converting UUID from BSON to JSON: {0}")] UuidConversion(#[from] bson::uuid::Error), - #[error("input object of type {0:?} is missing a field, \"{1}\"")] + #[error("input object of type {0} is missing a field, \"{1}\"")] MissingObjectField(Type, String), #[error("error converting value to JSON: {0}")] Serde(#[from] serde_json::Error), // TODO: It would be great if we could capture a path into the larger BSON value here - #[error("expected a value of type {0:?}, but got {1}")] + #[error("expected a value of type {0}, but got {1}")] TypeMismatch(Type, Bson), #[error("unknown object type, \"{0}\"")] @@ -52,6 +52,7 @@ pub fn bson_to_json(mode: ExtendedJsonMode, expected_type: &Type, value: Bson) - } Type::Object(object_type) => convert_object(mode, object_type, value), Type::ArrayOf(element_type) => convert_array(mode, element_type, value), + Type::Tuple(element_types) => convert_tuple(mode, element_types, value), Type::Nullable(t) => convert_nullable(mode, t, value), } } @@ -74,7 +75,9 @@ fn bson_scalar_to_json( (BsonScalarType::Double, v) => convert_small_number(expected_type, v), (BsonScalarType::Int, v) => convert_small_number(expected_type, v), (BsonScalarType::Long, Bson::Int64(n)) => Ok(Value::String(n.to_string())), + (BsonScalarType::Long, Bson::Int32(n)) => Ok(Value::String(n.to_string())), (BsonScalarType::Decimal, Bson::Decimal128(n)) => Ok(Value::String(n.to_string())), + (BsonScalarType::Decimal, Bson::Double(n)) => Ok(Value::String(n.to_string())), (BsonScalarType::String, Bson::String(s)) => Ok(Value::String(s)), (BsonScalarType::Symbol, Bson::Symbol(s)) => Ok(Value::String(s)), (BsonScalarType::Date, Bson::DateTime(date)) => convert_date(date), @@ -116,6 +119,22 @@ fn convert_array(mode: ExtendedJsonMode, element_type: &Type, value: Bson) -> Re Ok(Value::Array(json_array)) } +fn convert_tuple(mode: ExtendedJsonMode, element_types: &[Type], value: Bson) -> Result { + let values = match value { + Bson::Array(values) => Ok(values), + _ => Err(BsonToJsonError::TypeMismatch( + Type::Tuple(element_types.to_vec()), + value, + )), + }?; + let json_array = element_types + .iter() + .zip(values) + .map(|(element_type, value)| bson_to_json(mode, element_type, value)) + .try_collect()?; + Ok(Value::Array(json_array)) +} + fn convert_object(mode: ExtendedJsonMode, object_type: &ObjectType, value: Bson) -> Result { let input_doc = match value { Bson::Document(fields) => Ok(fields), @@ -234,16 +253,13 @@ mod tests { #[test] fn serializes_document_with_missing_nullable_field() -> anyhow::Result<()> { - let expected_type = Type::Object(ObjectType { - name: Some("test_object".into()), - fields: [( - "field".into(), - Type::Nullable(Box::new(Type::Scalar(MongoScalarType::Bson( - BsonScalarType::String, - )))), - )] - .into(), - }); + let expected_type = Type::named_object( + "test_object", + [( + "field", + Type::nullable(Type::Scalar(MongoScalarType::Bson(BsonScalarType::String))), + )], + ); let value = bson::doc! {}; let actual = bson_to_json(ExtendedJsonMode::Canonical, &expected_type, value.into())?; assert_eq!(actual, json!({})); diff --git a/crates/mongodb-agent-common/src/query/serialization/json_formats.rs b/crates/mongodb-agent-common/src/query/serialization/json_formats.rs index 9ab6c8d0..85a435f9 100644 --- a/crates/mongodb-agent-common/src/query/serialization/json_formats.rs +++ b/crates/mongodb-agent-common/src/query/serialization/json_formats.rs @@ -6,6 +6,25 @@ use mongodb::bson::{self, Bson}; use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, hex::Hex, serde_as}; +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Either { + Left(T), + Right(U), +} + +impl Either { + pub fn into_left(self) -> T + where + T: From, + { + match self { + Either::Left(l) => l, + Either::Right(r) => r.into(), + } + } +} + #[serde_as] #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -84,6 +103,15 @@ impl From for Regex { } } +impl From for Regex { + fn from(value: String) -> Self { + Regex { + pattern: value, + options: String::new(), + } + } +} + #[derive(Deserialize, Serialize)] pub struct Timestamp { t: u32, diff --git a/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs b/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs index ea855132..7c04b91a 100644 --- a/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs +++ b/crates/mongodb-agent-common/src/query/serialization/json_to_bson.rs @@ -66,6 +66,7 @@ pub fn json_to_bson(expected_type: &Type, value: Value) -> Result { Type::Object(object_type) => convert_object(object_type, value), Type::ArrayOf(element_type) => convert_array(element_type, value), Type::Nullable(t) => convert_nullable(t, value), + Type::Tuple(element_types) => convert_tuple(element_types, value), } } @@ -103,7 +104,11 @@ pub fn json_to_bson_scalar(expected_type: BsonScalarType, value: Value) -> Resul Value::Null => Bson::Undefined, _ => incompatible_scalar_type(S::Undefined, value)?, }, - S::Regex => deserialize::(expected_type, value)?.into(), + S::Regex => { + deserialize::>(expected_type, value)? + .into_left() + .into() + } S::Javascript => Bson::JavaScriptCode(deserialize(expected_type, value)?), S::JavascriptWithScope => { deserialize::(expected_type, value)?.into() @@ -126,6 +131,16 @@ fn convert_array(element_type: &Type, value: Value) -> Result { Ok(Bson::Array(bson_array)) } +fn convert_tuple(element_types: &[Type], value: Value) -> Result { + let input_elements: Vec = serde_json::from_value(value)?; + let bson_array = element_types + .iter() + .zip(input_elements) + .map(|(element_type, v)| json_to_bson(element_type, v)) + .try_collect()?; + Ok(Bson::Array(bson_array)) +} + fn convert_object(object_type: &ObjectType, value: Value) -> Result { let input_fields: BTreeMap = serde_json::from_value(value)?; let bson_doc: bson::Document = object_type @@ -245,35 +260,32 @@ mod tests { use super::json_to_bson; + use BsonScalarType as S; + #[test] #[allow(clippy::approx_constant)] fn deserializes_specialized_scalar_types() -> anyhow::Result<()> { - let object_type = ObjectType { - name: Some("scalar_test".into()), - fields: [ - ("double", BsonScalarType::Double), - ("int", BsonScalarType::Int), - ("long", BsonScalarType::Long), - ("decimal", BsonScalarType::Decimal), - ("string", BsonScalarType::String), - ("date", BsonScalarType::Date), - ("timestamp", BsonScalarType::Timestamp), - ("binData", BsonScalarType::BinData), - ("objectId", BsonScalarType::ObjectId), - ("bool", BsonScalarType::Bool), - ("null", BsonScalarType::Null), - ("undefined", BsonScalarType::Undefined), - ("regex", BsonScalarType::Regex), - ("javascript", BsonScalarType::Javascript), - ("javascriptWithScope", BsonScalarType::JavascriptWithScope), - ("minKey", BsonScalarType::MinKey), - ("maxKey", BsonScalarType::MaxKey), - ("symbol", BsonScalarType::Symbol), - ] - .into_iter() - .map(|(name, t)| (name.into(), Type::Scalar(MongoScalarType::Bson(t)))) - .collect(), - }; + let object_type = ObjectType::new([ + ("double", Type::scalar(S::Double)), + ("int", Type::scalar(S::Int)), + ("long", Type::scalar(S::Long)), + ("decimal", Type::scalar(S::Decimal)), + ("string", Type::scalar(S::String)), + ("date", Type::scalar(S::Date)), + ("timestamp", Type::scalar(S::Timestamp)), + ("binData", Type::scalar(S::BinData)), + ("objectId", Type::scalar(S::ObjectId)), + ("bool", Type::scalar(S::Bool)), + ("null", Type::scalar(S::Null)), + ("undefined", Type::scalar(S::Undefined)), + ("regex", Type::scalar(S::Regex)), + ("javascript", Type::scalar(S::Javascript)), + ("javascriptWithScope", Type::scalar(S::JavascriptWithScope)), + ("minKey", Type::scalar(S::MinKey)), + ("maxKey", Type::scalar(S::MaxKey)), + ("symbol", Type::scalar(S::Symbol)), + ]) + .named("scalar_test"); let input = json!({ "double": 3.14159, @@ -376,16 +388,13 @@ mod tests { #[test] fn deserializes_object_with_missing_nullable_field() -> anyhow::Result<()> { - let expected_type = Type::Object(ObjectType { - name: Some("test_object".into()), - fields: [( - "field".into(), - Type::Nullable(Box::new(Type::Scalar(MongoScalarType::Bson( - BsonScalarType::String, - )))), - )] - .into(), - }); + let expected_type = Type::named_object( + "test_object", + [( + "field", + Type::nullable(Type::scalar(BsonScalarType::String)), + )], + ); let value = json!({}); let actual = json_to_bson(&expected_type, value)?; assert_eq!(actual, bson!({})); diff --git a/crates/mongodb-agent-common/src/scalar_types_capabilities.rs b/crates/mongodb-agent-common/src/scalar_types_capabilities.rs index f77bcca9..3140217d 100644 --- a/crates/mongodb-agent-common/src/scalar_types_capabilities.rs +++ b/crates/mongodb-agent-common/src/scalar_types_capabilities.rs @@ -10,6 +10,7 @@ use ndc_models::{ use crate::aggregation_function::{AggregationFunction, AggregationFunction as A}; use crate::comparison_function::{ComparisonFunction, ComparisonFunction as C}; +use crate::mongo_query_plan as plan; use BsonScalarType as S; @@ -38,19 +39,29 @@ fn extended_json_scalar_type() -> (ndc_models::ScalarTypeName, ScalarType) { ( mongodb_support::EXTENDED_JSON_TYPE_NAME.into(), ScalarType { - representation: Some(TypeRepresentation::JSON), + representation: TypeRepresentation::JSON, aggregate_functions: aggregation_functions .into_iter() .map(|aggregation_function| { + use AggregateFunctionDefinition as NDC; + use AggregationFunction as Plan; let name = aggregation_function.graphql_name().into(); - let result_type = match aggregation_function { - AggregationFunction::Avg => ext_json_type.clone(), - AggregationFunction::Count => bson_to_named_type(S::Int), - AggregationFunction::Min => ext_json_type.clone(), - AggregationFunction::Max => ext_json_type.clone(), - AggregationFunction::Sum => ext_json_type.clone(), + let definition = match aggregation_function { + // Using custom instead of standard aggregations because we want the result + // types to be ExtendedJSON instead of specific numeric types + Plan::Avg => NDC::Custom { + result_type: Type::Named { + name: mongodb_support::EXTENDED_JSON_TYPE_NAME.into(), + }, + }, + Plan::Min => NDC::Min, + Plan::Max => NDC::Max, + Plan::Sum => NDC::Custom { + result_type: Type::Named { + name: mongodb_support::EXTENDED_JSON_TYPE_NAME.into(), + }, + }, }; - let definition = AggregateFunctionDefinition { result_type }; (name, definition) }) .collect(), @@ -58,16 +69,22 @@ fn extended_json_scalar_type() -> (ndc_models::ScalarTypeName, ScalarType) { .into_iter() .map(|comparison_fn| { let name = comparison_fn.graphql_name().into(); - let definition = match comparison_fn { - C::Equal => ComparisonOperatorDefinition::Equal, - C::Regex | C::IRegex => ComparisonOperatorDefinition::Custom { - argument_type: bson_to_named_type(S::String), + let ndc_definition = comparison_fn.ndc_definition(|func| match func { + C::Equal => ext_json_type.clone(), + C::In => Type::Array { + element_type: Box::new(ext_json_type.clone()), }, - _ => ComparisonOperatorDefinition::Custom { - argument_type: ext_json_type.clone(), + C::LessThan => ext_json_type.clone(), + C::LessThanOrEqual => ext_json_type.clone(), + C::GreaterThan => ext_json_type.clone(), + C::GreaterThanOrEqual => ext_json_type.clone(), + C::NotEqual => ext_json_type.clone(), + C::NotIn => Type::Array { + element_type: Box::new(ext_json_type.clone()), }, - }; - (name, definition) + C::Regex | C::IRegex => bson_to_named_type(S::Regex), + }); + (name, ndc_definition) }) .collect(), }, @@ -84,28 +101,29 @@ fn make_scalar_type(bson_scalar_type: BsonScalarType) -> (ndc_models::ScalarType (scalar_type_name.into(), scalar_type) } -fn bson_scalar_type_representation(bson_scalar_type: BsonScalarType) -> Option { +fn bson_scalar_type_representation(bson_scalar_type: BsonScalarType) -> TypeRepresentation { + use TypeRepresentation as R; match bson_scalar_type { - BsonScalarType::Double => Some(TypeRepresentation::Float64), - BsonScalarType::Decimal => Some(TypeRepresentation::BigDecimal), // Not quite.... Mongo Decimal is 128-bit, BigDecimal is unlimited - BsonScalarType::Int => Some(TypeRepresentation::Int32), - BsonScalarType::Long => Some(TypeRepresentation::Int64), - BsonScalarType::String => Some(TypeRepresentation::String), - BsonScalarType::Date => Some(TypeRepresentation::Timestamp), // Mongo Date is milliseconds since unix epoch - BsonScalarType::Timestamp => None, // Internal Mongo timestamp type - BsonScalarType::BinData => None, - BsonScalarType::UUID => Some(TypeRepresentation::String), - BsonScalarType::ObjectId => Some(TypeRepresentation::String), // Mongo ObjectId is usually expressed as a 24 char hex string (12 byte number) - BsonScalarType::Bool => Some(TypeRepresentation::Boolean), - BsonScalarType::Null => None, - BsonScalarType::Regex => None, - BsonScalarType::Javascript => None, - BsonScalarType::JavascriptWithScope => None, - BsonScalarType::MinKey => None, - BsonScalarType::MaxKey => None, - BsonScalarType::Undefined => None, - BsonScalarType::DbPointer => None, - BsonScalarType::Symbol => None, + S::Double => R::Float64, + S::Decimal => R::BigDecimal, // Not quite.... Mongo Decimal is 128-bit, BigDecimal is unlimited + S::Int => R::Int32, + S::Long => R::Int64, + S::String => R::String, + S::Date => R::TimestampTZ, // Mongo Date is milliseconds since unix epoch, but we serialize to JSON as an ISO string + S::Timestamp => R::JSON, // Internal Mongo timestamp type + S::BinData => R::JSON, + S::UUID => R::String, + S::ObjectId => R::String, // Mongo ObjectId is usually expressed as a 24 char hex string (12 byte number) - not using R::Bytes because that expects base64 + S::Bool => R::Boolean, + S::Null => R::JSON, + S::Regex => R::JSON, + S::Javascript => R::String, + S::JavascriptWithScope => R::JSON, + S::MinKey => R::JSON, + S::MaxKey => R::JSON, + S::Undefined => R::JSON, + S::DbPointer => R::JSON, + S::Symbol => R::String, } } @@ -115,14 +133,7 @@ fn bson_comparison_operators( comparison_operators(bson_scalar_type) .map(|(comparison_fn, argument_type)| { let fn_name = comparison_fn.graphql_name().into(); - match comparison_fn { - ComparisonFunction::Equal => (fn_name, ComparisonOperatorDefinition::Equal), - ComparisonFunction::In => (fn_name, ComparisonOperatorDefinition::In), - _ => ( - fn_name, - ComparisonOperatorDefinition::Custom { argument_type }, - ), - } + (fn_name, comparison_fn.ndc_definition(|_| argument_type)) }) .collect() } @@ -131,8 +142,7 @@ fn bson_aggregation_functions( bson_scalar_type: BsonScalarType, ) -> BTreeMap { aggregate_functions(bson_scalar_type) - .map(|(fn_name, result_type)| { - let aggregation_definition = AggregateFunctionDefinition { result_type }; + .map(|(fn_name, aggregation_definition)| { (fn_name.graphql_name().into(), aggregation_definition) }) .collect() @@ -144,26 +154,44 @@ fn bson_to_named_type(bson_scalar_type: BsonScalarType) -> Type { } } -pub fn aggregate_functions( +fn bson_to_scalar_type_name(bson_scalar_type: BsonScalarType) -> ndc_models::ScalarTypeName { + bson_scalar_type.graphql_name().into() +} + +fn aggregate_functions( scalar_type: BsonScalarType, -) -> impl Iterator { - let nullable_scalar_type = move || Type::Nullable { - underlying_type: Box::new(bson_to_named_type(scalar_type)), - }; - [(A::Count, bson_to_named_type(S::Int))] - .into_iter() - .chain(iter_if( - scalar_type.is_orderable(), - [A::Min, A::Max] - .into_iter() - .map(move |op| (op, nullable_scalar_type())), - )) - .chain(iter_if( - scalar_type.is_numeric(), - [A::Avg, A::Sum] - .into_iter() - .map(move |op| (op, nullable_scalar_type())), - )) +) -> impl Iterator { + use AggregateFunctionDefinition as NDC; + iter_if( + scalar_type.is_orderable(), + [(A::Min, NDC::Min), (A::Max, NDC::Max)].into_iter(), + ) + .chain(iter_if( + scalar_type.is_numeric(), + [ + ( + A::Avg, + NDC::Average { + result_type: bson_to_scalar_type_name( + A::expected_result_type(A::Avg, &plan::Type::scalar(scalar_type)) + .expect("average result type is defined"), + // safety: this expect is checked in integration tests + ), + }, + ), + ( + A::Sum, + NDC::Sum { + result_type: bson_to_scalar_type_name( + A::expected_result_type(A::Sum, &plan::Type::scalar(scalar_type)) + .expect("sum result type is defined"), + // safety: this expect is checked in integration tests + ), + }, + ), + ] + .into_iter(), + )) } pub fn comparison_operators( @@ -204,8 +232,8 @@ pub fn comparison_operators( .chain(match scalar_type { S::String => Box::new( [ - (C::Regex, bson_to_named_type(S::String)), - (C::IRegex, bson_to_named_type(S::String)), + (C::Regex, bson_to_named_type(S::Regex)), + (C::IRegex, bson_to_named_type(S::Regex)), ] .into_iter(), ), diff --git a/crates/mongodb-agent-common/src/test_helpers.rs b/crates/mongodb-agent-common/src/test_helpers.rs index c8cd2ccd..38f31651 100644 --- a/crates/mongodb-agent-common/src/test_helpers.rs +++ b/crates/mongodb-agent-common/src/test_helpers.rs @@ -20,7 +20,6 @@ pub fn make_nested_schema() -> MongoConfiguration { collection_type: "Author".into(), arguments: Default::default(), uniqueness_constraints: make_primary_key_uniqueness_constraint("authors"), - foreign_keys: Default::default(), }, ), collection("appearances"), // new helper gives more concise syntax @@ -87,6 +86,7 @@ pub fn make_nested_schema() -> MongoConfiguration { } /// Configuration for a MongoDB database with Chinook test data +#[allow(dead_code)] pub fn chinook_config() -> MongoConfiguration { MongoConfiguration(Configuration { collections: [ @@ -139,19 +139,20 @@ pub fn chinook_config() -> MongoConfiguration { }) } +#[allow(dead_code)] pub fn chinook_relationships() -> BTreeMap { [ ( "Albums", - ndc_test_helpers::relationship("Album", [("ArtistId", "ArtistId")]), + ndc_test_helpers::relationship("Album", [("ArtistId", &["ArtistId"])]), ), ( "Tracks", - ndc_test_helpers::relationship("Track", [("AlbumId", "AlbumId")]), + ndc_test_helpers::relationship("Track", [("AlbumId", &["AlbumId"])]), ), ( "Genre", - ndc_test_helpers::relationship("Genre", [("GenreId", "GenreId")]).object_type(), + ndc_test_helpers::relationship("Genre", [("GenreId", &["GenreId"])]).object_type(), ), ] .into_iter() diff --git a/crates/mongodb-connector/src/capabilities.rs b/crates/mongodb-connector/src/capabilities.rs index 8fc7cdf2..6e7a5724 100644 --- a/crates/mongodb-connector/src/capabilities.rs +++ b/crates/mongodb-connector/src/capabilities.rs @@ -1,21 +1,38 @@ use ndc_sdk::models::{ - Capabilities, ExistsCapabilities, LeafCapability, NestedFieldCapabilities, QueryCapabilities, - RelationshipCapabilities, + AggregateCapabilities, Capabilities, ExistsCapabilities, GroupByCapabilities, LeafCapability, + NestedArrayFilterByCapabilities, NestedFieldCapabilities, NestedFieldFilterByCapabilities, + QueryCapabilities, RelationshipCapabilities, }; pub fn mongo_capabilities() -> Capabilities { Capabilities { query: QueryCapabilities { - aggregates: Some(LeafCapability {}), + aggregates: Some(AggregateCapabilities { + filter_by: None, + group_by: Some(GroupByCapabilities { + filter: None, + order: None, + paginate: None, + }), + }), variables: Some(LeafCapability {}), explain: Some(LeafCapability {}), nested_fields: NestedFieldCapabilities { - filter_by: Some(LeafCapability {}), + filter_by: Some(NestedFieldFilterByCapabilities { + nested_arrays: Some(NestedArrayFilterByCapabilities { + contains: Some(LeafCapability {}), + is_empty: Some(LeafCapability {}), + }), + }), order_by: Some(LeafCapability {}), aggregates: Some(LeafCapability {}), + nested_collections: None, // TODO: ENG-1464 }, exists: ExistsCapabilities { + named_scopes: None, // TODO: ENG-1487 + unrelated: Some(LeafCapability {}), nested_collections: Some(LeafCapability {}), + nested_scalar_collections: None, // TODO: ENG-1488 }, }, mutation: ndc_sdk::models::MutationCapabilities { @@ -25,6 +42,7 @@ pub fn mongo_capabilities() -> Capabilities { relationships: Some(RelationshipCapabilities { relation_comparisons: Some(LeafCapability {}), order_by_aggregate: None, + nested: None, // TODO: ENG-1490 }), } } diff --git a/crates/mongodb-connector/src/mongo_connector.rs b/crates/mongodb-connector/src/mongo_connector.rs index 3545621f..648b5548 100644 --- a/crates/mongodb-connector/src/mongo_connector.rs +++ b/crates/mongodb-connector/src/mongo_connector.rs @@ -31,7 +31,7 @@ impl ConnectorSetup for MongoConnector { #[instrument(err, skip_all)] async fn parse_configuration( &self, - configuration_dir: impl AsRef + Send, + configuration_dir: &Path, ) -> connector::Result { let configuration = Configuration::parse_configuration(configuration_dir) .await diff --git a/crates/mongodb-connector/src/schema.rs b/crates/mongodb-connector/src/schema.rs index 1e92d403..bdc922f5 100644 --- a/crates/mongodb-connector/src/schema.rs +++ b/crates/mongodb-connector/src/schema.rs @@ -1,6 +1,7 @@ use mongodb_agent_common::{ mongo_query_plan::MongoConfiguration, scalar_types_capabilities::SCALAR_TYPES, }; +use mongodb_support::BsonScalarType; use ndc_query_plan::QueryContext as _; use ndc_sdk::{connector, models as ndc}; @@ -20,6 +21,13 @@ pub async fn get_schema(config: &MongoConfiguration) -> connector::Result for Selection { } } +impl From for Bson { + fn from(value: Selection) -> Self { + value.0.into() + } +} + impl From for bson::Document { fn from(value: Selection) -> Self { value.0 diff --git a/crates/mongodb-support/src/bson_type.rs b/crates/mongodb-support/src/bson_type.rs index c1950ec6..adf5673f 100644 --- a/crates/mongodb-support/src/bson_type.rs +++ b/crates/mongodb-support/src/bson_type.rs @@ -257,6 +257,31 @@ impl BsonScalarType { } } + pub fn is_fractional(self) -> bool { + match self { + S::Double => true, + S::Decimal => true, + S::Int => false, + S::Long => false, + S::String => false, + S::Date => false, + S::Timestamp => false, + S::BinData => false, + S::UUID => false, + S::ObjectId => false, + S::Bool => false, + S::Null => false, + S::Regex => false, + S::Javascript => false, + S::JavascriptWithScope => false, + S::MinKey => false, + S::MaxKey => false, + S::Undefined => false, + S::DbPointer => false, + S::Symbol => false, + } + } + pub fn is_comparable(self) -> bool { match self { S::Double => true, diff --git a/crates/ndc-query-plan/Cargo.toml b/crates/ndc-query-plan/Cargo.toml index 732640c9..63ab6865 100644 --- a/crates/ndc-query-plan/Cargo.toml +++ b/crates/ndc-query-plan/Cargo.toml @@ -9,7 +9,7 @@ indent = "^0.1" indexmap = { workspace = true } itertools = { workspace = true } ndc-models = { workspace = true } -nonempty = "^0.10" +nonempty = { workspace = true } serde_json = { workspace = true } thiserror = "1" ref-cast = { workspace = true } diff --git a/crates/ndc-query-plan/src/lib.rs b/crates/ndc-query-plan/src/lib.rs index 725ba0cd..000e7e5b 100644 --- a/crates/ndc-query-plan/src/lib.rs +++ b/crates/ndc-query-plan/src/lib.rs @@ -6,10 +6,11 @@ pub mod vec_set; pub use mutation_plan::*; pub use plan_for_query_request::{ - plan_for_mutation_request, plan_for_query_request, + plan_for_mutation_request::plan_for_mutation_request, + plan_for_query_request, query_context::QueryContext, query_plan_error::QueryPlanError, type_annotated_field::{type_annotated_field, type_annotated_nested_field}, }; pub use query_plan::*; -pub use type_system::{inline_object_types, ObjectType, Type}; +pub use type_system::{inline_object_types, ObjectField, ObjectType, Type}; diff --git a/crates/ndc-query-plan/src/plan_for_query_request/helpers.rs b/crates/ndc-query-plan/src/plan_for_query_request/helpers.rs index e88e0a2b..11abe277 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/helpers.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/helpers.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; -use ndc_models as ndc; +use itertools::Itertools as _; +use ndc_models::{self as ndc}; use crate::{self as plan}; @@ -11,7 +12,7 @@ type Result = std::result::Result; pub fn find_object_field<'a, S>( object_type: &'a plan::ObjectType, field_name: &ndc::FieldName, -) -> Result<&'a plan::Type> { +) -> Result<&'a plan::ObjectField> { object_type.fields.get(field_name).ok_or_else(|| { QueryPlanError::UnknownObjectTypeField { object_type: object_type.name.clone(), @@ -21,28 +22,29 @@ pub fn find_object_field<'a, S>( }) } -pub fn find_object_field_path<'a, S>( +pub fn get_object_field_by_path<'a, S>( object_type: &'a plan::ObjectType, field_name: &ndc::FieldName, - field_path: Option<&Vec>, -) -> Result<&'a plan::Type> { + field_path: Option<&[ndc::FieldName]>, +) -> Result<&'a plan::ObjectField> { match field_path { None => find_object_field(object_type, field_name), - Some(field_path) => find_object_field_path_helper(object_type, field_name, field_path), + Some(field_path) => get_object_field_by_path_helper(object_type, field_name, field_path), } } -fn find_object_field_path_helper<'a, S>( +fn get_object_field_by_path_helper<'a, S>( object_type: &'a plan::ObjectType, field_name: &ndc::FieldName, field_path: &[ndc::FieldName], -) -> Result<&'a plan::Type> { - let field_type = find_object_field(object_type, field_name)?; +) -> Result<&'a plan::ObjectField> { + let object_field = find_object_field(object_type, field_name)?; + let field_type = &object_field.r#type; match field_path { - [] => Ok(field_type), + [] => Ok(object_field), [nested_field_name, rest @ ..] => { let o = find_object_type(field_type, &object_type.name, field_name)?; - find_object_field_path_helper(o, nested_field_name, rest) + get_object_field_by_path_helper(o, nested_field_name, rest) } } } @@ -65,38 +67,59 @@ fn find_object_type<'a, S>( }), crate::Type::Nullable(t) => find_object_type(t, parent_type, field_name), crate::Type::Object(object_type) => Ok(object_type), + crate::Type::Tuple(ts) => { + let object_types = ts + .iter() + .flat_map(|t| find_object_type(t, parent_type, field_name)) + .collect_vec(); + if object_types.len() == 1 { + Ok(object_types[0]) + } else { + Err(QueryPlanError::ExpectedObjectTypeAtField { + parent_type: parent_type.to_owned(), + field_name: field_name.to_owned(), + got: "array".to_owned(), + }) + } + } } } -/// Given the type of a collection and a field path returns the object type of the nested object at -/// that path. +/// Given the type of a collection and a field path returns the type of the nested values in an +/// array field at that path. pub fn find_nested_collection_type( collection_object_type: plan::ObjectType, field_path: &[ndc::FieldName], -) -> Result> +) -> Result> where - S: Clone, + S: Clone + std::fmt::Debug, { - fn normalize_object_type( - field_path: &[ndc::FieldName], - t: plan::Type, - ) -> Result> { - match t { - plan::Type::Object(t) => Ok(t), - plan::Type::ArrayOf(t) => normalize_object_type(field_path, *t), - plan::Type::Nullable(t) => normalize_object_type(field_path, *t), - _ => Err(QueryPlanError::ExpectedObject { - path: field_path.iter().map(|f| f.to_string()).collect(), - }), + let nested_field = match field_path { + [field_name] => get_object_field_by_path(&collection_object_type, field_name, None), + [field_name, rest_of_path @ ..] => { + get_object_field_by_path(&collection_object_type, field_name, Some(rest_of_path)) } - } + [] => Err(QueryPlanError::UnknownCollection(field_path.join("."))), + }?; + let element_type = nested_field.r#type.clone().into_array_element_type()?; + Ok(element_type) +} - field_path - .iter() - .try_fold(collection_object_type, |obj_type, field_name| { - let field_type = find_object_field(&obj_type, field_name)?.clone(); - normalize_object_type(field_path, field_type) - }) +/// Given the type of a collection and a field path returns the object type of the nested object at +/// that path. +/// +/// This function differs from [find_nested_collection_type] in that it this one returns +/// [plan::ObjectType] instead of [plan::Type], and returns an error if the nested type is not an +/// object type. +pub fn find_nested_collection_object_type( + collection_object_type: plan::ObjectType, + field_path: &[ndc::FieldName], +) -> Result> +where + S: Clone + std::fmt::Debug, +{ + let collection_element_type = find_nested_collection_type(collection_object_type, field_path)?; + collection_element_type.into_object_type() } pub fn lookup_relationship<'a>( @@ -107,45 +130,3 @@ pub fn lookup_relationship<'a>( .get(relationship) .ok_or_else(|| QueryPlanError::UnspecifiedRelation(relationship.to_owned())) } - -/// Special case handling for array comparisons! Normally we assume that the right operand of Equal -/// is the same type as the left operand. BUT MongoDB allows comparing arrays to scalar values in -/// which case the condition passes if any array element is equal to the given scalar value. So -/// this function needs to return a scalar type if the user is expecting array-to-scalar -/// comparison, or an array type if the user is expecting array-to-array comparison. Or if the -/// column does not have an array type we fall back to the default assumption that the value type -/// should be the same as the column type. -/// -/// For now this assumes that if the column has an array type, the value type is a scalar type. -/// That's the simplest option since we don't support array-to-array comparisons yet. -/// -/// TODO: When we do support array-to-array comparisons we will need to either: -/// -/// - input the [ndc::ComparisonValue] into this function, and any query request variables; check -/// that the given JSON value or variable values are not array values, and if so assume the value -/// type should be a scalar type -/// - or get the GraphQL Engine to include a type with [ndc::ComparisonValue] in which case we can -/// use that as the value type -/// -/// It is important that queries behave the same when given an inline value or variables. So we -/// can't just check the value of an [ndc::ComparisonValue::Scalar], and punt on an -/// [ndc::ComparisonValue::Variable] input. The latter requires accessing query request variables, -/// and it will take a little more work to thread those through the code to make them available -/// here. -pub fn value_type_in_possible_array_equality_comparison( - column_type: plan::Type, -) -> plan::Type -where - S: Clone, -{ - match column_type { - plan::Type::ArrayOf(t) => *t, - plan::Type::Nullable(t) => match *t { - v @ plan::Type::ArrayOf(_) => { - value_type_in_possible_array_equality_comparison(v.clone()) - } - t => plan::Type::Nullable(Box::new(t)), - }, - _ => column_type, - } -} diff --git a/crates/ndc-query-plan/src/plan_for_query_request/mod.rs b/crates/ndc-query-plan/src/plan_for_query_request/mod.rs index 1faa0045..f5d87585 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/mod.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/mod.rs @@ -1,6 +1,9 @@ mod helpers; mod plan_for_arguments; -mod plan_for_mutation_request; +mod plan_for_expression; +mod plan_for_grouping; +pub mod plan_for_mutation_request; +mod plan_for_relationship; pub mod query_context; pub mod query_plan_error; mod query_plan_state; @@ -12,20 +15,18 @@ mod plan_test_helpers; #[cfg(test)] mod tests; -use std::{collections::VecDeque, iter::once}; - -use crate::{self as plan, type_annotated_field, ObjectType, QueryPlan, Scope}; -use helpers::{find_nested_collection_type, value_type_in_possible_array_equality_comparison}; +use crate::{self as plan, type_annotated_field, QueryPlan, Scope}; use indexmap::IndexMap; use itertools::Itertools; -use ndc::{ExistsInCollection, QueryRequest}; -use ndc_models as ndc; +use ndc_models::{self as ndc, QueryRequest}; +use plan_for_relationship::plan_for_relationship_path; use query_plan_state::QueryPlanInfo; -pub use self::plan_for_mutation_request::plan_for_mutation_request; use self::{ - helpers::{find_object_field, find_object_field_path, lookup_relationship}, - plan_for_arguments::plan_for_arguments, + helpers::{find_object_field, get_object_field_by_path}, + plan_for_arguments::{plan_arguments_from_plan_parameters, plan_for_arguments}, + plan_for_expression::plan_for_expression, + plan_for_grouping::plan_for_grouping, query_context::QueryContext, query_plan_error::QueryPlanError, query_plan_state::QueryPlanState, @@ -99,8 +100,10 @@ pub fn plan_for_query( ) -> Result> { let mut plan_state = plan_state.state_for_subquery(); - let aggregates = - plan_for_aggregates(plan_state.context, collection_object_type, query.aggregates)?; + let aggregates = query + .aggregates + .map(|aggregates| plan_for_aggregates(&mut plan_state, collection_object_type, aggregates)) + .transpose()?; let fields = plan_for_fields( &mut plan_state, root_collection_object_type, @@ -135,66 +138,94 @@ pub fn plan_for_query( }) .transpose()?; + let groups = query + .groups + .map(|grouping| { + plan_for_grouping( + &mut plan_state, + root_collection_object_type, + collection_object_type, + grouping, + ) + }) + .transpose()?; + Ok(plan::Query { aggregates, - aggregates_limit: limit, fields, order_by, limit, offset, predicate, + groups, relationships: plan_state.into_relationships(), scope: None, }) } fn plan_for_aggregates( - context: &T, + plan_state: &mut QueryPlanState<'_, T>, collection_object_type: &plan::ObjectType, - ndc_aggregates: Option>, -) -> Result>>> { + ndc_aggregates: IndexMap, +) -> Result>> { ndc_aggregates - .map(|aggregates| -> Result<_> { - aggregates - .into_iter() - .map(|(name, aggregate)| { - Ok(( - name, - plan_for_aggregate(context, collection_object_type, aggregate)?, - )) - }) - .collect() + .into_iter() + .map(|(name, aggregate)| { + Ok(( + name, + plan_for_aggregate(plan_state, collection_object_type, aggregate)?, + )) }) - .transpose() + .collect() } fn plan_for_aggregate( - context: &T, + plan_state: &mut QueryPlanState<'_, T>, collection_object_type: &plan::ObjectType, aggregate: ndc::Aggregate, ) -> Result> { match aggregate { ndc::Aggregate::ColumnCount { column, + arguments, distinct, field_path, - } => Ok(plan::Aggregate::ColumnCount { - column, - field_path, - distinct, - }), + } => { + let object_field = collection_object_type.get(&column)?; + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; + Ok(plan::Aggregate::ColumnCount { + column, + arguments: plan_arguments, + distinct, + field_path, + }) + } ndc::Aggregate::SingleColumn { column, + arguments, function, field_path, } => { - let object_type_field_type = - find_object_field_path(collection_object_type, &column, field_path.as_ref())?; - // let column_scalar_type_name = get_scalar_type_name(&object_type_field.r#type)?; - let (function, definition) = - context.find_aggregation_function_definition(object_type_field_type, &function)?; + let nested_object_field = + get_object_field_by_path(collection_object_type, &column, field_path.as_deref())?; + let column_type = &nested_object_field.r#type; + let object_field = collection_object_type.get(&column)?; + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; + let (function, definition) = plan_state + .context + .find_aggregation_function_definition(column_type, &function)?; Ok(plan::Aggregate::SingleColumn { column, + column_type: column_type.clone(), + arguments: plan_arguments, field_path, function, result_type: definition.result_type.clone(), @@ -260,504 +291,133 @@ fn plan_for_order_by_element( ) -> Result> { let target = match element.target { ndc::OrderByTarget::Column { - name, - field_path, path, - } => plan::OrderByTarget::Column { - name: name.clone(), + name, + arguments, field_path, - path: plan_for_relationship_path( - plan_state, - root_collection_object_type, - object_type, - path, - vec![name], - )? - .0, - }, - ndc::OrderByTarget::SingleColumnAggregate { - column, - function, - path, - field_path: _, } => { - let (plan_path, target_object_type) = plan_for_relationship_path( + let (relationship_names, collection_object_type) = plan_for_relationship_path( plan_state, root_collection_object_type, object_type, path, - vec![], // TODO: MDB-156 propagate requested aggregate to relationship query + vec![name.clone()], )?; - let column_type = find_object_field(&target_object_type, &column)?; - let (function, function_definition) = plan_state - .context - .find_aggregation_function_definition(column_type, &function)?; + let object_field = collection_object_type.get(&name)?; - plan::OrderByTarget::SingleColumnAggregate { - column, - function, - result_type: function_definition.result_type.clone(), - path: plan_path, + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; + + plan::OrderByTarget::Column { + path: relationship_names, + name: name.clone(), + arguments: plan_arguments, + field_path, } } - ndc::OrderByTarget::StarCountAggregate { path } => { - let (plan_path, _) = plan_for_relationship_path( + ndc::OrderByTarget::Aggregate { + path, + aggregate: + ndc::Aggregate::ColumnCount { + column, + arguments, + field_path, + distinct, + }, + } => { + let (plan_path, collection_object_type) = plan_for_relationship_path( plan_state, root_collection_object_type, object_type, path, - vec![], // TODO: MDB-157 propagate requested aggregate to relationship query + vec![], // TODO: ENG-1019 propagate requested aggregate to relationship query )?; - plan::OrderByTarget::StarCountAggregate { path: plan_path } - } - }; - Ok(plan::OrderByElement { - order_direction: element.order_direction, - target, - }) -} + let object_field = collection_object_type.get(&column)?; -/// Returns list of aliases for joins to traverse, plus the object type of the final collection in -/// the path. -fn plan_for_relationship_path( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - object_type: &plan::ObjectType, - relationship_path: Vec, - requested_columns: Vec, // columns to select from last path element -) -> Result<(Vec, ObjectType)> { - let end_of_relationship_path_object_type = relationship_path - .last() - .map(|last_path_element| { - let relationship = lookup_relationship( - plan_state.collection_relationships, - &last_path_element.relationship, + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, )?; - plan_state - .context - .find_collection_object_type(&relationship.target_collection) - }) - .transpose()?; - let target_object_type = end_of_relationship_path_object_type.unwrap_or(object_type.clone()); - - let reversed_relationship_path = { - let mut path = relationship_path; - path.reverse(); - path - }; - - let vec_deque = plan_for_relationship_path_helper( - plan_state, - root_collection_object_type, - reversed_relationship_path, - requested_columns, - )?; - let aliases = vec_deque.into_iter().collect(); - - Ok((aliases, target_object_type)) -} - -fn plan_for_relationship_path_helper( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - mut reversed_relationship_path: Vec, - requested_columns: Vec, // columns to select from last path element -) -> Result> { - if reversed_relationship_path.is_empty() { - return Ok(VecDeque::new()); - } - - // safety: we just made an early return if the path is empty - let head = reversed_relationship_path.pop().unwrap(); - let tail = reversed_relationship_path; - let is_last = tail.is_empty(); - - let ndc::PathElement { - relationship, - arguments, - predicate, - } = head; - - let relationship_def = lookup_relationship(plan_state.collection_relationships, &relationship)?; - let related_collection_type = plan_state - .context - .find_collection_object_type(&relationship_def.target_collection)?; - let mut nested_state = plan_state.state_for_subquery(); - - // If this is the last path element then we need to apply the requested fields to the - // relationship query. Otherwise we need to recursively process the rest of the path. Both - // cases take ownership of `requested_columns` so we group them together. - let (mut rest_path, fields) = if is_last { - let fields = requested_columns - .into_iter() - .map(|column_name| { - let column_type = - find_object_field(&related_collection_type, &column_name)?.clone(); - Ok(( - column_name.clone(), - plan::Field::Column { - column: column_name, - fields: None, - column_type, - }, - )) - }) - .collect::>()?; - (VecDeque::new(), Some(fields)) - } else { - let rest = plan_for_relationship_path_helper( - &mut nested_state, - root_collection_object_type, - tail, - requested_columns, - )?; - (rest, None) - }; - - let predicate_plan = predicate - .map(|p| { - plan_for_expression( - &mut nested_state, - root_collection_object_type, - &related_collection_type, - *p, - ) - }) - .transpose()?; - - let nested_relationships = nested_state.into_relationships(); - - let relationship_query = plan::Query { - predicate: predicate_plan, - relationships: nested_relationships, - fields, - ..Default::default() - }; - let relation_key = - plan_state.register_relationship(relationship, arguments, relationship_query)?; - - rest_path.push_front(relation_key); - Ok(rest_path) -} - -fn plan_for_expression( - plan_state: &mut QueryPlanState, - root_collection_object_type: &plan::ObjectType, - object_type: &plan::ObjectType, - expression: ndc::Expression, -) -> Result> { - match expression { - ndc::Expression::And { expressions } => Ok(plan::Expression::And { - expressions: expressions - .into_iter() - .map(|expr| { - plan_for_expression(plan_state, root_collection_object_type, object_type, expr) - }) - .collect::>()?, - }), - ndc::Expression::Or { expressions } => Ok(plan::Expression::Or { - expressions: expressions - .into_iter() - .map(|expr| { - plan_for_expression(plan_state, root_collection_object_type, object_type, expr) - }) - .collect::>()?, - }), - ndc::Expression::Not { expression } => Ok(plan::Expression::Not { - expression: Box::new(plan_for_expression( - plan_state, - root_collection_object_type, - object_type, - *expression, - )?), - }), - ndc::Expression::UnaryComparisonOperator { column, operator } => { - Ok(plan::Expression::UnaryComparisonOperator { - column: plan_for_comparison_target( - plan_state, - root_collection_object_type, - object_type, + plan::OrderByTarget::Aggregate { + path: plan_path, + aggregate: plan::Aggregate::ColumnCount { column, - )?, - operator, - }) - } - ndc::Expression::BinaryComparisonOperator { - column, - operator, - value, - } => plan_for_binary_comparison( - plan_state, - root_collection_object_type, - object_type, - column, - operator, - value, - ), - ndc::Expression::Exists { - in_collection, - predicate, - } => plan_for_exists( - plan_state, - root_collection_object_type, - in_collection, - predicate, - ), - } -} - -fn plan_for_binary_comparison( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - object_type: &plan::ObjectType, - column: ndc::ComparisonTarget, - operator: ndc::ComparisonOperatorName, - value: ndc::ComparisonValue, -) -> Result> { - let comparison_target = - plan_for_comparison_target(plan_state, root_collection_object_type, object_type, column)?; - let (operator, operator_definition) = plan_state - .context - .find_comparison_operator(comparison_target.get_field_type(), &operator)?; - let value_type = match operator_definition { - plan::ComparisonOperatorDefinition::Equal => { - let column_type = comparison_target.get_field_type().clone(); - value_type_in_possible_array_equality_comparison(column_type) - } - plan::ComparisonOperatorDefinition::In => { - plan::Type::ArrayOf(Box::new(comparison_target.get_field_type().clone())) + arguments: plan_arguments, + field_path, + distinct, + }, + } } - plan::ComparisonOperatorDefinition::Custom { argument_type } => argument_type.clone(), - }; - Ok(plan::Expression::BinaryComparisonOperator { - operator, - value: plan_for_comparison_value( - plan_state, - root_collection_object_type, - object_type, - value_type, - value, - )?, - column: comparison_target, - }) -} - -fn plan_for_comparison_target( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - object_type: &plan::ObjectType, - target: ndc::ComparisonTarget, -) -> Result> { - match target { - ndc::ComparisonTarget::Column { - name, - field_path, + ndc::OrderByTarget::Aggregate { path, + aggregate: + ndc::Aggregate::SingleColumn { + column, + arguments, + field_path, + function, + }, } => { - let requested_columns = vec![name.clone()]; - let (path, target_object_type) = plan_for_relationship_path( + let (plan_path, collection_object_type) = plan_for_relationship_path( plan_state, root_collection_object_type, object_type, path, - requested_columns, + vec![], // TODO: ENG-1019 propagate requested aggregate to relationship query )?; - let field_type = - find_object_field_path(&target_object_type, &name, field_path.as_ref())?.clone(); - Ok(plan::ComparisonTarget::Column { - name, - field_path, - path, - field_type, - }) - } - ndc::ComparisonTarget::RootCollectionColumn { name, field_path } => { - let field_type = - find_object_field_path(root_collection_object_type, &name, field_path.as_ref())?.clone(); - Ok(plan::ComparisonTarget::ColumnInScope { - name, - field_path, - field_type, - scope: plan_state.scope.clone(), - }) - } - } -} -fn plan_for_comparison_value( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - object_type: &plan::ObjectType, - expected_type: plan::Type, - value: ndc::ComparisonValue, -) -> Result> { - match value { - ndc::ComparisonValue::Column { column } => Ok(plan::ComparisonValue::Column { - column: plan_for_comparison_target( - plan_state, - root_collection_object_type, - object_type, - column, - )?, - }), - ndc::ComparisonValue::Scalar { value } => Ok(plan::ComparisonValue::Scalar { - value, - value_type: expected_type, - }), - ndc::ComparisonValue::Variable { name } => { - plan_state.register_variable_use(&name, expected_type.clone()); - Ok(plan::ComparisonValue::Variable { - name, - variable_type: expected_type, - }) - } - } -} - -fn plan_for_exists( - plan_state: &mut QueryPlanState<'_, T>, - root_collection_object_type: &plan::ObjectType, - in_collection: ExistsInCollection, - predicate: Option>, -) -> Result> { - let mut nested_state = plan_state.state_for_subquery(); - - let (in_collection, predicate) = match in_collection { - ndc::ExistsInCollection::Related { - relationship, - arguments, - } => { - let ndc_relationship = - lookup_relationship(plan_state.collection_relationships, &relationship)?; - let collection_object_type = plan_state - .context - .find_collection_object_type(&ndc_relationship.target_collection)?; - - let predicate = predicate - .map(|expression| { - plan_for_expression( - &mut nested_state, - root_collection_object_type, - &collection_object_type, - *expression, - ) - }) - .transpose()?; - - let fields = predicate.as_ref().map(|p| { - p.query_local_comparison_targets() - .map(|comparison_target| { - ( - comparison_target.column_name().to_owned(), - plan::Field::Column { - column: comparison_target.column_name().clone(), - column_type: comparison_target.get_field_type().clone(), - fields: None, - }, - ) - }) - .collect() - }); - - let relationship_query = plan::Query { - fields, - relationships: nested_state.into_relationships(), - ..Default::default() - }; + let object_field = collection_object_type.get(&column)?; - let relationship_key = - plan_state.register_relationship(relationship, arguments, relationship_query)?; - - let in_collection = plan::ExistsInCollection::Related { - relationship: relationship_key, - }; + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; - Ok((in_collection, predicate)) as Result<_> - } - ndc::ExistsInCollection::Unrelated { - collection, - arguments, - } => { - let collection_object_type = plan_state + let object_field = find_object_field(&collection_object_type, &column)?; + let column_type = &object_field.r#type; + let (function, function_definition) = plan_state .context - .find_collection_object_type(&collection)?; - - let predicate = predicate - .map(|expression| { - plan_for_expression( - &mut nested_state, - root_collection_object_type, - &collection_object_type, - *expression, - ) - }) - .transpose()?; - - let join_query = plan::Query { - predicate: predicate.clone(), - relationships: nested_state.into_relationships(), - ..Default::default() - }; - - let join_key = plan_state.register_unrelated_join(collection, arguments, join_query)?; + .find_aggregation_function_definition(column_type, &function)?; - let in_collection = plan::ExistsInCollection::Unrelated { - unrelated_collection: join_key, - }; - Ok((in_collection, predicate)) + plan::OrderByTarget::Aggregate { + path: plan_path, + aggregate: plan::Aggregate::SingleColumn { + column, + column_type: column_type.clone(), + arguments: plan_arguments, + field_path, + function, + result_type: function_definition.result_type.clone(), + }, + } } - ndc::ExistsInCollection::NestedCollection { - column_name, - arguments, - field_path, + ndc::OrderByTarget::Aggregate { + path, + aggregate: ndc::Aggregate::StarCount {}, } => { - let arguments = if arguments.is_empty() { - Default::default() - } else { - Err(QueryPlanError::NotImplemented( - "arguments on nested fields".to_string(), - ))? - }; - - // To support field arguments here we need a way to look up field parameters (a map of - // supported argument names to types). When we have that replace the above `arguments` - // assignment with this one: - // let arguments = plan_for_arguments(plan_state, parameters, arguments)?; - - let nested_collection_type = find_nested_collection_type( - root_collection_object_type.clone(), - &field_path - .clone() - .into_iter() - .chain(once(column_name.clone())) - .collect_vec(), + let (plan_path, _) = plan_for_relationship_path( + plan_state, + root_collection_object_type, + object_type, + path, + vec![], // TODO: ENG-1019 propagate requested aggregate to relationship query )?; - - let in_collection = plan::ExistsInCollection::NestedCollection { - column_name, - arguments, - field_path, - }; - - let predicate = predicate - .map(|expression| { - plan_for_expression( - &mut nested_state, - root_collection_object_type, - &nested_collection_type, - *expression, - ) - }) - .transpose()?; - - Ok((in_collection, predicate)) + plan::OrderByTarget::Aggregate { + path: plan_path, + aggregate: plan::Aggregate::StarCount, + } } - }?; + }; - Ok(plan::Expression::Exists { - in_collection, - predicate: predicate.map(Box::new), + Ok(plan::OrderByElement { + order_direction: element.order_direction, + target, }) } diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_for_arguments.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_arguments.rs index 6f485448..b15afb1c 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/plan_for_arguments.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_arguments.rs @@ -44,7 +44,7 @@ pub fn plan_for_mutation_procedure_arguments( ) } -/// Convert maps of [ndc::Argument] values to maps of [plan::Argument] +/// Convert maps of [ndc::RelationshipArgument] values to maps of [plan::RelationshipArgument] pub fn plan_for_relationship_arguments( plan_state: &mut QueryPlanState<'_, T>, parameters: &BTreeMap, @@ -70,17 +70,54 @@ pub fn plan_for_relationship_arguments( Ok(arguments) } +/// Create a map of plan arguments when we already have plan types for parameters. +pub fn plan_arguments_from_plan_parameters( + plan_state: &mut QueryPlanState<'_, T>, + parameters: &BTreeMap>, + arguments: BTreeMap, +) -> Result>> { + let arguments = plan_for_arguments_generic( + plan_state, + parameters, + arguments, + |_plan_state, plan_type, argument| match argument { + ndc::Argument::Variable { name } => Ok(plan::Argument::Variable { + name, + argument_type: plan_type.clone(), + }), + ndc::Argument::Literal { value } => Ok(plan::Argument::Literal { + value, + argument_type: plan_type.clone(), + }), + }, + )?; + + for argument in arguments.values() { + if let plan::Argument::Variable { + name, + argument_type, + } = argument + { + plan_state.register_variable_use(name, argument_type.clone()) + } + } + + Ok(arguments) +} + fn plan_for_argument( plan_state: &mut QueryPlanState<'_, T>, - parameter_type: &ndc::Type, + argument_info: &ndc::ArgumentInfo, argument: ndc::Argument, ) -> Result> { match argument { ndc::Argument::Variable { name } => Ok(plan::Argument::Variable { name, - argument_type: plan_state.context.ndc_to_plan_type(parameter_type)?, + argument_type: plan_state + .context + .ndc_to_plan_type(&argument_info.argument_type)?, }), - ndc::Argument::Literal { value } => match parameter_type { + ndc::Argument::Literal { value } => match &argument_info.argument_type { ndc::Type::Predicate { object_type_name } => Ok(plan::Argument::Predicate { expression: plan_for_predicate(plan_state, object_type_name, value)?, }), @@ -94,10 +131,10 @@ fn plan_for_argument( fn plan_for_mutation_procedure_argument( plan_state: &mut QueryPlanState<'_, T>, - parameter_type: &ndc::Type, + argument_info: &ndc::ArgumentInfo, value: serde_json::Value, ) -> Result> { - match parameter_type { + match &argument_info.argument_type { ndc::Type::Predicate { object_type_name } => { Ok(plan::MutationProcedureArgument::Predicate { expression: plan_for_predicate(plan_state, object_type_name, value)?, @@ -112,19 +149,20 @@ fn plan_for_mutation_procedure_argument( fn plan_for_relationship_argument( plan_state: &mut QueryPlanState<'_, T>, - parameter_type: &ndc::Type, + argument_info: &ndc::ArgumentInfo, argument: ndc::RelationshipArgument, ) -> Result> { + let argument_type = &argument_info.argument_type; match argument { ndc::RelationshipArgument::Variable { name } => Ok(plan::RelationshipArgument::Variable { name, - argument_type: plan_state.context.ndc_to_plan_type(parameter_type)?, + argument_type: plan_state.context.ndc_to_plan_type(argument_type)?, }), ndc::RelationshipArgument::Column { name } => Ok(plan::RelationshipArgument::Column { name, - argument_type: plan_state.context.ndc_to_plan_type(parameter_type)?, + argument_type: plan_state.context.ndc_to_plan_type(argument_type)?, }), - ndc::RelationshipArgument::Literal { value } => match parameter_type { + ndc::RelationshipArgument::Literal { value } => match argument_type { ndc::Type::Predicate { object_type_name } => { Ok(plan::RelationshipArgument::Predicate { expression: plan_for_predicate(plan_state, object_type_name, value)?, @@ -151,19 +189,19 @@ fn plan_for_predicate( /// Convert maps of [ndc::Argument] or [ndc::RelationshipArgument] values to [plan::Argument] or /// [plan::RelationshipArgument] respectively. -fn plan_for_arguments_generic( +fn plan_for_arguments_generic( plan_state: &mut QueryPlanState<'_, T>, - parameters: &BTreeMap, + parameters: &BTreeMap, mut arguments: BTreeMap, convert_argument: F, ) -> Result> where - F: Fn(&mut QueryPlanState<'_, T>, &ndc::Type, NdcArgument) -> Result, + F: Fn(&mut QueryPlanState<'_, T>, &Parameter, NdcArgument) -> Result, { validate_no_excess_arguments(parameters, &arguments)?; let (arguments, missing): ( - Vec<(ndc::ArgumentName, NdcArgument, &ndc::ArgumentInfo)>, + Vec<(ndc::ArgumentName, NdcArgument, &Parameter)>, Vec, ) = parameters .iter() @@ -185,7 +223,7 @@ where ) = arguments .into_iter() .map(|(name, argument, argument_info)| { - match convert_argument(plan_state, &argument_info.argument_type, argument) { + match convert_argument(plan_state, argument_info, argument) { Ok(argument) => Ok((name, argument)), Err(err) => Err((name, err)), } @@ -198,8 +236,8 @@ where Ok(resolved) } -pub fn validate_no_excess_arguments( - parameters: &BTreeMap, +pub fn validate_no_excess_arguments( + parameters: &BTreeMap, arguments: &BTreeMap, ) -> Result<()> { let excess: Vec = arguments diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_for_expression.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_expression.rs new file mode 100644 index 00000000..8c30d984 --- /dev/null +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_expression.rs @@ -0,0 +1,431 @@ +use std::iter::once; + +use indexmap::IndexMap; +use itertools::Itertools as _; +use ndc_models::{self as ndc, ExistsInCollection}; + +use crate::{self as plan, QueryContext, QueryPlanError}; + +use super::{ + helpers::{ + find_nested_collection_object_type, find_nested_collection_type, + get_object_field_by_path, lookup_relationship, + }, + plan_for_arguments::plan_arguments_from_plan_parameters, + plan_for_relationship::plan_for_relationship_path, + query_plan_state::QueryPlanState, +}; + +type Result = std::result::Result; + +pub fn plan_for_expression( + plan_state: &mut QueryPlanState, + root_collection_object_type: &plan::ObjectType, + object_type: &plan::ObjectType, + expression: ndc::Expression, +) -> Result> { + match expression { + ndc::Expression::And { expressions } => Ok(plan::Expression::And { + expressions: expressions + .into_iter() + .map(|expr| { + plan_for_expression(plan_state, root_collection_object_type, object_type, expr) + }) + .collect::>()?, + }), + ndc::Expression::Or { expressions } => Ok(plan::Expression::Or { + expressions: expressions + .into_iter() + .map(|expr| { + plan_for_expression(plan_state, root_collection_object_type, object_type, expr) + }) + .collect::>()?, + }), + ndc::Expression::Not { expression } => Ok(plan::Expression::Not { + expression: Box::new(plan_for_expression( + plan_state, + root_collection_object_type, + object_type, + *expression, + )?), + }), + ndc::Expression::UnaryComparisonOperator { column, operator } => { + Ok(plan::Expression::UnaryComparisonOperator { + column: plan_for_comparison_target(plan_state, object_type, column)?, + operator, + }) + } + ndc::Expression::BinaryComparisonOperator { + column, + operator, + value, + } => plan_for_binary_comparison( + plan_state, + root_collection_object_type, + object_type, + column, + operator, + value, + ), + ndc::Expression::ArrayComparison { column, comparison } => plan_for_array_comparison( + plan_state, + root_collection_object_type, + object_type, + column, + comparison, + ), + ndc::Expression::Exists { + in_collection, + predicate, + } => plan_for_exists( + plan_state, + root_collection_object_type, + in_collection, + predicate, + ), + } +} + +fn plan_for_binary_comparison( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + object_type: &plan::ObjectType, + column: ndc::ComparisonTarget, + operator: ndc::ComparisonOperatorName, + value: ndc::ComparisonValue, +) -> Result> { + let comparison_target = plan_for_comparison_target(plan_state, object_type, column)?; + let (operator, operator_definition) = plan_state + .context + .find_comparison_operator(comparison_target.target_type(), &operator)?; + let value_type = operator_definition.argument_type(comparison_target.target_type()); + Ok(plan::Expression::BinaryComparisonOperator { + operator, + value: plan_for_comparison_value( + plan_state, + root_collection_object_type, + object_type, + value_type, + value, + )?, + column: comparison_target, + }) +} + +fn plan_for_array_comparison( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + object_type: &plan::ObjectType, + column: ndc::ComparisonTarget, + comparison: ndc::ArrayComparison, +) -> Result> { + let comparison_target = plan_for_comparison_target(plan_state, object_type, column)?; + let plan_comparison = match comparison { + ndc::ArrayComparison::Contains { value } => { + let array_element_type = comparison_target + .target_type() + .clone() + .into_array_element_type()?; + let value = plan_for_comparison_value( + plan_state, + root_collection_object_type, + object_type, + array_element_type, + value, + )?; + plan::ArrayComparison::Contains { value } + } + ndc::ArrayComparison::IsEmpty => plan::ArrayComparison::IsEmpty, + }; + Ok(plan::Expression::ArrayComparison { + column: comparison_target, + comparison: plan_comparison, + }) +} + +fn plan_for_comparison_target( + plan_state: &mut QueryPlanState<'_, T>, + object_type: &plan::ObjectType, + target: ndc::ComparisonTarget, +) -> Result> { + match target { + ndc::ComparisonTarget::Column { + name, + arguments, + field_path, + } => { + let object_field = + get_object_field_by_path(object_type, &name, field_path.as_deref())?.clone(); + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; + Ok(plan::ComparisonTarget::Column { + name, + arguments: plan_arguments, + field_path, + field_type: object_field.r#type, + }) + } + ndc::ComparisonTarget::Aggregate { .. } => { + // TODO: ENG-1457 implement query.aggregates.filter_by + Err(QueryPlanError::NotImplemented( + "filter by aggregate".to_string(), + )) + } + } +} + +fn plan_for_comparison_value( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + object_type: &plan::ObjectType, + expected_type: plan::Type, + value: ndc::ComparisonValue, +) -> Result> { + match value { + ndc::ComparisonValue::Column { + path, + name, + arguments, + field_path, + scope, + } => { + let (plan_path, collection_object_type) = plan_for_relationship_path( + plan_state, + root_collection_object_type, + object_type, + path, + vec![name.clone()], + )?; + let object_field = collection_object_type.get(&name)?; + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &object_field.parameters, + arguments, + )?; + Ok(plan::ComparisonValue::Column { + path: plan_path, + name, + arguments: plan_arguments, + field_path, + field_type: object_field.r#type.clone(), + scope, + }) + } + ndc::ComparisonValue::Scalar { value } => Ok(plan::ComparisonValue::Scalar { + value, + value_type: expected_type, + }), + ndc::ComparisonValue::Variable { name } => { + plan_state.register_variable_use(&name, expected_type.clone()); + Ok(plan::ComparisonValue::Variable { + name, + variable_type: expected_type, + }) + } + } +} + +fn plan_for_exists( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + in_collection: ExistsInCollection, + predicate: Option>, +) -> Result> { + let mut nested_state = plan_state.state_for_subquery(); + + let (in_collection, predicate) = match in_collection { + ndc::ExistsInCollection::Related { + relationship, + arguments, + field_path: _, // TODO: ENG-1490 requires propagating this, probably through the `register_relationship` call + } => { + let ndc_relationship = + lookup_relationship(plan_state.collection_relationships, &relationship)?; + let collection_object_type = plan_state + .context + .find_collection_object_type(&ndc_relationship.target_collection)?; + + let predicate = predicate + .map(|expression| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &collection_object_type, + *expression, + ) + }) + .transpose()?; + + // TODO: ENG-1457 When we implement query.aggregates.filter_by we'll need to collect aggregates + // here as well as fields. + let fields = predicate.as_ref().map(|p| { + let mut fields = IndexMap::new(); + for comparison_target in p.query_local_comparison_targets() { + match comparison_target.into_owned() { + plan::ComparisonTarget::Column { + name, + arguments: _, + field_type, + .. + } => fields.insert( + name.clone(), + plan::Field::Column { + column: name, + fields: None, + column_type: field_type, + }, + ), + }; + } + fields + }); + + let relationship_query = plan::Query { + fields, + relationships: nested_state.into_relationships(), + ..Default::default() + }; + + let relationship_key = + plan_state.register_relationship(relationship, arguments, relationship_query)?; + + let in_collection = plan::ExistsInCollection::Related { + relationship: relationship_key, + }; + + Ok((in_collection, predicate)) as Result<_> + } + ndc::ExistsInCollection::Unrelated { + collection, + arguments, + } => { + let collection_object_type = plan_state + .context + .find_collection_object_type(&collection)?; + + let predicate = predicate + .map(|expression| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &collection_object_type, + *expression, + ) + }) + .transpose()?; + + let join_query = plan::Query { + predicate: predicate.clone(), + relationships: nested_state.into_relationships(), + ..Default::default() + }; + + let join_key = plan_state.register_unrelated_join(collection, arguments, join_query)?; + + let in_collection = plan::ExistsInCollection::Unrelated { + unrelated_collection: join_key, + }; + Ok((in_collection, predicate)) + } + ndc::ExistsInCollection::NestedCollection { + column_name, + arguments, + field_path, + } => { + let object_field = root_collection_object_type.get(&column_name)?; + let plan_arguments = plan_arguments_from_plan_parameters( + &mut nested_state, + &object_field.parameters, + arguments, + )?; + + let nested_collection_type = find_nested_collection_object_type( + root_collection_object_type.clone(), + &field_path + .clone() + .into_iter() + .chain(once(column_name.clone())) + .collect_vec(), + )?; + + let in_collection = plan::ExistsInCollection::NestedCollection { + column_name, + arguments: plan_arguments, + field_path, + }; + + let predicate = predicate + .map(|expression| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &nested_collection_type, + *expression, + ) + }) + .transpose()?; + + Ok((in_collection, predicate)) + } + ExistsInCollection::NestedScalarCollection { + column_name, + arguments, + field_path, + } => { + let object_field = root_collection_object_type.get(&column_name)?; + let plan_arguments = plan_arguments_from_plan_parameters( + &mut nested_state, + &object_field.parameters, + arguments, + )?; + + let nested_collection_type = find_nested_collection_type( + root_collection_object_type.clone(), + &field_path + .clone() + .into_iter() + .chain(once(column_name.clone())) + .collect_vec(), + )?; + + let virtual_object_type = plan::ObjectType { + name: None, + fields: [( + "__value".into(), + plan::ObjectField { + r#type: nested_collection_type, + parameters: Default::default(), + }, + )] + .into(), + }; + + let in_collection = plan::ExistsInCollection::NestedScalarCollection { + column_name, + arguments: plan_arguments, + field_path, + }; + + let predicate = predicate + .map(|expression| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &virtual_object_type, + *expression, + ) + }) + .transpose()?; + + Ok((in_collection, predicate)) + } + }?; + + Ok(plan::Expression::Exists { + in_collection, + predicate: predicate.map(Box::new), + }) +} diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_for_grouping.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_grouping.rs new file mode 100644 index 00000000..6d848e67 --- /dev/null +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_grouping.rs @@ -0,0 +1,241 @@ +use ndc_models::{self as ndc}; + +use crate::{self as plan, ConnectorTypes, QueryContext, QueryPlanError}; + +use super::{ + helpers::get_object_field_by_path, plan_for_aggregate, plan_for_aggregates, + plan_for_arguments::plan_arguments_from_plan_parameters, + plan_for_relationship::plan_for_relationship_path, query_plan_state::QueryPlanState, +}; + +type Result = std::result::Result; + +pub fn plan_for_grouping( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + collection_object_type: &plan::ObjectType, + grouping: ndc::Grouping, +) -> Result> { + let dimensions = grouping + .dimensions + .into_iter() + .map(|d| { + plan_for_dimension( + plan_state, + root_collection_object_type, + collection_object_type, + d, + ) + }) + .collect::>()?; + + let aggregates = plan_for_aggregates( + plan_state, + collection_object_type, + grouping + .aggregates + .into_iter() + .map(|(key, aggregate)| (key.into(), aggregate)) + .collect(), + )?; + + let predicate = grouping + .predicate + .map(|predicate| plan_for_group_expression(plan_state, collection_object_type, predicate)) + .transpose()?; + + let order_by = grouping + .order_by + .map(|order_by| plan_for_group_order_by(plan_state, collection_object_type, order_by)) + .transpose()?; + + let plan_grouping = plan::Grouping { + dimensions, + aggregates, + predicate, + order_by, + limit: grouping.limit, + offset: grouping.offset, + }; + Ok(plan_grouping) +} + +fn plan_for_dimension( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + collection_object_type: &plan::ObjectType, + dimension: ndc::Dimension, +) -> Result> { + let plan_dimension = match dimension { + ndc_models::Dimension::Column { + path, + column_name, + arguments, + field_path, + } => { + let (relationship_path, collection_type) = plan_for_relationship_path( + plan_state, + root_collection_object_type, + collection_object_type, + path, + vec![column_name.clone()], + )?; + + let plan_arguments = plan_arguments_from_plan_parameters( + plan_state, + &collection_type.get(&column_name)?.parameters, + arguments, + )?; + + let object_field = + get_object_field_by_path(&collection_type, &column_name, field_path.as_deref())? + .clone(); + + let references_relationship = !relationship_path.is_empty(); + let field_type = if references_relationship { + plan::Type::array_of(object_field.r#type) + } else { + object_field.r#type + }; + + plan::Dimension::Column { + path: relationship_path, + column_name, + arguments: plan_arguments, + field_path, + field_type, + } + } + }; + Ok(plan_dimension) +} + +fn plan_for_group_expression( + plan_state: &mut QueryPlanState, + object_type: &plan::ObjectType, + expression: ndc::GroupExpression, +) -> Result> { + match expression { + ndc::GroupExpression::And { expressions } => Ok(plan::GroupExpression::And { + expressions: expressions + .into_iter() + .map(|expr| plan_for_group_expression(plan_state, object_type, expr)) + .collect::>()?, + }), + ndc::GroupExpression::Or { expressions } => Ok(plan::GroupExpression::Or { + expressions: expressions + .into_iter() + .map(|expr| plan_for_group_expression(plan_state, object_type, expr)) + .collect::>()?, + }), + ndc::GroupExpression::Not { expression } => Ok(plan::GroupExpression::Not { + expression: Box::new(plan_for_group_expression( + plan_state, + object_type, + *expression, + )?), + }), + ndc::GroupExpression::UnaryComparisonOperator { target, operator } => { + Ok(plan::GroupExpression::UnaryComparisonOperator { + target: plan_for_group_comparison_target(plan_state, object_type, target)?, + operator, + }) + } + ndc::GroupExpression::BinaryComparisonOperator { + target, + operator, + value, + } => { + let target = plan_for_group_comparison_target(plan_state, object_type, target)?; + let (operator, operator_definition) = plan_state + .context + .find_comparison_operator(&target.result_type(), &operator)?; + let value_type = operator_definition.argument_type(&target.result_type()); + Ok(plan::GroupExpression::BinaryComparisonOperator { + target, + operator, + value: plan_for_group_comparison_value(plan_state, value_type, value)?, + }) + } + } +} + +fn plan_for_group_comparison_target( + plan_state: &mut QueryPlanState, + object_type: &plan::ObjectType, + target: ndc::GroupComparisonTarget, +) -> Result> { + let plan_target = match target { + ndc::GroupComparisonTarget::Aggregate { aggregate } => { + let target_aggregate = plan_for_aggregate(plan_state, object_type, aggregate)?; + plan::GroupComparisonTarget::Aggregate { + aggregate: target_aggregate, + } + } + }; + Ok(plan_target) +} + +fn plan_for_group_comparison_value( + plan_state: &mut QueryPlanState, + expected_type: plan::Type, + value: ndc::GroupComparisonValue, +) -> Result> { + match value { + ndc::GroupComparisonValue::Scalar { value } => Ok(plan::GroupComparisonValue::Scalar { + value, + value_type: expected_type, + }), + ndc::GroupComparisonValue::Variable { name } => { + plan_state.register_variable_use(&name, expected_type.clone()); + Ok(plan::GroupComparisonValue::Variable { + name, + variable_type: expected_type, + }) + } + } +} + +fn plan_for_group_order_by( + plan_state: &mut QueryPlanState<'_, T>, + collection_object_type: &plan::ObjectType, + order_by: ndc::GroupOrderBy, +) -> Result> { + Ok(plan::GroupOrderBy { + elements: order_by + .elements + .into_iter() + .map(|elem| plan_for_group_order_by_element(plan_state, collection_object_type, elem)) + .collect::>()?, + }) +} + +fn plan_for_group_order_by_element( + plan_state: &mut QueryPlanState<'_, T>, + collection_object_type: &plan::ObjectType<::ScalarType>, + element: ndc::GroupOrderByElement, +) -> Result> { + Ok(plan::GroupOrderByElement { + order_direction: element.order_direction, + target: plan_for_group_order_by_target(plan_state, collection_object_type, element.target)?, + }) +} + +fn plan_for_group_order_by_target( + plan_state: &mut QueryPlanState<'_, T>, + collection_object_type: &plan::ObjectType, + target: ndc::GroupOrderByTarget, +) -> Result> { + match target { + ndc::GroupOrderByTarget::Dimension { index } => { + Ok(plan::GroupOrderByTarget::Dimension { index }) + } + ndc::GroupOrderByTarget::Aggregate { aggregate } => { + let target_aggregate = + plan_for_aggregate(plan_state, collection_object_type, aggregate)?; + Ok(plan::GroupOrderByTarget::Aggregate { + aggregate: target_aggregate, + }) + } + } +} diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_for_relationship.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_relationship.rs new file mode 100644 index 00000000..de98e178 --- /dev/null +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_for_relationship.rs @@ -0,0 +1,137 @@ +use std::collections::VecDeque; + +use crate::{self as plan, ObjectType, QueryContext, QueryPlanError}; +use ndc_models::{self as ndc}; + +use super::{ + helpers::{find_object_field, lookup_relationship}, + plan_for_expression, + query_plan_state::QueryPlanState, +}; + +type Result = std::result::Result; + +/// Returns list of aliases for joins to traverse, plus the object type of the final collection in +/// the path. +pub fn plan_for_relationship_path( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + object_type: &plan::ObjectType, + relationship_path: Vec, + requested_columns: Vec, // columns to select from last path element +) -> Result<(Vec, ObjectType)> { + let end_of_relationship_path_object_type = relationship_path + .last() + .map(|last_path_element| { + let relationship = lookup_relationship( + plan_state.collection_relationships, + &last_path_element.relationship, + )?; + plan_state + .context + .find_collection_object_type(&relationship.target_collection) + }) + .transpose()?; + let target_object_type = end_of_relationship_path_object_type.unwrap_or(object_type.clone()); + + let reversed_relationship_path = { + let mut path = relationship_path; + path.reverse(); + path + }; + + let vec_deque = plan_for_relationship_path_helper( + plan_state, + root_collection_object_type, + reversed_relationship_path, + requested_columns, + )?; + let aliases = vec_deque.into_iter().collect(); + + Ok((aliases, target_object_type)) +} + +fn plan_for_relationship_path_helper( + plan_state: &mut QueryPlanState<'_, T>, + root_collection_object_type: &plan::ObjectType, + mut reversed_relationship_path: Vec, + requested_columns: Vec, // columns to select from last path element +) -> Result> { + if reversed_relationship_path.is_empty() { + return Ok(VecDeque::new()); + } + + // safety: we just made an early return if the path is empty + let head = reversed_relationship_path.pop().unwrap(); + let tail = reversed_relationship_path; + let is_last = tail.is_empty(); + + let ndc::PathElement { + field_path: _, // TODO: ENG-1458 support nested relationships + relationship, + arguments, + predicate, + } = head; + + let relationship_def = lookup_relationship(plan_state.collection_relationships, &relationship)?; + let related_collection_type = plan_state + .context + .find_collection_object_type(&relationship_def.target_collection)?; + let mut nested_state = plan_state.state_for_subquery(); + + // If this is the last path element then we need to apply the requested fields to the + // relationship query. Otherwise we need to recursively process the rest of the path. Both + // cases take ownership of `requested_columns` so we group them together. + let (mut rest_path, fields) = if is_last { + let fields = requested_columns + .into_iter() + .map(|column_name| { + let object_field = + find_object_field(&related_collection_type, &column_name)?.clone(); + Ok(( + column_name.clone(), + plan::Field::Column { + column: column_name, + fields: None, + column_type: object_field.r#type, + }, + )) + }) + .collect::>()?; + (VecDeque::new(), Some(fields)) + } else { + let rest = plan_for_relationship_path_helper( + &mut nested_state, + root_collection_object_type, + tail, + requested_columns, + )?; + (rest, None) + }; + + let predicate_plan = predicate + .map(|p| { + plan_for_expression( + &mut nested_state, + root_collection_object_type, + &related_collection_type, + *p, + ) + }) + .transpose()?; + + let nested_relationships = nested_state.into_relationships(); + + let relationship_query = plan::Query { + predicate: predicate_plan, + relationships: nested_relationships, + fields, + ..Default::default() + }; + + let relation_key = + plan_state.register_relationship(relationship, arguments, relationship_query)?; + + rest_path.push_front(relation_key); + Ok(rest_path) +} diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/mod.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/mod.rs index 8518fd90..970f4d34 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/mod.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/mod.rs @@ -15,11 +15,10 @@ use ndc_test_helpers::{ use crate::{ConnectorTypes, QueryContext, QueryPlanError, Type}; -#[allow(unused_imports)] pub use self::{ - query::{query, QueryBuilder}, - relationships::{relationship, RelationshipBuilder}, - type_helpers::{date, double, int, object_type, string}, + query::QueryBuilder, + relationships::relationship, + type_helpers::{date, double, int, string}, }; #[derive(Clone, Debug, Default)] @@ -34,6 +33,14 @@ impl ConnectorTypes for TestContext { type AggregateFunction = AggregateFunction; type ComparisonOperator = ComparisonOperator; type ScalarType = ScalarType; + + fn count_aggregate_type() -> Type { + int() + } + + fn string_type() -> Type { + string() + } } impl QueryContext for TestContext { @@ -95,7 +102,7 @@ impl QueryContext for TestContext { } } -#[derive(Clone, Copy, Debug, PartialEq, Sequence)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Sequence)] pub enum AggregateFunction { Average, } @@ -108,7 +115,7 @@ impl NamedEnum for AggregateFunction { } } -#[derive(Clone, Copy, Debug, PartialEq, Sequence)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Sequence)] pub enum ComparisonOperator { Equal, Regex, @@ -123,7 +130,7 @@ impl NamedEnum for ComparisonOperator { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Sequence)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Sequence)] pub enum ScalarType { Bool, Date, @@ -173,13 +180,11 @@ fn scalar_types() -> BTreeMap { ( ScalarType::Double.name().to_owned(), ndc::ScalarType { - representation: Some(TypeRepresentation::Float64), + representation: TypeRepresentation::Float64, aggregate_functions: [( AggregateFunction::Average.name().into(), - ndc::AggregateFunctionDefinition { - result_type: ndc::Type::Named { - name: ScalarType::Double.name().into(), - }, + ndc::AggregateFunctionDefinition::Average { + result_type: ScalarType::Double.name().into(), }, )] .into(), @@ -193,13 +198,11 @@ fn scalar_types() -> BTreeMap { ( ScalarType::Int.name().to_owned(), ndc::ScalarType { - representation: Some(TypeRepresentation::Int32), + representation: TypeRepresentation::Int32, aggregate_functions: [( AggregateFunction::Average.name().into(), - ndc::AggregateFunctionDefinition { - result_type: ndc::Type::Named { - name: ScalarType::Double.name().into(), - }, + ndc::AggregateFunctionDefinition::Average { + result_type: ScalarType::Double.name().into(), }, )] .into(), @@ -213,7 +216,7 @@ fn scalar_types() -> BTreeMap { ( ScalarType::String.name().to_owned(), ndc::ScalarType { - representation: Some(TypeRepresentation::String), + representation: TypeRepresentation::String, aggregate_functions: Default::default(), comparison_operators: [ ( @@ -249,7 +252,6 @@ pub fn make_flat_schema() -> TestContext { collection_type: "Author".into(), arguments: Default::default(), uniqueness_constraints: make_primary_key_uniqueness_constraint("authors"), - foreign_keys: Default::default(), }, ), ( @@ -260,7 +262,6 @@ pub fn make_flat_schema() -> TestContext { collection_type: "Article".into(), arguments: Default::default(), uniqueness_constraints: make_primary_key_uniqueness_constraint("articles"), - foreign_keys: Default::default(), }, ), ]), @@ -297,7 +298,6 @@ pub fn make_nested_schema() -> TestContext { collection_type: "Author".into(), arguments: Default::default(), uniqueness_constraints: make_primary_key_uniqueness_constraint("authors"), - foreign_keys: Default::default(), }, ), collection("appearances"), // new helper gives more concise syntax diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/query.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/query.rs index ddb9df8c..444870b4 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/query.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/query.rs @@ -1,8 +1,7 @@ use indexmap::IndexMap; use crate::{ - Aggregate, ConnectorTypes, Expression, Field, OrderBy, OrderByElement, Query, Relationships, - Scope, + Aggregate, ConnectorTypes, Expression, Field, Grouping, OrderBy, OrderByElement, Query, Relationships, Scope }; #[derive(Clone, Debug, Default)] @@ -10,10 +9,10 @@ pub struct QueryBuilder { aggregates: Option>>, fields: Option>>, limit: Option, - aggregates_limit: Option, offset: Option, order_by: Option>, predicate: Option>, + groups: Option>, relationships: Relationships, scope: Option, } @@ -29,10 +28,10 @@ impl QueryBuilder { fields: None, aggregates: Default::default(), limit: None, - aggregates_limit: None, offset: None, order_by: None, predicate: None, + groups: None, relationships: Default::default(), scope: None, } @@ -88,10 +87,10 @@ impl From> for Query { aggregates: value.aggregates, fields: value.fields, limit: value.limit, - aggregates_limit: value.aggregates_limit, offset: value.offset, order_by: value.order_by, predicate: value.predicate, + groups: value.groups, relationships: value.relationships, scope: value.scope, } diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/relationships.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/relationships.rs index 0ab7cfbd..ab8f3226 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/relationships.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/relationships.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; -use ndc_models::RelationshipType; +use ndc_models::{FieldName, RelationshipType}; +use nonempty::NonEmpty; use crate::{ConnectorTypes, Field, Relationship, RelationshipArgument}; @@ -8,7 +9,7 @@ use super::QueryBuilder; #[derive(Clone, Debug)] pub struct RelationshipBuilder { - column_mapping: BTreeMap, + column_mapping: BTreeMap>, relationship_type: RelationshipType, target_collection: ndc_models::CollectionName, arguments: BTreeMap>, @@ -42,11 +43,22 @@ impl RelationshipBuilder { pub fn column_mapping( mut self, - column_mapping: impl IntoIterator, + column_mapping: impl IntoIterator< + Item = ( + impl Into, + impl IntoIterator>, + ), + >, ) -> Self { self.column_mapping = column_mapping .into_iter() - .map(|(source, target)| (source.to_string().into(), target.to_string().into())) + .map(|(source, target)| { + ( + source.into(), + NonEmpty::collect(target.into_iter().map(Into::into)) + .expect("target path in relationship column mapping may not be empty"), + ) + }) .collect(); self } diff --git a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/type_helpers.rs b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/type_helpers.rs index 7d0dc453..05875471 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/type_helpers.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/plan_test_helpers/type_helpers.rs @@ -1,4 +1,4 @@ -use crate::{ObjectType, Type}; +use crate::Type; use super::ScalarType; @@ -17,15 +17,3 @@ pub fn int() -> Type { pub fn string() -> Type { Type::Scalar(ScalarType::String) } - -pub fn object_type( - fields: impl IntoIterator>)>, -) -> Type { - Type::Object(ObjectType { - name: None, - fields: fields - .into_iter() - .map(|(name, field)| (name.to_string().into(), field.into())) - .collect(), - }) -} diff --git a/crates/ndc-query-plan/src/plan_for_query_request/query_context.rs b/crates/ndc-query-plan/src/plan_for_query_request/query_context.rs index 64a947e1..eb180b43 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/query_context.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/query_context.rs @@ -54,11 +54,32 @@ pub trait QueryContext: ConnectorTypes { Ok(( func, plan::AggregateFunctionDefinition { - result_type: self.ndc_to_plan_type(&definition.result_type)?, + result_type: self.aggregate_function_result_type(definition, input_type)?, }, )) } + fn aggregate_function_result_type( + &self, + definition: &ndc::AggregateFunctionDefinition, + input_type: &plan::Type, + ) -> Result> { + let t = match definition { + ndc::AggregateFunctionDefinition::Min => input_type.clone().into_nullable(), + ndc::AggregateFunctionDefinition::Max => input_type.clone().into_nullable(), + ndc::AggregateFunctionDefinition::Sum { result_type } + | ndc::AggregateFunctionDefinition::Average { result_type } => { + let scalar_type = Self::lookup_scalar_type(result_type) + .ok_or_else(|| QueryPlanError::UnknownScalarType(result_type.clone()))?; + plan::Type::Scalar(scalar_type).into_nullable() + } + ndc::AggregateFunctionDefinition::Custom { result_type } => { + self.ndc_to_plan_type(result_type)? + } + }; + Ok(t) + } + fn find_comparison_operator( &self, left_operand_type: &Type, @@ -72,15 +93,10 @@ pub trait QueryContext: ConnectorTypes { { let (operator, definition) = Self::lookup_comparison_operator(self, left_operand_type, op_name)?; - let plan_def = match definition { - ndc::ComparisonOperatorDefinition::Equal => plan::ComparisonOperatorDefinition::Equal, - ndc::ComparisonOperatorDefinition::In => plan::ComparisonOperatorDefinition::In, - ndc::ComparisonOperatorDefinition::Custom { argument_type } => { - plan::ComparisonOperatorDefinition::Custom { - argument_type: self.ndc_to_plan_type(argument_type)?, - } - } - }; + let plan_def = + plan::ComparisonOperatorDefinition::from_ndc_definition(definition, |ndc_type| { + self.ndc_to_plan_type(ndc_type) + })?; Ok((operator, plan_def)) } diff --git a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs index 4467f802..2283ed1f 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_error.rs @@ -29,6 +29,11 @@ pub enum QueryPlanError { #[error("not implemented: {}", .0)] NotImplemented(String), + #[error("relationship, {relationship_name}, has an empty target path")] + RelationshipEmptyTarget { + relationship_name: ndc::RelationshipName, + }, + #[error("{0}")] RelationshipUnification(#[from] RelationshipUnificationError), diff --git a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_state.rs b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_state.rs index d82e5183..89ccefb7 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/query_plan_state.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/query_plan_state.rs @@ -5,6 +5,7 @@ use std::{ }; use ndc_models as ndc; +use nonempty::NonEmpty; use crate::{ plan_for_query_request::helpers::lookup_relationship, @@ -96,8 +97,23 @@ impl QueryPlanState<'_, T> { Default::default() }; + let column_mapping = ndc_relationship + .column_mapping + .iter() + .map(|(source, target_path)| { + Ok(( + source.clone(), + NonEmpty::collect(target_path.iter().cloned()).ok_or_else(|| { + QueryPlanError::RelationshipEmptyTarget { + relationship_name: ndc_relationship_name.clone(), + } + })?, + )) + }) + .collect::>>()?; + let relationship = Relationship { - column_mapping: ndc_relationship.column_mapping.clone(), + column_mapping, relationship_type: ndc_relationship.relationship_type, target_collection: ndc_relationship.target_collection.clone(), arguments, diff --git a/crates/ndc-query-plan/src/plan_for_query_request/tests.rs b/crates/ndc-query-plan/src/plan_for_query_request/tests.rs index d6ae2409..6e2251b8 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/tests.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/tests.rs @@ -1,507 +1,517 @@ use ndc_models::{self as ndc, OrderByTarget, OrderDirection, RelationshipType}; use ndc_test_helpers::*; +use nonempty::NonEmpty; use pretty_assertions::assert_eq; -use serde_json::json; use crate::{ self as plan, - plan_for_query_request::plan_test_helpers::{ - self, make_flat_schema, make_nested_schema, TestContext, - }, - query_plan::UnrelatedJoin, - ExistsInCollection, Expression, Field, OrderBy, Query, QueryContext, QueryPlan, Relationship, + plan_for_query_request::plan_test_helpers::{self, make_flat_schema, make_nested_schema}, + QueryContext, QueryPlan, Type, }; use super::plan_for_query_request; -#[test] -fn translates_query_request_relationships() -> Result<(), anyhow::Error> { - let request = query_request() - .collection("schools") - .relationships([ - ( - "school_classes", - relationship("classes", [("_id", "school_id")]), - ), - ( - "class_students", - relationship("students", [("_id", "class_id")]), - ), - ( - "class_department", - relationship("departments", [("department_id", "_id")]).object_type(), - ), - ( - "school_directory", - relationship("directory", [("_id", "school_id")]).object_type(), - ), - ( - "student_advisor", - relationship("advisors", [("advisor_id", "_id")]).object_type(), - ), - ( - "existence_check", - relationship("some_collection", [("some_id", "_id")]), - ), - ]) - .query( - query() - .fields([relation_field!("class_name" => "school_classes", query() - .fields([ - relation_field!("student_name" => "class_students") - ]) - )]) - .order_by(vec![ndc::OrderByElement { - order_direction: OrderDirection::Asc, - target: OrderByTarget::Column { - name: "advisor_name".into(), - field_path: None, - path: vec![ - path_element("school_classes".into()) - .predicate(binop( - "Equal", - target!( - "_id", - relations: [ - // path_element("school_classes"), - path_element("class_department".into()), - ], - ), - column_value!( - "math_department_id", - relations: [path_element("school_directory".into())], - ), - )) - .into(), - path_element("class_students".into()).into(), - path_element("student_advisor".into()).into(), - ], - }, - }]) - // The `And` layer checks that we properly recursive into Expressions - .predicate(and([ndc::Expression::Exists { - in_collection: related!("existence_check"), - predicate: None, - }])), - ) - .into(); +// TODO: ENG-1487 we need named scopes to define this query in ndc-spec 0.2 +// #[test] +// fn translates_query_request_relationships() -> Result<(), anyhow::Error> { +// let request = query_request() +// .collection("schools") +// .relationships([ +// ( +// "school_classes", +// relationship("classes", [("_id", &["school_id"])]), +// ), +// ( +// "class_students", +// relationship("students", [("_id", &["class_id"])]), +// ), +// ( +// "class_department", +// relationship("departments", [("department_id", &["_id"])]).object_type(), +// ), +// ( +// "school_directory", +// relationship("directory", [("_id", &["school_id"])]).object_type(), +// ), +// ( +// "student_advisor", +// relationship("advisors", [("advisor_id", &["_id"])]).object_type(), +// ), +// ( +// "existence_check", +// relationship("some_collection", [("some_id", &["_id"])]), +// ), +// ]) +// .query( +// query() +// .fields([relation_field!("class_name" => "school_classes", query() +// .fields([ +// relation_field!("student_name" => "class_students") +// ]) +// )]) +// .order_by(vec![ndc::OrderByElement { +// order_direction: OrderDirection::Asc, +// target: OrderByTarget::Column { +// name: "advisor_name".into(), +// arguments: Default::default(), +// field_path: None, +// path: vec![ +// path_element("school_classes") +// .predicate( +// exists( +// in_related("class_department"), +// binop( +// "Equal", +// target!("_id"), +// column_value("math_department_id") +// .path([path_element("school_directory")]) +// .scope(2) +// .into() +// ), +// ) +// ) +// .into(), +// path_element("class_students").into(), +// path_element("student_advisor").into(), +// ], +// }, +// }]) +// // The `And` layer checks that we properly recurse into Expressions +// .predicate(and([ndc::Expression::Exists { +// in_collection: related!("existence_check"), +// predicate: None, +// }])), +// ) +// .into(); +// +// let expected = QueryPlan { +// collection: "schools".into(), +// arguments: Default::default(), +// variables: None, +// variable_types: Default::default(), +// unrelated_collections: Default::default(), +// query: Query { +// predicate: Some(Expression::And { +// expressions: vec![Expression::Exists { +// in_collection: ExistsInCollection::Related { +// relationship: "existence_check".into(), +// }, +// predicate: None, +// }], +// }), +// order_by: Some(OrderBy { +// elements: [plan::OrderByElement { +// order_direction: OrderDirection::Asc, +// target: plan::OrderByTarget::Column { +// name: "advisor_name".into(), +// arguments: Default::default(), +// field_path: Default::default(), +// path: [ +// "school_classes_0".into(), +// "class_students".into(), +// "student_advisor".into(), +// ] +// .into(), +// }, +// }] +// .into(), +// }), +// relationships: [ +// // We join on the school_classes relationship twice. This one is for the `order_by` +// // comparison in the top-level request query +// ( +// "school_classes_0".into(), +// Relationship { +// column_mapping: [("_id".into(), vec!["school_id".into()])].into(), +// relationship_type: RelationshipType::Array, +// target_collection: "classes".into(), +// arguments: Default::default(), +// query: Query { +// predicate: Some(Expression::Exists { +// in_collection: ExistsInCollection::Related { +// relationship: "school_directory".into(), +// }, +// predicate: Some(Box::new(plan::Expression::BinaryComparisonOperator { +// column: plan::ComparisonTarget::Column { +// name: "_id".into(), +// arguments: Default::default(), +// field_path: None, +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::Int, +// ), +// }, +// operator: plan_test_helpers::ComparisonOperator::Equal, +// value: plan::ComparisonValue::Column { +// name: "math_department_id".into(), +// arguments: Default::default(), +// field_path: None, +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::Int, +// ), +// path: vec!["school_directory".into()], +// scope: Default::default(), +// }, +// })) +// }), +// relationships: [( +// "class_department".into(), +// plan::Relationship { +// target_collection: "departments".into(), +// column_mapping: [("department_id".into(), vec!["_id".into()])].into(), +// relationship_type: RelationshipType::Object, +// arguments: Default::default(), +// query: plan::Query { +// fields: Some([ +// ("_id".into(), plan::Field::Column { column: "_id".into(), fields: None, column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int) }) +// ].into()), +// ..Default::default() +// }, +// }, +// ), ( +// "class_students".into(), +// plan::Relationship { +// target_collection: "students".into(), +// column_mapping: [("_id".into(), vec!["class_id".into()])].into(), +// relationship_type: RelationshipType::Array, +// arguments: Default::default(), +// query: plan::Query { +// relationships: [( +// "student_advisor".into(), +// plan::Relationship { +// column_mapping: [( +// "advisor_id".into(), +// vec!["_id".into()], +// )] +// .into(), +// relationship_type: RelationshipType::Object, +// target_collection: "advisors".into(), +// arguments: Default::default(), +// query: plan::Query { +// fields: Some( +// [( +// "advisor_name".into(), +// plan::Field::Column { +// column: "advisor_name".into(), +// fields: None, +// column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), +// }, +// )] +// .into(), +// ), +// ..Default::default() +// }, +// }, +// )] +// .into(), +// ..Default::default() +// }, +// }, +// ), +// ( +// "school_directory".into(), +// Relationship { +// target_collection: "directory".into(), +// column_mapping: [("_id".into(), vec!["school_id".into()])].into(), +// relationship_type: RelationshipType::Object, +// arguments: Default::default(), +// query: Query { +// fields: Some([ +// ("math_department_id".into(), plan::Field::Column { column: "math_department_id".into(), fields: None, column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int) }) +// ].into()), +// ..Default::default() +// }, +// }, +// ), +// ] +// .into(), +// ..Default::default() +// }, +// }, +// ), +// // This is the second join on school_classes - this one provides the relationship +// // field for the top-level request query +// ( +// "school_classes".into(), +// Relationship { +// column_mapping: [("_id".into(), vec!["school_id".into()])].into(), +// relationship_type: RelationshipType::Array, +// target_collection: "classes".into(), +// arguments: Default::default(), +// query: Query { +// fields: Some( +// [( +// "student_name".into(), +// plan::Field::Relationship { +// relationship: "class_students".into(), +// aggregates: None, +// fields: None, +// }, +// )] +// .into(), +// ), +// relationships: [( +// "class_students".into(), +// plan::Relationship { +// target_collection: "students".into(), +// column_mapping: [("_id".into(), vec!["class_id".into()])].into(), +// relationship_type: RelationshipType::Array, +// arguments: Default::default(), +// query: Query { +// scope: Some(plan::Scope::Named("scope_1".into())), +// ..Default::default() +// }, +// }, +// )].into(), +// scope: Some(plan::Scope::Named("scope_0".into())), +// ..Default::default() +// }, +// }, +// ), +// ( +// "existence_check".into(), +// Relationship { +// column_mapping: [("some_id".into(), vec!["_id".into()])].into(), +// relationship_type: RelationshipType::Array, +// target_collection: "some_collection".into(), +// arguments: Default::default(), +// query: Query { +// predicate: None, +// ..Default::default() +// }, +// }, +// ), +// ] +// .into(), +// fields: Some( +// [( +// "class_name".into(), +// Field::Relationship { +// relationship: "school_classes".into(), +// aggregates: None, +// fields: Some( +// [( +// "student_name".into(), +// Field::Relationship { +// relationship: "class_students".into(), +// aggregates: None, +// fields: None, +// }, +// )] +// .into(), +// ), +// }, +// )] +// .into(), +// ), +// scope: Some(plan::Scope::Root), +// ..Default::default() +// }, +// }; +// +// let context = TestContext { +// collections: [ +// collection("schools"), +// collection("classes"), +// collection("students"), +// collection("departments"), +// collection("directory"), +// collection("advisors"), +// collection("some_collection"), +// ] +// .into(), +// object_types: [ +// ("schools".into(), object_type([("_id", named_type("Int"))])), +// ( +// "classes".into(), +// object_type([ +// ("_id", named_type("Int")), +// ("school_id", named_type("Int")), +// ("department_id", named_type("Int")), +// ]), +// ), +// ( +// "students".into(), +// object_type([ +// ("_id", named_type("Int")), +// ("class_id", named_type("Int")), +// ("advisor_id", named_type("Int")), +// ("student_name", named_type("String")), +// ]), +// ), +// ( +// "departments".into(), +// object_type([("_id", named_type("Int"))]), +// ), +// ( +// "directory".into(), +// object_type([ +// ("_id", named_type("Int")), +// ("school_id", named_type("Int")), +// ("math_department_id", named_type("Int")), +// ]), +// ), +// ( +// "advisors".into(), +// object_type([ +// ("_id", named_type("Int")), +// ("advisor_name", named_type("String")), +// ]), +// ), +// ( +// "some_collection".into(), +// object_type([("_id", named_type("Int")), ("some_id", named_type("Int"))]), +// ), +// ] +// .into(), +// ..Default::default() +// }; +// +// let query_plan = plan_for_query_request(&context, request)?; +// +// assert_eq!(query_plan, expected); +// Ok(()) +// } - let expected = QueryPlan { - collection: "schools".into(), - arguments: Default::default(), - variables: None, - variable_types: Default::default(), - unrelated_collections: Default::default(), - query: Query { - predicate: Some(Expression::And { - expressions: vec![Expression::Exists { - in_collection: ExistsInCollection::Related { - relationship: "existence_check".into(), - }, - predicate: None, - }], - }), - order_by: Some(OrderBy { - elements: [plan::OrderByElement { - order_direction: OrderDirection::Asc, - target: plan::OrderByTarget::Column { - name: "advisor_name".into(), - field_path: Default::default(), - path: [ - "school_classes_0".into(), - "class_students".into(), - "student_advisor".into(), - ] - .into(), - }, - }] - .into(), - }), - relationships: [ - ( - "school_classes_0".into(), - Relationship { - column_mapping: [("_id".into(), "school_id".into())].into(), - relationship_type: RelationshipType::Array, - target_collection: "classes".into(), - arguments: Default::default(), - query: Query { - predicate: Some(plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "_id".into(), - field_path: None, - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::Int, - ), - path: vec!["class_department".into()], - }, - operator: plan_test_helpers::ComparisonOperator::Equal, - value: plan::ComparisonValue::Column { - column: plan::ComparisonTarget::Column { - name: "math_department_id".into(), - field_path: None, - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::Int, - ), - path: vec!["school_directory".into()], - }, - }, - }), - relationships: [( - "class_department".into(), - plan::Relationship { - target_collection: "departments".into(), - column_mapping: [("department_id".into(), "_id".into())].into(), - relationship_type: RelationshipType::Object, - arguments: Default::default(), - query: plan::Query { - fields: Some([ - ("_id".into(), plan::Field::Column { column: "_id".into(), fields: None, column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int) }) - ].into()), - ..Default::default() - }, - }, - ), ( - "class_students".into(), - plan::Relationship { - target_collection: "students".into(), - column_mapping: [("_id".into(), "class_id".into())].into(), - relationship_type: RelationshipType::Array, - arguments: Default::default(), - query: plan::Query { - relationships: [( - "student_advisor".into(), - plan::Relationship { - column_mapping: [( - "advisor_id".into(), - "_id".into(), - )] - .into(), - relationship_type: RelationshipType::Object, - target_collection: "advisors".into(), - arguments: Default::default(), - query: plan::Query { - fields: Some( - [( - "advisor_name".into(), - plan::Field::Column { - column: "advisor_name".into(), - fields: None, - column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), - }, - )] - .into(), - ), - ..Default::default() - }, - }, - )] - .into(), - ..Default::default() - }, - }, - ), - ( - "school_directory".into(), - Relationship { - target_collection: "directory".into(), - column_mapping: [("_id".into(), "school_id".into())].into(), - relationship_type: RelationshipType::Object, - arguments: Default::default(), - query: Query { - fields: Some([ - ("math_department_id".into(), plan::Field::Column { column: "math_department_id".into(), fields: None, column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int) }) - ].into()), - ..Default::default() - }, - }, - ), - ] - .into(), - ..Default::default() - }, - }, - ), - ( - "school_classes".into(), - Relationship { - column_mapping: [("_id".into(), "school_id".into())].into(), - relationship_type: RelationshipType::Array, - target_collection: "classes".into(), - arguments: Default::default(), - query: Query { - fields: Some( - [( - "student_name".into(), - plan::Field::Relationship { - relationship: "class_students".into(), - aggregates: None, - fields: None, - }, - )] - .into(), - ), - relationships: [( - "class_students".into(), - plan::Relationship { - target_collection: "students".into(), - column_mapping: [("_id".into(), "class_id".into())].into(), - relationship_type: RelationshipType::Array, - arguments: Default::default(), - query: Query { - scope: Some(plan::Scope::Named("scope_1".into())), - ..Default::default() - }, - }, - )].into(), - scope: Some(plan::Scope::Named("scope_0".into())), - ..Default::default() - }, - }, - ), - ( - "existence_check".into(), - Relationship { - column_mapping: [("some_id".into(), "_id".into())].into(), - relationship_type: RelationshipType::Array, - target_collection: "some_collection".into(), - arguments: Default::default(), - query: Query { - predicate: None, - ..Default::default() - }, - }, - ), - ] - .into(), - fields: Some( - [( - "class_name".into(), - Field::Relationship { - relationship: "school_classes".into(), - aggregates: None, - fields: Some( - [( - "student_name".into(), - Field::Relationship { - relationship: "class_students".into(), - aggregates: None, - fields: None, - }, - )] - .into(), - ), - }, - )] - .into(), - ), - scope: Some(plan::Scope::Root), - ..Default::default() - }, - }; +// TODO: ENG-1487 update this test to use named scopes instead of root column reference - let context = TestContext { - collections: [ - collection("schools"), - collection("classes"), - collection("students"), - collection("departments"), - collection("directory"), - collection("advisors"), - collection("some_collection"), - ] - .into(), - object_types: [ - ("schools".into(), object_type([("_id", named_type("Int"))])), - ( - "classes".into(), - object_type([ - ("_id", named_type("Int")), - ("school_id", named_type("Int")), - ("department_id", named_type("Int")), - ]), - ), - ( - "students".into(), - object_type([ - ("_id", named_type("Int")), - ("class_id", named_type("Int")), - ("advisor_id", named_type("Int")), - ("student_name", named_type("String")), - ]), - ), - ( - "departments".into(), - object_type([("_id", named_type("Int"))]), - ), - ( - "directory".into(), - object_type([ - ("_id", named_type("Int")), - ("school_id", named_type("Int")), - ("math_department_id", named_type("Int")), - ]), - ), - ( - "advisors".into(), - object_type([ - ("_id", named_type("Int")), - ("advisor_name", named_type("String")), - ]), - ), - ( - "some_collection".into(), - object_type([("_id", named_type("Int")), ("some_id", named_type("Int"))]), - ), - ] - .into(), - ..Default::default() - }; - - let query_plan = plan_for_query_request(&context, request)?; - - assert_eq!(query_plan, expected); - Ok(()) -} - -#[test] -fn translates_root_column_references() -> Result<(), anyhow::Error> { - let query_context = make_flat_schema(); - let query = query_request() - .collection("authors") - .query(query().fields([field!("last_name")]).predicate(exists( - unrelated!("articles"), - and([ - binop("Equal", target!("author_id"), column_value!(root("id"))), - binop("Regex", target!("title"), value!("Functional.*")), - ]), - ))) - .into(); - let query_plan = plan_for_query_request(&query_context, query)?; - - let expected = QueryPlan { - collection: "authors".into(), - query: plan::Query { - predicate: Some(plan::Expression::Exists { - in_collection: plan::ExistsInCollection::Unrelated { - unrelated_collection: "__join_articles_0".into(), - }, - predicate: Some(Box::new(plan::Expression::And { - expressions: vec![ - plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "author_id".into(), - field_path: Default::default(), - field_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int), - path: Default::default(), - }, - operator: plan_test_helpers::ComparisonOperator::Equal, - value: plan::ComparisonValue::Column { - column: plan::ComparisonTarget::ColumnInScope { - name: "id".into(), - field_path: Default::default(), - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::Int, - ), - scope: plan::Scope::Root, - }, - }, - }, - plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "title".into(), - field_path: Default::default(), - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::String, - ), - path: Default::default(), - }, - operator: plan_test_helpers::ComparisonOperator::Regex, - value: plan::ComparisonValue::Scalar { - value: json!("Functional.*"), - value_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::String, - ), - }, - }, - ], - })), - }), - fields: Some( - [( - "last_name".into(), - plan::Field::Column { - column: "last_name".into(), - fields: None, - column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), - }, - )] - .into(), - ), - scope: Some(plan::Scope::Root), - ..Default::default() - }, - unrelated_collections: [( - "__join_articles_0".into(), - UnrelatedJoin { - target_collection: "articles".into(), - arguments: Default::default(), - query: plan::Query { - predicate: Some(plan::Expression::And { - expressions: vec![ - plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "author_id".into(), - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::Int, - ), - field_path: None, - path: vec![], - }, - operator: plan_test_helpers::ComparisonOperator::Equal, - value: plan::ComparisonValue::Column { - column: plan::ComparisonTarget::ColumnInScope { - name: "id".into(), - scope: plan::Scope::Root, - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::Int, - ), - field_path: None, - }, - }, - }, - plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "title".into(), - field_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::String, - ), - field_path: None, - path: vec![], - }, - operator: plan_test_helpers::ComparisonOperator::Regex, - value: plan::ComparisonValue::Scalar { - value: "Functional.*".into(), - value_type: plan::Type::Scalar( - plan_test_helpers::ScalarType::String, - ), - }, - }, - ], - }), - ..Default::default() - }, - }, - )] - .into(), - arguments: Default::default(), - variables: Default::default(), - variable_types: Default::default(), - }; - - assert_eq!(query_plan, expected); - Ok(()) -} +// #[test] +// fn translates_root_column_references() -> Result<(), anyhow::Error> { +// let query_context = make_flat_schema(); +// let query = query_request() +// .collection("authors") +// .query(query().fields([field!("last_name")]).predicate(exists( +// unrelated!("articles"), +// and([ +// binop("Equal", target!("author_id"), column_value!(root("id"))), +// binop("Regex", target!("title"), value!("Functional.*")), +// ]), +// ))) +// .into(); +// let query_plan = plan_for_query_request(&query_context, query)?; +// +// let expected = QueryPlan { +// collection: "authors".into(), +// query: plan::Query { +// predicate: Some(plan::Expression::Exists { +// in_collection: plan::ExistsInCollection::Unrelated { +// unrelated_collection: "__join_articles_0".into(), +// }, +// predicate: Some(Box::new(plan::Expression::And { +// expressions: vec![ +// plan::Expression::BinaryComparisonOperator { +// column: plan::ComparisonTarget::Column { +// name: "author_id".into(), +// field_path: Default::default(), +// field_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Int), +// path: Default::default(), +// }, +// operator: plan_test_helpers::ComparisonOperator::Equal, +// value: plan::ComparisonValue::Column { +// column: plan::ComparisonTarget::ColumnInScope { +// name: "id".into(), +// field_path: Default::default(), +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::Int, +// ), +// scope: plan::Scope::Root, +// }, +// }, +// }, +// plan::Expression::BinaryComparisonOperator { +// column: plan::ComparisonTarget::Column { +// name: "title".into(), +// field_path: Default::default(), +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::String, +// ), +// path: Default::default(), +// }, +// operator: plan_test_helpers::ComparisonOperator::Regex, +// value: plan::ComparisonValue::Scalar { +// value: json!("Functional.*"), +// value_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::String, +// ), +// }, +// }, +// ], +// })), +// }), +// fields: Some( +// [( +// "last_name".into(), +// plan::Field::Column { +// column: "last_name".into(), +// fields: None, +// column_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), +// }, +// )] +// .into(), +// ), +// scope: Some(plan::Scope::Root), +// ..Default::default() +// }, +// unrelated_collections: [( +// "__join_articles_0".into(), +// UnrelatedJoin { +// target_collection: "articles".into(), +// arguments: Default::default(), +// query: plan::Query { +// predicate: Some(plan::Expression::And { +// expressions: vec![ +// plan::Expression::BinaryComparisonOperator { +// column: plan::ComparisonTarget::Column { +// name: "author_id".into(), +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::Int, +// ), +// field_path: None, +// path: vec![], +// }, +// operator: plan_test_helpers::ComparisonOperator::Equal, +// value: plan::ComparisonValue::Column { +// column: plan::ComparisonTarget::ColumnInScope { +// name: "id".into(), +// scope: plan::Scope::Root, +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::Int, +// ), +// field_path: None, +// }, +// }, +// }, +// plan::Expression::BinaryComparisonOperator { +// column: plan::ComparisonTarget::Column { +// name: "title".into(), +// field_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::String, +// ), +// field_path: None, +// path: vec![], +// }, +// operator: plan_test_helpers::ComparisonOperator::Regex, +// value: plan::ComparisonValue::Scalar { +// value: "Functional.*".into(), +// value_type: plan::Type::Scalar( +// plan_test_helpers::ScalarType::String, +// ), +// }, +// }, +// ], +// }), +// ..Default::default() +// }, +// }, +// )] +// .into(), +// arguments: Default::default(), +// variables: Default::default(), +// variable_types: Default::default(), +// }; +// +// assert_eq!(query_plan, expected); +// Ok(()) +// } #[test] fn translates_aggregate_selections() -> Result<(), anyhow::Error> { @@ -511,7 +521,7 @@ fn translates_aggregate_selections() -> Result<(), anyhow::Error> { .query(query().aggregates([ star_count_aggregate!("count_star"), column_count_aggregate!("count_id" => "last_name", distinct: true), - column_aggregate!("avg_id" => "id", "Average"), + ("avg_id", column_aggregate("id", "Average").into()), ])) .into(); let query_plan = plan_for_query_request(&query_context, query)?; @@ -526,6 +536,7 @@ fn translates_aggregate_selections() -> Result<(), anyhow::Error> { "count_id".into(), plan::Aggregate::ColumnCount { column: "last_name".into(), + arguments: Default::default(), field_path: None, distinct: true, }, @@ -534,9 +545,12 @@ fn translates_aggregate_selections() -> Result<(), anyhow::Error> { "avg_id".into(), plan::Aggregate::SingleColumn { column: "id".into(), + column_type: Type::scalar(plan_test_helpers::ScalarType::Int), + arguments: Default::default(), field_path: None, function: plan_test_helpers::AggregateFunction::Average, - result_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Double), + result_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Double) + .into_nullable(), }, ), ] @@ -576,17 +590,21 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a .order_by(vec![ ndc::OrderByElement { order_direction: OrderDirection::Asc, - target: OrderByTarget::SingleColumnAggregate { - column: "year".into(), - function: "Average".into(), - path: vec![path_element("author_articles".into()).into()], - field_path: None, + target: OrderByTarget::Aggregate { + path: vec![path_element("author_articles").into()], + aggregate: ndc::Aggregate::SingleColumn { + column: "year".into(), + arguments: Default::default(), + field_path: None, + function: "Average".into(), + }, }, }, ndc::OrderByElement { order_direction: OrderDirection::Desc, target: OrderByTarget::Column { name: "id".into(), + arguments: Default::default(), field_path: None, path: vec![], }, @@ -595,7 +613,7 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a ) .relationships([( "author_articles", - relationship("articles", [("id", "author_id")]), + relationship("articles", [("id", &["author_id"])]), )]) .into(); let query_plan = plan_for_query_request(&query_context, query)?; @@ -608,12 +626,10 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a relationship: "author_articles".into(), }, predicate: Some(Box::new(plan::Expression::BinaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "title".into(), - field_path: Default::default(), - field_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), - path: Default::default(), - }, + column: plan::ComparisonTarget::column( + "title", + plan::Type::scalar(plan_test_helpers::ScalarType::String), + ), operator: plan_test_helpers::ComparisonOperator::Regex, value: plan::ComparisonValue::Scalar { value: "Functional.*".into(), @@ -625,17 +641,26 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a elements: vec![ plan::OrderByElement { order_direction: OrderDirection::Asc, - target: plan::OrderByTarget::SingleColumnAggregate { - column: "year".into(), - function: plan_test_helpers::AggregateFunction::Average, - result_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Double), + target: plan::OrderByTarget::Aggregate { path: vec!["author_articles".into()], + aggregate: plan::Aggregate::SingleColumn { + column: "year".into(), + column_type: Type::scalar(plan_test_helpers::ScalarType::Int).into_nullable(), + arguments: Default::default(), + field_path: Default::default(), + function: plan_test_helpers::AggregateFunction::Average, + result_type: plan::Type::Scalar( + plan_test_helpers::ScalarType::Double, + ) + .into_nullable(), + }, }, }, plan::OrderByElement { order_direction: OrderDirection::Desc, target: plan::OrderByTarget::Column { name: "id".into(), + arguments: Default::default(), field_path: None, path: vec![], }, @@ -657,6 +682,7 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a plan::Field::Relationship { relationship: "author_articles".into(), aggregates: None, + groups: None, fields: Some( [ ( @@ -693,7 +719,7 @@ fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), a "author_articles".into(), plan::Relationship { target_collection: "articles".into(), - column_mapping: [("id".into(), "author_id".into())].into(), + column_mapping: [("id".into(), NonEmpty::singleton("author_id".into()))].into(), relationship_type: RelationshipType::Array, arguments: Default::default(), query: plan::Query { @@ -856,15 +882,13 @@ fn translates_predicate_referencing_field_of_related_collection() -> anyhow::Res let query_context = make_nested_schema(); let request = query_request() .collection("appearances") - .relationships([("author", relationship("authors", [("authorId", "id")]))]) + .relationships([("author", relationship("authors", [("authorId", &["id"])]))]) .query( query() .fields([relation_field!("presenter" => "author", query().fields([ field!("name"), ]))]) - .predicate(not(is_null( - target!("name", relations: [path_element("author".into())]), - ))), + .predicate(exists(in_related("author"), not(is_null(target!("name"))))), ) .into(); let query_plan = plan_for_query_request(&query_context, request)?; @@ -872,16 +896,21 @@ fn translates_predicate_referencing_field_of_related_collection() -> anyhow::Res let expected = QueryPlan { collection: "appearances".into(), query: plan::Query { - predicate: Some(plan::Expression::Not { - expression: Box::new(plan::Expression::UnaryComparisonOperator { - column: plan::ComparisonTarget::Column { - name: "name".into(), - field_path: None, - field_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), - path: vec!["author".into()], - }, - operator: ndc_models::UnaryComparisonOperator::IsNull, - }), + predicate: Some(plan::Expression::Exists { + in_collection: plan::ExistsInCollection::Related { + relationship: "author".into(), + }, + predicate: Some(Box::new(plan::Expression::Not { + expression: Box::new(plan::Expression::UnaryComparisonOperator { + column: plan::ComparisonTarget::Column { + name: "name".into(), + arguments: Default::default(), + field_path: None, + field_type: plan::Type::Scalar(plan_test_helpers::ScalarType::String), + }, + operator: ndc_models::UnaryComparisonOperator::IsNull, + }), + })), }), fields: Some( [( @@ -889,6 +918,7 @@ fn translates_predicate_referencing_field_of_related_collection() -> anyhow::Res plan::Field::Relationship { relationship: "author".into(), aggregates: None, + groups: None, fields: Some( [( "name".into(), @@ -909,7 +939,7 @@ fn translates_predicate_referencing_field_of_related_collection() -> anyhow::Res relationships: [( "author".into(), plan::Relationship { - column_mapping: [("authorId".into(), "id".into())].into(), + column_mapping: [("authorId".into(), NonEmpty::singleton("id".into()))].into(), relationship_type: RelationshipType::Array, target_collection: "authors".into(), arguments: Default::default(), diff --git a/crates/ndc-query-plan/src/plan_for_query_request/type_annotated_field.rs b/crates/ndc-query-plan/src/plan_for_query_request/type_annotated_field.rs index fa6de979..2fca802f 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/type_annotated_field.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/type_annotated_field.rs @@ -44,7 +44,8 @@ fn type_annotated_field_helper( fields, arguments: _, } => { - let column_type = find_object_field(collection_object_type, &column)?; + let column_field = find_object_field(collection_object_type, &column)?; + let column_type = &column_field.r#type; let fields = fields .map(|nested_field| { type_annotated_nested_field_helper( @@ -89,6 +90,7 @@ fn type_annotated_field_helper( // with fields and aggregates from other references to the same relationship. let aggregates = query_plan.aggregates.clone(); let fields = query_plan.fields.clone(); + let groups = query_plan.groups.clone(); let relationship_key = plan_state.register_relationship(relationship, arguments, query_plan)?; @@ -96,6 +98,7 @@ fn type_annotated_field_helper( relationship: relationship_key, aggregates, fields, + groups, } } }; @@ -162,6 +165,10 @@ fn type_annotated_nested_field_helper( )?), }) } + // TODO: ENG-1464 + (ndc::NestedField::Collection(_), _) => Err(QueryPlanError::NotImplemented( + "query.nested_fields.nested_collections".to_string(), + ))?, (nested, Type::Nullable(t)) => { // let path = append_to_path(path, []) type_annotated_nested_field_helper( diff --git a/crates/ndc-query-plan/src/plan_for_query_request/unify_relationship_references.rs b/crates/ndc-query-plan/src/plan_for_query_request/unify_relationship_references.rs index 1d16e70c..be2bae6c 100644 --- a/crates/ndc-query-plan/src/plan_for_query_request/unify_relationship_references.rs +++ b/crates/ndc-query-plan/src/plan_for_query_request/unify_relationship_references.rs @@ -7,8 +7,8 @@ use ndc_models as ndc; use thiserror::Error; use crate::{ - Aggregate, ConnectorTypes, Expression, Field, NestedArray, NestedField, NestedObject, Query, - Relationship, RelationshipArgument, Relationships, + Aggregate, ConnectorTypes, Expression, Field, GroupExpression, Grouping, NestedArray, + NestedField, NestedObject, Query, Relationship, RelationshipArgument, Relationships, }; #[derive(Debug, Error)] @@ -95,7 +95,6 @@ where let mismatching_fields = [ (a.limit != b.limit, "limit"), - (a.aggregates_limit != b.aggregates_limit, "aggregates_limit"), (a.offset != b.offset, "offset"), (a.order_by != b.order_by, "order_by"), (predicate_a != predicate_b, "predicate"), @@ -117,13 +116,13 @@ where })?; let query = Query { - aggregates: unify_aggregates(a.aggregates, b.aggregates)?, + aggregates: unify_options(a.aggregates, b.aggregates, unify_aggregates)?, fields: unify_fields(a.fields, b.fields)?, limit: a.limit, - aggregates_limit: a.aggregates_limit, offset: a.offset, order_by: a.order_by, predicate: predicate_a, + groups: unify_options(a.groups, b.groups, unify_groups)?, relationships: unify_nested_relationships(a.relationships, b.relationships)?, scope, }; @@ -131,9 +130,9 @@ where } fn unify_aggregates( - a: Option>>, - b: Option>>, -) -> Result>>> + a: IndexMap>, + b: IndexMap>, +) -> Result>> where T: ConnectorTypes, { @@ -210,11 +209,13 @@ where relationship: relationship_a, aggregates: aggregates_a, fields: fields_a, + groups: groups_a, }, Field::Relationship { relationship: relationship_b, aggregates: aggregates_b, fields: fields_b, + groups: groups_b, }, ) => { if relationship_a != relationship_b { @@ -224,8 +225,9 @@ where } else { Ok(Field::Relationship { relationship: relationship_b, - aggregates: unify_aggregates(aggregates_a, aggregates_b)?, + aggregates: unify_options(aggregates_a, aggregates_b, unify_aggregates)?, fields: unify_fields(fields_a, fields_b)?, + groups: unify_options(groups_a, groups_b, unify_groups)?, }) } } @@ -284,6 +286,39 @@ where .try_collect() } +fn unify_groups(a: Grouping, b: Grouping) -> Result> +where + T: ConnectorTypes, +{ + let predicate_a = a.predicate.and_then(GroupExpression::simplify); + let predicate_b = b.predicate.and_then(GroupExpression::simplify); + + let mismatching_fields = [ + (a.dimensions != b.dimensions, "dimensions"), + (predicate_a != predicate_b, "predicate"), + (a.order_by != b.order_by, "order_by"), + (a.limit != b.limit, "limit"), + (a.offset != b.offset, "offset"), + ] + .into_iter() + .filter_map(|(is_mismatch, field_name)| if is_mismatch { Some(field_name) } else { None }) + .collect_vec(); + + if !mismatching_fields.is_empty() { + return Err(RelationshipUnificationError::Mismatch(mismatching_fields)); + } + + let unified = Grouping { + dimensions: a.dimensions, + aggregates: unify_aggregates(a.aggregates, b.aggregates)?, + predicate: predicate_a, + order_by: a.order_by, + limit: a.limit, + offset: a.offset, + }; + Ok(unified) +} + /// In some cases we receive the predicate expression `Some(Expression::And [])` which does not /// filter out anything, but fails equality checks with `None`. Simplifying that expression to /// `None` allows us to unify relationship references that we wouldn't otherwise be able to. @@ -341,9 +376,9 @@ mod tests { use crate::{ field, object, plan_for_query_request::plan_test_helpers::{ - date, double, int, object_type, relationship, string, TestContext, + date, double, int, relationship, string, TestContext, }, - Relationship, + Relationship, Type, }; use super::unify_relationship_references; @@ -395,10 +430,10 @@ mod tests { #[test] fn unifies_nested_field_selections() -> anyhow::Result<()> { - let tomatoes_type = object_type([ + let tomatoes_type = Type::object([ ( "viewer", - object_type([("numReviews", int()), ("rating", double())]), + Type::object([("numReviews", int()), ("rating", double())]), ), ("lastUpdated", date()), ]); diff --git a/crates/ndc-query-plan/src/query_plan.rs b/crates/ndc-query-plan/src/query_plan.rs deleted file mode 100644 index ef1cb6b4..00000000 --- a/crates/ndc-query-plan/src/query_plan.rs +++ /dev/null @@ -1,468 +0,0 @@ -use std::{collections::BTreeMap, fmt::Debug, iter}; - -use derivative::Derivative; -use indexmap::IndexMap; -use itertools::Either; -use ndc_models::{self as ndc, FieldName, OrderDirection, RelationshipType, UnaryComparisonOperator}; - -use crate::{vec_set::VecSet, Type}; - -pub trait ConnectorTypes { - type ScalarType: Clone + Debug + PartialEq + Eq; - type AggregateFunction: Clone + Debug + PartialEq; - type ComparisonOperator: Clone + Debug + PartialEq; -} - -#[derive(Derivative)] -#[derivative( - Clone(bound = ""), - Debug(bound = ""), - PartialEq(bound = "T::ScalarType: PartialEq") -)] -pub struct QueryPlan { - pub collection: ndc::CollectionName, - pub query: Query, - pub arguments: BTreeMap>, - pub variables: Option>, - - /// Types for values from the `variables` map as inferred by usages in the query request. It is - /// possible for the same variable to be used in multiple contexts with different types. This - /// map provides sets of all observed types. - /// - /// The observed type may be `None` if the type of a variable use could not be inferred. - pub variable_types: VariableTypes, - - // TODO: type for unrelated collection - pub unrelated_collections: BTreeMap>, -} - -impl QueryPlan { - pub fn has_variables(&self) -> bool { - self.variables.is_some() - } -} - -pub type Arguments = BTreeMap>; -pub type Relationships = BTreeMap>; -pub type VariableSet = BTreeMap; -pub type VariableTypes = BTreeMap>>; - -#[derive(Derivative)] -#[derivative( - Clone(bound = ""), - Debug(bound = ""), - Default(bound = ""), - PartialEq(bound = "") -)] -pub struct Query { - pub aggregates: Option>>, - pub fields: Option>>, - pub limit: Option, - pub aggregates_limit: Option, - pub offset: Option, - pub order_by: Option>, - pub predicate: Option>, - - /// Relationships referenced by fields and expressions in this query or sub-query. Does not - /// include relationships in sub-queries nested under this one. - pub relationships: Relationships, - - /// Some relationship references may introduce a named "scope" so that other parts of the query - /// request can reference fields of documents in the related collection. The connector must - /// introduce a variable, or something similar, for such references. - pub scope: Option, -} - -impl Query { - pub fn has_aggregates(&self) -> bool { - if let Some(aggregates) = &self.aggregates { - !aggregates.is_empty() - } else { - false - } - } - - pub fn has_fields(&self) -> bool { - if let Some(fields) = &self.fields { - !fields.is_empty() - } else { - false - } - } -} - -#[derive(Derivative)] -#[derivative( - Clone(bound = ""), - Debug(bound = ""), - PartialEq(bound = "T::ScalarType: PartialEq") -)] -pub enum Argument { - /// The argument is provided by reference to a variable - Variable { - name: ndc::VariableName, - argument_type: Type, - }, - /// The argument is provided as a literal value - Literal { - value: serde_json::Value, - argument_type: Type, - }, - /// The argument was a literal value that has been parsed as an [Expression] - Predicate { expression: Expression }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct Relationship { - pub column_mapping: BTreeMap, - pub relationship_type: RelationshipType, - pub target_collection: ndc::CollectionName, - pub arguments: BTreeMap>, - pub query: Query, -} - -#[derive(Derivative)] -#[derivative( - Clone(bound = ""), - Debug(bound = ""), - PartialEq(bound = "T::ScalarType: PartialEq") -)] -pub enum RelationshipArgument { - /// The argument is provided by reference to a variable - Variable { - name: ndc::VariableName, - argument_type: Type, - }, - /// The argument is provided as a literal value - Literal { - value: serde_json::Value, - argument_type: Type, - }, - // The argument is provided based on a column of the source collection - Column { - name: ndc::FieldName, - argument_type: Type, - }, - /// The argument was a literal value that has been parsed as an [Expression] - Predicate { expression: Expression }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct UnrelatedJoin { - pub target_collection: ndc::CollectionName, - pub arguments: BTreeMap>, - pub query: Query, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Scope { - Root, - Named(String), -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum Aggregate { - ColumnCount { - /// The column to apply the count aggregate function to - column: ndc::FieldName, - /// Path to a nested field within an object column - field_path: Option>, - /// Whether or not only distinct items should be counted - distinct: bool, - }, - SingleColumn { - /// The column to apply the aggregation function to - column: ndc::FieldName, - /// Path to a nested field within an object column - field_path: Option>, - /// Single column aggregate function name. - function: T::AggregateFunction, - result_type: Type, - }, - StarCount, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct NestedObject { - pub fields: IndexMap>, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct NestedArray { - pub fields: Box>, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum NestedField { - Object(NestedObject), - Array(NestedArray), -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum Field { - Column { - column: ndc::FieldName, - - /// When the type of the column is a (possibly-nullable) array or object, - /// the caller can request a subset of the complete column data, - /// by specifying fields to fetch here. - /// If omitted, the column data will be fetched in full. - fields: Option>, - - column_type: Type, - }, - Relationship { - /// The name of the relationship to follow for the subquery - this is the key in the - /// [Query] relationships map in this module, it is **not** the key in the - /// [ndc::QueryRequest] collection_relationships map. - relationship: ndc::RelationshipName, - aggregates: Option>>, - fields: Option>>, - }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum Expression { - And { - expressions: Vec>, - }, - Or { - expressions: Vec>, - }, - Not { - expression: Box>, - }, - UnaryComparisonOperator { - column: ComparisonTarget, - operator: UnaryComparisonOperator, - }, - BinaryComparisonOperator { - column: ComparisonTarget, - operator: T::ComparisonOperator, - value: ComparisonValue, - }, - Exists { - in_collection: ExistsInCollection, - predicate: Option>>, - }, -} - -impl Expression { - /// Get an iterator of columns referenced by the expression, not including columns of related - /// collections - pub fn query_local_comparison_targets<'a>( - &'a self, - ) -> Box> + 'a> { - match self { - Expression::And { expressions } => Box::new( - expressions - .iter() - .flat_map(|e| e.query_local_comparison_targets()), - ), - Expression::Or { expressions } => Box::new( - expressions - .iter() - .flat_map(|e| e.query_local_comparison_targets()), - ), - Expression::Not { expression } => expression.query_local_comparison_targets(), - Expression::UnaryComparisonOperator { column, .. } => { - Box::new(Self::local_columns_from_comparison_target(column)) - } - Expression::BinaryComparisonOperator { column, value, .. } => { - let value_targets = match value { - ComparisonValue::Column { column } => { - Either::Left(Self::local_columns_from_comparison_target(column)) - } - _ => Either::Right(iter::empty()), - }; - Box::new(Self::local_columns_from_comparison_target(column).chain(value_targets)) - } - Expression::Exists { .. } => Box::new(iter::empty()), - } - } - - fn local_columns_from_comparison_target( - target: &ComparisonTarget, - ) -> impl Iterator> { - match target { - t @ ComparisonTarget::Column { path, .. } => { - if path.is_empty() { - Either::Left(iter::once(t)) - } else { - Either::Right(iter::empty()) - } - } - t @ ComparisonTarget::ColumnInScope { .. } => Either::Left(iter::once(t)), - } - } -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct OrderBy { - /// The elements to order by, in priority order - pub elements: Vec>, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct OrderByElement { - pub order_direction: OrderDirection, - pub target: OrderByTarget, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum OrderByTarget { - Column { - /// The name of the column - name: ndc::FieldName, - - /// Path to a nested field within an object column - field_path: Option>, - - /// Any relationships to traverse to reach this column. These are translated from - /// [ndc::OrderByElement] values in the [ndc::QueryRequest] to names of relation - /// fields for the [QueryPlan]. - path: Vec, - }, - SingleColumnAggregate { - /// The column to apply the aggregation function to - column: ndc::FieldName, - /// Single column aggregate function name. - function: T::AggregateFunction, - - result_type: Type, - - /// Any relationships to traverse to reach this aggregate. These are translated from - /// [ndc::OrderByElement] values in the [ndc::QueryRequest] to names of relation - /// fields for the [QueryPlan]. - path: Vec, - }, - StarCountAggregate { - /// Any relationships to traverse to reach this aggregate. These are translated from - /// [ndc::OrderByElement] values in the [ndc::QueryRequest] to names of relation - /// fields for the [QueryPlan]. - path: Vec, - }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum ComparisonTarget { - Column { - /// The name of the column - name: ndc::FieldName, - - /// Path to a nested field within an object column - field_path: Option>, - - field_type: Type, - - /// Any relationships to traverse to reach this column. These are translated from - /// [ndc::PathElement] values in the [ndc::QueryRequest] to names of relation - /// fields for the [QueryPlan]. - path: Vec, - }, - ColumnInScope { - /// The name of the column - name: ndc::FieldName, - - /// The named scope that identifies the collection to reference. This corresponds to the - /// `scope` field of the [Query] type. - scope: Scope, - - /// Path to a nested field within an object column - field_path: Option>, - - field_type: Type, - }, -} - -impl ComparisonTarget { - pub fn column_name(&self) -> &ndc::FieldName { - match self { - ComparisonTarget::Column { name, .. } => name, - ComparisonTarget::ColumnInScope { name, .. } => name, - } - } - - pub fn relationship_path(&self) -> &[ndc::RelationshipName] { - match self { - ComparisonTarget::Column { path, .. } => path, - ComparisonTarget::ColumnInScope { .. } => &[], - } - } -} - -impl ComparisonTarget { - pub fn get_field_type(&self) -> &Type { - match self { - ComparisonTarget::Column { field_type, .. } => field_type, - ComparisonTarget::ColumnInScope { field_type, .. } => field_type, - } - } -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum ComparisonValue { - Column { - column: ComparisonTarget, - }, - Scalar { - value: serde_json::Value, - value_type: Type, - }, - Variable { - name: ndc::VariableName, - variable_type: Type, - }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub struct AggregateFunctionDefinition { - /// The scalar or object type of the result of this function - pub result_type: Type, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum ComparisonOperatorDefinition { - Equal, - In, - Custom { - /// The type of the argument to this operator - argument_type: Type, - }, -} - -#[derive(Derivative)] -#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] -pub enum ExistsInCollection { - Related { - /// Key of the relation in the [Query] joins map. Relationships are scoped to the sub-query - /// that defines the relation source. - relationship: ndc::RelationshipName, - }, - Unrelated { - /// Key of the relation in the [QueryPlan] joins map. Unrelated collections are not scoped - /// to a sub-query, instead they are given in the root [QueryPlan]. - unrelated_collection: String, - }, - NestedCollection { - column_name: ndc::FieldName, - arguments: BTreeMap>, - /// Path to a nested collection via object columns - field_path: Vec, - }, -} diff --git a/crates/ndc-query-plan/src/query_plan/aggregation.rs b/crates/ndc-query-plan/src/query_plan/aggregation.rs new file mode 100644 index 00000000..b6778318 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/aggregation.rs @@ -0,0 +1,213 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use derivative::Derivative; +use indexmap::IndexMap; +use ndc_models::{self as ndc, ArgumentName, FieldName}; + +use crate::Type; + +use super::{Argument, ConnectorTypes}; + +pub type Arguments = BTreeMap>; + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum Aggregate { + ColumnCount { + /// The column to apply the count aggregate function to + column: ndc::FieldName, + /// Arguments to satisfy the column specified by 'column' + arguments: BTreeMap>, + /// Path to a nested field within an object column + field_path: Option>, + /// Whether or not only distinct items should be counted + distinct: bool, + }, + SingleColumn { + /// The column to apply the aggregation function to + column: ndc::FieldName, + column_type: Type, + /// Arguments to satisfy the column specified by 'column' + arguments: BTreeMap>, + /// Path to a nested field within an object column + field_path: Option>, + /// Single column aggregate function name. + function: T::AggregateFunction, + result_type: Type, + }, + StarCount, +} + +impl Aggregate { + pub fn result_type(&self) -> Cow> { + match self { + Aggregate::ColumnCount { .. } => Cow::Owned(T::count_aggregate_type()), + Aggregate::SingleColumn { result_type, .. } => Cow::Borrowed(result_type), + Aggregate::StarCount => Cow::Owned(T::count_aggregate_type()), + } + } + + pub fn is_count(&self) -> bool { + match self { + Aggregate::ColumnCount { .. } => true, + Aggregate::SingleColumn { .. } => false, + Aggregate::StarCount => true, + } + } +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct Grouping { + /// Dimensions along which to partition the data + pub dimensions: Vec>, + /// Aggregates to compute in each group + pub aggregates: IndexMap>, + /// Optionally specify a predicate to apply after grouping rows. + /// Only used if the 'query.aggregates.group_by.filter' capability is supported. + pub predicate: Option>, + /// Optionally specify how groups should be ordered + /// Only used if the 'query.aggregates.group_by.order' capability is supported. + pub order_by: Option>, + /// Optionally limit to N groups + /// Only used if the 'query.aggregates.group_by.paginate' capability is supported. + pub limit: Option, + /// Optionally offset from the Nth group + /// Only used if the 'query.aggregates.group_by.paginate' capability is supported. + pub offset: Option, +} + +/// [GroupExpression] is like [Expression] but without [Expression::ArrayComparison] or +/// [Expression::Exists] variants. +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum GroupExpression { + And { + expressions: Vec>, + }, + Or { + expressions: Vec>, + }, + Not { + expression: Box>, + }, + UnaryComparisonOperator { + target: GroupComparisonTarget, + operator: ndc::UnaryComparisonOperator, + }, + BinaryComparisonOperator { + target: GroupComparisonTarget, + operator: T::ComparisonOperator, + value: GroupComparisonValue, + }, +} + +impl GroupExpression { + /// In some cases we receive the predicate expression `Some(Expression::And [])` which does not + /// filter out anything, but fails equality checks with `None`. Simplifying that expression to + /// `None` allows us to unify relationship references that we wouldn't otherwise be able to. + pub fn simplify(self) -> Option { + match self { + GroupExpression::And { expressions } if expressions.is_empty() => None, + e => Some(e), + } + } +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum GroupComparisonTarget { + Aggregate { aggregate: Aggregate }, +} + +impl GroupComparisonTarget { + pub fn result_type(&self) -> Cow> { + match self { + GroupComparisonTarget::Aggregate { aggregate } => aggregate.result_type(), + } + } +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum GroupComparisonValue { + /// A scalar value to compare against + Scalar { + value: serde_json::Value, + value_type: Type, + }, + /// A value to compare against that is to be drawn from the query's variables. + /// Only used if the 'query.variables' capability is supported. + Variable { + name: ndc::VariableName, + variable_type: Type, + }, +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum Dimension { + Column { + /// Any (object) relationships to traverse to reach this column. + /// Only non-empty if the 'relationships' capability is supported. + /// + /// These are translated from [ndc::PathElement] values in the to names of relation fields + /// for the [crate::QueryPlan]. + path: Vec, + /// The name of the column + column_name: FieldName, + /// Arguments to satisfy the column specified by 'column_name' + arguments: BTreeMap>, + /// Path to a nested field within an object column + field_path: Option>, + /// Type of the field that you get **after** follwing `field_path` to a possibly-nested + /// field. + /// + /// If this column references a field in a related collection then this type will be an + /// array type whose element type is the type of the related field. The array type wrapper + /// applies regardless of whether the relationship is an array or an object relationship. + field_type: Type, + }, +} + +impl Dimension { + pub fn value_type(&self) -> &Type { + match self { + Dimension::Column { field_type, .. } => field_type, + } + } +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct GroupOrderBy { + /// The elements to order by, in priority order + pub elements: Vec>, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct GroupOrderByElement { + pub order_direction: ndc::OrderDirection, + pub target: GroupOrderByTarget, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum GroupOrderByTarget { + Dimension { + /// The index of the dimension to order by, selected from the + /// dimensions provided in the `Grouping` request. + index: usize, + }, + Aggregate { + /// Aggregation method to apply + aggregate: Aggregate, + }, +} diff --git a/crates/ndc-query-plan/src/query_plan/connector_types.rs b/crates/ndc-query-plan/src/query_plan/connector_types.rs new file mode 100644 index 00000000..94b65b4e --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/connector_types.rs @@ -0,0 +1,15 @@ +use std::fmt::Debug; +use std::hash::Hash; + +use crate::Type; + +pub trait ConnectorTypes { + type ScalarType: Clone + Debug + Hash + PartialEq + Eq; + type AggregateFunction: Clone + Debug + Hash + PartialEq + Eq; + type ComparisonOperator: Clone + Debug + Hash + PartialEq + Eq; + + /// Result type for count aggregations + fn count_aggregate_type() -> Type; + + fn string_type() -> Type; +} diff --git a/crates/ndc-query-plan/src/query_plan/expression.rs b/crates/ndc-query-plan/src/query_plan/expression.rs new file mode 100644 index 00000000..5f854259 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/expression.rs @@ -0,0 +1,299 @@ +use std::{borrow::Cow, collections::BTreeMap, iter}; + +use derivative::Derivative; +use itertools::Either; +use ndc_models::{self as ndc, ArgumentName, FieldName}; + +use crate::Type; + +use super::{Argument, ConnectorTypes}; + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum Expression { + And { + expressions: Vec>, + }, + Or { + expressions: Vec>, + }, + Not { + expression: Box>, + }, + UnaryComparisonOperator { + column: ComparisonTarget, + operator: ndc::UnaryComparisonOperator, + }, + BinaryComparisonOperator { + column: ComparisonTarget, + operator: T::ComparisonOperator, + value: ComparisonValue, + }, + /// A comparison against a nested array column. + /// Only used if the 'query.nested_fields.filter_by.nested_arrays' capability is supported. + ArrayComparison { + column: ComparisonTarget, + comparison: ArrayComparison, + }, + Exists { + in_collection: ExistsInCollection, + predicate: Option>>, + }, +} + +impl Expression { + /// In some cases we receive the predicate expression `Some(Expression::And [])` which does not + /// filter out anything, but fails equality checks with `None`. Simplifying that expression to + /// `None` allows us to unify relationship references that we wouldn't otherwise be able to. + pub fn simplify(self) -> Option { + match self { + Expression::And { expressions } if expressions.is_empty() => None, + e => Some(e), + } + } + + /// Get an iterator of columns referenced by the expression, not including columns of related + /// collections. This is used to build a plan for joining the referenced collection - we need + /// to include fields in the join that the expression needs to access. + // + // TODO: ENG-1457 When we implement query.aggregates.filter_by we'll need to collect aggregates + // references. That's why this function returns [ComparisonTarget] instead of [Field]. + pub fn query_local_comparison_targets<'a>( + &'a self, + ) -> Box>> + 'a> { + match self { + Expression::And { expressions } => Box::new( + expressions + .iter() + .flat_map(|e| e.query_local_comparison_targets()), + ), + Expression::Or { expressions } => Box::new( + expressions + .iter() + .flat_map(|e| e.query_local_comparison_targets()), + ), + Expression::Not { expression } => expression.query_local_comparison_targets(), + Expression::UnaryComparisonOperator { column, .. } => { + Box::new(std::iter::once(Cow::Borrowed(column))) + } + Expression::BinaryComparisonOperator { column, value, .. } => Box::new( + std::iter::once(Cow::Borrowed(column)) + .chain(Self::local_targets_from_comparison_value(value).map(Cow::Owned)), + ), + Expression::ArrayComparison { column, comparison } => { + let value_targets = match comparison { + ArrayComparison::Contains { value } => Either::Left( + Self::local_targets_from_comparison_value(value).map(Cow::Owned), + ), + ArrayComparison::IsEmpty => Either::Right(std::iter::empty()), + }; + Box::new(std::iter::once(Cow::Borrowed(column)).chain(value_targets)) + } + Expression::Exists { .. } => Box::new(iter::empty()), + } + } + + fn local_targets_from_comparison_value( + value: &ComparisonValue, + ) -> impl Iterator> { + match value { + ComparisonValue::Column { + path, + name, + arguments, + field_path, + field_type, + .. + } => { + if path.is_empty() { + Either::Left(iter::once(ComparisonTarget::Column { + name: name.clone(), + arguments: arguments.clone(), + field_path: field_path.clone(), + field_type: field_type.clone(), + })) + } else { + Either::Right(iter::empty()) + } + } + _ => Either::Right(std::iter::empty()), + } + } +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum ArrayComparison { + /// Check if the array contains the specified value. + /// Only used if the 'query.nested_fields.filter_by.nested_arrays.contains' capability is supported. + Contains { value: ComparisonValue }, + /// Check is the array is empty. + /// Only used if the 'query.nested_fields.filter_by.nested_arrays.is_empty' capability is supported. + IsEmpty, +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum ComparisonTarget { + /// The comparison targets a column. + Column { + /// The name of the column + name: ndc::FieldName, + + /// Arguments to satisfy the column specified by 'name' + arguments: BTreeMap>, + + /// Path to a nested field within an object column + field_path: Option>, + + /// Type of the field that you get *after* follwing `field_path` to a possibly-nested + /// field. + field_type: Type, + }, + // TODO: ENG-1457 Add this variant to support query.aggregates.filter_by + // /// The comparison targets the result of aggregation. + // /// Only used if the 'query.aggregates.filter_by' capability is supported. + // Aggregate { + // /// Non-empty collection of relationships to traverse + // path: Vec, + // /// The aggregation method to use + // aggregate: Aggregate, + // }, +} + +impl ComparisonTarget { + pub fn column(name: impl Into, field_type: Type) -> Self { + Self::Column { + name: name.into(), + arguments: Default::default(), + field_path: Default::default(), + field_type, + } + } + + pub fn target_type(&self) -> &Type { + match self { + ComparisonTarget::Column { field_type, .. } => field_type, + // TODO: ENG-1457 + // ComparisonTarget::Aggregate { aggregate, .. } => aggregate.result_type, + } + } +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum ComparisonValue { + Column { + /// Any relationships to traverse to reach this column. + /// Only non-empty if the 'relationships.relation_comparisons' is supported. + path: Vec, + /// The name of the column + name: ndc::FieldName, + /// Arguments to satisfy the column specified by 'name' + arguments: BTreeMap>, + /// Path to a nested field within an object column. + /// Only non-empty if the 'query.nested_fields.filter_by' capability is supported. + field_path: Option>, + /// Type of the field that you get *after* follwing `field_path` to a possibly-nested + /// field. + field_type: Type, + /// The scope in which this column exists, identified + /// by an top-down index into the stack of scopes. + /// The stack grows inside each `Expression::Exists`, + /// so scope 0 (the default) refers to the current collection, + /// and each subsequent index refers to the collection outside + /// its predecessor's immediately enclosing `Expression::Exists` + /// expression. + /// Only used if the 'query.exists.named_scopes' capability is supported. + scope: Option, + }, + Scalar { + value: serde_json::Value, + value_type: Type, + }, + Variable { + name: ndc::VariableName, + variable_type: Type, + }, +} + +impl ComparisonValue { + pub fn column(name: impl Into, field_type: Type) -> Self { + Self::Column { + path: Default::default(), + name: name.into(), + arguments: Default::default(), + field_path: Default::default(), + field_type, + scope: Default::default(), + } + } +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum ExistsInCollection { + /// The rows to evaluate the exists predicate against come from a related collection. + /// Only used if the 'relationships' capability is supported. + Related { + /// Key of the relation in the [Query] joins map. Relationships are scoped to the sub-query + /// that defines the relation source. + relationship: ndc::RelationshipName, + }, + /// The rows to evaluate the exists predicate against come from an unrelated collection + /// Only used if the 'query.exists.unrelated' capability is supported. + Unrelated { + /// Key of the relation in the [QueryPlan] joins map. Unrelated collections are not scoped + /// to a sub-query, instead they are given in the root [QueryPlan]. + unrelated_collection: String, + }, + /// The rows to evaluate the exists predicate against come from a nested array field. + /// Only used if the 'query.exists.nested_collections' capability is supported. + NestedCollection { + column_name: ndc::FieldName, + arguments: BTreeMap>, + /// Path to a nested collection via object columns + field_path: Vec, + }, + /// Specifies a column that contains a nested array of scalars. The + /// array will be brought into scope of the nested expression where + /// each element becomes an object with one '__value' column that + /// contains the element value. + /// Only used if the 'query.exists.nested_scalar_collections' capability is supported. + NestedScalarCollection { + column_name: FieldName, + arguments: BTreeMap>, + /// Path to a nested collection via object columns + field_path: Vec, + }, +} diff --git a/crates/ndc-query-plan/src/query_plan/fields.rs b/crates/ndc-query-plan/src/query_plan/fields.rs new file mode 100644 index 00000000..c2f88957 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/fields.rs @@ -0,0 +1,54 @@ +use derivative::Derivative; +use indexmap::IndexMap; +use ndc_models as ndc; + +use crate::Type; + +use super::{Aggregate, ConnectorTypes, Grouping}; + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum Field { + Column { + column: ndc::FieldName, + + /// When the type of the column is a (possibly-nullable) array or object, + /// the caller can request a subset of the complete column data, + /// by specifying fields to fetch here. + /// If omitted, the column data will be fetched in full. + fields: Option>, + + column_type: Type, + }, + Relationship { + /// The name of the relationship to follow for the subquery - this is the key in the + /// [Query] relationships map in this module, it is **not** the key in the + /// [ndc::QueryRequest] collection_relationships map. + relationship: ndc::RelationshipName, + aggregates: Option>>, + fields: Option>>, + groups: Option>, + }, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct NestedObject { + pub fields: IndexMap>, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct NestedArray { + pub fields: Box>, +} + +// TODO: ENG-1464 define NestedCollection struct + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum NestedField { + Object(NestedObject), + Array(NestedArray), + // TODO: ENG-1464 add `Collection(NestedCollection)` variant +} diff --git a/crates/ndc-query-plan/src/query_plan/mod.rs b/crates/ndc-query-plan/src/query_plan/mod.rs new file mode 100644 index 00000000..1ba7757c --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/mod.rs @@ -0,0 +1,14 @@ +mod aggregation; +pub use aggregation::*; +mod connector_types; +pub use connector_types::*; +mod expression; +pub use expression::*; +mod fields; +pub use fields::*; +mod ordering; +pub use ordering::*; +mod requests; +pub use requests::*; +mod schema; +pub use schema::*; diff --git a/crates/ndc-query-plan/src/query_plan/ordering.rs b/crates/ndc-query-plan/src/query_plan/ordering.rs new file mode 100644 index 00000000..2e2cb0b7 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/ordering.rs @@ -0,0 +1,46 @@ +use std::collections::BTreeMap; + +use derivative::Derivative; +use ndc_models::{self as ndc, ArgumentName, OrderDirection}; + +use super::{Aggregate, Argument, ConnectorTypes}; + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct OrderBy { + /// The elements to order by, in priority order + pub elements: Vec>, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct OrderByElement { + pub order_direction: OrderDirection, + pub target: OrderByTarget, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum OrderByTarget { + Column { + /// Any relationships to traverse to reach this column. These are translated from + /// [ndc::OrderByElement] values in the [ndc::QueryRequest] to names of relation + /// fields for the [crate::QueryPlan]. + path: Vec, + + /// The name of the column + name: ndc::FieldName, + + /// Arguments to satisfy the column specified by 'name' + arguments: BTreeMap>, + + /// Path to a nested field within an object column + field_path: Option>, + }, + Aggregate { + /// Non-empty collection of relationships to traverse + path: Vec, + /// The aggregation method to use + aggregate: Aggregate, + }, +} diff --git a/crates/ndc-query-plan/src/query_plan/requests.rs b/crates/ndc-query-plan/src/query_plan/requests.rs new file mode 100644 index 00000000..a5dc7ed6 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/requests.rs @@ -0,0 +1,171 @@ +use std::collections::BTreeMap; + +use derivative::Derivative; +use indexmap::IndexMap; +use ndc_models::{self as ndc, RelationshipType}; +use nonempty::NonEmpty; + +use crate::{vec_set::VecSet, Type}; + +use super::{Aggregate, ConnectorTypes, Expression, Field, Grouping, OrderBy}; + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + PartialEq(bound = "T::ScalarType: PartialEq") +)] +pub struct QueryPlan { + pub collection: ndc::CollectionName, + pub query: Query, + pub arguments: BTreeMap>, + pub variables: Option>, + + /// Types for values from the `variables` map as inferred by usages in the query request. It is + /// possible for the same variable to be used in multiple contexts with different types. This + /// map provides sets of all observed types. + /// + /// The observed type may be `None` if the type of a variable use could not be inferred. + pub variable_types: VariableTypes, + + // TODO: type for unrelated collection + pub unrelated_collections: BTreeMap>, +} + +impl QueryPlan { + pub fn has_variables(&self) -> bool { + self.variables.is_some() + } +} + +pub type Relationships = BTreeMap>; +pub type VariableSet = BTreeMap; +pub type VariableTypes = BTreeMap>>; + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Default(bound = ""), + PartialEq(bound = "") +)] +pub struct Query { + pub aggregates: Option>>, + pub fields: Option>>, + pub limit: Option, + pub offset: Option, + pub order_by: Option>, + pub predicate: Option>, + pub groups: Option>, + + /// Relationships referenced by fields and expressions in this query or sub-query. Does not + /// include relationships in sub-queries nested under this one. + pub relationships: Relationships, + + /// Some relationship references may introduce a named "scope" so that other parts of the query + /// request can reference fields of documents in the related collection. The connector must + /// introduce a variable, or something similar, for such references. + pub scope: Option, +} + +impl Query { + pub fn has_aggregates(&self) -> bool { + if let Some(aggregates) = &self.aggregates { + !aggregates.is_empty() + } else { + false + } + } + + pub fn has_fields(&self) -> bool { + if let Some(fields) = &self.fields { + !fields.is_empty() + } else { + false + } + } + + pub fn has_groups(&self) -> bool { + self.groups.is_some() + } +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + Hash(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub enum Argument { + /// The argument is provided by reference to a variable + Variable { + name: ndc::VariableName, + argument_type: Type, + }, + /// The argument is provided as a literal value + Literal { + value: serde_json::Value, + argument_type: Type, + }, + /// The argument was a literal value that has been parsed as an [Expression] + Predicate { expression: Expression }, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct Relationship { + /// A mapping between columns on the source row to columns on the target collection. + /// The column on the target collection is specified via a field path (ie. an array of field + /// names that descend through nested object fields). The field path will only contain a single item, + /// meaning a column on the target collection's type, unless the 'relationships.nested' + /// capability is supported, in which case multiple items denotes a nested object field. + pub column_mapping: BTreeMap>, + pub relationship_type: RelationshipType, + /// The name of a collection + pub target_collection: ndc::CollectionName, + /// Values to be provided to any collection arguments + pub arguments: BTreeMap>, + pub query: Query, +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + PartialEq(bound = "T::ScalarType: PartialEq") +)] +pub enum RelationshipArgument { + /// The argument is provided by reference to a variable + Variable { + name: ndc::VariableName, + argument_type: Type, + }, + /// The argument is provided as a literal value + Literal { + value: serde_json::Value, + argument_type: Type, + }, + // The argument is provided based on a column of the source collection + Column { + name: ndc::FieldName, + argument_type: Type, + }, + /// The argument was a literal value that has been parsed as an [Expression] + Predicate { expression: Expression }, +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct UnrelatedJoin { + pub target_collection: ndc::CollectionName, + pub arguments: BTreeMap>, + pub query: Query, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Scope { + Root, + Named(String), +} diff --git a/crates/ndc-query-plan/src/query_plan/schema.rs b/crates/ndc-query-plan/src/query_plan/schema.rs new file mode 100644 index 00000000..36ee6dc2 --- /dev/null +++ b/crates/ndc-query-plan/src/query_plan/schema.rs @@ -0,0 +1,80 @@ +use derivative::Derivative; +use ndc_models as ndc; + +use crate::Type; + +use super::ConnectorTypes; + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub enum ComparisonOperatorDefinition { + Equal, + In, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, + Contains, + ContainsInsensitive, + StartsWith, + StartsWithInsensitive, + EndsWith, + EndsWithInsensitive, + Custom { + /// The type of the argument to this operator + argument_type: Type, + }, +} + +impl ComparisonOperatorDefinition { + pub fn argument_type(self, left_operand_type: &Type) -> Type { + use ComparisonOperatorDefinition as C; + match self { + C::In => Type::ArrayOf(Box::new(left_operand_type.clone())), + C::Equal + | C::LessThan + | C::LessThanOrEqual + | C::GreaterThan + | C::GreaterThanOrEqual => left_operand_type.clone(), + C::Contains + | C::ContainsInsensitive + | C::StartsWith + | C::StartsWithInsensitive + | C::EndsWith + | C::EndsWithInsensitive => T::string_type(), + C::Custom { argument_type } => argument_type, + } + } + + pub fn from_ndc_definition( + ndc_definition: &ndc::ComparisonOperatorDefinition, + map_type: impl FnOnce(&ndc::Type) -> Result, E>, + ) -> Result { + use ndc::ComparisonOperatorDefinition as NDC; + let definition = match ndc_definition { + NDC::Equal => Self::Equal, + NDC::In => Self::In, + NDC::LessThan => Self::LessThan, + NDC::LessThanOrEqual => Self::LessThanOrEqual, + NDC::GreaterThan => Self::GreaterThan, + NDC::GreaterThanOrEqual => Self::GreaterThanOrEqual, + NDC::Contains => Self::Contains, + NDC::ContainsInsensitive => Self::ContainsInsensitive, + NDC::StartsWith => Self::StartsWith, + NDC::StartsWithInsensitive => Self::StartsWithInsensitive, + NDC::EndsWith => Self::EndsWith, + NDC::EndsWithInsensitive => Self::EndsWithInsensitive, + NDC::Custom { argument_type } => Self::Custom { + argument_type: map_type(argument_type)?, + }, + }; + Ok(definition) + } +} + +#[derive(Derivative)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +pub struct AggregateFunctionDefinition { + /// The scalar or object type of the result of this function + pub result_type: Type, +} diff --git a/crates/ndc-query-plan/src/type_system.rs b/crates/ndc-query-plan/src/type_system.rs index 7fea0395..dce58f1d 100644 --- a/crates/ndc-query-plan/src/type_system.rs +++ b/crates/ndc-query-plan/src/type_system.rs @@ -1,13 +1,15 @@ use ref_cast::RefCast; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt::Display}; use itertools::Itertools as _; -use ndc_models as ndc; +use ndc_models::{self as ndc, ArgumentName, ObjectTypeName}; use crate::{self as plan, QueryPlanError}; +type Result = std::result::Result; + /// The type of values that a column, field, or argument may take. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Type { Scalar(ScalarType), /// The name of an object type declared in `objectTypes` @@ -15,9 +17,36 @@ pub enum Type { ArrayOf(Box>), /// A nullable form of any of the other types Nullable(Box>), + /// Used internally + Tuple(Vec>), } impl Type { + pub fn array_of(t: Self) -> Self { + Self::ArrayOf(Box::new(t)) + } + + pub fn named_object( + name: impl Into, + fields: impl IntoIterator, impl Into>)>, + ) -> Self { + Self::Object(ObjectType::new(fields).named(name)) + } + + pub fn nullable(t: Self) -> Self { + t.into_nullable() + } + + pub fn object( + fields: impl IntoIterator, impl Into>)>, + ) -> Self { + Self::Object(ObjectType::new(fields)) + } + + pub fn scalar(scalar_type: impl Into) -> Self { + Self::Scalar(scalar_type.into()) + } + pub fn into_nullable(self) -> Self { match self { t @ Type::Nullable(_) => t, @@ -32,19 +61,163 @@ impl Type { _ => false, } } + + pub fn into_array_element_type(self) -> Result + where + S: Clone + std::fmt::Debug, + { + match self { + Type::ArrayOf(t) => Ok(*t), + Type::Nullable(t) => t.into_array_element_type(), + t => Err(QueryPlanError::TypeMismatch(format!( + "expected an array, but got type {t:?}" + ))), + } + } + + pub fn into_object_type(self) -> Result> + where + S: std::fmt::Debug, + { + match self { + Type::Object(object_type) => Ok(object_type), + Type::Nullable(t) => t.into_object_type(), + t => Err(QueryPlanError::TypeMismatch(format!( + "expected object type, but got {t:?}" + ))), + } + } +} + +impl Display for Type { + /// Display types using GraphQL-style syntax + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn helper(t: &Type, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + where + S: Display, + { + match t { + Type::Scalar(s) => write!(f, "{}", s), + Type::Object(ot) => write!(f, "{ot}"), + Type::ArrayOf(t) => write!(f, "[{t}]"), + Type::Nullable(t) => write!(f, "{t}"), + Type::Tuple(ts) => { + write!(f, "(")?; + for (index, t) in ts.iter().enumerate() { + write!(f, "{t}")?; + if index < ts.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, ")") + } + } + } + match self { + Type::Nullable(t) => helper(t, f), + t => { + helper(t, f)?; + write!(f, "!") + } + } + } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct ObjectType { /// A type name may be tracked for error reporting. The name does not affect how query plans /// are generated. pub name: Option, - pub fields: BTreeMap>, + pub fields: BTreeMap>, } impl ObjectType { + pub fn new( + fields: impl IntoIterator, impl Into>)>, + ) -> Self { + ObjectType { + name: None, + fields: fields + .into_iter() + .map(|(name, field)| (name.into(), field.into())) + .collect(), + } + } + + pub fn named(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + pub fn named_fields(&self) -> impl Iterator)> { - self.fields.iter() + self.fields + .iter() + .map(|(name, field)| (name, &field.r#type)) + } + + pub fn get(&self, field_name: &ndc::FieldName) -> Result<&ObjectField> { + self.fields + .get(field_name) + .ok_or_else(|| QueryPlanError::UnknownObjectTypeField { + object_type: None, + field_name: field_name.clone(), + path: Default::default(), + }) + } +} + +impl Display for ObjectType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ ")?; + for (index, (name, field)) in self.fields.iter().enumerate() { + write!(f, "{name}: {}", field.r#type)?; + if index < self.fields.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, " }}")?; + Ok(()) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct ObjectField { + pub r#type: Type, + /// The arguments available to the field - Matches implementation from CollectionInfo + pub parameters: BTreeMap>, +} + +impl ObjectField { + pub fn new(r#type: Type) -> Self { + Self { + r#type, + parameters: Default::default(), + } + } + + pub fn into_nullable(self) -> Self { + let new_field_type = match self.r#type { + t @ Type::Nullable(_) => t, + t => Type::Nullable(Box::new(t)), + }; + Self { + r#type: new_field_type, + parameters: self.parameters, + } + } + + pub fn with_parameters(mut self, parameters: BTreeMap>) -> Self { + self.parameters = parameters; + self + } +} + +impl From> for ObjectField { + fn from(value: Type) -> Self { + ObjectField { + r#type: value, + parameters: Default::default(), + } } } @@ -56,7 +229,7 @@ pub fn inline_object_types( object_types: &BTreeMap, t: &ndc::Type, lookup_scalar_type: fn(&ndc::ScalarTypeName) -> Option, -) -> Result, QueryPlanError> { +) -> Result> { let plan_type = match t { ndc::Type::Named { name } => lookup_type(object_types, name, lookup_scalar_type)?, @@ -77,7 +250,7 @@ fn lookup_type( object_types: &BTreeMap, name: &ndc::TypeName, lookup_scalar_type: fn(&ndc::ScalarTypeName) -> Option, -) -> Result, QueryPlanError> { +) -> Result> { if let Some(scalar_type) = lookup_scalar_type(ndc::ScalarTypeName::ref_cast(name)) { return Ok(Type::Scalar(scalar_type)); } @@ -93,7 +266,7 @@ fn lookup_object_type_helper( object_types: &BTreeMap, name: &ndc::ObjectTypeName, lookup_scalar_type: fn(&ndc::ScalarTypeName) -> Option, -) -> Result, QueryPlanError> { +) -> Result> { let object_type = object_types .get(name) .ok_or_else(|| QueryPlanError::UnknownObjectType(name.to_string()))?; @@ -104,12 +277,18 @@ fn lookup_object_type_helper( .fields .iter() .map(|(name, field)| { + let field_type = + inline_object_types(object_types, &field.r#type, lookup_scalar_type)?; Ok(( name.to_owned(), - inline_object_types(object_types, &field.r#type, lookup_scalar_type)?, - )) as Result<_, QueryPlanError> + plan::ObjectField { + r#type: field_type, + parameters: Default::default(), // TODO: connect ndc arguments to plan + // parameters + }, + )) }) - .try_collect()?, + .try_collect::<_, _, QueryPlanError>()?, }; Ok(plan_object_type) } @@ -118,6 +297,6 @@ pub fn lookup_object_type( object_types: &BTreeMap, name: &ndc::ObjectTypeName, lookup_scalar_type: fn(&ndc::ScalarTypeName) -> Option, -) -> Result, QueryPlanError> { +) -> Result> { lookup_object_type_helper(object_types, name, lookup_scalar_type) } diff --git a/crates/ndc-test-helpers/src/aggregates.rs b/crates/ndc-test-helpers/src/aggregates.rs index 212222c1..16c1eb75 100644 --- a/crates/ndc-test-helpers/src/aggregates.rs +++ b/crates/ndc-test-helpers/src/aggregates.rs @@ -1,15 +1,48 @@ -#[macro_export()] -macro_rules! column_aggregate { - ($name:literal => $column:literal, $function:literal) => { - ( - $name, - $crate::ndc_models::Aggregate::SingleColumn { - column: $column.into(), - function: $function.into(), - field_path: None, - }, - ) - }; +use std::collections::BTreeMap; + +use ndc_models::{Aggregate, AggregateFunctionName, Argument, ArgumentName, FieldName}; + +use crate::column::Column; + +pub struct AggregateColumnBuilder { + column: FieldName, + arguments: BTreeMap, + field_path: Option>, + function: AggregateFunctionName, +} + +pub fn column_aggregate( + column: impl Into, + function: impl Into, +) -> AggregateColumnBuilder { + let column = column.into(); + AggregateColumnBuilder { + column: column.column, + function: function.into(), + arguments: column.arguments, + field_path: column.field_path, + } +} + +impl AggregateColumnBuilder { + pub fn field_path( + mut self, + field_path: impl IntoIterator>, + ) -> Self { + self.field_path = Some(field_path.into_iter().map(Into::into).collect()); + self + } +} + +impl From for Aggregate { + fn from(builder: AggregateColumnBuilder) -> Self { + Aggregate::SingleColumn { + column: builder.column, + arguments: builder.arguments, + function: builder.function, + field_path: builder.field_path, + } + } } #[macro_export()] @@ -26,6 +59,7 @@ macro_rules! column_count_aggregate { $name, $crate::ndc_models::Aggregate::ColumnCount { column: $column.into(), + arguments: Default::default(), distinct: $distinct.to_owned(), field_path: None, }, diff --git a/crates/ndc-test-helpers/src/collection_info.rs b/crates/ndc-test-helpers/src/collection_info.rs index 3e042711..040a8694 100644 --- a/crates/ndc-test-helpers/src/collection_info.rs +++ b/crates/ndc-test-helpers/src/collection_info.rs @@ -9,7 +9,6 @@ pub fn collection(name: impl Display + Clone) -> (ndc_models::CollectionName, Co arguments: Default::default(), collection_type: name.to_string().into(), uniqueness_constraints: make_primary_key_uniqueness_constraint(name.clone()), - foreign_keys: Default::default(), }; (name.to_string().into(), coll) } diff --git a/crates/ndc-test-helpers/src/column.rs b/crates/ndc-test-helpers/src/column.rs new file mode 100644 index 00000000..ce492ab6 --- /dev/null +++ b/crates/ndc-test-helpers/src/column.rs @@ -0,0 +1,63 @@ +use std::collections::BTreeMap; + +use itertools::Itertools as _; +use ndc_models::{Argument, ArgumentName, FieldName, PathElement, RelationshipName}; + +use crate::path_element; + +/// An intermediate struct that can be used to populate ComparisonTarget::Column, +/// Dimension::Column, etc. +pub struct Column { + pub path: Vec, + pub column: FieldName, + pub arguments: BTreeMap, + pub field_path: Option>, +} + +impl Column { + pub fn path(mut self, elements: impl IntoIterator>) -> Self { + self.path = elements.into_iter().map(Into::into).collect(); + self + } + + pub fn from_relationship(mut self, name: impl Into) -> Self { + self.path = vec![path_element(name).into()]; + self + } +} + +pub fn column(name: impl Into) -> Column { + Column { + path: Default::default(), + column: name.into(), + arguments: Default::default(), + field_path: Default::default(), + } +} + +impl From<&str> for Column { + fn from(input: &str) -> Self { + let mut parts = input.split("."); + let column = parts + .next() + .expect("a column reference must not be an empty string") + .into(); + let field_path = parts.map(Into::into).collect_vec(); + Column { + path: Default::default(), + column, + arguments: Default::default(), + field_path: if field_path.is_empty() { + None + } else { + Some(field_path) + }, + } + } +} + +impl From for Column { + fn from(name: FieldName) -> Self { + column(name) + } +} diff --git a/crates/ndc-test-helpers/src/comparison_target.rs b/crates/ndc-test-helpers/src/comparison_target.rs index 41463113..2bad170c 100644 --- a/crates/ndc-test-helpers/src/comparison_target.rs +++ b/crates/ndc-test-helpers/src/comparison_target.rs @@ -3,42 +3,18 @@ macro_rules! target { ($column:literal) => { $crate::ndc_models::ComparisonTarget::Column { name: $column.into(), + arguments: Default::default(), field_path: None, - path: vec![], } }; ($column:literal, field_path:$field_path:expr $(,)?) => { $crate::ndc_models::ComparisonTarget::Column { name: $column.into(), + arguments: Default::default(), field_path: $field_path.into_iter().map(|x| x.into()).collect(), - path: vec![], - } - }; - ($column:literal, relations:$path:expr $(,)?) => { - $crate::ndc_models::ComparisonTarget::Column { - name: $column.into(), - field_path: None, - path: $path.into_iter().map(|x| x.into()).collect(), - } - }; - ($column:literal, field_path:$field_path:expr, relations:$path:expr $(,)?) => { - $crate::ndc_models::ComparisonTarget::Column { - name: $column.into(), - // field_path: $field_path.into_iter().map(|x| x.into()).collect(), - path: $path.into_iter().map(|x| x.into()).collect(), } }; ($target:expr) => { $target }; } - -pub fn root(name: S) -> ndc_models::ComparisonTarget -where - S: ToString, -{ - ndc_models::ComparisonTarget::RootCollectionColumn { - name: name.to_string().into(), - field_path: None, - } -} diff --git a/crates/ndc-test-helpers/src/comparison_value.rs b/crates/ndc-test-helpers/src/comparison_value.rs index 350378e1..cfbeca92 100644 --- a/crates/ndc-test-helpers/src/comparison_value.rs +++ b/crates/ndc-test-helpers/src/comparison_value.rs @@ -1,11 +1,6 @@ -#[macro_export] -macro_rules! column_value { - ($($column:tt)+) => { - $crate::ndc_models::ComparisonValue::Column { - column: $crate::target!($($column)+), - } - }; -} +use std::collections::BTreeMap; + +use ndc_models::{Argument, ArgumentName, ComparisonValue, FieldName, PathElement}; #[macro_export] macro_rules! value { @@ -27,3 +22,65 @@ macro_rules! variable { $crate::ndc_models::ComparisonValue::Variable { name: $expr } }; } + +#[derive(Debug)] +pub struct ColumnValueBuilder { + path: Vec, + name: FieldName, + arguments: BTreeMap, + field_path: Option>, + scope: Option, +} + +pub fn column_value(name: impl Into) -> ColumnValueBuilder { + ColumnValueBuilder { + path: Default::default(), + name: name.into(), + arguments: Default::default(), + field_path: Default::default(), + scope: Default::default(), + } +} + +impl ColumnValueBuilder { + pub fn path(mut self, path: impl IntoIterator>) -> Self { + self.path = path.into_iter().map(Into::into).collect(); + self + } + + pub fn arguments( + mut self, + arguments: impl IntoIterator, impl Into)>, + ) -> Self { + self.arguments = arguments + .into_iter() + .map(|(name, arg)| (name.into(), arg.into())) + .collect(); + self + } + + pub fn field_path( + mut self, + field_path: impl IntoIterator>, + ) -> Self { + self.field_path = Some(field_path.into_iter().map(Into::into).collect()); + self + } + + pub fn scope(mut self, scope: usize) -> Self { + self.scope = Some(scope); + self + } +} + +impl From for ComparisonValue { + fn from(builder: ColumnValueBuilder) -> Self { + ComparisonValue::Column { + path: builder.path, + name: builder.name, + arguments: builder.arguments, + field_path: builder.field_path, + scope: builder.scope, + } + } +} diff --git a/crates/ndc-test-helpers/src/exists_in_collection.rs b/crates/ndc-test-helpers/src/exists_in_collection.rs index e13826c6..e7a581c0 100644 --- a/crates/ndc-test-helpers/src/exists_in_collection.rs +++ b/crates/ndc-test-helpers/src/exists_in_collection.rs @@ -1,13 +1,19 @@ +use std::collections::BTreeMap; + +use ndc_models::{Argument, ArgumentName, ExistsInCollection, FieldName}; + #[macro_export] macro_rules! related { ($rel:literal) => { $crate::ndc_models::ExistsInCollection::Related { + field_path: Default::default(), relationship: $rel.into(), arguments: Default::default(), } }; ($rel:literal, $args:expr $(,)?) => { $crate::ndc_models::ExistsInCollection::Related { + field_path: Default::default(), relationship: $rel.into(), arguments: $args.into_iter().map(|x| x.into()).collect(), } @@ -29,3 +35,49 @@ macro_rules! unrelated { } }; } + +#[derive(Debug)] +pub struct ExistsInNestedCollectionBuilder { + column_name: FieldName, + arguments: BTreeMap, + field_path: Vec, +} + +pub fn exists_in_nested(column_name: impl Into) -> ExistsInNestedCollectionBuilder { + ExistsInNestedCollectionBuilder { + column_name: column_name.into(), + arguments: Default::default(), + field_path: Default::default(), + } +} + +impl ExistsInNestedCollectionBuilder { + pub fn arguments( + mut self, + arguments: impl IntoIterator, impl Into)>, + ) -> Self { + self.arguments = arguments + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + self + } + + pub fn field_path( + mut self, + field_path: impl IntoIterator>, + ) -> Self { + self.field_path = field_path.into_iter().map(Into::into).collect(); + self + } +} + +impl From for ExistsInCollection { + fn from(builder: ExistsInNestedCollectionBuilder) -> Self { + ExistsInCollection::NestedCollection { + column_name: builder.column_name, + arguments: builder.arguments, + field_path: builder.field_path, + } + } +} diff --git a/crates/ndc-test-helpers/src/expressions.rs b/crates/ndc-test-helpers/src/expressions.rs index 6b35ae2a..16aa63fc 100644 --- a/crates/ndc-test-helpers/src/expressions.rs +++ b/crates/ndc-test-helpers/src/expressions.rs @@ -1,5 +1,6 @@ use ndc_models::{ - ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, UnaryComparisonOperator, + ArrayComparison, ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, + RelationshipName, UnaryComparisonOperator, }; pub fn and(operands: I) -> Expression @@ -57,9 +58,39 @@ where } } -pub fn exists(in_collection: ExistsInCollection, predicate: Expression) -> Expression { +pub fn exists( + in_collection: impl Into, + predicate: impl Into, +) -> Expression { Expression::Exists { - in_collection, - predicate: Some(Box::new(predicate)), + in_collection: in_collection.into(), + predicate: Some(Box::new(predicate.into())), + } +} + +pub fn in_related(relationship: impl Into) -> ExistsInCollection { + ExistsInCollection::Related { + field_path: Default::default(), + relationship: relationship.into(), + arguments: Default::default(), + } +} + +pub fn array_contains( + column: impl Into, + value: impl Into, +) -> Expression { + Expression::ArrayComparison { + column: column.into(), + comparison: ArrayComparison::Contains { + value: value.into(), + }, + } +} + +pub fn is_empty(column: impl Into) -> Expression { + Expression::ArrayComparison { + column: column.into(), + comparison: ArrayComparison::IsEmpty, } } diff --git a/crates/ndc-test-helpers/src/groups.rs b/crates/ndc-test-helpers/src/groups.rs new file mode 100644 index 00000000..4899f3b2 --- /dev/null +++ b/crates/ndc-test-helpers/src/groups.rs @@ -0,0 +1,144 @@ +use std::collections::BTreeMap; + +use indexmap::IndexMap; +use ndc_models::{ + Aggregate, Argument, ArgumentName, Dimension, FieldName, GroupExpression, GroupOrderBy, + GroupOrderByElement, Grouping, OrderBy, OrderDirection, PathElement, +}; + +use crate::column::Column; + +#[derive(Clone, Debug, Default)] +pub struct GroupingBuilder { + dimensions: Vec, + aggregates: IndexMap, + predicate: Option, + order_by: Option, + limit: Option, + offset: Option, +} + +pub fn grouping() -> GroupingBuilder { + Default::default() +} + +impl GroupingBuilder { + pub fn dimensions( + mut self, + dimensions: impl IntoIterator>, + ) -> Self { + self.dimensions = dimensions.into_iter().map(Into::into).collect(); + self + } + + pub fn aggregates( + mut self, + aggregates: impl IntoIterator, impl Into)>, + ) -> Self { + self.aggregates = aggregates + .into_iter() + .map(|(name, aggregate)| (name.into(), aggregate.into())) + .collect(); + self + } + + pub fn predicate(mut self, predicate: impl Into) -> Self { + self.predicate = Some(predicate.into()); + self + } + + pub fn order_by(mut self, order_by: impl Into) -> Self { + self.order_by = Some(order_by.into()); + self + } + + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + pub fn offset(mut self, offset: u32) -> Self { + self.offset = Some(offset); + self + } +} + +impl From for Grouping { + fn from(value: GroupingBuilder) -> Self { + Grouping { + dimensions: value.dimensions, + aggregates: value.aggregates, + predicate: value.predicate, + order_by: value.order_by, + limit: value.limit, + offset: value.offset, + } + } +} + +#[derive(Clone, Debug)] +pub struct DimensionColumnBuilder { + path: Vec, + column_name: FieldName, + arguments: BTreeMap, + field_path: Option>, +} + +pub fn dimension_column(column: impl Into) -> DimensionColumnBuilder { + let column = column.into(); + DimensionColumnBuilder { + path: column.path, + column_name: column.column, + arguments: column.arguments, + field_path: column.field_path, + } +} + +impl DimensionColumnBuilder { + pub fn path(mut self, path: impl IntoIterator>) -> Self { + self.path = path.into_iter().map(Into::into).collect(); + self + } + + pub fn arguments( + mut self, + arguments: impl IntoIterator, impl Into)>, + ) -> Self { + self.arguments = arguments + .into_iter() + .map(|(name, argument)| (name.into(), argument.into())) + .collect(); + self + } + + pub fn field_path( + mut self, + field_path: impl IntoIterator>, + ) -> Self { + self.field_path = Some(field_path.into_iter().map(Into::into).collect()); + self + } +} + +impl From for Dimension { + fn from(value: DimensionColumnBuilder) -> Self { + Dimension::Column { + path: value.path, + column_name: value.column_name, + arguments: value.arguments, + field_path: value.field_path, + } + } +} + +/// Produces a consistent ordering for up to 10 dimensions +pub fn ordered_dimensions() -> GroupOrderBy { + GroupOrderBy { + elements: (0..10) + .map(|index| GroupOrderByElement { + order_direction: OrderDirection::Asc, + target: ndc_models::GroupOrderByTarget::Dimension { index }, + }) + .collect(), + } +} diff --git a/crates/ndc-test-helpers/src/lib.rs b/crates/ndc-test-helpers/src/lib.rs index 706cefd6..1d79d525 100644 --- a/crates/ndc-test-helpers/src/lib.rs +++ b/crates/ndc-test-helpers/src/lib.rs @@ -2,12 +2,16 @@ #![allow(unused_imports)] mod aggregates; +pub use aggregates::*; mod collection_info; +mod column; +pub use column::*; mod comparison_target; mod comparison_value; mod exists_in_collection; mod expressions; mod field; +mod groups; mod object_type; mod order_by; mod path_element; @@ -19,7 +23,7 @@ use std::collections::BTreeMap; use indexmap::IndexMap; use ndc_models::{ - Aggregate, Argument, Expression, Field, OrderBy, OrderByElement, PathElement, Query, + Aggregate, Argument, Expression, Field, FieldName, OrderBy, OrderByElement, PathElement, Query, QueryRequest, Relationship, RelationshipArgument, RelationshipType, }; @@ -33,6 +37,7 @@ pub use comparison_value::*; pub use exists_in_collection::*; pub use expressions::*; pub use field::*; +pub use groups::*; pub use object_type::*; pub use order_by::*; pub use path_element::*; @@ -142,6 +147,7 @@ pub struct QueryBuilder { offset: Option, order_by: Option, predicate: Option, + groups: Option, } pub fn query() -> QueryBuilder { @@ -157,6 +163,7 @@ impl QueryBuilder { offset: None, order_by: None, predicate: None, + groups: None, } } @@ -170,11 +177,14 @@ impl QueryBuilder { self } - pub fn aggregates(mut self, aggregates: [(&str, Aggregate); S]) -> Self { + pub fn aggregates( + mut self, + aggregates: impl IntoIterator, impl Into)>, + ) -> Self { self.aggregates = Some( aggregates .into_iter() - .map(|(name, aggregate)| (name.to_owned().into(), aggregate)) + .map(|(name, aggregate)| (name.into(), aggregate.into())) .collect(), ); self @@ -199,6 +209,11 @@ impl QueryBuilder { self.predicate = Some(expression); self } + + pub fn groups(mut self, groups: impl Into) -> Self { + self.groups = Some(groups.into()); + self + } } impl From for Query { @@ -210,6 +225,7 @@ impl From for Query { offset: value.offset, order_by: value.order_by, predicate: value.predicate, + groups: value.groups, } } } diff --git a/crates/ndc-test-helpers/src/object_type.rs b/crates/ndc-test-helpers/src/object_type.rs index 01feb919..f4978ce5 100644 --- a/crates/ndc-test-helpers/src/object_type.rs +++ b/crates/ndc-test-helpers/src/object_type.rs @@ -20,5 +20,6 @@ pub fn object_type( ) }) .collect(), + foreign_keys: Default::default(), } } diff --git a/crates/ndc-test-helpers/src/order_by.rs b/crates/ndc-test-helpers/src/order_by.rs index 9ea8c778..22e9bce3 100644 --- a/crates/ndc-test-helpers/src/order_by.rs +++ b/crates/ndc-test-helpers/src/order_by.rs @@ -5,6 +5,7 @@ macro_rules! asc { order_direction: $crate::ndc_models::OrderDirection::Asc, target: $crate::ndc_models::OrderByTarget::Column { name: $crate::ndc_models::FieldName::new($crate::smol_str::SmolStr::new($name)), + arguments: Default::default(), field_path: None, path: vec![], }, @@ -19,6 +20,7 @@ macro_rules! desc { order_direction: $crate::ndc_models::OrderDirection::Desc, target: $crate::ndc_models::OrderByTarget::Column { name: $crate::ndc_models::FieldName::new($crate::smol_str::SmolStr::new($name)), + arguments: Default::default(), field_path: None, path: vec![], }, diff --git a/crates/ndc-test-helpers/src/path_element.rs b/crates/ndc-test-helpers/src/path_element.rs index b0c89d5b..25cc4d5d 100644 --- a/crates/ndc-test-helpers/src/path_element.rs +++ b/crates/ndc-test-helpers/src/path_element.rs @@ -1,16 +1,17 @@ use std::collections::BTreeMap; -use ndc_models::{Expression, PathElement, RelationshipArgument}; +use ndc_models::{Expression, FieldName, PathElement, RelationshipArgument}; #[derive(Clone, Debug)] pub struct PathElementBuilder { relationship: ndc_models::RelationshipName, arguments: Option>, + field_path: Option>, predicate: Option>, } -pub fn path_element(relationship: ndc_models::RelationshipName) -> PathElementBuilder { - PathElementBuilder::new(relationship) +pub fn path_element(relationship: impl Into) -> PathElementBuilder { + PathElementBuilder::new(relationship.into()) } impl PathElementBuilder { @@ -18,6 +19,7 @@ impl PathElementBuilder { PathElementBuilder { relationship, arguments: None, + field_path: None, predicate: None, } } @@ -26,6 +28,14 @@ impl PathElementBuilder { self.predicate = Some(Box::new(expression)); self } + + pub fn field_path( + mut self, + field_path: impl IntoIterator>, + ) -> Self { + self.field_path = Some(field_path.into_iter().map(Into::into).collect()); + self + } } impl From for PathElement { @@ -33,6 +43,7 @@ impl From for PathElement { PathElement { relationship: value.relationship, arguments: value.arguments.unwrap_or_default(), + field_path: value.field_path, predicate: value.predicate, } } diff --git a/crates/ndc-test-helpers/src/query_response.rs b/crates/ndc-test-helpers/src/query_response.rs index 72970bb2..6b87f5c6 100644 --- a/crates/ndc-test-helpers/src/query_response.rs +++ b/crates/ndc-test-helpers/src/query_response.rs @@ -1,5 +1,5 @@ use indexmap::IndexMap; -use ndc_models::{QueryResponse, RowFieldValue, RowSet}; +use ndc_models::{FieldName, Group, QueryResponse, RowFieldValue, RowSet}; #[derive(Clone, Debug, Default)] pub struct QueryResponseBuilder { @@ -30,6 +30,7 @@ impl QueryResponseBuilder { self.row_sets.push(RowSet { aggregates: None, rows: Some(vec![]), + groups: Default::default(), }); self } @@ -45,6 +46,7 @@ impl From for QueryResponse { pub struct RowSetBuilder { aggregates: IndexMap, rows: Vec>, + groups: Option>, } impl RowSetBuilder { @@ -54,13 +56,10 @@ impl RowSetBuilder { pub fn aggregates( mut self, - aggregates: impl IntoIterator)>, + aggregates: impl IntoIterator, impl Into)>, ) -> Self { - self.aggregates.extend( - aggregates - .into_iter() - .map(|(k, v)| (k.to_string().into(), v.into())), - ); + self.aggregates + .extend(aggregates.into_iter().map(|(k, v)| (k.into(), v.into()))); self } @@ -89,10 +88,24 @@ impl RowSetBuilder { ); self } + + pub fn groups( + mut self, + groups: impl IntoIterator>, + ) -> Self { + self.groups = Some(groups.into_iter().map(Into::into).collect()); + self + } } impl From for RowSet { - fn from(RowSetBuilder { aggregates, rows }: RowSetBuilder) -> Self { + fn from( + RowSetBuilder { + aggregates, + rows, + groups, + }: RowSetBuilder, + ) -> Self { RowSet { aggregates: if aggregates.is_empty() { None @@ -100,6 +113,7 @@ impl From for RowSet { Some(aggregates) }, rows: if rows.is_empty() { None } else { Some(rows) }, + groups, } } } @@ -117,3 +131,16 @@ pub fn query_response() -> QueryResponseBuilder { pub fn row_set() -> RowSetBuilder { Default::default() } + +pub fn group( + dimensions: impl IntoIterator>, + aggregates: impl IntoIterator)>, +) -> Group { + Group { + dimensions: dimensions.into_iter().map(Into::into).collect(), + aggregates: aggregates + .into_iter() + .map(|(name, value)| (name.to_string(), value.into())) + .collect(), + } +} diff --git a/crates/ndc-test-helpers/src/relationships.rs b/crates/ndc-test-helpers/src/relationships.rs index 6166e809..053bb7c7 100644 --- a/crates/ndc-test-helpers/src/relationships.rs +++ b/crates/ndc-test-helpers/src/relationships.rs @@ -4,7 +4,7 @@ use ndc_models::{Relationship, RelationshipArgument, RelationshipType}; #[derive(Clone, Debug)] pub struct RelationshipBuilder { - column_mapping: BTreeMap, + column_mapping: BTreeMap>, relationship_type: RelationshipType, target_collection: ndc_models::CollectionName, arguments: BTreeMap, @@ -12,17 +12,22 @@ pub struct RelationshipBuilder { pub fn relationship( target: &str, - column_mapping: [(&str, &str); S], + column_mapping: [(&str, &[&str]); S], ) -> RelationshipBuilder { RelationshipBuilder::new(target, column_mapping) } impl RelationshipBuilder { - pub fn new(target: &str, column_mapping: [(&str, &str); S]) -> Self { + pub fn new(target: &str, column_mapping: [(&str, &[&str]); S]) -> Self { RelationshipBuilder { column_mapping: column_mapping .into_iter() - .map(|(source, target)| (source.to_owned().into(), target.to_owned().into())) + .map(|(source, target)| { + ( + source.to_owned().into(), + target.iter().map(|s| s.to_owned().into()).collect(), + ) + }) .collect(), relationship_type: RelationshipType::Array, target_collection: target.to_owned().into(), diff --git a/crates/test-helpers/src/arb_plan_type.rs b/crates/test-helpers/src/arb_plan_type.rs index 0ffe5ac1..4dfdff84 100644 --- a/crates/test-helpers/src/arb_plan_type.rs +++ b/crates/test-helpers/src/arb_plan_type.rs @@ -1,5 +1,5 @@ use configuration::MongoScalarType; -use ndc_query_plan::{ObjectType, Type}; +use ndc_query_plan::{ObjectField, ObjectType, Type}; use proptest::{collection::btree_map, prelude::*}; use crate::arb_type::arb_bson_scalar_type; @@ -14,9 +14,18 @@ pub fn arb_plan_type() -> impl Strategy> { any::>(), btree_map(any::().prop_map_into(), inner, 1..=10) ) - .prop_map(|(name, fields)| Type::Object(ObjectType { + .prop_map(|(name, field_types)| Type::Object(ObjectType { name: name.map(|n| n.into()), - fields + fields: field_types + .into_iter() + .map(|(name, t)| ( + name, + ObjectField { + r#type: t, + parameters: Default::default() + } + )) + .collect(), })) ] }) diff --git a/docs/release-checklist.md b/docs/release-checklist.md index f4c82b16..5f6b91f9 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -137,6 +137,9 @@ The content should have this format, }, "source": { "hash": "" + }, + "test": { + "test_config_path": "../../tests/test-config.json" } } ``` @@ -158,6 +161,9 @@ For example, }, "source": { "hash": "b95da1815a9b686e517aa78f677752e36e0bfda0" + }, + "test": { + "test_config_path": "../../tests/test-config.json" } } ``` diff --git a/fixtures/hasura/app/connector/test_cases/schema/departments.json b/fixtures/hasura/app/connector/test_cases/schema/departments.json new file mode 100644 index 00000000..5f8996b4 --- /dev/null +++ b/fixtures/hasura/app/connector/test_cases/schema/departments.json @@ -0,0 +1,24 @@ +{ + "name": "departments", + "collections": { + "departments": { + "type": "departments" + } + }, + "objectTypes": { + "departments": { + "fields": { + "_id": { + "type": { + "scalar": "objectId" + } + }, + "description": { + "type": { + "scalar": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/hasura/app/connector/test_cases/schema/schools.json b/fixtures/hasura/app/connector/test_cases/schema/schools.json new file mode 100644 index 00000000..0ebed63e --- /dev/null +++ b/fixtures/hasura/app/connector/test_cases/schema/schools.json @@ -0,0 +1,43 @@ +{ + "name": "schools", + "collections": { + "schools": { + "type": "schools" + } + }, + "objectTypes": { + "schools": { + "fields": { + "_id": { + "type": { + "scalar": "objectId" + } + }, + "departments": { + "type": { + "object": "schools_departments" + } + }, + "name": { + "type": { + "scalar": "string" + } + } + } + }, + "schools_departments": { + "fields": { + "english_department_id": { + "type": { + "scalar": "objectId" + } + }, + "math_department_id": { + "type": { + "scalar": "objectId" + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/hasura/app/metadata/Album.hml b/fixtures/hasura/app/metadata/Album.hml index eb4505fe..d18208be 100644 --- a/fixtures/hasura/app/metadata/Album.hml +++ b/fixtures/hasura/app/metadata/Album.hml @@ -5,7 +5,7 @@ definition: name: Album fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: albumId type: Int! - name: artistId @@ -56,7 +56,7 @@ definition: type: Album comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: albumId booleanExpressionType: IntBoolExp - fieldName: artistId @@ -83,7 +83,7 @@ definition: aggregatedType: Album aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: albumId aggregateExpression: IntAggExp - fieldName: artistId diff --git a/fixtures/hasura/app/metadata/Artist.hml b/fixtures/hasura/app/metadata/Artist.hml index 38755178..2ba6e1ac 100644 --- a/fixtures/hasura/app/metadata/Artist.hml +++ b/fixtures/hasura/app/metadata/Artist.hml @@ -5,7 +5,7 @@ definition: name: Artist fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: artistId type: Int! - name: name @@ -50,7 +50,7 @@ definition: type: Artist comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: artistId booleanExpressionType: IntBoolExp - fieldName: name @@ -74,7 +74,7 @@ definition: aggregatedType: Artist aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: artistId aggregateExpression: IntAggExp - fieldName: name diff --git a/fixtures/hasura/app/metadata/ArtistsWithAlbumsAndTracks.hml b/fixtures/hasura/app/metadata/ArtistsWithAlbumsAndTracks.hml index 9d6f0cd2..11217659 100644 --- a/fixtures/hasura/app/metadata/ArtistsWithAlbumsAndTracks.hml +++ b/fixtures/hasura/app/metadata/ArtistsWithAlbumsAndTracks.hml @@ -5,7 +5,7 @@ definition: name: AlbumWithTracks fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: title type: String! - name: tracks @@ -47,7 +47,7 @@ definition: name: ArtistWithAlbumsAndTracks fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: albums type: "[AlbumWithTracks!]!" - name: name @@ -92,7 +92,7 @@ definition: type: AlbumWithTracks comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: title booleanExpressionType: StringBoolExp comparableRelationships: [] @@ -113,7 +113,7 @@ definition: type: ArtistWithAlbumsAndTracks comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: albums booleanExpressionType: AlbumWithTracksBoolExp - fieldName: name @@ -136,7 +136,7 @@ definition: aggregatedType: ArtistWithAlbumsAndTracks aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: name aggregateExpression: StringAggExp count: diff --git a/fixtures/hasura/app/metadata/Customer.hml b/fixtures/hasura/app/metadata/Customer.hml index 61dfddc6..b853b340 100644 --- a/fixtures/hasura/app/metadata/Customer.hml +++ b/fixtures/hasura/app/metadata/Customer.hml @@ -5,7 +5,7 @@ definition: name: Customer fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: address type: String! - name: city @@ -116,7 +116,7 @@ definition: type: Customer comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: address booleanExpressionType: StringBoolExp - fieldName: city @@ -163,7 +163,7 @@ definition: aggregatedType: Customer aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: address aggregateExpression: StringAggExp - fieldName: city diff --git a/fixtures/hasura/app/metadata/Departments.hml b/fixtures/hasura/app/metadata/Departments.hml new file mode 100644 index 00000000..92fa76ce --- /dev/null +++ b/fixtures/hasura/app/metadata/Departments.hml @@ -0,0 +1,122 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Departments + fields: + - name: id + type: ObjectId! + - name: description + type: String! + graphql: + typeName: Departments + inputTypeName: DepartmentsInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: departments + fieldMapping: + id: + column: + name: _id + description: + column: + name: description + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Departments + permissions: + - role: admin + output: + allowedFields: + - id + - description + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: DepartmentsBoolExp + operand: + object: + type: Departments + comparableFields: + - fieldName: id + booleanExpressionType: ObjectIdBoolExp + - fieldName: description + booleanExpressionType: StringBoolExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: DepartmentsBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: DepartmentsAggExp + operand: + object: + aggregatedType: Departments + aggregatableFields: + - fieldName: id + aggregateExpression: ObjectIdAggExp + - fieldName: description + aggregateExpression: StringAggExp + count: + enable: true + graphql: + selectTypeName: DepartmentsAggExp + +--- +kind: Model +version: v1 +definition: + name: Departments + objectType: Departments + source: + dataConnectorName: test_cases + collection: departments + filterExpressionType: DepartmentsBoolExp + aggregateExpression: DepartmentsAggExp + orderableFields: + - fieldName: id + orderByDirections: + enableAll: true + - fieldName: description + orderByDirections: + enableAll: true + graphql: + selectMany: + queryRootField: departments + subscription: + rootField: departments + selectUniques: + - queryRootField: departmentsById + uniqueIdentifier: + - id + subscription: + rootField: departmentsById + orderByExpressionType: DepartmentsOrderBy + filterInputTypeName: DepartmentsFilterInput + aggregate: + queryRootField: departmentsAggregate + subscription: + rootField: departmentsAggregate + +--- +kind: ModelPermissions +version: v1 +definition: + modelName: Departments + permissions: + - role: admin + select: + filter: null + allowSubscriptions: true + diff --git a/fixtures/hasura/app/metadata/Employee.hml b/fixtures/hasura/app/metadata/Employee.hml index 5f926da4..151b55c0 100644 --- a/fixtures/hasura/app/metadata/Employee.hml +++ b/fixtures/hasura/app/metadata/Employee.hml @@ -5,7 +5,7 @@ definition: name: Employee fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: address type: String! - name: birthDate @@ -128,7 +128,7 @@ definition: type: Employee comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: address booleanExpressionType: StringBoolExp - fieldName: birthDate @@ -180,7 +180,7 @@ definition: aggregatedType: Employee aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: address aggregateExpression: StringAggExp - fieldName: birthDate diff --git a/fixtures/hasura/app/metadata/Genre.hml b/fixtures/hasura/app/metadata/Genre.hml index 6f718cdb..a64a1ad1 100644 --- a/fixtures/hasura/app/metadata/Genre.hml +++ b/fixtures/hasura/app/metadata/Genre.hml @@ -5,7 +5,7 @@ definition: name: Genre fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: genreId type: Int! - name: name @@ -50,7 +50,7 @@ definition: type: Genre comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: genreId booleanExpressionType: IntBoolExp - fieldName: name @@ -74,7 +74,7 @@ definition: aggregatedType: Genre aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: genreId aggregateExpression: IntAggExp - fieldName: name diff --git a/fixtures/hasura/app/metadata/Invoice.hml b/fixtures/hasura/app/metadata/Invoice.hml index 611f4faf..9d12ec8f 100644 --- a/fixtures/hasura/app/metadata/Invoice.hml +++ b/fixtures/hasura/app/metadata/Invoice.hml @@ -5,7 +5,7 @@ definition: name: Invoice fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: billingAddress type: String! - name: billingCity @@ -92,7 +92,7 @@ definition: type: Invoice comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: billingAddress booleanExpressionType: StringBoolExp - fieldName: billingCity @@ -131,7 +131,7 @@ definition: aggregatedType: Invoice aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: billingAddress aggregateExpression: StringAggExp - fieldName: billingCity diff --git a/fixtures/hasura/app/metadata/InvoiceLine.hml b/fixtures/hasura/app/metadata/InvoiceLine.hml index a6a79cdb..9456c12b 100644 --- a/fixtures/hasura/app/metadata/InvoiceLine.hml +++ b/fixtures/hasura/app/metadata/InvoiceLine.hml @@ -5,7 +5,7 @@ definition: name: InvoiceLine fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: invoiceId type: Int! - name: invoiceLineId @@ -68,7 +68,7 @@ definition: type: InvoiceLine comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: invoiceId booleanExpressionType: IntBoolExp - fieldName: invoiceLineId @@ -99,7 +99,7 @@ definition: aggregatedType: InvoiceLine aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: invoiceId aggregateExpression: IntAggExp - fieldName: invoiceLineId diff --git a/fixtures/hasura/app/metadata/MediaType.hml b/fixtures/hasura/app/metadata/MediaType.hml index fc2ab999..7c2f3c4e 100644 --- a/fixtures/hasura/app/metadata/MediaType.hml +++ b/fixtures/hasura/app/metadata/MediaType.hml @@ -5,7 +5,7 @@ definition: name: MediaType fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: mediaTypeId type: Int! - name: name @@ -50,7 +50,7 @@ definition: type: MediaType comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: mediaTypeId booleanExpressionType: IntBoolExp - fieldName: name @@ -74,7 +74,7 @@ definition: aggregatedType: MediaType aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: mediaTypeId aggregateExpression: IntAggExp - fieldName: name diff --git a/fixtures/hasura/app/metadata/NestedCollection.hml b/fixtures/hasura/app/metadata/NestedCollection.hml index 4923afb9..880803e3 100644 --- a/fixtures/hasura/app/metadata/NestedCollection.hml +++ b/fixtures/hasura/app/metadata/NestedCollection.hml @@ -31,7 +31,7 @@ definition: name: NestedCollection fields: - name: id - type: ObjectId_2! + type: ObjectId! - name: institution type: String! - name: staff @@ -95,7 +95,7 @@ definition: type: NestedCollection comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_2 + booleanExpressionType: ObjectIdBoolExp - fieldName: institution booleanExpressionType: StringBoolExp - fieldName: staff @@ -118,7 +118,7 @@ definition: aggregatedType: NestedCollection aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_2 + aggregateExpression: ObjectIdAggExp - fieldName: institution aggregateExpression: StringAggExp count: diff --git a/fixtures/hasura/app/metadata/NestedFieldWithDollar.hml b/fixtures/hasura/app/metadata/NestedFieldWithDollar.hml index b1ca6f75..b02d7b9e 100644 --- a/fixtures/hasura/app/metadata/NestedFieldWithDollar.hml +++ b/fixtures/hasura/app/metadata/NestedFieldWithDollar.hml @@ -35,7 +35,7 @@ definition: name: NestedFieldWithDollar fields: - name: id - type: ObjectId_2! + type: ObjectId! - name: configuration type: NestedFieldWithDollarConfiguration! graphql: @@ -93,7 +93,7 @@ definition: type: NestedFieldWithDollar comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_2 + booleanExpressionType: ObjectIdBoolExp - fieldName: configuration booleanExpressionType: NestedFieldWithDollarConfigurationBoolExp comparableRelationships: [] @@ -114,7 +114,7 @@ definition: aggregatedType: NestedFieldWithDollar aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_2 + aggregateExpression: ObjectIdAggExp count: enable: true graphql: diff --git a/fixtures/hasura/app/metadata/Playlist.hml b/fixtures/hasura/app/metadata/Playlist.hml index 3fcf6bea..dd966838 100644 --- a/fixtures/hasura/app/metadata/Playlist.hml +++ b/fixtures/hasura/app/metadata/Playlist.hml @@ -5,7 +5,7 @@ definition: name: Playlist fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: name type: String! - name: playlistId @@ -50,7 +50,7 @@ definition: type: Playlist comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: name booleanExpressionType: StringBoolExp - fieldName: playlistId @@ -74,7 +74,7 @@ definition: aggregatedType: Playlist aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: name aggregateExpression: StringAggExp - fieldName: playlistId diff --git a/fixtures/hasura/app/metadata/PlaylistTrack.hml b/fixtures/hasura/app/metadata/PlaylistTrack.hml index 02c4d289..973388d8 100644 --- a/fixtures/hasura/app/metadata/PlaylistTrack.hml +++ b/fixtures/hasura/app/metadata/PlaylistTrack.hml @@ -5,7 +5,7 @@ definition: name: PlaylistTrack fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: playlistId type: Int! - name: trackId @@ -50,7 +50,7 @@ definition: type: PlaylistTrack comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: playlistId booleanExpressionType: IntBoolExp - fieldName: trackId @@ -75,7 +75,7 @@ definition: aggregatedType: PlaylistTrack aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: playlistId aggregateExpression: IntAggExp - fieldName: trackId diff --git a/fixtures/hasura/app/metadata/Schools.hml b/fixtures/hasura/app/metadata/Schools.hml new file mode 100644 index 00000000..8f5e624a --- /dev/null +++ b/fixtures/hasura/app/metadata/Schools.hml @@ -0,0 +1,210 @@ +--- +kind: ObjectType +version: v1 +definition: + name: SchoolsDepartments + fields: + - name: englishDepartmentId + type: ObjectId! + - name: mathDepartmentId + type: ObjectId! + graphql: + typeName: SchoolsDepartments + inputTypeName: SchoolsDepartmentsInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: schools_departments + fieldMapping: + englishDepartmentId: + column: + name: english_department_id + mathDepartmentId: + column: + name: math_department_id + +--- +kind: TypePermissions +version: v1 +definition: + typeName: SchoolsDepartments + permissions: + - role: admin + output: + allowedFields: + - englishDepartmentId + - mathDepartmentId + +--- +kind: ObjectType +version: v1 +definition: + name: Schools + fields: + - name: id + type: ObjectId! + - name: departments + type: SchoolsDepartments! + - name: name + type: String! + graphql: + typeName: Schools + inputTypeName: SchoolsInput + dataConnectorTypeMapping: + - dataConnectorName: test_cases + dataConnectorObjectType: schools + fieldMapping: + id: + column: + name: _id + departments: + column: + name: departments + name: + column: + name: name + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Schools + permissions: + - role: admin + output: + allowedFields: + - id + - departments + - name + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: SchoolsDepartmentsBoolExp + operand: + object: + type: SchoolsDepartments + comparableFields: + - fieldName: englishDepartmentId + booleanExpressionType: ObjectIdBoolExp + - fieldName: mathDepartmentId + booleanExpressionType: ObjectIdBoolExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: SchoolsDepartmentsBoolExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: SchoolsBoolExp + operand: + object: + type: Schools + comparableFields: + - fieldName: id + booleanExpressionType: ObjectIdBoolExp + - fieldName: departments + booleanExpressionType: SchoolsDepartmentsBoolExp + - fieldName: name + booleanExpressionType: StringBoolExp + comparableRelationships: [] + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: SchoolsBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: SchoolsDepartmentsAggExp + operand: + object: + aggregatedType: SchoolsDepartments + aggregatableFields: + - fieldName: englishDepartmentId + aggregateExpression: ObjectIdAggExp + - fieldName: mathDepartmentId + aggregateExpression: ObjectIdAggExp + count: + enable: true + graphql: + selectTypeName: SchoolsDepartmentsAggExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: SchoolsAggExp + operand: + object: + aggregatedType: Schools + aggregatableFields: + - fieldName: id + aggregateExpression: ObjectIdAggExp + - fieldName: departments + aggregateExpression: SchoolsDepartmentsAggExp + - fieldName: name + aggregateExpression: StringAggExp + count: + enable: true + graphql: + selectTypeName: SchoolsAggExp + +--- +kind: Model +version: v1 +definition: + name: Schools + objectType: Schools + source: + dataConnectorName: test_cases + collection: schools + filterExpressionType: SchoolsBoolExp + aggregateExpression: SchoolsAggExp + orderableFields: + - fieldName: id + orderByDirections: + enableAll: true + - fieldName: departments + orderByDirections: + enableAll: true + - fieldName: name + orderByDirections: + enableAll: true + graphql: + selectMany: + queryRootField: schools + subscription: + rootField: schools + selectUniques: + - queryRootField: schoolsById + uniqueIdentifier: + - id + subscription: + rootField: schoolsById + orderByExpressionType: SchoolsOrderBy + filterInputTypeName: SchoolsFilterInput + aggregate: + queryRootField: schoolsAggregate + subscription: + rootField: schoolsAggregate + +--- +kind: ModelPermissions +version: v1 +definition: + modelName: Schools + permissions: + - role: admin + select: + filter: null + allowSubscriptions: true + diff --git a/fixtures/hasura/app/metadata/Track.hml b/fixtures/hasura/app/metadata/Track.hml index b29ed569..f3a84064 100644 --- a/fixtures/hasura/app/metadata/Track.hml +++ b/fixtures/hasura/app/metadata/Track.hml @@ -5,7 +5,7 @@ definition: name: Track fields: - name: id - type: ObjectId_1! + type: ObjectId! - name: albumId type: Int! - name: bytes @@ -92,7 +92,7 @@ definition: type: Track comparableFields: - fieldName: id - booleanExpressionType: ObjectIdBoolExp_1 + booleanExpressionType: ObjectIdBoolExp - fieldName: albumId booleanExpressionType: IntBoolExp - fieldName: bytes @@ -134,7 +134,7 @@ definition: aggregatedType: Track aggregatableFields: - fieldName: id - aggregateExpression: ObjectIdAggExp_1 + aggregateExpression: ObjectIdAggExp - fieldName: albumId aggregateExpression: IntAggExp - fieldName: bytes diff --git a/fixtures/hasura/app/metadata/WeirdFieldNames.hml b/fixtures/hasura/app/metadata/WeirdFieldNames.hml index 03d33ac1..784959b7 100644 --- a/fixtures/hasura/app/metadata/WeirdFieldNames.hml +++ b/fixtures/hasura/app/metadata/WeirdFieldNames.hml @@ -101,7 +101,7 @@ definition: - name: invalidObjectName type: WeirdFieldNamesInvalidObjectName! - name: id - type: ObjectId_2! + type: ObjectId! - name: validObjectName type: WeirdFieldNamesValidObjectName! graphql: @@ -215,7 +215,7 @@ definition: - fieldName: invalidObjectName booleanExpressionType: WeirdFieldNamesInvalidObjectNameBoolExp - fieldName: id - booleanExpressionType: ObjectIdBoolExp_2 + booleanExpressionType: ObjectIdBoolExp - fieldName: validObjectName booleanExpressionType: WeirdFieldNamesValidObjectNameBoolExp comparableRelationships: [] @@ -238,7 +238,7 @@ definition: - fieldName: invalidName aggregateExpression: IntAggExp - fieldName: id - aggregateExpression: ObjectIdAggExp_2 + aggregateExpression: ObjectIdAggExp count: enable: true graphql: diff --git a/fixtures/hasura/app/metadata/chinook.hml b/fixtures/hasura/app/metadata/chinook.hml index a23c4937..1175ffaf 100644 --- a/fixtures/hasura/app/metadata/chinook.hml +++ b/fixtures/hasura/app/metadata/chinook.hml @@ -9,12 +9,36 @@ definition: write: valueFromEnv: APP_CHINOOK_WRITE_URL schema: - version: v0.1 + version: v0.2 + capabilities: + version: 0.2.0 + capabilities: + query: + aggregates: {} + variables: {} + explain: {} + nested_fields: + filter_by: + nested_arrays: + contains: {} + is_empty: {} + order_by: {} + aggregates: {} + nested_collections: {} + exists: + unrelated: {} + nested_collections: {} + mutation: {} + relationships: + relation_comparisons: {} schema: scalar_types: BinData: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -40,6 +64,7 @@ definition: type: boolean aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -65,46 +90,27 @@ definition: type: timestamp aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Date + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Date + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Date + type: less_than _lte: - type: custom - argument_type: - type: named - name: Date + type: less_than_or_equal _neq: type: custom argument_type: @@ -118,8 +124,11 @@ definition: type: named name: Date DbPointer: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -145,58 +154,33 @@ definition: type: bigdecimal aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Decimal + type: less_than _lte: - type: custom - argument_type: - type: named - name: Decimal + type: less_than_or_equal _neq: type: custom argument_type: @@ -214,58 +198,33 @@ definition: type: float64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Double + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Double + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Double + type: less_than _lte: - type: custom - argument_type: - type: named - name: Double + type: less_than_or_equal _neq: type: custom argument_type: @@ -283,22 +242,21 @@ definition: type: json aggregate_functions: avg: + type: custom result_type: type: named name: ExtendedJSON count: + type: custom result_type: type: named name: Int max: - result_type: - type: named - name: ExtendedJSON + type: max min: - result_type: - type: named - name: ExtendedJSON + type: min sum: + type: custom result_type: type: named name: ExtendedJSON @@ -306,35 +264,20 @@ definition: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than _gte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than_or_equal _in: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than _lte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than_or_equal _neq: type: custom argument_type: @@ -343,70 +286,47 @@ definition: _nin: type: custom argument_type: - type: named - name: ExtendedJSON + type: array + element_type: + type: named + name: ExtendedJSON _regex: type: custom argument_type: type: named - name: String + name: Regex Int: representation: type: int32 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Int + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Int + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Int + type: less_than _lte: - type: custom - argument_type: - type: named - name: Int + type: less_than_or_equal _neq: type: custom argument_type: @@ -420,15 +340,21 @@ definition: type: named name: Int Javascript: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int comparison_operators: {} JavascriptWithScope: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -438,58 +364,33 @@ definition: type: int64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Long + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Long + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Long + type: less_than _lte: - type: custom - argument_type: - type: named - name: Long + type: less_than_or_equal _neq: type: custom argument_type: @@ -503,8 +404,11 @@ definition: type: named name: Long MaxKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -526,8 +430,11 @@ definition: type: named name: MaxKey MinKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -548,9 +455,12 @@ definition: element_type: type: named name: MinKey - "Null": + 'Null': + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -563,19 +473,20 @@ definition: type: custom argument_type: type: named - name: "Null" + name: 'Null' _nin: type: custom argument_type: type: array element_type: type: named - name: "Null" + name: 'Null' ObjectId: representation: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -597,8 +508,11 @@ definition: type: named name: ObjectId Regex: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -608,51 +522,32 @@ definition: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: String + type: greater_than _gte: - type: custom - argument_type: - type: named - name: String + type: greater_than_or_equal _in: type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: String + type: less_than _lte: - type: custom - argument_type: - type: named - name: String + type: less_than_or_equal _neq: type: custom argument_type: @@ -669,10 +564,13 @@ definition: type: custom argument_type: type: named - name: String + name: Regex Symbol: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -694,48 +592,31 @@ definition: type: named name: Symbol Timestamp: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than _lte: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than_or_equal _neq: type: custom argument_type: @@ -749,8 +630,11 @@ definition: type: named name: Timestamp Undefined: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -774,10 +658,6 @@ definition: object_types: Album: fields: - _id: - type: - type: named - name: ObjectId AlbumId: type: type: named @@ -790,12 +670,13 @@ definition: type: type: named name: String - AlbumWithTracks: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + AlbumWithTracks: + fields: Title: type: type: named @@ -806,12 +687,13 @@ definition: element_type: type: named name: Track - Artist: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Artist: + fields: ArtistId: type: type: named @@ -820,12 +702,13 @@ definition: type: type: named name: String - ArtistWithAlbumsAndTracks: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + ArtistWithAlbumsAndTracks: + fields: Albums: type: type: array @@ -836,12 +719,13 @@ definition: type: type: named name: String - Customer: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Customer: + fields: Address: type: type: named @@ -904,12 +788,13 @@ definition: type: type: named name: Int - Employee: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Employee: + fields: Address: type: type: named @@ -972,12 +857,13 @@ definition: type: type: named name: String - Genre: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Genre: + fields: GenreId: type: type: named @@ -986,9 +872,14 @@ definition: type: type: named name: String + _id: + type: + type: named + name: ObjectId + foreign_keys: {} InsertArtist: fields: - "n": + n: type: type: named name: Int @@ -996,12 +887,9 @@ definition: type: type: named name: Double + foreign_keys: {} Invoice: fields: - _id: - type: - type: named - name: ObjectId BillingAddress: type: type: named @@ -1042,12 +930,13 @@ definition: type: type: named name: Decimal - InvoiceLine: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + InvoiceLine: + fields: InvoiceId: type: type: named @@ -1068,12 +957,13 @@ definition: type: type: named name: Decimal - MediaType: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + MediaType: + fields: MediaTypeId: type: type: named @@ -1082,12 +972,13 @@ definition: type: type: named name: String - Playlist: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Playlist: + fields: Name: type: type: named @@ -1096,12 +987,13 @@ definition: type: type: named name: Int - PlaylistTrack: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + PlaylistTrack: + fields: PlaylistId: type: type: named @@ -1110,12 +1002,13 @@ definition: type: type: named name: Int - Track: - fields: _id: type: type: named name: ObjectId + foreign_keys: {} + Track: + fields: AlbumId: type: type: named @@ -1154,147 +1047,128 @@ definition: type: type: named name: Decimal - collections: - - name: Album - arguments: {} - type: Album - uniqueness_constraints: - Album_id: - unique_columns: - - _id - foreign_keys: {} - - name: Artist - arguments: {} - type: Artist - uniqueness_constraints: - Artist_id: - unique_columns: - - _id - foreign_keys: {} - - name: Customer - arguments: {} - type: Customer - uniqueness_constraints: - Customer_id: - unique_columns: - - _id - foreign_keys: {} - - name: Employee - arguments: {} - type: Employee - uniqueness_constraints: - Employee_id: - unique_columns: - - _id - foreign_keys: {} - - name: Genre - arguments: {} - type: Genre - uniqueness_constraints: - Genre_id: - unique_columns: - - _id - foreign_keys: {} - - name: Invoice - arguments: {} - type: Invoice - uniqueness_constraints: - Invoice_id: - unique_columns: - - _id - foreign_keys: {} - - name: InvoiceLine - arguments: {} - type: InvoiceLine - uniqueness_constraints: - InvoiceLine_id: - unique_columns: - - _id - foreign_keys: {} - - name: MediaType - arguments: {} - type: MediaType - uniqueness_constraints: - MediaType_id: - unique_columns: - - _id - foreign_keys: {} - - name: Playlist - arguments: {} - type: Playlist - uniqueness_constraints: - Playlist_id: - unique_columns: - - _id - foreign_keys: {} - - name: PlaylistTrack - arguments: {} - type: PlaylistTrack - uniqueness_constraints: - PlaylistTrack_id: - unique_columns: - - _id - foreign_keys: {} - - name: Track - arguments: {} - type: Track - uniqueness_constraints: - Track_id: - unique_columns: - - _id - foreign_keys: {} - - name: artists_with_albums_and_tracks - description: combines artist, albums, and tracks into a single document per artist - arguments: {} - type: ArtistWithAlbumsAndTracks - uniqueness_constraints: - artists_with_albums_and_tracks_id: - unique_columns: - - _id + _id: + type: + type: named + name: ObjectId foreign_keys: {} + collections: + - name: Album + arguments: {} + type: Album + uniqueness_constraints: + Album_id: + unique_columns: + - _id + - name: Artist + arguments: {} + type: Artist + uniqueness_constraints: + Artist_id: + unique_columns: + - _id + - name: Customer + arguments: {} + type: Customer + uniqueness_constraints: + Customer_id: + unique_columns: + - _id + - name: Employee + arguments: {} + type: Employee + uniqueness_constraints: + Employee_id: + unique_columns: + - _id + - name: Genre + arguments: {} + type: Genre + uniqueness_constraints: + Genre_id: + unique_columns: + - _id + - name: Invoice + arguments: {} + type: Invoice + uniqueness_constraints: + Invoice_id: + unique_columns: + - _id + - name: InvoiceLine + arguments: {} + type: InvoiceLine + uniqueness_constraints: + InvoiceLine_id: + unique_columns: + - _id + - name: MediaType + arguments: {} + type: MediaType + uniqueness_constraints: + MediaType_id: + unique_columns: + - _id + - name: Playlist + arguments: {} + type: Playlist + uniqueness_constraints: + Playlist_id: + unique_columns: + - _id + - name: PlaylistTrack + arguments: {} + type: PlaylistTrack + uniqueness_constraints: + PlaylistTrack_id: + unique_columns: + - _id + - name: Track + arguments: {} + type: Track + uniqueness_constraints: + Track_id: + unique_columns: + - _id + - name: artists_with_albums_and_tracks + description: combines artist, albums, and tracks into a single document per artist + arguments: {} + type: ArtistWithAlbumsAndTracks + uniqueness_constraints: + artists_with_albums_and_tracks_id: + unique_columns: + - _id functions: [] procedures: - - name: insertArtist - description: Example of a database update using a native mutation - arguments: - id: - type: - type: named - name: Int - name: - type: - type: named - name: String - result_type: - type: named - name: InsertArtist - - name: updateTrackPrices - description: Update unit price of every track that matches predicate - arguments: - newPrice: - type: - type: named - name: Decimal - where: - type: - type: predicate - object_type_name: Track - result_type: - type: named - name: InsertArtist - capabilities: - version: 0.1.6 + - name: insertArtist + description: Example of a database update using a native mutation + arguments: + id: + type: + type: named + name: Int + name: + type: + type: named + name: String + result_type: + type: named + name: InsertArtist + - name: updateTrackPrices + description: Update unit price of every track that matches predicate + arguments: + newPrice: + type: + type: named + name: Decimal + where: + type: + type: predicate + object_type_name: Track + result_type: + type: named + name: InsertArtist capabilities: query: - aggregates: {} - variables: {} - explain: {} - nested_fields: - filter_by: {} - order_by: {} - aggregates: {} - exists: - nested_collections: {} - mutation: {} - relationships: - relation_comparisons: {} + aggregates: + count_scalar_type: Int diff --git a/fixtures/hasura/app/metadata/sample_mflix-types.hml b/fixtures/hasura/app/metadata/sample_mflix-types.hml deleted file mode 100644 index 0675e1a7..00000000 --- a/fixtures/hasura/app/metadata/sample_mflix-types.hml +++ /dev/null @@ -1,601 +0,0 @@ ---- -kind: ScalarType -version: v1 -definition: - name: ObjectId - graphql: - typeName: ObjectId - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: ObjectIdBoolExp - operand: - scalar: - type: ObjectId - comparisonOperators: - - name: _eq - argumentType: ObjectId! - - name: _in - argumentType: "[ObjectId!]!" - - name: _neq - argumentType: ObjectId! - - name: _nin - argumentType: "[ObjectId!]!" - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: ObjectId - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: ObjectIdBoolExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: ObjectId - representation: ObjectId - graphql: - comparisonExpressionTypeName: ObjectIdComparisonExp - ---- -kind: ScalarType -version: v1 -definition: - name: Date - graphql: - typeName: Date - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: DateBoolExp - operand: - scalar: - type: Date - comparisonOperators: - - name: _eq - argumentType: Date! - - name: _gt - argumentType: Date! - - name: _gte - argumentType: Date! - - name: _in - argumentType: "[Date!]!" - - name: _lt - argumentType: Date! - - name: _lte - argumentType: Date! - - name: _neq - argumentType: Date! - - name: _nin - argumentType: "[Date!]!" - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Date - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: DateBoolExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: Date - representation: Date - graphql: - comparisonExpressionTypeName: DateComparisonExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: String - representation: String - graphql: - comparisonExpressionTypeName: StringComparisonExp - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: StringBoolExp - operand: - scalar: - type: String - comparisonOperators: - - name: _eq - argumentType: String! - - name: _gt - argumentType: String! - - name: _gte - argumentType: String! - - name: _in - argumentType: "[String!]!" - - name: _iregex - argumentType: String! - - name: _lt - argumentType: String! - - name: _lte - argumentType: String! - - name: _neq - argumentType: String! - - name: _nin - argumentType: "[String!]!" - - name: _regex - argumentType: String! - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: String - operatorMapping: {} - - dataConnectorName: chinook - dataConnectorScalarType: String - operatorMapping: {} - - dataConnectorName: test_cases - dataConnectorScalarType: String - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: StringBoolExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: Int - representation: Int - graphql: - comparisonExpressionTypeName: IntComparisonExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: ObjectIdAggExp - operand: - scalar: - aggregatedType: ObjectId - aggregationFunctions: - - name: count - returnType: Int! - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: ObjectId - functionMapping: - count: - name: count - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: ObjectIdAggExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: DateAggExp - operand: - scalar: - aggregatedType: Date - aggregationFunctions: - - name: count - returnType: Int! - - name: max - returnType: Date - - name: min - returnType: Date - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Date - functionMapping: - count: - name: count - max: - name: max - min: - name: min - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: DateAggExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: StringAggExp - operand: - scalar: - aggregatedType: String - aggregationFunctions: - - name: count - returnType: Int! - - name: max - returnType: String - - name: min - returnType: String - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: String - functionMapping: - count: - name: count - max: - name: max - min: - name: min - - dataConnectorName: chinook - dataConnectorScalarType: String - functionMapping: - count: - name: count - max: - name: max - min: - name: min - - dataConnectorName: test_cases - dataConnectorScalarType: String - functionMapping: - count: - name: count - max: - name: max - min: - name: min - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: StringAggExp - ---- -kind: ScalarType -version: v1 -definition: - name: Double - graphql: - typeName: Double - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: DoubleBoolExp - operand: - scalar: - type: Double - comparisonOperators: - - name: _eq - argumentType: Double! - - name: _gt - argumentType: Double! - - name: _gte - argumentType: Double! - - name: _in - argumentType: "[Double!]!" - - name: _lt - argumentType: Double! - - name: _lte - argumentType: Double! - - name: _neq - argumentType: Double! - - name: _nin - argumentType: "[Double!]!" - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Double - operatorMapping: {} - - dataConnectorName: chinook - dataConnectorScalarType: Double - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: DoubleBoolExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: DoubleAggExp - operand: - scalar: - aggregatedType: Double - aggregationFunctions: - - name: avg - returnType: Double - - name: count - returnType: Int! - - name: max - returnType: Double - - name: min - returnType: Double - - name: sum - returnType: Double - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Double - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - - dataConnectorName: chinook - dataConnectorScalarType: Double - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - - dataConnectorName: test_cases - dataConnectorScalarType: Double - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: DoubleAggExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: Double - representation: Double - graphql: - comparisonExpressionTypeName: DoubleComparisonExp - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: IntBoolExp - operand: - scalar: - type: Int - comparisonOperators: - - name: _eq - argumentType: Int! - - name: _gt - argumentType: Int! - - name: _gte - argumentType: Int! - - name: _in - argumentType: "[Int!]!" - - name: _lt - argumentType: Int! - - name: _lte - argumentType: Int! - - name: _neq - argumentType: Int! - - name: _nin - argumentType: "[Int!]!" - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Int - operatorMapping: {} - - dataConnectorName: chinook - dataConnectorScalarType: Int - operatorMapping: {} - - dataConnectorName: test_cases - dataConnectorScalarType: Int - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: IntBoolExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: IntAggExp - operand: - scalar: - aggregatedType: Int - aggregationFunctions: - - name: avg - returnType: Int - - name: count - returnType: Int! - - name: max - returnType: Int - - name: min - returnType: Int - - name: sum - returnType: Int - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: Int - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - - dataConnectorName: chinook - dataConnectorScalarType: Int - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - - dataConnectorName: test_cases - dataConnectorScalarType: Int - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: IntAggExp - ---- -kind: ScalarType -version: v1 -definition: - name: ExtendedJson - graphql: - typeName: ExtendedJson - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: ExtendedJsonBoolExp - operand: - scalar: - type: ExtendedJson - comparisonOperators: - - name: _eq - argumentType: ExtendedJson! - - name: _gt - argumentType: ExtendedJson! - - name: _gte - argumentType: ExtendedJson! - - name: _in - argumentType: ExtendedJson! - - name: _iregex - argumentType: String! - - name: _lt - argumentType: ExtendedJson! - - name: _lte - argumentType: ExtendedJson! - - name: _neq - argumentType: ExtendedJson! - - name: _nin - argumentType: ExtendedJson! - - name: _regex - argumentType: String! - dataConnectorOperatorMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: ExtendedJSON - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: ExtendedJsonBoolExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: sample_mflix - dataConnectorScalarType: ExtendedJSON - representation: ExtendedJson - graphql: - comparisonExpressionTypeName: ExtendedJsonComparisonExp - ---- -kind: AggregateExpression -version: v1 -definition: - name: ExtendedJsonAggExp - operand: - scalar: - aggregatedType: ExtendedJson - aggregationFunctions: - - name: avg - returnType: ExtendedJson! - - name: count - returnType: Int! - - name: max - returnType: ExtendedJson! - - name: min - returnType: ExtendedJson! - - name: sum - returnType: ExtendedJson! - dataConnectorAggregationFunctionMapping: - - dataConnectorName: sample_mflix - dataConnectorScalarType: ExtendedJSON - functionMapping: - avg: - name: avg - count: - name: count - max: - name: max - min: - name: min - sum: - name: sum - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: ExtendedJsonAggExp - diff --git a/fixtures/hasura/app/metadata/sample_mflix.hml b/fixtures/hasura/app/metadata/sample_mflix.hml index e5cd1f4c..b49a9f0f 100644 --- a/fixtures/hasura/app/metadata/sample_mflix.hml +++ b/fixtures/hasura/app/metadata/sample_mflix.hml @@ -9,12 +9,36 @@ definition: write: valueFromEnv: APP_SAMPLE_MFLIX_WRITE_URL schema: - version: v0.1 + version: v0.2 + capabilities: + version: 0.2.0 + capabilities: + query: + aggregates: {} + variables: {} + explain: {} + nested_fields: + filter_by: + nested_arrays: + contains: {} + is_empty: {} + order_by: {} + aggregates: {} + nested_collections: {} + exists: + unrelated: {} + nested_collections: {} + mutation: {} + relationships: + relation_comparisons: {} schema: scalar_types: BinData: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -40,6 +64,7 @@ definition: type: boolean aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -65,46 +90,27 @@ definition: type: timestamp aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Date + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Date + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Date + type: less_than _lte: - type: custom - argument_type: - type: named - name: Date + type: less_than_or_equal _neq: type: custom argument_type: @@ -118,8 +124,11 @@ definition: type: named name: Date DbPointer: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -145,58 +154,33 @@ definition: type: bigdecimal aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Decimal + type: less_than _lte: - type: custom - argument_type: - type: named - name: Decimal + type: less_than_or_equal _neq: type: custom argument_type: @@ -214,58 +198,33 @@ definition: type: float64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Double + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Double + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Double + type: less_than _lte: - type: custom - argument_type: - type: named - name: Double + type: less_than_or_equal _neq: type: custom argument_type: @@ -283,22 +242,21 @@ definition: type: json aggregate_functions: avg: + type: custom result_type: type: named name: ExtendedJSON count: + type: custom result_type: type: named name: Int max: - result_type: - type: named - name: ExtendedJSON + type: max min: - result_type: - type: named - name: ExtendedJSON + type: min sum: + type: custom result_type: type: named name: ExtendedJSON @@ -306,35 +264,20 @@ definition: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than _gte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than_or_equal _in: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than _lte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than_or_equal _neq: type: custom argument_type: @@ -343,70 +286,47 @@ definition: _nin: type: custom argument_type: - type: named - name: ExtendedJSON + type: array + element_type: + type: named + name: ExtendedJSON _regex: type: custom argument_type: type: named - name: String + name: Regex Int: representation: type: int32 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Int + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Int + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Int + type: less_than _lte: - type: custom - argument_type: - type: named - name: Int + type: less_than_or_equal _neq: type: custom argument_type: @@ -420,15 +340,21 @@ definition: type: named name: Int Javascript: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int comparison_operators: {} JavascriptWithScope: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -438,58 +364,33 @@ definition: type: int64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Long + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Long + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Long + type: less_than _lte: - type: custom - argument_type: - type: named - name: Long + type: less_than_or_equal _neq: type: custom argument_type: @@ -503,8 +404,11 @@ definition: type: named name: Long MaxKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -526,8 +430,11 @@ definition: type: named name: MaxKey MinKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -548,9 +455,12 @@ definition: element_type: type: named name: MinKey - "Null": + 'Null': + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -563,19 +473,20 @@ definition: type: custom argument_type: type: named - name: "Null" + name: 'Null' _nin: type: custom argument_type: type: array element_type: type: named - name: "Null" + name: 'Null' ObjectId: representation: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -597,8 +508,11 @@ definition: type: named name: ObjectId Regex: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -608,51 +522,32 @@ definition: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: String + type: greater_than _gte: - type: custom - argument_type: - type: named - name: String + type: greater_than_or_equal _in: type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: String + type: less_than _lte: - type: custom - argument_type: - type: named - name: String + type: less_than_or_equal _neq: type: custom argument_type: @@ -669,10 +564,13 @@ definition: type: custom argument_type: type: named - name: String + name: Regex Symbol: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -694,48 +592,31 @@ definition: type: named name: Symbol Timestamp: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than _lte: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than_or_equal _neq: type: custom argument_type: @@ -749,8 +630,11 @@ definition: type: named name: Timestamp Undefined: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -784,12 +668,14 @@ definition: underlying_type: type: named name: ExtendedJSON + foreign_keys: {} Hello: fields: __value: type: type: named name: String + foreign_keys: {} comments: fields: _id: @@ -816,6 +702,7 @@ definition: type: type: named name: String + foreign_keys: {} eq_title_project: fields: _id: @@ -844,12 +731,14 @@ definition: type: type: named name: eq_title_project_what + foreign_keys: {} eq_title_project_bar: fields: foo: type: type: named name: movies_imdb + foreign_keys: {} eq_title_project_foo: fields: bar: @@ -858,18 +747,21 @@ definition: underlying_type: type: named name: movies_tomatoes_critic + foreign_keys: {} eq_title_project_what: fields: the: type: type: named name: eq_title_project_what_the + foreign_keys: {} eq_title_project_what_the: fields: heck: type: type: named name: String + foreign_keys: {} movies: fields: _id: @@ -1000,6 +892,7 @@ definition: type: type: named name: Int + foreign_keys: {} movies_awards: fields: nominations: @@ -1014,6 +907,7 @@ definition: type: type: named name: Int + foreign_keys: {} movies_imdb: fields: id: @@ -1028,6 +922,7 @@ definition: type: type: named name: Int + foreign_keys: {} movies_tomatoes: fields: boxOffice: @@ -1086,6 +981,7 @@ definition: underlying_type: type: named name: String + foreign_keys: {} movies_tomatoes_critic: fields: meter: @@ -1104,6 +1000,7 @@ definition: underlying_type: type: named name: Double + foreign_keys: {} movies_tomatoes_viewer: fields: meter: @@ -1122,6 +1019,7 @@ definition: underlying_type: type: named name: Double + foreign_keys: {} native_query_project: fields: _id: @@ -1150,12 +1048,14 @@ definition: type: type: named name: native_query_project_what + foreign_keys: {} native_query_project_bar: fields: foo: type: type: named name: movies_imdb + foreign_keys: {} native_query_project_foo: fields: bar: @@ -1164,18 +1064,21 @@ definition: underlying_type: type: named name: movies_tomatoes_critic + foreign_keys: {} native_query_project_what: fields: the: type: type: named name: native_query_project_what_the + foreign_keys: {} native_query_project_what_the: fields: heck: type: type: named name: String + foreign_keys: {} sessions: fields: _id: @@ -1190,6 +1093,7 @@ definition: type: type: named name: String + foreign_keys: {} theaters: fields: _id: @@ -1204,6 +1108,7 @@ definition: type: type: named name: Int + foreign_keys: {} theaters_location: fields: address: @@ -1214,6 +1119,7 @@ definition: type: type: named name: theaters_location_geo + foreign_keys: {} theaters_location_address: fields: city: @@ -1238,6 +1144,7 @@ definition: type: type: named name: String + foreign_keys: {} theaters_location_geo: fields: coordinates: @@ -1250,6 +1157,7 @@ definition: type: type: named name: String + foreign_keys: {} title_word_frequency_group: fields: _id: @@ -1260,6 +1168,7 @@ definition: type: type: named name: Int + foreign_keys: {} users: fields: _id: @@ -1284,116 +1193,97 @@ definition: underlying_type: type: named name: users_preferences + foreign_keys: {} users_preferences: fields: {} - collections: - - name: comments - arguments: {} - type: comments - uniqueness_constraints: - comments_id: - unique_columns: - - _id - foreign_keys: {} - - name: eq_title - arguments: - title: - type: - type: named - name: String - year: - type: - type: named - name: Int - type: eq_title_project - uniqueness_constraints: - eq_title_id: - unique_columns: - - _id - foreign_keys: {} - - name: extended_json_test_data - description: various values that all have the ExtendedJSON type - arguments: {} - type: DocWithExtendedJsonValue - uniqueness_constraints: {} - foreign_keys: {} - - name: movies - arguments: {} - type: movies - uniqueness_constraints: - movies_id: - unique_columns: - - _id - foreign_keys: {} - - name: native_query - arguments: - title: - type: - type: named - name: String - type: native_query_project - uniqueness_constraints: - native_query_id: - unique_columns: - - _id - foreign_keys: {} - - name: sessions - arguments: {} - type: sessions - uniqueness_constraints: - sessions_id: - unique_columns: - - _id - foreign_keys: {} - - name: theaters - arguments: {} - type: theaters - uniqueness_constraints: - theaters_id: - unique_columns: - - _id - foreign_keys: {} - - name: title_word_frequency - arguments: {} - type: title_word_frequency_group - uniqueness_constraints: - title_word_frequency_id: - unique_columns: - - _id - foreign_keys: {} - - name: users - arguments: {} - type: users - uniqueness_constraints: - users_id: - unique_columns: - - _id foreign_keys: {} + collections: + - name: comments + arguments: {} + type: comments + uniqueness_constraints: + comments_id: + unique_columns: + - _id + - name: eq_title + arguments: + title: + type: + type: named + name: String + year: + type: + type: named + name: Int + type: eq_title_project + uniqueness_constraints: + eq_title_id: + unique_columns: + - _id + - name: extended_json_test_data + description: various values that all have the ExtendedJSON type + arguments: {} + type: DocWithExtendedJsonValue + uniqueness_constraints: {} + - name: movies + arguments: {} + type: movies + uniqueness_constraints: + movies_id: + unique_columns: + - _id + - name: native_query + arguments: + title: + type: + type: named + name: String + type: native_query_project + uniqueness_constraints: + native_query_id: + unique_columns: + - _id + - name: sessions + arguments: {} + type: sessions + uniqueness_constraints: + sessions_id: + unique_columns: + - _id + - name: theaters + arguments: {} + type: theaters + uniqueness_constraints: + theaters_id: + unique_columns: + - _id + - name: title_word_frequency + arguments: {} + type: title_word_frequency_group + uniqueness_constraints: + title_word_frequency_id: + unique_columns: + - _id + - name: users + arguments: {} + type: users + uniqueness_constraints: + users_id: + unique_columns: + - _id functions: - - name: hello - description: Basic test of native queries - arguments: - name: - type: - type: named - name: String - result_type: - type: named - name: String + - name: hello + description: Basic test of native queries + arguments: + name: + type: + type: named + name: String + result_type: + type: named + name: String procedures: [] - capabilities: - version: 0.1.6 capabilities: query: - aggregates: {} - variables: {} - explain: {} - nested_fields: - filter_by: {} - order_by: {} - aggregates: {} - exists: - nested_collections: {} - mutation: {} - relationships: - relation_comparisons: {} + aggregates: + count_scalar_type: Int diff --git a/fixtures/hasura/app/metadata/test_cases-types.hml b/fixtures/hasura/app/metadata/test_cases-types.hml deleted file mode 100644 index 440117db..00000000 --- a/fixtures/hasura/app/metadata/test_cases-types.hml +++ /dev/null @@ -1,99 +0,0 @@ ---- -kind: ScalarType -version: v1 -definition: - name: ObjectId_2 - graphql: - typeName: ObjectId2 - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: ObjectIdBoolExp_2 - operand: - scalar: - type: ObjectId_2 - comparisonOperators: - - name: _eq - argumentType: ObjectId_2! - - name: _in - argumentType: "[ObjectId_2!]!" - - name: _neq - argumentType: ObjectId_2! - - name: _nin - argumentType: "[ObjectId_2!]!" - dataConnectorOperatorMapping: - - dataConnectorName: test_cases - dataConnectorScalarType: ObjectId - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: ObjectIdBoolExp2 - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: test_cases - dataConnectorScalarType: ObjectId - representation: ObjectId_2 - graphql: - comparisonExpressionTypeName: ObjectId2ComparisonExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: test_cases - dataConnectorScalarType: String - representation: String - graphql: - comparisonExpressionTypeName: StringComparisonExp_2 - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: test_cases - dataConnectorScalarType: Int - representation: Int - graphql: - comparisonExpressionTypeName: IntComparisonExp_2 - ---- -kind: AggregateExpression -version: v1 -definition: - name: ObjectIdAggExp_2 - operand: - scalar: - aggregatedType: ObjectId_2 - aggregationFunctions: - - name: count - returnType: Int! - dataConnectorAggregationFunctionMapping: - - dataConnectorName: test_cases - dataConnectorScalarType: ObjectId - functionMapping: - count: - name: count - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: ObjectIdAggExp2 - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: test_cases - dataConnectorScalarType: Double - representation: Double - graphql: - comparisonExpressionTypeName: DoubleComparisonExp diff --git a/fixtures/hasura/app/metadata/test_cases.hml b/fixtures/hasura/app/metadata/test_cases.hml index 8ade514b..eaf77cf0 100644 --- a/fixtures/hasura/app/metadata/test_cases.hml +++ b/fixtures/hasura/app/metadata/test_cases.hml @@ -9,12 +9,36 @@ definition: write: valueFromEnv: APP_TEST_CASES_WRITE_URL schema: - version: v0.1 + version: v0.2 + capabilities: + version: 0.2.0 + capabilities: + query: + aggregates: {} + variables: {} + explain: {} + nested_fields: + filter_by: + nested_arrays: + contains: {} + is_empty: {} + order_by: {} + aggregates: {} + nested_collections: {} + exists: + unrelated: {} + nested_collections: {} + mutation: {} + relationships: + relation_comparisons: {} schema: scalar_types: BinData: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -40,6 +64,7 @@ definition: type: boolean aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -65,46 +90,27 @@ definition: type: timestamp aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Date + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Date + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Date + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Date + type: less_than _lte: - type: custom - argument_type: - type: named - name: Date + type: less_than_or_equal _neq: type: custom argument_type: @@ -118,8 +124,11 @@ definition: type: named name: Date DbPointer: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -145,58 +154,33 @@ definition: type: bigdecimal aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Decimal + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Decimal + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Decimal + type: less_than _lte: - type: custom - argument_type: - type: named - name: Decimal + type: less_than_or_equal _neq: type: custom argument_type: @@ -214,58 +198,33 @@ definition: type: float64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Double + type: sum + result_type: Double comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Double + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Double + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Double + type: less_than _lte: - type: custom - argument_type: - type: named - name: Double + type: less_than_or_equal _neq: type: custom argument_type: @@ -283,22 +242,21 @@ definition: type: json aggregate_functions: avg: + type: custom result_type: type: named name: ExtendedJSON count: + type: custom result_type: type: named name: Int max: - result_type: - type: named - name: ExtendedJSON + type: max min: - result_type: - type: named - name: ExtendedJSON + type: min sum: + type: custom result_type: type: named name: ExtendedJSON @@ -306,35 +264,20 @@ definition: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than _gte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: greater_than_or_equal _in: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than _lte: - type: custom - argument_type: - type: named - name: ExtendedJSON + type: less_than_or_equal _neq: type: custom argument_type: @@ -343,70 +286,47 @@ definition: _nin: type: custom argument_type: - type: named - name: ExtendedJSON + type: array + element_type: + type: named + name: ExtendedJSON _regex: type: custom argument_type: type: named - name: String + name: Regex Int: representation: type: int32 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Int + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Int + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Int + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Int + type: less_than _lte: - type: custom - argument_type: - type: named - name: Int + type: less_than_or_equal _neq: type: custom argument_type: @@ -420,15 +340,21 @@ definition: type: named name: Int Javascript: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int comparison_operators: {} JavascriptWithScope: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -438,58 +364,33 @@ definition: type: int64 aggregate_functions: avg: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: average + result_type: Double count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: min sum: - result_type: - type: nullable - underlying_type: - type: named - name: Long + type: sum + result_type: Long comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Long + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Long + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Long + type: less_than _lte: - type: custom - argument_type: - type: named - name: Long + type: less_than_or_equal _neq: type: custom argument_type: @@ -503,8 +404,11 @@ definition: type: named name: Long MaxKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -526,8 +430,11 @@ definition: type: named name: MaxKey MinKey: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -548,9 +455,12 @@ definition: element_type: type: named name: MinKey - "Null": + 'Null': + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -563,19 +473,20 @@ definition: type: custom argument_type: type: named - name: "Null" + name: 'Null' _nin: type: custom argument_type: type: array element_type: type: named - name: "Null" + name: 'Null' ObjectId: representation: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -597,8 +508,11 @@ definition: type: named name: ObjectId Regex: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -608,51 +522,32 @@ definition: type: string aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: String + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: String + type: greater_than _gte: - type: custom - argument_type: - type: named - name: String + type: greater_than_or_equal _in: type: in _iregex: type: custom argument_type: type: named - name: String + name: Regex _lt: - type: custom - argument_type: - type: named - name: String + type: less_than _lte: - type: custom - argument_type: - type: named - name: String + type: less_than_or_equal _neq: type: custom argument_type: @@ -669,10 +564,13 @@ definition: type: custom argument_type: type: named - name: String + name: Regex Symbol: + representation: + type: string aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -694,48 +592,31 @@ definition: type: named name: Symbol Timestamp: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int max: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: max min: - result_type: - type: nullable - underlying_type: - type: named - name: Timestamp + type: min comparison_operators: _eq: type: equal _gt: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than _gte: - type: custom - argument_type: - type: named - name: Timestamp + type: greater_than_or_equal _in: type: in _lt: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than _lte: - type: custom - argument_type: - type: named - name: Timestamp + type: less_than_or_equal _neq: type: custom argument_type: @@ -749,8 +630,11 @@ definition: type: named name: Timestamp Undefined: + representation: + type: json aggregate_functions: count: + type: custom result_type: type: named name: Int @@ -772,6 +656,49 @@ definition: type: named name: Undefined object_types: + departments: + fields: + _id: + type: + type: named + name: ObjectId + description: + type: + type: named + name: String + foreign_keys: {} + schools: + fields: + _id: + type: + type: named + name: ObjectId + departments: + type: + type: named + name: schools_departments + name: + type: + type: named + name: String + foreign_keys: {} + schools_departments: + fields: + english_department_id: + type: + type: named + name: ObjectId + math_department_id: + type: + type: named + name: ObjectId + description: + type: + type: nullable + underlying_type: + type: named + name: String + foreign_keys: {} nested_collection: fields: _id: @@ -788,12 +715,14 @@ definition: element_type: type: named name: nested_collection_staff + foreign_keys: {} nested_collection_staff: fields: name: type: type: named name: String + foreign_keys: {} nested_field_with_dollar: fields: _id: @@ -804,6 +733,7 @@ definition: type: type: named name: nested_field_with_dollar_configuration + foreign_keys: {} nested_field_with_dollar_configuration: fields: $schema: @@ -812,6 +742,7 @@ definition: underlying_type: type: named name: String + foreign_keys: {} weird_field_names: fields: $invalid.array: @@ -836,64 +767,67 @@ definition: type: type: named name: weird_field_names_valid_object_name + foreign_keys: {} weird_field_names_$invalid.array: fields: $invalid.element: type: type: named name: Int + foreign_keys: {} weird_field_names_$invalid.object.name: fields: valid_name: type: type: named name: Int + foreign_keys: {} weird_field_names_valid_object_name: fields: $invalid.nested.name: type: type: named name: Int - collections: - - name: nested_collection - arguments: {} - type: nested_collection - uniqueness_constraints: - nested_collection_id: - unique_columns: - - _id - foreign_keys: {} - - name: nested_field_with_dollar - arguments: {} - type: nested_field_with_dollar - uniqueness_constraints: - nested_field_with_dollar_id: - unique_columns: - - _id - foreign_keys: {} - - name: weird_field_names - arguments: {} - type: weird_field_names - uniqueness_constraints: - weird_field_names_id: - unique_columns: - - _id foreign_keys: {} + collections: + - name: departments + arguments: {} + type: departments + uniqueness_constraints: + nested_field_with_dollar_id: + unique_columns: + - _id + - name: schools + arguments: {} + type: schools + uniqueness_constraints: + nested_field_with_dollar_id: + unique_columns: + - _id + - name: nested_collection + arguments: {} + type: nested_collection + uniqueness_constraints: + nested_collection_id: + unique_columns: + - _id + - name: nested_field_with_dollar + arguments: {} + type: nested_field_with_dollar + uniqueness_constraints: + nested_field_with_dollar_id: + unique_columns: + - _id + - name: weird_field_names + arguments: {} + type: weird_field_names + uniqueness_constraints: + weird_field_names_id: + unique_columns: + - _id functions: [] procedures: [] - capabilities: - version: 0.1.6 capabilities: query: - aggregates: {} - variables: {} - explain: {} - nested_fields: - filter_by: {} - order_by: {} - aggregates: {} - exists: - nested_collections: {} - mutation: {} - relationships: - relation_comparisons: {} + aggregates: + count_scalar_type: Int diff --git a/fixtures/hasura/app/metadata/types/date.hml b/fixtures/hasura/app/metadata/types/date.hml new file mode 100644 index 00000000..fc3cdceb --- /dev/null +++ b/fixtures/hasura/app/metadata/types/date.hml @@ -0,0 +1,85 @@ +--- +kind: ScalarType +version: v1 +definition: + name: Date + graphql: + typeName: Date + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: DateBoolExp + operand: + scalar: + type: Date + comparisonOperators: + - name: _eq + argumentType: Date! + - name: _gt + argumentType: Date! + - name: _gte + argumentType: Date! + - name: _in + argumentType: "[Date!]!" + - name: _lt + argumentType: Date! + - name: _lte + argumentType: Date! + - name: _neq + argumentType: Date! + - name: _nin + argumentType: "[Date!]!" + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Date + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: DateBoolExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: Date + representation: Date + graphql: + comparisonExpressionTypeName: DateComparisonExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: DateAggExp + operand: + scalar: + aggregatedType: Date + aggregationFunctions: + - name: count + returnType: Int! + - name: max + returnType: Date + - name: min + returnType: Date + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Date + functionMapping: + count: + name: count + max: + name: max + min: + name: min + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: DateAggExp diff --git a/fixtures/hasura/app/metadata/chinook-types.hml b/fixtures/hasura/app/metadata/types/decimal.hml similarity index 52% rename from fixtures/hasura/app/metadata/chinook-types.hml rename to fixtures/hasura/app/metadata/types/decimal.hml index ef109d7b..4a30e020 100644 --- a/fixtures/hasura/app/metadata/chinook-types.hml +++ b/fixtures/hasura/app/metadata/types/decimal.hml @@ -2,99 +2,39 @@ kind: ScalarType version: v1 definition: - name: ObjectId_1 - graphql: - typeName: ObjectId1 - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: ObjectIdBoolExp_1 - operand: - scalar: - type: ObjectId_1 - comparisonOperators: - - name: _eq - argumentType: ObjectId_1! - - name: _in - argumentType: "[ObjectId_1!]!" - - name: _neq - argumentType: ObjectId_1! - - name: _nin - argumentType: "[ObjectId_1!]!" - dataConnectorOperatorMapping: - - dataConnectorName: chinook - dataConnectorScalarType: ObjectId - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true + name: Decimal graphql: - typeName: ObjectIdBoolExp1 + typeName: Decimal --- kind: DataConnectorScalarRepresentation version: v1 definition: dataConnectorName: chinook - dataConnectorScalarType: ObjectId - representation: ObjectId_1 + dataConnectorScalarType: Decimal + representation: Decimal graphql: - comparisonExpressionTypeName: ObjectId1ComparisonExp + comparisonExpressionTypeName: DecimalComparisonExp --- kind: DataConnectorScalarRepresentation version: v1 definition: - dataConnectorName: chinook - dataConnectorScalarType: Int - representation: Int + dataConnectorName: sample_mflix + dataConnectorScalarType: Decimal + representation: Decimal graphql: - comparisonExpressionTypeName: IntComparisonExp_1 + comparisonExpressionTypeName: DecimalComparisonExp --- kind: DataConnectorScalarRepresentation version: v1 definition: - dataConnectorName: chinook - dataConnectorScalarType: String - representation: String - graphql: - comparisonExpressionTypeName: StringComparisonExp_1 - ---- -kind: AggregateExpression -version: v1 -definition: - name: ObjectIdAggExp_1 - operand: - scalar: - aggregatedType: ObjectId_1 - aggregationFunctions: - - name: count - returnType: Int! - dataConnectorAggregationFunctionMapping: - - dataConnectorName: chinook - dataConnectorScalarType: ObjectId - functionMapping: - count: - name: count - count: - enable: true - countDistinct: - enable: true - graphql: - selectTypeName: ObjectIdAggExp1 - ---- -kind: ScalarType -version: v1 -definition: - name: Decimal + dataConnectorName: test_cases + dataConnectorScalarType: Decimal + representation: Decimal graphql: - typeName: Decimal + comparisonExpressionTypeName: DecimalComparisonExp --- kind: BooleanExpressionType @@ -132,16 +72,6 @@ definition: graphql: typeName: DecimalBoolExp ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: chinook - dataConnectorScalarType: Decimal - representation: Decimal - graphql: - comparisonExpressionTypeName: DecimalComparisonExp - --- kind: AggregateExpression version: v1 @@ -152,7 +82,7 @@ definition: aggregatedType: Decimal aggregationFunctions: - name: avg - returnType: Decimal + returnType: Double - name: count returnType: Int! - name: max @@ -160,7 +90,7 @@ definition: - name: min returnType: Decimal - name: sum - returnType: Decimal + returnType: Double dataConnectorAggregationFunctionMapping: - dataConnectorName: chinook dataConnectorScalarType: Decimal @@ -175,20 +105,35 @@ definition: name: min sum: name: sum + - dataConnectorName: sample_mflix + dataConnectorScalarType: Decimal + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: test_cases + dataConnectorScalarType: Decimal + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum count: enable: true countDistinct: enable: true graphql: selectTypeName: DecimalAggExp - ---- -kind: DataConnectorScalarRepresentation -version: v1 -definition: - dataConnectorName: chinook - dataConnectorScalarType: Double - representation: Double - graphql: - comparisonExpressionTypeName: DoubleComparisonExp - diff --git a/fixtures/hasura/app/metadata/types/double.hml b/fixtures/hasura/app/metadata/types/double.hml new file mode 100644 index 00000000..8d9ca0bc --- /dev/null +++ b/fixtures/hasura/app/metadata/types/double.hml @@ -0,0 +1,142 @@ +--- +kind: ScalarType +version: v1 +definition: + name: Double + graphql: + typeName: Double + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: chinook + dataConnectorScalarType: Double + representation: Double + graphql: + comparisonExpressionTypeName: DoubleComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: Double + representation: Double + graphql: + comparisonExpressionTypeName: DoubleComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Double + representation: Double + graphql: + comparisonExpressionTypeName: DoubleComparisonExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: DoubleBoolExp + operand: + scalar: + type: Double + comparisonOperators: + - name: _eq + argumentType: Double! + - name: _gt + argumentType: Double! + - name: _gte + argumentType: Double! + - name: _in + argumentType: "[Double!]!" + - name: _lt + argumentType: Double! + - name: _lte + argumentType: Double! + - name: _neq + argumentType: Double! + - name: _nin + argumentType: "[Double!]!" + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Double + operatorMapping: {} + - dataConnectorName: chinook + dataConnectorScalarType: Double + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: DoubleBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: DoubleAggExp + operand: + scalar: + aggregatedType: Double + aggregationFunctions: + - name: avg + returnType: Double + - name: count + returnType: Int! + - name: max + returnType: Double + - name: min + returnType: Double + - name: sum + returnType: Double + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Double + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: chinook + dataConnectorScalarType: Double + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: test_cases + dataConnectorScalarType: Double + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: DoubleAggExp diff --git a/fixtures/hasura/app/metadata/types/extendedJSON.hml b/fixtures/hasura/app/metadata/types/extendedJSON.hml new file mode 100644 index 00000000..fad40c22 --- /dev/null +++ b/fixtures/hasura/app/metadata/types/extendedJSON.hml @@ -0,0 +1,97 @@ +--- +kind: ScalarType +version: v1 +definition: + name: ExtendedJson + graphql: + typeName: ExtendedJson + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: ExtendedJsonBoolExp + operand: + scalar: + type: ExtendedJson + comparisonOperators: + - name: _eq + argumentType: ExtendedJson! + - name: _gt + argumentType: ExtendedJson! + - name: _gte + argumentType: ExtendedJson! + - name: _in + argumentType: ExtendedJson! + - name: _iregex + argumentType: String! + - name: _lt + argumentType: ExtendedJson! + - name: _lte + argumentType: ExtendedJson! + - name: _neq + argumentType: ExtendedJson! + - name: _nin + argumentType: ExtendedJson! + - name: _regex + argumentType: String! + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: ExtendedJSON + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: ExtendedJsonBoolExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: ExtendedJSON + representation: ExtendedJson + graphql: + comparisonExpressionTypeName: ExtendedJsonComparisonExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: ExtendedJsonAggExp + operand: + scalar: + aggregatedType: ExtendedJson + aggregationFunctions: + - name: avg + returnType: ExtendedJson! + - name: count + returnType: Int! + - name: max + returnType: ExtendedJson! + - name: min + returnType: ExtendedJson! + - name: sum + returnType: ExtendedJson! + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: ExtendedJSON + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: ExtendedJsonAggExp diff --git a/fixtures/hasura/app/metadata/types/int.hml b/fixtures/hasura/app/metadata/types/int.hml new file mode 100644 index 00000000..88d6333b --- /dev/null +++ b/fixtures/hasura/app/metadata/types/int.hml @@ -0,0 +1,137 @@ +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: chinook + dataConnectorScalarType: Int + representation: Int + graphql: + comparisonExpressionTypeName: IntComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: Int + representation: Int + graphql: + comparisonExpressionTypeName: IntComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Int + representation: Int + graphql: + comparisonExpressionTypeName: IntComparisonExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: IntBoolExp + operand: + scalar: + type: Int + comparisonOperators: + - name: _eq + argumentType: Int! + - name: _gt + argumentType: Int! + - name: _gte + argumentType: Int! + - name: _in + argumentType: "[Int!]!" + - name: _lt + argumentType: Int! + - name: _lte + argumentType: Int! + - name: _neq + argumentType: Int! + - name: _nin + argumentType: "[Int!]!" + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Int + operatorMapping: {} + - dataConnectorName: chinook + dataConnectorScalarType: Int + operatorMapping: {} + - dataConnectorName: test_cases + dataConnectorScalarType: Int + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: IntBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: IntAggExp + operand: + scalar: + aggregatedType: Int + aggregationFunctions: + - name: avg + returnType: Double + - name: count + returnType: Int! + - name: max + returnType: Int + - name: min + returnType: Int + - name: sum + returnType: Long + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Int + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: chinook + dataConnectorScalarType: Int + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: test_cases + dataConnectorScalarType: Int + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: IntAggExp diff --git a/fixtures/hasura/app/metadata/types/long.hml b/fixtures/hasura/app/metadata/types/long.hml new file mode 100644 index 00000000..68f08e76 --- /dev/null +++ b/fixtures/hasura/app/metadata/types/long.hml @@ -0,0 +1,145 @@ +--- +kind: ScalarType +version: v1 +definition: + name: Long + graphql: + typeName: Long + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: chinook + dataConnectorScalarType: Long + representation: Long + graphql: + comparisonExpressionTypeName: LongComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: Long + representation: Long + graphql: + comparisonExpressionTypeName: LongComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: Long + representation: Long + graphql: + comparisonExpressionTypeName: LongComparisonExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: LongBoolExp + operand: + scalar: + type: Long + comparisonOperators: + - name: _eq + argumentType: Long! + - name: _gt + argumentType: Long! + - name: _gte + argumentType: Long! + - name: _in + argumentType: "[Long!]!" + - name: _lt + argumentType: Long! + - name: _lte + argumentType: Long! + - name: _neq + argumentType: Long! + - name: _nin + argumentType: "[Long!]!" + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Long + operatorMapping: {} + - dataConnectorName: chinook + dataConnectorScalarType: Long + operatorMapping: {} + - dataConnectorName: test_cases + dataConnectorScalarType: Long + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: LongBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: LongAggExp + operand: + scalar: + aggregatedType: Long + aggregationFunctions: + - name: avg + returnType: Double + - name: count + returnType: Int! + - name: max + returnType: Long + - name: min + returnType: Long + - name: sum + returnType: Long + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: Long + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: chinook + dataConnectorScalarType: Long + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + - dataConnectorName: test_cases + dataConnectorScalarType: Long + functionMapping: + avg: + name: avg + count: + name: count + max: + name: max + min: + name: min + sum: + name: sum + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: LongAggExp diff --git a/fixtures/hasura/app/metadata/types/objectId.hml b/fixtures/hasura/app/metadata/types/objectId.hml new file mode 100644 index 00000000..80647c95 --- /dev/null +++ b/fixtures/hasura/app/metadata/types/objectId.hml @@ -0,0 +1,104 @@ +--- +kind: ScalarType +version: v1 +definition: + name: ObjectId + graphql: + typeName: ObjectId + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: chinook + dataConnectorScalarType: ObjectId + representation: ObjectId + graphql: + comparisonExpressionTypeName: ObjectIdComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: ObjectId + representation: ObjectId + graphql: + comparisonExpressionTypeName: ObjectIdComparisonExp + +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: ObjectId + representation: ObjectId + graphql: + comparisonExpressionTypeName: ObjectIdComparisonExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: ObjectIdBoolExp + operand: + scalar: + type: ObjectId + comparisonOperators: + - name: _eq + argumentType: ObjectId! + - name: _in + argumentType: "[ObjectId!]!" + - name: _neq + argumentType: ObjectId! + - name: _nin + argumentType: "[ObjectId!]!" + dataConnectorOperatorMapping: + - dataConnectorName: chinook + dataConnectorScalarType: ObjectId + operatorMapping: {} + - dataConnectorName: sample_mflix + dataConnectorScalarType: ObjectId + operatorMapping: {} + - dataConnectorName: test_cases + dataConnectorScalarType: ObjectId + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: ObjectIdBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: ObjectIdAggExp + operand: + scalar: + aggregatedType: ObjectId + aggregationFunctions: + - name: count + returnType: Int! + dataConnectorAggregationFunctionMapping: + - dataConnectorName: chinook + dataConnectorScalarType: ObjectId + functionMapping: + count: + name: count + - dataConnectorName: sample_mflix + dataConnectorScalarType: ObjectId + functionMapping: + count: + name: count + - dataConnectorName: test_cases + dataConnectorScalarType: ObjectId + functionMapping: + count: + name: count + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: ObjectIdAggExp diff --git a/fixtures/hasura/app/metadata/types/string.hml b/fixtures/hasura/app/metadata/types/string.hml new file mode 100644 index 00000000..54d1047e --- /dev/null +++ b/fixtures/hasura/app/metadata/types/string.hml @@ -0,0 +1,125 @@ +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: chinook + dataConnectorScalarType: String + representation: String + graphql: + comparisonExpressionTypeName: StringComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: sample_mflix + dataConnectorScalarType: String + representation: String + graphql: + comparisonExpressionTypeName: StringComparisonExp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: test_cases + dataConnectorScalarType: String + representation: String + graphql: + comparisonExpressionTypeName: StringComparisonExp + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: StringBoolExp + operand: + scalar: + type: String + comparisonOperators: + - name: _eq + argumentType: String! + - name: _gt + argumentType: String! + - name: _gte + argumentType: String! + - name: _in + argumentType: "[String!]!" + - name: _iregex + argumentType: String! + - name: _lt + argumentType: String! + - name: _lte + argumentType: String! + - name: _neq + argumentType: String! + - name: _nin + argumentType: "[String!]!" + - name: _regex + argumentType: String! + dataConnectorOperatorMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: String + operatorMapping: {} + - dataConnectorName: chinook + dataConnectorScalarType: String + operatorMapping: {} + - dataConnectorName: test_cases + dataConnectorScalarType: String + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: StringBoolExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: StringAggExp + operand: + scalar: + aggregatedType: String + aggregationFunctions: + - name: count + returnType: Int! + - name: max + returnType: String + - name: min + returnType: String + dataConnectorAggregationFunctionMapping: + - dataConnectorName: sample_mflix + dataConnectorScalarType: String + functionMapping: + count: + name: count + max: + name: max + min: + name: min + - dataConnectorName: chinook + dataConnectorScalarType: String + functionMapping: + count: + name: count + max: + name: max + min: + name: min + - dataConnectorName: test_cases + dataConnectorScalarType: String + functionMapping: + count: + name: count + max: + name: max + min: + name: min + count: + enable: true + countDistinct: + enable: true + graphql: + selectTypeName: StringAggExp diff --git a/fixtures/mongodb/sample_mflix/movies.json b/fixtures/mongodb/sample_mflix/movies.json index c957d784..3cf5fd14 100644 --- a/fixtures/mongodb/sample_mflix/movies.json +++ b/fixtures/mongodb/sample_mflix/movies.json @@ -1,7 +1,7 @@ {"_id":{"$oid":"573a1390f29313caabcd4135"},"plot":"Three men hammer on an anvil and pass a bottle of beer around.","genres":["Short"],"runtime":{"$numberInt":"1"},"cast":["Charles Kayser","John Ott"],"num_mflix_comments":{"$numberInt":"1"},"title":"Blacksmith Scene","fullplot":"A stationary camera looks at a large anvil with a blacksmith behind it and one on either side. The smith in the middle draws a heated metal rod from the fire, places it on the anvil, and all three begin a rhythmic hammering. After several blows, the metal goes back in the fire. One smith pulls out a bottle of beer, and they each take a swig. Then, out comes the glowing metal and the hammering resumes.","countries":["USA"],"released":{"$date":{"$numberLong":"-2418768000000"}},"directors":["William K.L. Dickson"],"rated":"UNRATED","awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-26 00:03:50.133000000","year":{"$numberInt":"1893"},"imdb":{"rating":{"$numberDouble":"6.2"},"votes":{"$numberInt":"1189"},"id":{"$numberInt":"5"}},"type":"movie","tomatoes":{"viewer":{"rating":{"$numberInt":"3"},"numReviews":{"$numberInt":"184"},"meter":{"$numberInt":"32"}},"lastUpdated":{"$date":{"$numberLong":"1435516449000"}}}} {"_id":{"$oid":"573a1390f29313caabcd42e8"},"plot":"A group of bandits stage a brazen train hold-up, only to find a determined posse hot on their heels.","genres":["Short","Western"],"runtime":{"$numberInt":"11"},"cast":["A.C. Abadie","Gilbert M. 'Broncho Billy' Anderson","George Barnes","Justus D. Barnes"],"poster":"https://m.media-amazon.com/images/M/MV5BMTU3NjE5NzYtYTYyNS00MDVmLWIwYjgtMmYwYWIxZDYyNzU2XkEyXkFqcGdeQXVyNzQzNzQxNzI@._V1_SY1000_SX677_AL_.jpg","title":"The Great Train Robbery","fullplot":"Among the earliest existing films in American cinema - notable as the first film that presented a narrative story to tell - it depicts a group of cowboy outlaws who hold up a train and rob the passengers. They are then pursued by a Sheriff's posse. Several scenes have color included - all hand tinted.","languages":["English"],"released":{"$date":{"$numberLong":"-2085523200000"}},"directors":["Edwin S. Porter"],"rated":"TV-G","awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-13 00:27:59.177000000","year":{"$numberInt":"1903"},"imdb":{"rating":{"$numberDouble":"7.4"},"votes":{"$numberInt":"9847"},"id":{"$numberInt":"439"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.7"},"numReviews":{"$numberInt":"2559"},"meter":{"$numberInt":"75"}},"fresh":{"$numberInt":"6"},"critic":{"rating":{"$numberDouble":"7.6"},"numReviews":{"$numberInt":"6"},"meter":{"$numberInt":"100"}},"rotten":{"$numberInt":"0"},"lastUpdated":{"$date":{"$numberLong":"1439061370000"}}}} {"_id":{"$oid":"573a1390f29313caabcd4323"},"plot":"A young boy, opressed by his mother, goes on an outing in the country with a social welfare group where he dares to dream of a land where the cares of his ordinary life fade.","genres":["Short","Drama","Fantasy"],"runtime":{"$numberInt":"14"},"rated":"UNRATED","cast":["Martin Fuller","Mrs. William Bechtel","Walter Edwin","Ethel Jewett"],"num_mflix_comments":{"$numberInt":"2"},"poster":"https://m.media-amazon.com/images/M/MV5BMTMzMDcxMjgyNl5BMl5BanBnXkFtZTcwOTgxNjg4Mg@@._V1_SY1000_SX677_AL_.jpg","title":"The Land Beyond the Sunset","fullplot":"Thanks to the Fresh Air Fund, a slum child escapes his drunken mother for a day's outing in the country. Upon arriving, he and the other children are told a story about a mythical land of no pain. Rather then return to the slum at day's end, the lad seeks to journey to that beautiful land beyond the sunset.","languages":["English"],"released":{"$date":{"$numberLong":"-1804377600000"}},"directors":["Harold M. Shaw"],"writers":["Dorothy G. Shore"],"awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-29 00:27:45.437000000","year":{"$numberInt":"1912"},"imdb":{"rating":{"$numberDouble":"7.1"},"votes":{"$numberInt":"448"},"id":{"$numberInt":"488"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.7"},"numReviews":{"$numberInt":"53"},"meter":{"$numberInt":"67"}},"lastUpdated":{"$date":{"$numberLong":"1430161595000"}}}} -{"_id":{"$oid":"573a1390f29313caabcd446f"},"plot":"A greedy tycoon decides, on a whim, to corner the world market in wheat. This doubles the price of bread, forcing the grain's producers into charity lines and further into poverty. The film...","genres":["Short","Drama"],"runtime":{"$numberInt":"14"},"cast":["Frank Powell","Grace Henderson","James Kirkwood","Linda Arvidson"],"num_mflix_comments":{"$numberInt":"1"},"title":"A Corner in Wheat","fullplot":"A greedy tycoon decides, on a whim, to corner the world market in wheat. This doubles the price of bread, forcing the grain's producers into charity lines and further into poverty. The film continues to contrast the ironic differences between the lives of those who work to grow the wheat and the life of the man who dabbles in its sale for profit.","languages":["English"],"released":{"$date":{"$numberLong":"-1895097600000"}},"directors":["D.W. Griffith"],"rated":"G","awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-13 00:46:30.660000000","year":{"$numberInt":"1909"},"imdb":{"rating":{"$numberDouble":"6.6"},"votes":{"$numberInt":"1375"},"id":{"$numberInt":"832"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.6"},"numReviews":{"$numberInt":"109"},"meter":{"$numberInt":"73"}},"lastUpdated":{"$date":{"$numberLong":"1431369413000"}}}} +{"_id":{"$oid":"573a1390f29313caabcd446f"},"plot":"A greedy tycoon decides, on a whim, to corner the world market in wheat. This doubles the price of bread, forcing the grain's producers into charity lines and further into poverty. The film...","genres":["Short","Drama"],"runtime":{"$numberInt":"14"},"cast":["Frank Powell","Grace Henderson","James Kirkwood","Linda Arvidson"],"num_mflix_comments":{"$numberInt":"1"},"title":"A Corner in Wheat","fullplot":"A greedy tycoon decides, on a whim, to corner the world market in wheat. This doubles the price of bread, forcing the grain's producers into charity lines and further into poverty. The film continues to contrast the ironic differences between the lives of those who work to grow the wheat and the life of the man who dabbles in its sale for profit.","languages":["English"],"released":{"$date":{"$numberLong":"-1895097600000"}},"directors":["D.W. Griffith"],"writers":[],"rated":"G","awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-13 00:46:30.660000000","year":{"$numberInt":"1909"},"imdb":{"rating":{"$numberDouble":"6.6"},"votes":{"$numberInt":"1375"},"id":{"$numberInt":"832"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.6"},"numReviews":{"$numberInt":"109"},"meter":{"$numberInt":"73"}},"lastUpdated":{"$date":{"$numberLong":"1431369413000"}}}} {"_id":{"$oid":"573a1390f29313caabcd4803"},"plot":"Cartoon figures announce, via comic strip balloons, that they will move - and move they do, in a wildly exaggerated style.","genres":["Animation","Short","Comedy"],"runtime":{"$numberInt":"7"},"cast":["Winsor McCay"],"num_mflix_comments":{"$numberInt":"1"},"poster":"https://m.media-amazon.com/images/M/MV5BYzg2NjNhNTctMjUxMi00ZWU4LWI3ZjYtNTI0NTQxNThjZTk2XkEyXkFqcGdeQXVyNzg5OTk2OA@@._V1_SY1000_SX677_AL_.jpg","title":"Winsor McCay, the Famous Cartoonist of the N.Y. Herald and His Moving Comics","fullplot":"Cartoonist Winsor McCay agrees to create a large set of drawings that will be photographed and made into a motion picture. The job requires plenty of drawing supplies, and the cartoonist must also overcome some mishaps caused by an assistant. Finally, the work is done, and everyone can see the resulting animated picture.","languages":["English"],"released":{"$date":{"$numberLong":"-1853539200000"}},"directors":["Winsor McCay","J. Stuart Blackton"],"writers":["Winsor McCay (comic strip \"Little Nemo in Slumberland\")","Winsor McCay (screenplay)"],"awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-29 01:09:03.030000000","year":{"$numberInt":"1911"},"imdb":{"rating":{"$numberDouble":"7.3"},"votes":{"$numberInt":"1034"},"id":{"$numberInt":"1737"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.4"},"numReviews":{"$numberInt":"89"},"meter":{"$numberInt":"47"}},"lastUpdated":{"$date":{"$numberLong":"1440096684000"}}}} {"_id":{"$oid":"573a1390f29313caabcd4eaf"},"plot":"A woman, with the aid of her police officer sweetheart, endeavors to uncover the prostitution ring that has kidnapped her sister, and the philanthropist who secretly runs it.","genres":["Crime","Drama"],"runtime":{"$numberInt":"88"},"cast":["Jane Gail","Ethel Grandin","William H. Turner","Matt Moore"],"num_mflix_comments":{"$numberInt":"2"},"poster":"https://m.media-amazon.com/images/M/MV5BYzk0YWQzMGYtYTM5MC00NjM2LWE5YzYtMjgyNDVhZDg1N2YzXkEyXkFqcGdeQXVyMzE0MjY5ODA@._V1_SY1000_SX677_AL_.jpg","title":"Traffic in Souls","lastupdated":"2015-09-15 02:07:14.247000000","languages":["English"],"released":{"$date":{"$numberLong":"-1770508800000"}},"directors":["George Loane Tucker"],"rated":"TV-PG","awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"year":{"$numberInt":"1913"},"imdb":{"rating":{"$numberInt":"6"},"votes":{"$numberInt":"371"},"id":{"$numberInt":"3471"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberInt":"3"},"numReviews":{"$numberInt":"85"},"meter":{"$numberInt":"57"}},"dvd":{"$date":{"$numberLong":"1219708800000"}},"lastUpdated":{"$date":{"$numberLong":"1439231635000"}}}} {"_id":{"$oid":"573a1390f29313caabcd50e5"},"plot":"The cartoonist, Winsor McCay, brings the Dinosaurus back to life in the figure of his latest creation, Gertie the Dinosaur.","genres":["Animation","Short","Comedy"],"runtime":{"$numberInt":"12"},"cast":["Winsor McCay","George McManus","Roy L. McCardell"],"num_mflix_comments":{"$numberInt":"1"},"poster":"https://m.media-amazon.com/images/M/MV5BMTQxNzI4ODQ3NF5BMl5BanBnXkFtZTgwNzY5NzMwMjE@._V1_SY1000_SX677_AL_.jpg","title":"Gertie the Dinosaur","fullplot":"Winsor Z. McCay bets another cartoonist that he can animate a dinosaur. So he draws a big friendly herbivore called Gertie. Then he get into his own picture. Gertie walks through the picture, eats a tree, meets her creator, and takes him carefully on her back for a ride.","languages":["English"],"released":{"$date":{"$numberLong":"-1745020800000"}},"directors":["Winsor McCay"],"writers":["Winsor McCay"],"awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-08-18 01:03:15.313000000","year":{"$numberInt":"1914"},"imdb":{"rating":{"$numberDouble":"7.3"},"votes":{"$numberInt":"1837"},"id":{"$numberInt":"4008"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"3.7"},"numReviews":{"$numberInt":"29"}},"lastUpdated":{"$date":{"$numberLong":"1439234403000"}}}} diff --git a/fixtures/mongodb/test_cases/departments.json b/fixtures/mongodb/test_cases/departments.json new file mode 100644 index 00000000..557e4621 --- /dev/null +++ b/fixtures/mongodb/test_cases/departments.json @@ -0,0 +1,2 @@ +{ "_id": { "$oid": "67857bc2f317ca21359981d5" }, "description": "West Valley English" } +{ "_id": { "$oid": "67857be3f317ca21359981d6" }, "description": "West Valley Math" } diff --git a/fixtures/mongodb/test_cases/import.sh b/fixtures/mongodb/test_cases/import.sh index 9d512a9a..3c7f671f 100755 --- a/fixtures/mongodb/test_cases/import.sh +++ b/fixtures/mongodb/test_cases/import.sh @@ -11,9 +11,9 @@ set -euo pipefail FIXTURES=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) echo "📡 Importing test case data..." -mongoimport --db test_cases --collection weird_field_names --file "$FIXTURES"/weird_field_names.json -mongoimport --db test_cases --collection nested_collection --file "$FIXTURES"/nested_collection.json -mongoimport --db test_cases --collection nested_field_with_dollar --file "$FIXTURES"/nested_field_with_dollar.json -mongoimport --db test_cases --collection uuids --file "$FIXTURES"/uuids.json +for fixture in "$FIXTURES"/*.json; do + collection=$(basename "$fixture" .json) + mongoimport --db test_cases --collection "$collection" --file "$fixture" +done echo "✅ test case data imported..." diff --git a/fixtures/mongodb/test_cases/schools.json b/fixtures/mongodb/test_cases/schools.json new file mode 100644 index 00000000..c2cc732a --- /dev/null +++ b/fixtures/mongodb/test_cases/schools.json @@ -0,0 +1 @@ +{ "_id": { "$oid": "67857b7ef317ca21359981d4" }, "name": "West Valley", "departments": { "english_department_id": { "$oid": "67857bc2f317ca21359981d5" }, "math_department_id": { "$oid": "67857be3f317ca21359981d6" } } } diff --git a/flake.lock b/flake.lock index bc4bc551..79c8ca2f 100644 --- a/flake.lock +++ b/flake.lock @@ -110,11 +110,11 @@ "graphql-engine-source": { "flake": false, "locked": { - "lastModified": 1733318858, - "narHash": "sha256-7/nTrhvRvKnHnDwBxLPpAfwHg06qLyQd3S1iuzQjI5o=", + "lastModified": 1738870584, + "narHash": "sha256-YYp1IJpEv+MIsIVQ25rw2/aKHWZZ9avIW7GMXYJPkJU=", "owner": "hasura", "repo": "graphql-engine", - "rev": "8b7ad6684f30266326c49208b8c36251b984bb18", + "rev": "249552b0ea8669d37b77da205abac2c2b41e5b34", "type": "github" }, "original": { @@ -145,11 +145,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1733604522, - "narHash": "sha256-9XNxIgOGq8MJ3a1GPE1lGaMBSz6Ossgv/Ec+KhyaC68=", + "lastModified": 1738802037, + "narHash": "sha256-2rFnj+lf9ecXH+/qFA2ncyz/+mH/ho+XftUgVXrLjBQ=", "owner": "hasura", "repo": "ddn-cli-nix", - "rev": "8e9695beabd6d111a69ae288f8abba6ebf8d1c82", + "rev": "d439eab6b2254977234261081191f5d83bce49fd", "type": "github" }, "original": {