diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d678dd..32416b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This changelog documents the changes between release versions. ### Fixed - Filtering on field of related collection inside nested object that is not selected for output ([#171](https://github.com/hasura/ndc-mongodb/pull/171)) +- Ensure correct ordering of sort criteria in MongoDB query plan ([#172](https://github.com/hasura/ndc-mongodb/pull/172)) ## [1.8.2] - 2025-06-13 diff --git a/crates/integration-tests/src/tests/filtering.rs b/crates/integration-tests/src/tests/filtering.rs index 2d8fba81..f1014200 100644 --- a/crates/integration-tests/src/tests/filtering.rs +++ b/crates/integration-tests/src/tests/filtering.rs @@ -1,5 +1,6 @@ use insta::assert_yaml_snapshot; use ndc_test_helpers::{binop, field, query, query_request, target, value, variable}; +use serde_json::json; use crate::{connector::Connector, graphql_query, run_connector_query}; @@ -105,3 +106,33 @@ async fn filters_by_uuid() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn filters_by_multiple_criteria_using_variable() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query TestQuery($filter: [MoviesOrderBy!] = {}) { + movies( + order_by: $filter + where: { year: { _gt: 0 }, imdb: { rating: { _gt: 9 } } } + limit: 15 + ) { + title + year + rated + } + } + "# + ) + .variables(json!({ + "filter": [ + { "year": "Desc" }, + { "rated": "Desc" }, + ] + })) + .run() + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_multiple_criteria_using_variable.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_multiple_criteria_using_variable.snap new file mode 100644 index 00000000..dfa968ed --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__filtering__filters_by_multiple_criteria_using_variable.snap @@ -0,0 +1,52 @@ +--- +source: crates/integration-tests/src/tests/filtering.rs +expression: "graphql_query(r#\"\n query TestQuery($filter: [MoviesOrderBy!] = {}) {\n movies(\n order_by: $filter\n where: { year: { _gt: 0 }, imdb: { rating: { _gt: 9 } } }\n limit: 15\n ) {\n title\n year\n rated\n }\n }\n \"#).variables(json!({\n \"filter\": [{ \"year\": \"Desc\" }, { \"rated\": \"Desc\" },]\n})).run().await?" +--- +data: + movies: + - title: "A Brave Heart: The Lizzie Velasquez Story" + year: 2015 + rated: PG-13 + - title: The Real Miyagi + year: 2015 + rated: ~ + - title: Over the Garden Wall + year: 2014 + rated: TV-PG + - title: Frozen Planet + year: 2011 + rated: ~ + - title: Human Planet + year: 2011 + rated: ~ + - title: Life + year: 2009 + rated: PG + - title: Planet Earth + year: 2006 + rated: TV-G + - title: Band of Brothers + year: 2001 + rated: TV-MA + - title: The Blue Planet + year: 2001 + rated: ~ + - title: Pride and Prejudice + year: 1995 + rated: ~ + - title: Baseball + year: 1994 + rated: TV-PG + - title: The Shawshank Redemption + year: 1994 + rated: R + - title: The Shawshank Redemption + year: 1994 + rated: R + - title: The Civil War + year: 1990 + rated: ~ + - title: The Civil War + year: 1990 + rated: ~ +errors: ~ diff --git a/crates/mongodb-agent-common/src/query/make_sort.rs b/crates/mongodb-agent-common/src/query/make_sort.rs index 7adad5a8..ad97783c 100644 --- a/crates/mongodb-agent-common/src/query/make_sort.rs +++ b/crates/mongodb-agent-common/src/query/make_sort.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, iter::once}; +use indexmap::IndexMap; use itertools::join; use mongodb::bson::bson; use mongodb_support::aggregate::{SortDocument, Stage}; @@ -42,7 +43,7 @@ pub fn make_sort_stages(order_by: &OrderBy) -> Result> { fn make_sort(order_by: &OrderBy) -> Result<(SortDocument, RequiredAliases<'_>)> { let OrderBy { elements } = order_by; - let keys_directions_expressions: BTreeMap>)> = + let keys_directions_expressions: IndexMap>)> = elements .iter() .map(|obe| { @@ -53,7 +54,7 @@ fn make_sort(order_by: &OrderBy) -> Result<(SortDocument, RequiredAliases<'_>)> }; Ok((key, (obe.order_direction, required_alias))) }) - .collect::>>()?; + .collect::>>()?; // IndexMap preserves insertion order let sort_document = keys_directions_expressions .iter() @@ -112,7 +113,7 @@ fn safe_alias(target: &OrderByTarget) -> Result { #[cfg(test)] mod tests { - use mongodb::bson::doc; + use mongodb::bson::{self, doc}; use mongodb_support::aggregate::SortDocument; use ndc_models::{FieldName, OrderDirection}; use ndc_query_plan::OrderByElement; @@ -175,4 +176,44 @@ mod tests { assert_eq!(actual, (expected_sort_doc, expected_aliases)); Ok(()) } + + #[test] + fn serializes_sort_criteria_in_expected_order() -> anyhow::Result<()> { + let first_criteria = "year"; + let second_criteria = "rated"; + let order_by = OrderBy { + elements: vec![ + OrderByElement { + order_direction: OrderDirection::Desc, + target: ndc_query_plan::OrderByTarget::Column { + name: first_criteria.into(), + field_path: None, + path: Default::default(), + }, + }, + OrderByElement { + order_direction: OrderDirection::Desc, + target: ndc_query_plan::OrderByTarget::Column { + name: second_criteria.into(), + field_path: None, + path: Default::default(), + }, + }, + ], + }; + let (sort_doc, _) = make_sort(&order_by)?; + let serialized = bson::to_document(&sort_doc)?; + let mut sort_keys = serialized.keys(); + assert_eq!( + sort_keys.next(), + Some(&first_criteria.to_string()), + "first sort criteria" + ); + assert_eq!( + sort_keys.next(), + Some(&second_criteria.to_string()), + "second sort criteria" + ); + Ok(()) + } }