diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b9ed62..f4805ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,18 @@ This changelog documents the changes between release versions. ## [Unreleased] +### Added + +- You can now aggregate values in nested object fields ([#136](https://github.com/hasura/ndc-mongodb/pull/136)) + +### Changed + +- Result types for aggregation operations other than count are now nullable ([#136](https://github.com/hasura/ndc-mongodb/pull/136)) + ### Fixed - Upgrade dependencies to get fix for RUSTSEC-2024-0421, a vulnerability in domain name comparisons ([#138](https://github.com/hasura/ndc-mongodb/pull/138)) +- Aggregations on empty document sets now produce `null` instead of failing with an error ([#136](https://github.com/hasura/ndc-mongodb/pull/136)) #### Fix for RUSTSEC-2024-0421 / CVE-2024-12224 diff --git a/crates/integration-tests/src/tests/aggregation.rs b/crates/integration-tests/src/tests/aggregation.rs index afa2fbdd..dedfad6a 100644 --- a/crates/integration-tests/src/tests/aggregation.rs +++ b/crates/integration-tests/src/tests/aggregation.rs @@ -101,3 +101,103 @@ async fn aggregates_mixture_of_numeric_and_null_values() -> anyhow::Result<()> { ); Ok(()) } + +#[tokio::test] +async fn returns_null_when_aggregating_empty_result_set() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + moviesAggregate(filter_input: {where: {title: {_eq: "no such movie"}}}) { + runtime { + avg + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn returns_zero_when_counting_empty_result_set() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + moviesAggregate(filter_input: {where: {title: {_eq: "no such movie"}}}) { + _count + title { + count + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn returns_zero_when_counting_nested_fields_in_empty_result_set() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + moviesAggregate(filter_input: {where: {title: {_eq: "no such movie"}}}) { + awards { + nominations { + count + _count + } + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} + +#[tokio::test] +async fn aggregates_nested_field_values() -> anyhow::Result<()> { + assert_yaml_snapshot!( + graphql_query( + r#" + query { + moviesAggregate( + filter_input: {where: {title: {_in: ["Within Our Gates", "The Ace of Hearts"]}}} + ) { + tomatoes { + viewer { + rating { + avg + } + } + critic { + rating { + avg + } + } + } + imdb { + rating { + avg + } + } + } + } + "# + ) + .run() + .await? + ); + Ok(()) +} diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_nested_field_values.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_nested_field_values.snap new file mode 100644 index 00000000..51304f6d --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__aggregates_nested_field_values.snap @@ -0,0 +1,17 @@ +--- +source: crates/integration-tests/src/tests/aggregation.rs +expression: "graphql_query(r#\"\n query {\n moviesAggregate(\n filter_input: {where: {title: {_in: [\"Within Our Gates\", \"The Ace of Hearts\"]}}}\n ) {\n tomatoes {\n viewer {\n rating {\n avg\n }\n }\n critic {\n rating {\n avg\n }\n }\n }\n imdb {\n rating {\n avg\n }\n }\n }\n }\n \"#).run().await?" +--- +data: + moviesAggregate: + tomatoes: + viewer: + rating: + avg: 3.45 + critic: + rating: + avg: ~ + imdb: + rating: + avg: 6.65 +errors: ~ diff --git a/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_null_when_aggregating_empty_result_set.snap b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_null_when_aggregating_empty_result_set.snap new file mode 100644 index 00000000..00ed6601 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_null_when_aggregating_empty_result_set.snap @@ -0,0 +1,9 @@ +--- +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 runtime {\n avg\n }\n }\n }\n \"#).run().await?" +--- +data: + moviesAggregate: + runtime: + avg: ~ +errors: ~ 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 new file mode 100644 index 00000000..61d3c939 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_empty_result_set.snap @@ -0,0 +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?" +--- +data: + moviesAggregate: + _count: 0 + title: + 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 new file mode 100644 index 00000000..c621c020 --- /dev/null +++ b/crates/integration-tests/src/tests/snapshots/integration_tests__tests__aggregation__returns_zero_when_counting_nested_fields_in_empty_result_set.snap @@ -0,0 +1,11 @@ +--- +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?" +--- +data: + moviesAggregate: + awards: + nominations: + count: 0 + _count: 0 +errors: ~ diff --git a/crates/mongodb-agent-common/src/query/mod.rs b/crates/mongodb-agent-common/src/query/mod.rs index 3353b572..d6094ca6 100644 --- a/crates/mongodb-agent-common/src/query/mod.rs +++ b/crates/mongodb-agent-common/src/query/mod.rs @@ -110,11 +110,11 @@ mod tests { { "$facet": { "avg": [ - { "$match": { "gpa": { "$exists": true, "$ne": null } } }, + { "$match": { "gpa": { "$ne": null } } }, { "$group": { "_id": null, "result": { "$avg": "$gpa" } } }, ], "count": [ - { "$match": { "gpa": { "$exists": true, "$ne": null } } }, + { "$match": { "gpa": { "$ne": null } } }, { "$group": { "_id": "$gpa" } }, { "$count": "result" }, ], @@ -123,10 +123,17 @@ mod tests { { "$replaceWith": { "aggregates": { - "avg": { "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "avg" } } }, - } }, + "avg": { + "$ifNull": [ + { + "$getField": { + "field": "result", + "input": { "$first": { "$getField": { "$literal": "avg" } } }, + } + }, + null + ] + }, "count": { "$ifNull": [ { @@ -180,24 +187,31 @@ mod tests { { "$match": { "gpa": { "$lt": 4.0 } } }, { "$facet": { - "avg": [ - { "$match": { "gpa": { "$exists": true, "$ne": null } } }, - { "$group": { "_id": null, "result": { "$avg": "$gpa" } } }, - ], "__ROWS__": [{ "$replaceWith": { "student_gpa": { "$ifNull": ["$gpa", null] }, }, }], + "avg": [ + { "$match": { "gpa": { "$ne": null } } }, + { "$group": { "_id": null, "result": { "$avg": "$gpa" } } }, + ], }, }, { "$replaceWith": { "aggregates": { - "avg": { "$getField": { - "field": "result", - "input": { "$first": { "$getField": { "$literal": "avg" } } }, - } }, + "avg": { + "$ifNull": [ + { + "$getField": { + "field": "result", + "input": { "$first": { "$getField": { "$literal": "avg" } } }, + } + }, + null + ] + }, }, "rows": "$__ROWS__", }, diff --git a/crates/mongodb-agent-common/src/query/pipeline.rs b/crates/mongodb-agent-common/src/query/pipeline.rs index 9a515f37..f89d2c8f 100644 --- a/crates/mongodb-agent-common/src/query/pipeline.rs +++ b/crates/mongodb-agent-common/src/query/pipeline.rs @@ -1,18 +1,28 @@ use std::collections::BTreeMap; +use configuration::MongoScalarType; use itertools::Itertools; use mongodb::bson::{self, doc, Bson}; -use mongodb_support::aggregate::{Accumulator, Pipeline, Selection, Stage}; +use mongodb_support::{ + aggregate::{Accumulator, Pipeline, Selection, Stage}, + BsonScalarType, +}; +use ndc_models::FieldName; use tracing::instrument; use crate::{ aggregation_function::AggregationFunction, + comparison_function::ComparisonFunction, interface_types::MongoAgentError, - mongo_query_plan::{Aggregate, MongoConfiguration, Query, QueryPlan}, + mongo_query_plan::{ + Aggregate, ComparisonTarget, ComparisonValue, Expression, MongoConfiguration, Query, + QueryPlan, Type, + }, mongodb::{sanitize::get_field, selection_from_query_request}, }; use super::{ + column_ref::ColumnRef, constants::{RESULT_FIELD, ROWS_FIELD}, foreach::pipeline_for_foreach, make_selector, @@ -194,8 +204,12 @@ fn facet_pipelines_for_query( 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 { - value_expr + doc! { + "$ifNull": [value_expr, null] + } }; (key.to_string(), value_expr.into()) @@ -235,32 +249,62 @@ fn pipeline_for_aggregate( aggregate: Aggregate, limit: Option, ) -> Result { - // Group expressions use a dollar-sign prefix to indicate a reference to a document field. - // TODO: I don't think we need sanitizing, but I could use a second opinion -Jesse H. - let field_ref = |column: &str| Bson::String(format!("${column}")); + 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(), + } + } + + 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, distinct } if distinct => Pipeline::from_iter( - [ - Some(Stage::Match( - bson::doc! { column.as_str(): { "$exists": true, "$ne": null } }, - )), - limit.map(Into::into).map(Stage::Limit), - Some(Stage::Group { - key_expression: field_ref(column.as_str()), - accumulators: [].into(), - }), - Some(Stage::Count(RESULT_FIELD.to_string())), - ] - .into_iter() - .flatten(), - ), + 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, .. } => Pipeline::from_iter( + Aggregate::ColumnCount { + column, + field_path, + distinct: _, + } => Pipeline::from_iter( [ - Some(Stage::Match( - bson::doc! { column.as_str(): { "$exists": true, "$ne": null } }, - )), + 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())), ] @@ -269,22 +313,32 @@ fn pipeline_for_aggregate( ), Aggregate::SingleColumn { - column, function, .. + 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(column.as_str())), + Avg => Accumulator::Avg(field_ref), Count => Accumulator::Count, - Min => Accumulator::Min(field_ref(column.as_str())), - Max => Accumulator::Max(field_ref(column.as_str())), - Sum => Accumulator::Sum(field_ref(column.as_str())), + Min => Accumulator::Min(field_ref), + Max => Accumulator::Max(field_ref), + Sum => Accumulator::Sum(field_ref), }; Pipeline::from_iter( [ - Some(Stage::Match( - bson::doc! { column: { "$exists": true, "$ne": null } }, - )), + Some(filter_to_documents_with_value(target_field)?), limit.map(Into::into).map(Stage::Limit), Some(Stage::Group { key_expression: Bson::Null, diff --git a/crates/mongodb-agent-common/src/scalar_types_capabilities.rs b/crates/mongodb-agent-common/src/scalar_types_capabilities.rs index e0b12e87..ea7d2352 100644 --- a/crates/mongodb-agent-common/src/scalar_types_capabilities.rs +++ b/crates/mongodb-agent-common/src/scalar_types_capabilities.rs @@ -131,9 +131,7 @@ fn bson_aggregation_functions( ) -> BTreeMap { aggregate_functions(bson_scalar_type) .map(|(fn_name, result_type)| { - let aggregation_definition = AggregateFunctionDefinition { - result_type: bson_to_named_type(result_type), - }; + let aggregation_definition = AggregateFunctionDefinition { result_type }; (fn_name.graphql_name().into(), aggregation_definition) }) .collect() @@ -147,20 +145,23 @@ fn bson_to_named_type(bson_scalar_type: BsonScalarType) -> Type { pub fn aggregate_functions( scalar_type: BsonScalarType, -) -> impl Iterator { - [(A::Count, S::Int)] +) -> 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, scalar_type)), + .map(move |op| (op, nullable_scalar_type())), )) .chain(iter_if( scalar_type.is_numeric(), [A::Avg, A::Sum] .into_iter() - .map(move |op| (op, scalar_type)), + .map(move |op| (op, nullable_scalar_type())), )) } diff --git a/crates/mongodb-connector/src/capabilities.rs b/crates/mongodb-connector/src/capabilities.rs index 0d71a91e..8fc7cdf2 100644 --- a/crates/mongodb-connector/src/capabilities.rs +++ b/crates/mongodb-connector/src/capabilities.rs @@ -12,7 +12,7 @@ pub fn mongo_capabilities() -> Capabilities { nested_fields: NestedFieldCapabilities { filter_by: Some(LeafCapability {}), order_by: Some(LeafCapability {}), - aggregates: None, + aggregates: Some(LeafCapability {}), }, exists: ExistsCapabilities { nested_collections: Some(LeafCapability {}), 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 9ec88145..e88e0a2b 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 @@ -24,7 +24,7 @@ pub fn find_object_field<'a, S>( pub fn find_object_field_path<'a, S>( object_type: &'a plan::ObjectType, field_name: &ndc::FieldName, - field_path: &Option>, + field_path: Option<&Vec>, ) -> Result<&'a plan::Type> { match field_path { None => find_object_field(object_type, field_name), 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 faedbb69..1faa0045 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 @@ -177,19 +177,25 @@ fn plan_for_aggregate( ndc::Aggregate::ColumnCount { column, distinct, - field_path: _, - } => Ok(plan::Aggregate::ColumnCount { column, distinct }), + field_path, + } => Ok(plan::Aggregate::ColumnCount { + column, + field_path, + distinct, + }), ndc::Aggregate::SingleColumn { column, function, - field_path: _, + field_path, } => { - let object_type_field_type = find_object_field(collection_object_type, &column)?; + 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)?; Ok(plan::Aggregate::SingleColumn { column, + field_path, function, result_type: definition.result_type.clone(), }) @@ -559,7 +565,7 @@ fn plan_for_comparison_target( requested_columns, )?; let field_type = - find_object_field_path(&target_object_type, &name, &field_path)?.clone(); + find_object_field_path(&target_object_type, &name, field_path.as_ref())?.clone(); Ok(plan::ComparisonTarget::Column { name, field_path, @@ -569,7 +575,7 @@ fn plan_for_comparison_target( } ndc::ComparisonTarget::RootCollectionColumn { name, field_path } => { let field_type = - find_object_field_path(root_collection_object_type, &name, &field_path)?.clone(); + find_object_field_path(root_collection_object_type, &name, field_path.as_ref())?.clone(); Ok(plan::ComparisonTarget::ColumnInScope { name, field_path, 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 1d5d1c6e..d6ae2409 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 @@ -526,6 +526,7 @@ fn translates_aggregate_selections() -> Result<(), anyhow::Error> { "count_id".into(), plan::Aggregate::ColumnCount { column: "last_name".into(), + field_path: None, distinct: true, }, ), @@ -533,6 +534,7 @@ fn translates_aggregate_selections() -> Result<(), anyhow::Error> { "avg_id".into(), plan::Aggregate::SingleColumn { column: "id".into(), + field_path: None, function: plan_test_helpers::AggregateFunction::Average, result_type: plan::Type::Scalar(plan_test_helpers::ScalarType::Double), }, diff --git a/crates/ndc-query-plan/src/query_plan.rs b/crates/ndc-query-plan/src/query_plan.rs index c1a2bafa..ef1cb6b4 100644 --- a/crates/ndc-query-plan/src/query_plan.rs +++ b/crates/ndc-query-plan/src/query_plan.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, fmt::Debug, iter}; use derivative::Derivative; use indexmap::IndexMap; use itertools::Either; -use ndc_models::{self as ndc, OrderDirection, RelationshipType, UnaryComparisonOperator}; +use ndc_models::{self as ndc, FieldName, OrderDirection, RelationshipType, UnaryComparisonOperator}; use crate::{vec_set::VecSet, Type}; @@ -168,12 +168,16 @@ 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, diff --git a/fixtures/hasura/app/metadata/InsertArtist.hml b/fixtures/hasura/app/metadata/InsertArtist.hml index f239d680..22881d62 100644 --- a/fixtures/hasura/app/metadata/InsertArtist.hml +++ b/fixtures/hasura/app/metadata/InsertArtist.hml @@ -7,7 +7,7 @@ definition: - name: n type: Int! - name: ok - type: Double_1! + type: Double! graphql: typeName: InsertArtist inputTypeName: InsertArtistInput diff --git a/fixtures/hasura/app/metadata/Movies.hml b/fixtures/hasura/app/metadata/Movies.hml index 263beda9..6ec310cb 100644 --- a/fixtures/hasura/app/metadata/Movies.hml +++ b/fixtures/hasura/app/metadata/Movies.hml @@ -514,6 +514,120 @@ definition: graphql: typeName: MoviesBoolExp +--- +kind: AggregateExpression +version: v1 +definition: + name: MoviesAwardsAggExp + operand: + object: + aggregatedType: MoviesAwards + aggregatableFields: + - fieldName: nominations + aggregateExpression: IntAggExp + - fieldName: text + aggregateExpression: StringAggExp + - fieldName: wins + aggregateExpression: IntAggExp + count: + enable: true + graphql: + selectTypeName: MoviesAwardsAggExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: MoviesImdbAggExp + operand: + object: + aggregatedType: MoviesImdb + aggregatableFields: + - fieldName: id + aggregateExpression: IntAggExp + - fieldName: rating + aggregateExpression: DoubleAggExp + - fieldName: votes + aggregateExpression: IntAggExp + count: + enable: true + graphql: + selectTypeName: MoviesImdbAggExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: MoviesTomatoesAggExp + operand: + object: + aggregatedType: MoviesTomatoes + aggregatableFields: + - fieldName: boxOffice + aggregateExpression: StringAggExp + - fieldName: consensus + aggregateExpression: StringAggExp + - fieldName: critic + aggregateExpression: MoviesTomatoesCriticAggExp + - fieldName: dvd + aggregateExpression: DateAggExp + - fieldName: fresh + aggregateExpression: IntAggExp + - fieldName: lastUpdated + aggregateExpression: DateAggExp + - fieldName: production + aggregateExpression: StringAggExp + - fieldName: rotten + aggregateExpression: IntAggExp + - fieldName: viewer + aggregateExpression: MoviesTomatoesViewerAggExp + - fieldName: website + aggregateExpression: StringAggExp + count: + enable: true + graphql: + selectTypeName: MoviesTomatoesAggExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: MoviesTomatoesCriticAggExp + operand: + object: + aggregatedType: MoviesTomatoesCritic + aggregatableFields: + - fieldName: meter + aggregateExpression: IntAggExp + - fieldName: numReviews + aggregateExpression: IntAggExp + - fieldName: rating + aggregateExpression: DoubleAggExp + count: + enable: true + graphql: + selectTypeName: MoviesTomatoesCriticAggExp + +--- +kind: AggregateExpression +version: v1 +definition: + name: MoviesTomatoesViewerAggExp + operand: + object: + aggregatedType: MoviesTomatoesViewer + aggregatableFields: + - fieldName: meter + aggregateExpression: IntAggExp + - fieldName: numReviews + aggregateExpression: IntAggExp + - fieldName: rating + aggregateExpression: DoubleAggExp + count: + enable: true + graphql: + selectTypeName: MoviesTomatoesViewerAggExp + --- kind: AggregateExpression version: v1 @@ -549,6 +663,12 @@ definition: aggregateExpression: StringAggExp - fieldName: year aggregateExpression: IntAggExp + - fieldName: awards + aggregateExpression: MoviesAwardsAggExp + - fieldName: imdb + aggregateExpression: MoviesImdbAggExp + - fieldName: tomatoes + aggregateExpression: MoviesTomatoesAggExp count: enable: true graphql: diff --git a/fixtures/hasura/app/metadata/chinook-types.hml b/fixtures/hasura/app/metadata/chinook-types.hml index b2a2b1ad..ef109d7b 100644 --- a/fixtures/hasura/app/metadata/chinook-types.hml +++ b/fixtures/hasura/app/metadata/chinook-types.hml @@ -152,15 +152,15 @@ definition: aggregatedType: Decimal aggregationFunctions: - name: avg - returnType: Decimal! + returnType: Decimal - name: count returnType: Int! - name: max - returnType: Decimal! + returnType: Decimal - name: min - returnType: Decimal! + returnType: Decimal - name: sum - returnType: Decimal! + returnType: Decimal dataConnectorAggregationFunctionMapping: - dataConnectorName: chinook dataConnectorScalarType: Decimal @@ -182,57 +182,13 @@ definition: graphql: selectTypeName: DecimalAggExp ---- -kind: ScalarType -version: v1 -definition: - name: Double_1 - graphql: - typeName: Double1 - ---- -kind: BooleanExpressionType -version: v1 -definition: - name: DoubleBoolExp_1 - operand: - scalar: - type: Double_1 - comparisonOperators: - - name: _eq - argumentType: Double_1! - - name: _gt - argumentType: Double_1! - - name: _gte - argumentType: Double_1! - - name: _in - argumentType: "[Double_1!]!" - - name: _lt - argumentType: Double_1! - - name: _lte - argumentType: Double_1! - - name: _neq - argumentType: Double_1! - - name: _nin - argumentType: "[Double_1!]!" - dataConnectorOperatorMapping: - - dataConnectorName: chinook - dataConnectorScalarType: Double - operatorMapping: {} - logicalOperators: - enable: true - isNull: - enable: true - graphql: - typeName: DoubleBoolExp1 - --- kind: DataConnectorScalarRepresentation version: v1 definition: dataConnectorName: chinook dataConnectorScalarType: Double - representation: Double_1 + representation: Double graphql: - comparisonExpressionTypeName: Double1ComparisonExp + comparisonExpressionTypeName: DoubleComparisonExp diff --git a/fixtures/hasura/app/metadata/chinook.hml b/fixtures/hasura/app/metadata/chinook.hml index ce33d33f..a23c4937 100644 --- a/fixtures/hasura/app/metadata/chinook.hml +++ b/fixtures/hasura/app/metadata/chinook.hml @@ -70,12 +70,16 @@ definition: name: Int max: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date min: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date comparison_operators: _eq: type: equal @@ -142,24 +146,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal count: result_type: type: named name: Int max: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal min: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal sum: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal comparison_operators: _eq: type: equal @@ -203,24 +215,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double count: result_type: type: named name: Int max: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double min: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double sum: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double comparison_operators: _eq: type: equal @@ -336,24 +356,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int count: result_type: type: named name: Int max: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int min: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int sum: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int comparison_operators: _eq: type: equal @@ -411,24 +439,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long count: result_type: type: named name: Int max: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long min: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long sum: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long comparison_operators: _eq: type: equal @@ -577,12 +613,16 @@ definition: name: Int max: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String min: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String comparison_operators: _eq: type: equal @@ -661,12 +701,16 @@ definition: name: Int max: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp min: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp comparison_operators: _eq: type: equal @@ -1248,6 +1292,7 @@ definition: nested_fields: filter_by: {} order_by: {} + aggregates: {} exists: nested_collections: {} mutation: {} diff --git a/fixtures/hasura/app/metadata/sample_mflix-types.hml b/fixtures/hasura/app/metadata/sample_mflix-types.hml index b3b63d7b..0675e1a7 100644 --- a/fixtures/hasura/app/metadata/sample_mflix-types.hml +++ b/fixtures/hasura/app/metadata/sample_mflix-types.hml @@ -200,9 +200,9 @@ definition: - name: count returnType: Int! - name: max - returnType: Date! + returnType: Date - name: min - returnType: Date! + returnType: Date dataConnectorAggregationFunctionMapping: - dataConnectorName: sample_mflix dataConnectorScalarType: Date @@ -232,9 +232,9 @@ definition: - name: count returnType: Int! - name: max - returnType: String! + returnType: String - name: min - returnType: String! + returnType: String dataConnectorAggregationFunctionMapping: - dataConnectorName: sample_mflix dataConnectorScalarType: String @@ -307,6 +307,9 @@ definition: - dataConnectorName: sample_mflix dataConnectorScalarType: Double operatorMapping: {} + - dataConnectorName: chinook + dataConnectorScalarType: Double + operatorMapping: {} logicalOperators: enable: true isNull: @@ -314,6 +317,72 @@ definition: 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 @@ -376,15 +445,15 @@ definition: aggregatedType: Int aggregationFunctions: - name: avg - returnType: Int! + returnType: Int - name: count returnType: Int! - name: max - returnType: Int! + returnType: Int - name: min - returnType: Int! + returnType: Int - name: sum - returnType: Int! + returnType: Int dataConnectorAggregationFunctionMapping: - dataConnectorName: sample_mflix dataConnectorScalarType: Int diff --git a/fixtures/hasura/app/metadata/sample_mflix.hml b/fixtures/hasura/app/metadata/sample_mflix.hml index 50e46e73..e5cd1f4c 100644 --- a/fixtures/hasura/app/metadata/sample_mflix.hml +++ b/fixtures/hasura/app/metadata/sample_mflix.hml @@ -70,12 +70,16 @@ definition: name: Int max: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date min: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date comparison_operators: _eq: type: equal @@ -142,24 +146,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal count: result_type: type: named name: Int max: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal min: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal sum: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal comparison_operators: _eq: type: equal @@ -203,24 +215,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double count: result_type: type: named name: Int max: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double min: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double sum: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double comparison_operators: _eq: type: equal @@ -336,24 +356,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int count: result_type: type: named name: Int max: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int min: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int sum: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int comparison_operators: _eq: type: equal @@ -411,24 +439,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long count: result_type: type: named name: Int max: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long min: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long sum: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long comparison_operators: _eq: type: equal @@ -577,12 +613,16 @@ definition: name: Int max: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String min: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String comparison_operators: _eq: type: equal @@ -661,12 +701,16 @@ definition: name: Int max: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp min: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp comparison_operators: _eq: type: equal @@ -1347,6 +1391,7 @@ definition: nested_fields: filter_by: {} order_by: {} + aggregates: {} exists: nested_collections: {} mutation: {} diff --git a/fixtures/hasura/app/metadata/test_cases-types.hml b/fixtures/hasura/app/metadata/test_cases-types.hml index 89cc958e..440117db 100644 --- a/fixtures/hasura/app/metadata/test_cases-types.hml +++ b/fixtures/hasura/app/metadata/test_cases-types.hml @@ -88,3 +88,12 @@ definition: 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 fe00f6f2..8ade514b 100644 --- a/fixtures/hasura/app/metadata/test_cases.hml +++ b/fixtures/hasura/app/metadata/test_cases.hml @@ -70,12 +70,16 @@ definition: name: Int max: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date min: result_type: - type: named - name: Date + type: nullable + underlying_type: + type: named + name: Date comparison_operators: _eq: type: equal @@ -142,24 +146,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal count: result_type: type: named name: Int max: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal min: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal sum: result_type: - type: named - name: Decimal + type: nullable + underlying_type: + type: named + name: Decimal comparison_operators: _eq: type: equal @@ -203,24 +215,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double count: result_type: type: named name: Int max: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double min: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double sum: result_type: - type: named - name: Double + type: nullable + underlying_type: + type: named + name: Double comparison_operators: _eq: type: equal @@ -336,24 +356,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int count: result_type: type: named name: Int max: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int min: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int sum: result_type: - type: named - name: Int + type: nullable + underlying_type: + type: named + name: Int comparison_operators: _eq: type: equal @@ -411,24 +439,32 @@ definition: aggregate_functions: avg: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long count: result_type: type: named name: Int max: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long min: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long sum: result_type: - type: named - name: Long + type: nullable + underlying_type: + type: named + name: Long comparison_operators: _eq: type: equal @@ -577,12 +613,16 @@ definition: name: Int max: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String min: result_type: - type: named - name: String + type: nullable + underlying_type: + type: named + name: String comparison_operators: _eq: type: equal @@ -661,12 +701,16 @@ definition: name: Int max: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp min: result_type: - type: named - name: Timestamp + type: nullable + underlying_type: + type: named + name: Timestamp comparison_operators: _eq: type: equal @@ -847,6 +891,7 @@ definition: nested_fields: filter_by: {} order_by: {} + aggregates: {} exists: nested_collections: {} mutation: {}