Aggregates Root Field - Part 3: GraphQL API (#685)

This is Part 3 in a stacked PR set that delivers aggregate root field
support.
* Part 1: OpenDD: https://github.com/hasura/v3-engine/pull/683
* Part 2: Metadata Resolve: https://github.com/hasura/v3-engine/pull/684

JIRA: [V3ENGINE-159](https://hasurahq.atlassian.net/browse/V3ENGINE-159)

## Description
This PR implements the GraphQL API for aggregate root fields. The
GraphQL schema matches the design in the [Aggregate and Grouping
RFC](https://github.com/hasura/v3-engine/blob/main/rfcs/aggregations.md#aggregations-walkthrough).

### Schema Generation
The main new part of the GraphQL schema generation can be found in
`crates/schema/src/aggregates.rs`. This is where we generate the new
aggregate selection types. However, the root field generation can be
found in `crates/schema/src/query_root/select_aggregate.rs`.

The new `filter_input` type generation lives in
`crates/schema/src/model_filter_input.rs`. As this type effectively
encapsulates the existing field arguments used on the Select Many root
field, the code to generate them has moved into `model_filter_input.rs`
and `select_many.rs` simply reuses the functionality from there (without
actually using the filter input type!).

### IR
The main aggregates IR generation for the aggregate root field happens
in `crates/execute/src/ir/query_root/select_aggregate.rs`. It reads all
the input arguments to the root field and then kicks the selection logic
over to `model_aggregate_selection_ir` from
`crates/execute/src/ir/model_selection.rs`.

`crates/execute/src/ir/model_selection.rs` has received some refactoring
to facilitate that new `model_aggregate_selection_ir` function; it
mostly shares functionality with the existing `model_selection_ir`,
except instead of creating fields IR, it creates aggregates IR instead.
The actual reading of the aggregate selection happens in
`crates/execute/src/ir/aggregates.rs`.

The aggregates selection IR captures the nested JSON structure of the
aggregate selection, because NDC does not return aggregates in the same
nested JSON structure as the GraphQL request. NDC takes a flat list of
aggregate operations to run. This captured nested JSON structure is used
during response rewriting to convert NDC's flat list into the nested
structure that matches the GraphQL request.

The aggregate selection IR is placed onto `ModelSelection` alongside the
existing fields IR. Since both fields and aggregates can be put into the
one NDC request (even though they are not right now), this made sense.
They both translate onto one NDC `Query`. This necessitated making the
field selection optional on the `ModelSelection`
(`ModelSelection.selection`), since aggregate requests currently don't
use them.

### Planning
`crates/execute/src/plan/model_selection.rs` takes care of mapping the
aggregates into the NDC request from the generated IR.

There has been a new `ProcessResponseAs` variant added in
`crates/execute/src/plan.rs` to capture how to read and reshape an NDC
aggregates response. This is handled in
`crates/execute/src/process_response.rs` where the captured JSON
structure in the IR is used to restore NDC's flat aggregates list into
the required nested JSON output structure.

### Testing
The Custom Connector has been updated with functionality to allow
aggregates over nested object fields
(`crates/custom-connector/src/query.rs`).

New execution and introspection tests have been added to
`crates/engine/tests/execute/aggregates/` to test aggregates against
Postgres and the Custom Connector.

[V3ENGINE-159]:
https://hasurahq.atlassian.net/browse/V3ENGINE-159?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

V3_GIT_ORIGIN_REV_ID: ff47f13eaca70d10de21e102a6667110f8f8af40
This commit is contained in:
Daniel Chambers 2024-06-12 19:01:22 +10:00 committed by hasura-bot
parent 81ac867d16
commit 1c5008df7c
57 changed files with 14082 additions and 223 deletions

View File

@ -222,19 +222,23 @@ fn eval_aggregate(
ndc_models::Aggregate::StarCount {} => Ok(serde_json::Value::from(paginated.len())),
ndc_models::Aggregate::ColumnCount {
column,
field_path: _,
field_path,
distinct,
} => {
let values = paginated
.iter()
.map(|row| {
row.get(column).ok_or((
let column_value = row.get(column).ok_or((
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "invalid column name".into(),
details: serde_json::Value::Null,
}),
))
))?;
let field_path_slice = field_path
.as_ref()
.map_or_else(|| [].as_ref(), |p| p.as_slice());
extract_nested_field(column_value, field_path_slice)
})
.collect::<Result<Vec<_>>>()?;
@ -270,19 +274,23 @@ fn eval_aggregate(
}
ndc_models::Aggregate::SingleColumn {
column,
field_path: _,
field_path,
function,
} => {
let values = paginated
.iter()
.map(|row| {
row.get(column).ok_or((
let column_value = row.get(column).ok_or((
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "invalid column name".into(),
details: serde_json::Value::Null,
}),
))
))?;
let field_path_slice = field_path
.as_ref()
.map_or_else(|| [].as_ref(), |p| p.as_slice());
extract_nested_field(column_value, field_path_slice)
})
.collect::<Result<Vec<_>>>()?;
eval_aggregate_function(function, &values)
@ -290,33 +298,151 @@ fn eval_aggregate(
}
}
fn extract_nested_field<'a>(
value: &'a serde_json::Value,
field_path: &[String],
) -> Result<&'a serde_json::Value> {
if let Some((field, remaining_field_path)) = field_path.split_first() {
// Short circuit on null values
if value.is_null() {
return Ok(value);
}
let object_value = value.as_object().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message:
"expected object value when extracting a nested field from a field path"
.into(),
details: serde_json::Value::Null,
}),
)
})?;
let field_value = object_value.get(field).ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: format!("could not find field {} in nested object", field),
details: serde_json::Value::Null,
}),
)
})?;
extract_nested_field(field_value, remaining_field_path)
} else {
Ok(value)
}
}
fn eval_aggregate_function(
function: &str,
values: &[&serde_json::Value],
) -> Result<serde_json::Value> {
if let Some((first_value, _)) = values.split_first() {
if first_value.is_i64() {
eval_aggregate_function_i64(function, values)
} else if first_value.is_string() {
eval_aggregate_function_string(function, values)
} else {
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ndc_models::ErrorResponse {
message: "Can only aggregate over i64 or string values".into(),
details: serde_json::Value::Null,
}),
))
}
} else {
// This is a bit of a hack, as technically what this value is should be dependent
// on what type the aggregate function operand is, but it is valid while we only
// support min and max aggregate functions.
Ok(serde_json::Value::Null)
}
}
fn eval_aggregate_function_i64(
function: &str,
values: &[&serde_json::Value],
) -> Result<serde_json::Value> {
let int_values = values
.iter()
.map(|value| {
value.as_i64().ok_or((
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "column is not an integer".into(),
details: serde_json::Value::Null,
}),
))
.filter_map(|value| {
if value.is_null() {
None
} else {
Some(value.as_i64().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "aggregate value is not an integer".into(),
details: (*value).clone(),
}),
)
}))
}
})
.collect::<Result<Vec<_>>>()?;
let agg_value = match function {
"min" => Ok(int_values.iter().min()),
"max" => Ok(int_values.iter().max()),
"min" => Ok(int_values.into_iter().min()),
"max" => Ok(int_values.into_iter().max()),
_ => Err((
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "invalid aggregation function".into(),
details: serde_json::Value::Null,
message: "invalid integer aggregation function".into(),
details: Value::String(function.to_string()),
}),
)),
}?;
serde_json::to_value(agg_value).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ndc_models::ErrorResponse {
message: " ".into(),
details: serde_json::Value::Null,
}),
)
})
}
fn eval_aggregate_function_string(
function: &str,
values: &[&serde_json::Value],
) -> Result<serde_json::Value> {
let str_values = values
.iter()
.filter_map(|value| {
if value.is_null() {
None
} else {
Some(value.as_str().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "aggregate value is not a string".into(),
details: (*value).clone(),
}),
)
}))
}
})
.collect::<Result<Vec<_>>>()?;
let agg_value = match function {
"min" => Ok(str_values.into_iter().min()),
"max" => Ok(str_values.into_iter().max()),
_ => Err((
StatusCode::BAD_REQUEST,
Json(ndc_models::ErrorResponse {
message: "invalid string aggregation function".into(),
details: Value::String(function.to_string()),
}),
)),
}?;
serde_json::to_value(agg_value).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,

View File

@ -25,7 +25,7 @@ pub fn get_capabilities() -> ndc_models::CapabilitiesResponse {
aggregates: Some(ndc_models::LeafCapability {}),
variables: Some(ndc_models::LeafCapability {}),
nested_fields: ndc_models::NestedFieldCapabilities {
aggregates: None,
aggregates: Some(ndc_models::LeafCapability {}),
filter_by: None,
order_by: None,
},

View File

@ -16,7 +16,28 @@ pub(crate) fn scalar_types() -> BTreeMap<String, ndc_models::ScalarType> {
"String".into(),
ndc_models::ScalarType {
representation: Some(ndc_models::TypeRepresentation::String),
aggregate_functions: BTreeMap::new(),
aggregate_functions: BTreeMap::from_iter([
(
"max".into(),
ndc_models::AggregateFunctionDefinition {
result_type: ndc_models::Type::Nullable {
underlying_type: Box::new(ndc_models::Type::Named {
name: "String".into(),
}),
},
},
),
(
"min".into(),
ndc_models::AggregateFunctionDefinition {
result_type: ndc_models::Type::Nullable {
underlying_type: Box::new(ndc_models::Type::Named {
name: "String".into(),
}),
},
},
),
]),
comparison_operators: BTreeMap::from_iter([(
"like".into(),
ndc_models::ComparisonOperatorDefinition::Custom {

View File

@ -0,0 +1,667 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"name": "custom_connector",
"url": {
"singleUrl": {
"value": "http://custom_connector:8101"
}
},
"schema": {
"version": "v0.1",
"schema": {
"scalar_types": {
"Actor_Name": {
"representation": {
"type": "string"
},
"aggregate_functions": {},
"comparison_operators": {}
},
"Bool": {
"representation": {
"type": "boolean"
},
"aggregate_functions": {},
"comparison_operators": {
"eq": {
"type": "custom",
"argument_type": {
"type": "named",
"name": "Bool"
}
}
}
},
"HeaderMap": {
"representation": {
"type": "json"
},
"aggregate_functions": {},
"comparison_operators": {}
},
"Int": {
"representation": {
"type": "int32"
},
"aggregate_functions": {
"max": {
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "Int"
}
}
},
"min": {
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "Int"
}
}
}
},
"comparison_operators": {}
},
"String": {
"representation": {
"type": "string"
},
"aggregate_functions": {
"max": {
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "String"
}
}
},
"min": {
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "String"
}
}
}
},
"comparison_operators": {
"like": {
"type": "custom",
"argument_type": {
"type": "named",
"name": "String"
}
}
}
}
},
"object_types": {
"actor": {
"description": "An actor",
"fields": {
"favourite_author_id": {
"description": "The actor's favourite author ID",
"type": {
"type": "named",
"name": "Int"
}
},
"id": {
"description": "The actor's primary key",
"type": {
"type": "named",
"name": "Int"
}
},
"movie_id": {
"description": "The actor's movie ID",
"type": {
"type": "named",
"name": "Int"
}
},
"name": {
"description": "The actor's name",
"type": {
"type": "named",
"name": "String"
}
}
}
},
"institution": {
"description": "An institution",
"fields": {
"departments": {
"description": "The institution's departments",
"type": {
"type": "array",
"element_type": {
"type": "named",
"name": "String"
}
}
},
"id": {
"description": "The institution's primary key",
"type": {
"type": "named",
"name": "Int"
}
},
"location": {
"description": "The institution's location",
"type": {
"type": "named",
"name": "location"
}
},
"name": {
"description": "The institution's name",
"type": {
"type": "named",
"name": "String"
}
},
"staff": {
"description": "The institution's staff",
"type": {
"type": "array",
"element_type": {
"type": "named",
"name": "staff_member"
}
}
}
}
},
"location": {
"description": "A location",
"fields": {
"campuses": {
"description": "The location's campuses",
"type": {
"type": "array",
"element_type": {
"type": "named",
"name": "String"
}
}
},
"city": {
"description": "The location's city",
"type": {
"type": "named",
"name": "String"
}
},
"country": {
"description": "The location's country",
"type": {
"type": "named",
"name": "String"
}
}
}
},
"login_response": {
"description": "Response to a login action",
"fields": {
"headers": {
"description": "Response headers to be forwarded",
"type": {
"type": "named",
"name": "HeaderMap"
}
},
"response": {
"description": "Authentication successful or not",
"type": {
"type": "named",
"name": "Bool"
}
}
}
},
"movie": {
"description": "A movie",
"fields": {
"id": {
"description": "The movie's primary key",
"type": {
"type": "named",
"name": "Int"
}
},
"rating": {
"description": "The movie's rating",
"type": {
"type": "named",
"name": "Int"
}
},
"title": {
"description": "The movie's title",
"type": {
"type": "named",
"name": "String"
}
}
}
},
"name_query": {
"description": "parameters for querying by name",
"fields": {
"first_name": {
"description": "The actor's first name or null to match any first name",
"type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "String"
}
}
},
"last_name": {
"description": "The actor's last name or null to match any last",
"type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "String"
}
}
}
}
},
"staff_member": {
"description": "A staff member",
"fields": {
"first_name": {
"description": "The staff member's first name",
"type": {
"type": "named",
"name": "String"
}
},
"last_name": {
"description": "The staff member's last name",
"type": {
"type": "named",
"name": "String"
}
},
"specialities": {
"description": "The staff member's specialities",
"type": {
"type": "array",
"element_type": {
"type": "named",
"name": "String"
}
}
}
}
}
},
"collections": [
{
"name": "actors",
"description": "A collection of actors",
"arguments": {},
"type": "actor",
"uniqueness_constraints": {
"ActorByID": {
"unique_columns": ["id"]
}
},
"foreign_keys": {}
},
{
"name": "movies",
"description": "A collection of movies",
"arguments": {},
"type": "movie",
"uniqueness_constraints": {
"MovieByID": {
"unique_columns": ["id"]
}
},
"foreign_keys": {}
},
{
"name": "institutions",
"description": "A collection of institutions",
"arguments": {},
"type": "institution",
"uniqueness_constraints": {
"InstitutionByID": {
"unique_columns": ["id"]
}
},
"foreign_keys": {}
},
{
"name": "actors_by_movie",
"description": "Actors parameterized by movie",
"arguments": {
"movie_id": {
"type": {
"type": "named",
"name": "Int"
}
}
},
"type": "actor",
"uniqueness_constraints": {},
"foreign_keys": {}
},
{
"name": "movies_by_actor_name",
"description": "Movies filtered by actor name search parameters",
"arguments": {
"actor_name": {
"description": "the actor name components to search by",
"type": {
"type": "named",
"name": "name_query"
}
}
},
"type": "movie",
"uniqueness_constraints": {},
"foreign_keys": {}
}
],
"functions": [
{
"name": "latest_actor_id",
"description": "Get the ID of the most recent actor",
"arguments": {},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "Int"
}
}
},
{
"name": "latest_actor_name",
"description": "Get the name of the most recent actor",
"arguments": {},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "Actor_Name"
}
}
},
{
"name": "latest_actor",
"description": "Get the most recent actor",
"arguments": {},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "get_actor_by_id",
"description": "Get actor by ID",
"arguments": {
"id": {
"description": "the id of the actor to fetch",
"type": {
"type": "named",
"name": "Int"
}
}
},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "get_movie_by_id",
"description": "Get movie by ID",
"arguments": {
"movie_id": {
"description": "the id of the movie to fetch",
"type": {
"type": "named",
"name": "Int"
}
}
},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "movie"
}
}
},
{
"name": "get_actors_by_name",
"description": "Get actors by name",
"arguments": {
"name": {
"description": "the name components to search by",
"type": {
"type": "named",
"name": "name_query"
}
}
},
"result_type": {
"type": "array",
"element_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "get_actors_by_movie_id",
"description": "Get all actors from a movie by movie ID",
"arguments": {
"movie_id": {
"description": "the id of the movie to fetch the actors from",
"type": {
"type": "named",
"name": "Int"
}
}
},
"result_type": {
"type": "array",
"element_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "get_all_actors",
"description": "Get all the actors",
"arguments": {},
"result_type": {
"type": "array",
"element_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "get_all_movies",
"description": "Get all the movies",
"arguments": {},
"result_type": {
"type": "array",
"element_type": {
"type": "named",
"name": "movie"
}
}
},
{
"name": "get_institutions_by_institution_query",
"description": "Get institutions by specifying parts of institution object. For example by 'location.city'. All fields are optional.",
"arguments": {
"institution_query": {
"description": "The institution query object. All fields are optional",
"type": {
"type": "named",
"name": "institution"
}
}
},
"result_type": {
"type": "array",
"element_type": {
"type": "named",
"name": "institution"
}
}
}
],
"procedures": [
{
"name": "upsert_actor",
"description": "Insert or update an actor",
"arguments": {
"actor": {
"description": "The actor to insert or update",
"type": {
"type": "named",
"name": "actor"
}
}
},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "update_actor_name_by_id",
"description": "Update an actor name given the ID and new name",
"arguments": {
"id": {
"description": "the id of the actor to update",
"type": {
"type": "named",
"name": "Int"
}
},
"name": {
"description": "the new name of the actor",
"type": {
"type": "named",
"name": "String"
}
}
},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "actor"
}
}
},
{
"name": "login",
"description": "Perform a user login",
"arguments": {
"headers": {
"description": "headers required for authentication",
"type": {
"type": "named",
"name": "HeaderMap"
}
},
"password": {
"description": "password of the user",
"type": {
"type": "named",
"name": "String"
}
},
"username": {
"description": "username of the user",
"type": {
"type": "named",
"name": "String"
}
}
},
"result_type": {
"type": "named",
"name": "login_response"
}
},
{
"name": "noop_procedure",
"description": "Procedure which does not perform any actual mutuations on the data",
"arguments": {},
"result_type": {
"type": "nullable",
"underlying_type": {
"type": "named",
"name": "String"
}
}
}
]
},
"capabilities": {
"version": "0.1.3",
"capabilities": {
"query": {
"aggregates": {},
"variables": {},
"nested_fields": {
"aggregates": {}
}
},
"mutation": {},
"relationships": {
"relation_comparisons": {},
"order_by_aggregate": {}
}
}
}
}
},
"version": "v1",
"kind": "DataConnectorLink"
}
]
}
]
}

View File

@ -0,0 +1,217 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "Institution",
"fields": [
{
"name": "Departments",
"type": "[String!]!",
"description": "The institution's departments"
},
{
"name": "Id",
"type": "Int",
"description": "The institution's primary key"
},
{
"name": "Location",
"type": "Location!",
"description": "The institution's location"
},
{
"name": "Name",
"type": "String!",
"description": "The institution's name"
},
{
"name": "Staff",
"type": "[StaffMember!]!",
"description": "The institution's staff"
}
],
"description": "An institution",
"graphql": {
"typeName": "Institution"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "custom_connector",
"dataConnectorObjectType": "institution",
"fieldMapping": {
"Departments": {
"column": {
"name": "departments"
}
},
"Id": {
"column": {
"name": "id"
}
},
"Location": {
"column": {
"name": "location"
}
},
"Name": {
"column": {
"name": "name"
}
},
"Staff": {
"column": {
"name": "staff"
}
}
}
}
]
}
}
]
},
{
"name": "default",
"objects": [
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "Location",
"fields": [
{
"name": "Campuses",
"type": "[String!]!",
"description": "The location's campuses"
},
{
"name": "City",
"type": "String!",
"description": "The location's city"
},
{
"name": "Country",
"type": "String!",
"description": "The location's city"
}
],
"description": "A location",
"graphql": {
"typeName": "Location"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "custom_connector",
"dataConnectorObjectType": "location",
"fieldMapping": {
"Campuses": {
"column": {
"name": "campuses"
}
},
"City": {
"column": {
"name": "city"
}
},
"Country": {
"column": {
"name": "country"
}
}
}
}
]
}
}
]
},
{
"name": "default",
"objects": [
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "StaffMember",
"fields": [
{
"name": "FirstName",
"type": "String!",
"description": "The staff member's first name"
},
{
"name": "LastName",
"type": "String!",
"description": "The staff member's last name"
},
{
"name": "Specialities",
"type": "[String!]!",
"description": "The staff member's specialities"
}
],
"description": "A staff member",
"graphql": {
"typeName": "StaffMember"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "custom_connector",
"dataConnectorObjectType": "staff_member",
"fieldMapping": {
"FirstName": {
"column": {
"name": "first_name"
}
},
"LastName": {
"column": {
"name": "last_name"
}
},
"Specialities": {
"column": {
"name": "specialities"
}
}
}
}
]
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "custom_connector",
"dataConnectorScalarType": "String",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "custom_connector_String_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "custom_connector",
"dataConnectorScalarType": "Int",
"representation": "Int",
"graphql": {
"comparisonExpressionTypeName": "custom_connector_Int_comparisonexp"
}
}
}
]
}
]
}

View File

@ -0,0 +1,226 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "Invoice",
"fields": [
{
"name": "BillingAddress",
"type": "String",
"description": "The billing street address"
},
{
"name": "BillingCity",
"type": "String",
"description": "The billing address city"
},
{
"name": "BillingCountry",
"type": "String",
"description": "The billing address country"
},
{
"name": "BillingPostalCode",
"type": "String",
"description": "The billing address postal code"
},
{
"name": "BillingState",
"type": "String",
"description": "The billing address state"
},
{
"name": "CustomerId",
"type": "Int",
"description": "The ID of the Customer"
},
{
"name": "InvoiceDate",
"type": "Timestamp",
"description": "The date of the invoice"
},
{
"name": "InvoiceId",
"type": "Int",
"description": "The ID of the Invoice"
},
{
"name": "Total",
"type": "Numeric",
"description": "The total value of the Invoice"
}
],
"description": "An invoice where a customer purchases things",
"graphql": {
"typeName": "Invoice"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "Invoice",
"fieldMapping": {
"BillingAddress": {
"column": {
"name": "BillingAddress"
}
},
"BillingCity": {
"column": {
"name": "BillingCity"
}
},
"BillingCountry": {
"column": {
"name": "BillingCountry"
}
},
"BillingPostalCode": {
"column": {
"name": "BillingPostalCode"
}
},
"BillingState": {
"column": {
"name": "BillingState"
}
},
"CustomerId": {
"column": {
"name": "CustomerId"
}
},
"InvoiceDate": {
"column": {
"name": "InvoiceDate"
}
},
"InvoiceId": {
"column": {
"name": "InvoiceId"
}
},
"Total": {
"column": {
"name": "Total"
}
}
}
}
]
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "Timestamp",
"description": "Timestamp type",
"graphql": {
"typeName": "Timestamp"
}
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "Numeric",
"description": "Numeric type",
"graphql": {
"typeName": "Numeric"
}
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "Int64",
"description": "Int64 type",
"graphql": {
"typeName": "Int64"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "timestamp",
"representation": "Timestamp",
"graphql": {
"comparisonExpressionTypeName": "db_timestamp_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "numeric",
"representation": "Numeric",
"graphql": {
"comparisonExpressionTypeName": "db_numeric_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "varchar",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "db_varchar_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "int4",
"representation": "Int",
"graphql": {
"comparisonExpressionTypeName": "db_int4_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "int8",
"representation": "Int64",
"graphql": {
"comparisonExpressionTypeName": "db_int8_comparisonexp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "text",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "db_text_comparisonexp"
}
}
}
]
}
]
}

View File

@ -0,0 +1,56 @@
{
"version": "v2",
"supergraph": {
"objects": [
{
"kind": "GraphqlConfig",
"version": "v1",
"definition": {
"query": {
"rootOperationTypeName": "Query",
"argumentsInput": {
"fieldName": "args"
},
"limitInput": {
"fieldName": "limit"
},
"offsetInput": {
"fieldName": "offset"
},
"filterInput": {
"fieldName": "where",
"operatorNames": {
"and": "_and",
"or": "_or",
"not": "_not",
"isNull": "_is_null"
}
},
"orderByInput": {
"fieldName": "order_by",
"enumDirectionValues": {
"asc": "Asc",
"desc": "Desc"
},
"enumTypeNames": [
{
"directions": ["Asc", "Desc"],
"typeName": "OrderBy"
}
]
},
"aggregate": {
"filterInputFieldName": "filter_input",
"countFieldName": "_count",
"countDistinctFieldName": "_count_distinct"
}
},
"mutation": {
"rootOperationTypeName": "Mutation"
},
"apolloFederation": null
}
}
]
}
}

View File

@ -0,0 +1,33 @@
[
{
"data": {
"Invoice_aggregate": {
"BillingState": {
"_min": "AZ",
"_max": "WI",
"_count_distinct": 8
},
"InvoiceId": {
"min": 17,
"max": 71,
"count": 10
},
"Total": {
"_min": 0.99,
"_max": 13.86,
"_sum": 58.41,
"_stddev": 3.944846004598912
},
"count_all": 10
}
}
},
{
"data": null,
"errors": [
{
"message": "validation failed: the field BillingCountry on type Invoice_boolexp is not found"
}
]
}
]

View File

@ -0,0 +1,154 @@
query MyQuery {
__schema {
queryType {
name
fields {
name
description
args {
name
description
defaultValue
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
Invoice_filter_input_type: __type(name: "Invoice_filter_input") {
name
description
kind
inputFields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Invoice_aggregate_exp_type: __type(name: "Invoice_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
String_aggregate_exp_type: __type(name: "String_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Int_aggregate_exp_type: __type(name: "Int_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Numeric_aggregate_exp_type: __type(name: "Numeric_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Timestamp_aggregate_exp_type: __type(name: "Timestamp_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}

View File

@ -0,0 +1,521 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "Invoice",
"objectType": "Invoice",
"source": {
"dataConnectorName": "db",
"collection": "Invoice"
},
"filterExpressionType": "Invoice_boolexp",
"aggregateExpression": "Invoice_aggregate_exp",
"orderableFields": [
{
"fieldName": "BillingAddress",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingCity",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingCountry",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingPostalCode",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingState",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "CustomerId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "InvoiceDate",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "InvoiceId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Total",
"orderByDirections": {
"enableAll": true
}
}
],
"graphql": {
"filterInputTypeName": "Invoice_filter_input",
"aggregate": {
"queryRootField": "Invoice_aggregate"
},
"selectMany": {
"queryRootField": "Invoice"
},
"selectUniques": [
{
"queryRootField": "InvoiceByInvoiceId",
"uniqueIdentifier": ["InvoiceId"]
}
],
"orderByExpressionType": "Invoice_orderby"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Invoice_aggregate_exp",
"operand": {
"object": {
"aggregatedType": "Invoice",
"aggregatableFields": [
{
"fieldName": "BillingAddress",
"description": "Aggregation over the billing address",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingCity",
"description": "Aggregation over the billing city",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingCountry",
"description": "Aggregation over the billing country",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingPostalCode",
"description": "Aggregation over the billing postal code",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingState",
"description": "Aggregation over the billing state",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "CustomerId",
"description": "Aggregation over the customer ID",
"aggregateExpression": "Int_aggregate_exp"
},
{
"fieldName": "InvoiceDate",
"description": "Aggregation over the invoice date",
"aggregateExpression": "Timestamp_aggregate_exp"
},
{
"fieldName": "InvoiceId",
"description": "Aggregation over the invoice ID",
"aggregateExpression": "Int_aggregate_exp"
},
{
"fieldName": "Total",
"description": "Aggregation over the invoice total",
"aggregateExpression": "Numeric_aggregate_exp"
}
]
}
},
"count": {
"enable": true,
"description": "Count of invoices"
},
"description": "Aggregate expression for the Invoice type",
"graphql": {
"selectTypeName": "Invoice_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Int_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Int",
"aggregationFunctions": [
{
"name": "_sum",
"description": "Sum of all integers",
"returnType": "Int64!"
},
{
"name": "_min",
"description": "Smallest integer",
"returnType": "Int!"
},
{
"name": "_max",
"description": "Largest integer",
"returnType": "Int!"
},
{
"name": "_stddev",
"description": "Standard deviation across integers",
"returnType": "Numeric!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "int4",
"functionMapping": {
"_sum": {
"name": "sum"
},
"_min": {
"name": "min"
},
"_max": {
"name": "max"
},
"_stddev": {
"name": "stddev"
}
}
}
]
}
},
"count": {
"enable": true,
"description": "Count of all non-null integers"
},
"countDistinct": {
"enable": true,
"description": "Count of all distinct non-null integers"
},
"description": "Aggregate expression for the Int type",
"graphql": {
"selectTypeName": "Int_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Numeric_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Numeric",
"aggregationFunctions": [
{
"name": "_sum",
"returnType": "Numeric!"
},
{
"name": "_min",
"returnType": "Numeric!"
},
{
"name": "_max",
"returnType": "Numeric!"
},
{
"name": "_stddev",
"returnType": "Numeric!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "numeric",
"functionMapping": {
"_sum": {
"name": "sum"
},
"_min": {
"name": "min"
},
"_max": {
"name": "max"
},
"_stddev": {
"name": "stddev"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the Numeric type",
"graphql": {
"selectTypeName": "Numeric_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "String_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "String",
"aggregationFunctions": [
{
"name": "_min",
"returnType": "String!"
},
{
"name": "_max",
"returnType": "String!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "varchar",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
},
{
"dataConnectorName": "db",
"dataConnectorScalarType": "text",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the String type",
"graphql": {
"selectTypeName": "String_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Timestamp_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Timestamp",
"aggregationFunctions": [
{
"name": "_min",
"returnType": "Timestamp!"
},
{
"name": "_max",
"returnType": "Timestamp!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "timestamp",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the Timestamp type",
"graphql": {
"selectTypeName": "Timestamp_aggregate_exp"
}
}
},
{
"kind": "ObjectBooleanExpressionType",
"version": "v1",
"definition": {
"name": "Invoice_boolexp",
"objectType": "Invoice",
"dataConnectorName": "db",
"dataConnectorObjectType": "Invoice",
"comparableFields": [
{
"fieldName": "BillingAddress",
"operators": {
"enableAll": true
}
},
{
"fieldName": "BillingCity",
"operators": {
"enableAll": true
}
},
{
"fieldName": "BillingCountry",
"operators": {
"enableAll": true
}
},
{
"fieldName": "BillingPostalCode",
"operators": {
"enableAll": true
}
},
{
"fieldName": "BillingState",
"operators": {
"enableAll": true
}
},
{
"fieldName": "CustomerId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "InvoiceDate",
"operators": {
"enableAll": true
}
},
{
"fieldName": "InvoiceId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Total",
"operators": {
"enableAll": true
}
}
],
"graphql": {
"typeName": "Invoice_boolexp"
}
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "Invoice",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"BillingAddress",
"BillingCity",
"BillingCountry",
"BillingPostalCode",
"BillingState",
"CustomerId",
"InvoiceDate",
"InvoiceId",
"Total"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"BillingPostalCode",
"BillingState",
"CustomerId",
"InvoiceDate",
"InvoiceId",
"Total"
]
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "Invoice",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,28 @@
query {
Invoice_aggregate(
filter_input: {
where: { BillingCountry: { _eq: "USA" } }
order_by: { InvoiceId: Asc }
offset: 5
limit: 10
}
) {
BillingState {
_min
_max
_count_distinct
}
InvoiceId {
min: _min
max: _max
count: _count
}
Total {
_min
_max
_sum
_stddev
}
count_all: _count
}
}

View File

@ -0,0 +1,8 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user"
}
]

View File

@ -0,0 +1,28 @@
[
{
"data": {
"Institution_aggregate": {
"Location": {
"_count": 2,
"City": {
"_min": "Gothenburg",
"_max": "London"
},
"Country": {
"min_country": "Sweden",
"max_country": "UK"
}
},
"count_all": 3
}
}
},
{
"data": null,
"errors": [
{
"message": "validation failed: no such field on type Location_aggregate_exp: City"
}
]
}
]

View File

@ -0,0 +1,559 @@
[
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "Institution",
"description": null,
"args": [
{
"name": "limit",
"description": null,
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "offset",
"description": null,
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
},
{
"name": "Institution_aggregate",
"description": null,
"args": [
{
"name": "filter_input",
"description": null,
"defaultValue": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "Institution_filter_input",
"ofType": null
}
}
]
}
]
}
},
"Institution_filter_input_type": {
"name": "Institution_filter_input",
"description": null,
"kind": "INPUT_OBJECT",
"inputFields": [
{
"name": "limit",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "offset",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
},
"Institution_aggregate_exp_type": {
"name": "Institution_aggregate_exp",
"description": "Aggregate expression for the Institution type",
"kind": "OBJECT",
"fields": [
{
"name": "Id",
"description": "Aggregation over the institution id",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Int_aggregate_exp",
"ofType": null
}
}
},
{
"name": "Location",
"description": "Aggregation over the institution's location",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Location_aggregate_exp",
"ofType": null
}
}
},
{
"name": "Name",
"description": "Aggregation over the institution name",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "String_aggregate_exp",
"ofType": null
}
}
},
{
"name": "_count",
"description": "Count of institutions",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
]
},
"Location_aggregate_exp_type": {
"name": "Location_aggregate_exp",
"description": "Aggregate expression for the Location type",
"kind": "OBJECT",
"fields": [
{
"name": "City",
"description": "Aggregation over the location city",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "String_aggregate_exp",
"ofType": null
}
}
},
{
"name": "Country",
"description": "Aggregation over the location country",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "String_aggregate_exp",
"ofType": null
}
}
},
{
"name": "_count",
"description": "Count of locations",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
]
},
"String_aggregate_exp_type": {
"name": "String_aggregate_exp",
"description": "Aggregate expression for the String type",
"kind": "OBJECT",
"fields": [
{
"name": "_count",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_count_distinct",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_max",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
{
"name": "_min",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
]
},
"Int_aggregate_exp_type": {
"name": "Int_aggregate_exp",
"description": "Aggregate expression for the Int type",
"kind": "OBJECT",
"fields": [
{
"name": "_count",
"description": "Count of all non-null integers",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_count_distinct",
"description": "Count of all distinct non-null integers",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_max",
"description": "Largest integer",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "_min",
"description": "Smallest integer",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
}
}
},
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "Institution",
"description": null,
"args": [
{
"name": "limit",
"description": null,
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "offset",
"description": null,
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
},
{
"name": "Institution_aggregate",
"description": null,
"args": [
{
"name": "filter_input",
"description": null,
"defaultValue": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "Institution_filter_input",
"ofType": null
}
}
]
}
]
}
},
"Institution_filter_input_type": {
"name": "Institution_filter_input",
"description": null,
"kind": "INPUT_OBJECT",
"inputFields": [
{
"name": "limit",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "offset",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
},
"Institution_aggregate_exp_type": {
"name": "Institution_aggregate_exp",
"description": "Aggregate expression for the Institution type",
"kind": "OBJECT",
"fields": [
{
"name": "Id",
"description": "Aggregation over the institution id",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Int_aggregate_exp",
"ofType": null
}
}
},
{
"name": "Location",
"description": "Aggregation over the institution's location",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Location_aggregate_exp",
"ofType": null
}
}
},
{
"name": "Name",
"description": "Aggregation over the institution name",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "String_aggregate_exp",
"ofType": null
}
}
},
{
"name": "_count",
"description": "Count of institutions",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
]
},
"Location_aggregate_exp_type": {
"name": "Location_aggregate_exp",
"description": "Aggregate expression for the Location type",
"kind": "OBJECT",
"fields": [
{
"name": "Country",
"description": "Aggregation over the location country",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "String_aggregate_exp",
"ofType": null
}
}
},
{
"name": "_count",
"description": "Count of locations",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
]
},
"String_aggregate_exp_type": {
"name": "String_aggregate_exp",
"description": "Aggregate expression for the String type",
"kind": "OBJECT",
"fields": [
{
"name": "_count",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_count_distinct",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_max",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
{
"name": "_min",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
]
},
"Int_aggregate_exp_type": {
"name": "Int_aggregate_exp",
"description": "Aggregate expression for the Int type",
"kind": "OBJECT",
"fields": [
{
"name": "_count",
"description": "Count of all non-null integers",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_count_distinct",
"description": "Count of all distinct non-null integers",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
},
{
"name": "_max",
"description": "Largest integer",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"name": "_min",
"description": "Smallest integer",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
]
}
}
}
]

View File

@ -0,0 +1,133 @@
query MyQuery {
__schema {
queryType {
name
fields {
name
description
args {
name
description
defaultValue
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
Institution_filter_input_type: __type(name: "Institution_filter_input") {
name
description
kind
inputFields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Institution_aggregate_exp_type: __type(name: "Institution_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Location_aggregate_exp_type: __type(name: "Location_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
String_aggregate_exp_type: __type(name: "String_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Int_aggregate_exp_type: __type(name: "Int_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}

View File

@ -0,0 +1,326 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "Institution",
"objectType": "Institution",
"source": {
"dataConnectorName": "custom_connector",
"collection": "institutions"
},
"aggregateExpression": "Institution_aggregate_exp",
"orderableFields": [
{
"fieldName": "Departments",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Id",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Location",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Staff",
"orderByDirections": {
"enableAll": true
}
}
],
"graphql": {
"filterInputTypeName": "Institution_filter_input",
"aggregate": {
"queryRootField": "Institution_aggregate"
},
"selectMany": {
"queryRootField": "Institution"
},
"selectUniques": []
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Institution_aggregate_exp",
"operand": {
"object": {
"aggregatedType": "Institution",
"aggregatableFields": [
{
"fieldName": "Id",
"description": "Aggregation over the institution id",
"aggregateExpression": "Int_aggregate_exp"
},
{
"fieldName": "Name",
"description": "Aggregation over the institution name",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "Location",
"description": "Aggregation over the institution's location",
"aggregateExpression": "Location_aggregate_exp"
}
]
}
},
"count": {
"enable": true,
"description": "Count of institutions"
},
"description": "Aggregate expression for the Institution type",
"graphql": {
"selectTypeName": "Institution_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Location_aggregate_exp",
"operand": {
"object": {
"aggregatedType": "Location",
"aggregatableFields": [
{
"fieldName": "City",
"description": "Aggregation over the location city",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "Country",
"description": "Aggregation over the location country",
"aggregateExpression": "String_aggregate_exp"
}
]
}
},
"count": {
"enable": true,
"description": "Count of locations"
},
"description": "Aggregate expression for the Location type",
"graphql": {
"selectTypeName": "Location_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Int_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Int",
"aggregationFunctions": [
{
"name": "_min",
"description": "Smallest integer",
"returnType": "Int"
},
{
"name": "_max",
"description": "Largest integer",
"returnType": "Int"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "custom_connector",
"dataConnectorScalarType": "Int",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true,
"description": "Count of all non-null integers"
},
"countDistinct": {
"enable": true,
"description": "Count of all distinct non-null integers"
},
"description": "Aggregate expression for the Int type",
"graphql": {
"selectTypeName": "Int_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "String_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "String",
"aggregationFunctions": [
{
"name": "_min",
"returnType": "String"
},
{
"name": "_max",
"returnType": "String"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "custom_connector",
"dataConnectorScalarType": "String",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the String type",
"graphql": {
"selectTypeName": "String_aggregate_exp"
}
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "Institution",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"Departments",
"Id",
"Location",
"Name",
"Staff"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"Departments",
"Id",
"Location",
"Name",
"Staff"
]
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "Location",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["Campuses", "City", "Country"]
}
},
{
"role": "user",
"output": {
"allowedFields": ["Country"]
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "StaffMember",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["FirstName", "LastName", "Specialities"]
}
},
{
"role": "user",
"output": {
"allowedFields": ["FirstName", "LastName", "Specialities"]
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "Institution",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,16 @@
query {
Institution_aggregate {
Location {
_count
City {
_min
_max
}
Country {
min_country: _min
max_country: _max
}
}
count_all: _count
}
}

View File

@ -0,0 +1,8 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user"
}
]

View File

@ -0,0 +1,56 @@
[
{
"data": {
"Invoice_aggregate": {
"BillingCountry": {
"_min": "Argentina",
"_max": "USA",
"_count_distinct": 24
},
"InvoiceId": {
"min": 1,
"max": 412,
"count": 412
},
"Total": {
"_min": 0.99,
"_max": 25.86,
"_sum": 2328.6,
"_stddev": 4.745319693568106
},
"count_all": 412
}
}
},
{
"data": {
"Invoice_aggregate": {
"BillingCountry": {
"_min": "Australia",
"_max": "Australia",
"_count_distinct": 1
},
"InvoiceId": {
"min": 21,
"max": 305,
"count": 7
},
"Total": {
"_min": 0.99,
"_max": 13.86,
"_sum": 37.62,
"_stddev": 4.638483434424291
},
"count_all": 7
}
}
},
{
"data": null,
"errors": [
{
"message": "validation failed: no such field on type Invoice_aggregate_exp: BillingCountry"
}
]
}
]

View File

@ -0,0 +1,154 @@
query MyQuery {
__schema {
queryType {
name
fields {
name
description
args {
name
description
defaultValue
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
Invoice_filter_input_type: __type(name: "Invoice_filter_input") {
name
description
kind
inputFields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Invoice_aggregate_exp_type: __type(name: "Invoice_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
String_aggregate_exp_type: __type(name: "String_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Int_aggregate_exp_type: __type(name: "Int_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Numeric_aggregate_exp_type: __type(name: "Numeric_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
Timestamp_aggregate_exp_type: __type(name: "Timestamp_aggregate_exp") {
name
description
kind
fields {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}

View File

@ -0,0 +1,480 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "Invoice",
"objectType": "Invoice",
"source": {
"dataConnectorName": "db",
"collection": "Invoice"
},
"aggregateExpression": "Invoice_aggregate_exp",
"orderableFields": [
{
"fieldName": "BillingAddress",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingCity",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingCountry",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingPostalCode",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "BillingState",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "CustomerId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "InvoiceDate",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "InvoiceId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Total",
"orderByDirections": {
"enableAll": true
}
}
],
"graphql": {
"filterInputTypeName": "Invoice_filter_input",
"aggregate": {
"queryRootField": "Invoice_aggregate"
},
"selectMany": {
"queryRootField": "Invoice"
},
"selectUniques": [
{
"queryRootField": "InvoiceByInvoiceId",
"uniqueIdentifier": ["InvoiceId"]
}
]
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Invoice_aggregate_exp",
"operand": {
"object": {
"aggregatedType": "Invoice",
"aggregatableFields": [
{
"fieldName": "BillingAddress",
"description": "Aggregation over the billing address",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingCity",
"description": "Aggregation over the billing city",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingCountry",
"description": "Aggregation over the billing country",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingPostalCode",
"description": "Aggregation over the billing postal code",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "BillingState",
"description": "Aggregation over the billing state",
"aggregateExpression": "String_aggregate_exp"
},
{
"fieldName": "CustomerId",
"description": "Aggregation over the customer ID",
"aggregateExpression": "Int_aggregate_exp"
},
{
"fieldName": "InvoiceDate",
"description": "Aggregation over the invoice date",
"aggregateExpression": "Timestamp_aggregate_exp"
},
{
"fieldName": "InvoiceId",
"description": "Aggregation over the invoice ID",
"aggregateExpression": "Int_aggregate_exp"
},
{
"fieldName": "Total",
"description": "Aggregation over the invoice total",
"aggregateExpression": "Numeric_aggregate_exp"
}
]
}
},
"count": {
"enable": true,
"description": "Count of invoices"
},
"description": "Aggregate expression for the Invoice type",
"graphql": {
"selectTypeName": "Invoice_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Int_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Int",
"aggregationFunctions": [
{
"name": "_sum",
"description": "Sum of all integers",
"returnType": "Int64!"
},
{
"name": "_min",
"description": "Smallest integer",
"returnType": "Int!"
},
{
"name": "_max",
"description": "Largest integer",
"returnType": "Int!"
},
{
"name": "_stddev",
"description": "Standard deviation across integers",
"returnType": "Numeric!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "int4",
"functionMapping": {
"_sum": {
"name": "sum"
},
"_min": {
"name": "min"
},
"_max": {
"name": "max"
},
"_stddev": {
"name": "stddev"
}
}
}
]
}
},
"count": {
"enable": true,
"description": "Count of all non-null integers"
},
"countDistinct": {
"enable": true,
"description": "Count of all distinct non-null integers"
},
"description": "Aggregate expression for the Int type",
"graphql": {
"selectTypeName": "Int_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Numeric_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Numeric",
"aggregationFunctions": [
{
"name": "_sum",
"returnType": "Numeric!"
},
{
"name": "_min",
"returnType": "Numeric!"
},
{
"name": "_max",
"returnType": "Numeric!"
},
{
"name": "_stddev",
"returnType": "Numeric!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "numeric",
"functionMapping": {
"_sum": {
"name": "sum"
},
"_min": {
"name": "min"
},
"_max": {
"name": "max"
},
"_stddev": {
"name": "stddev"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the Numeric type",
"graphql": {
"selectTypeName": "Numeric_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "String_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "String",
"aggregationFunctions": [
{
"name": "_min",
"returnType": "String!"
},
{
"name": "_max",
"returnType": "String!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "varchar",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
},
{
"dataConnectorName": "db",
"dataConnectorScalarType": "text",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the String type",
"graphql": {
"selectTypeName": "String_aggregate_exp"
}
}
},
{
"kind": "AggregateExpression",
"version": "v1",
"definition": {
"name": "Timestamp_aggregate_exp",
"operand": {
"scalar": {
"aggregatedType": "Timestamp",
"aggregationFunctions": [
{
"name": "_min",
"returnType": "Timestamp!"
},
{
"name": "_max",
"returnType": "Timestamp!"
}
],
"dataConnectorAggregationFunctionMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "timestamp",
"functionMapping": {
"_min": {
"name": "min"
},
"_max": {
"name": "max"
}
}
}
]
}
},
"count": {
"enable": true
},
"countDistinct": {
"enable": true
},
"description": "Aggregate expression for the Timestamp type",
"graphql": {
"selectTypeName": "Timestamp_aggregate_exp"
}
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "Invoice",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"BillingAddress",
"BillingCity",
"BillingCountry",
"BillingPostalCode",
"BillingState",
"CustomerId",
"InvoiceDate",
"InvoiceId",
"Total"
]
}
},
{
"role": "australianuser",
"output": {
"allowedFields": [
"BillingAddress",
"BillingCity",
"BillingCountry",
"BillingPostalCode",
"BillingState",
"CustomerId",
"InvoiceDate",
"InvoiceId",
"Total"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"BillingPostalCode",
"BillingState",
"CustomerId",
"InvoiceDate",
"InvoiceId",
"Total"
]
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "Invoice",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "australianuser",
"select": {
"filter": {
"fieldComparison": {
"field": "BillingCountry",
"operator": "_eq",
"value": {
"literal": "Australia"
}
}
}
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,21 @@
query {
Invoice_aggregate {
BillingCountry {
_min
_max
_count_distinct
}
InvoiceId {
min: _min
max: _max
count: _count
}
Total {
_min
_max
_sum
_stddev
}
count_all: _count
}
}

View File

@ -0,0 +1,11 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "australianuser"
},
{
"x-hasura-role": "user"
}
]

View File

@ -1103,3 +1103,42 @@ fn test_apollo_federation_entities() -> anyhow::Result<()> {
&[common_metadata_path_string, common_apollo_metadata],
)
}
#[test]
fn test_aggregates_root_field_simple_select() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/simple_select";
common::test_execution_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/postgres_connector_schema.json",
"execute/aggregates/common_metadata/pg_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}
#[test]
fn test_aggregates_root_field_filtering() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/filtering";
common::test_execution_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/postgres_connector_schema.json",
"execute/aggregates/common_metadata/pg_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}
#[test]
fn test_aggregates_root_field_nested_object() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/nested_object";
common::test_execution_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/custom_connector_schema.json",
"execute/aggregates/common_metadata/custom_connector_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}

View File

@ -97,3 +97,42 @@ fn test_introspect_boolean_expression_in_command() -> anyhow::Result<()> {
],
)
}
#[test]
fn test_introspect_aggregates_root_field_simple_select() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/simple_select";
common::test_introspection_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/postgres_connector_schema.json",
"execute/aggregates/common_metadata/pg_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}
#[test]
fn test_introspect_aggregates_root_field_filtering() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/filtering";
common::test_introspection_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/postgres_connector_schema.json",
"execute/aggregates/common_metadata/pg_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}
#[test]
fn test_introspect_aggregates_root_field_nested_object() -> anyhow::Result<()> {
let test_path_string = "execute/aggregates/root_field/nested_object";
common::test_introspection_expectation(
test_path_string,
&[
"execute/aggregates/common_metadata/custom_connector_schema.json",
"execute/aggregates/common_metadata/custom_connector_types.json",
"execute/aggregates/common_metadata/supergraph.json",
],
)
}

View File

@ -149,7 +149,7 @@ pub(crate) async fn explain_mutation_plan(
async fn get_execution_steps<'s>(
http_context: &HttpContext,
alias: gql::ast::common::Alias,
process_response_as: &ProcessResponseAs<'s>,
process_response_as: &ProcessResponseAs<'s, 's>,
join_locations: JoinLocations<(RemoteJoin<'s, '_>, JoinId)>,
ndc_request: types::NDCRequest,
data_connector: &metadata_resolve::DataConnectorLink,
@ -167,7 +167,9 @@ async fn get_execution_steps<'s>(
},
)))
}
ProcessResponseAs::Array { .. } | ProcessResponseAs::Object { .. } => {
ProcessResponseAs::Array { .. }
| ProcessResponseAs::Object { .. }
| ProcessResponseAs::Aggregates { .. } => {
// A model execution node
let data_connector_explain =
fetch_explain_from_data_connector(http_context, &ndc_request, data_connector).await;

View File

@ -2,6 +2,7 @@ use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use serde::Serialize;
pub mod aggregates;
pub mod arguments;
pub mod commands;
pub mod error;

View File

@ -0,0 +1,239 @@
use std::collections::BTreeMap;
use indexmap::IndexMap;
use lang_graphql::{ast::common::Alias, normalized_ast};
use metadata_resolve::{Qualified, QualifiedTypeName, TypeMapping};
use open_dds::{
aggregates::DataConnectorAggregationFunctionName,
data_connector::DataConnectorName,
types::{CustomTypeName, FieldName},
};
use schema::{
AggregateOutputAnnotation, AggregationFunctionAnnotation, Annotation, OutputAnnotation, GDS,
};
use serde::Serialize;
use crate::ir::error;
/// IR that represents the selected fields of an output type.
#[derive(Debug, Serialize, Default, PartialEq)]
pub struct AggregateSelectionSet<'s> {
// The fields in the selection set. They are stored in the form that would
// be converted and sent over the wire. Serialized the map as ordered to
// produce deterministic golden files.
pub fields: IndexMap<String, AggregateFieldSelection<'s>>,
}
#[derive(Debug, Serialize, PartialEq)]
pub enum AggregateFieldSelection<'s> {
Count {
column_path: Vec<&'s str>,
graphql_field_path: Vec<Alias>,
},
CountDistinct {
column_path: Vec<&'s str>,
graphql_field_path: Vec<Alias>,
},
AggregationFunction {
function_name: &'s DataConnectorAggregationFunctionName,
column_path: nonempty::NonEmpty<&'s str>,
graphql_field_path: Vec<Alias>,
},
}
impl<'s> AggregateFieldSelection<'s> {
pub fn get_graphql_field_path(&self) -> &Vec<Alias> {
match self {
AggregateFieldSelection::Count {
graphql_field_path, ..
}
| AggregateFieldSelection::CountDistinct {
graphql_field_path, ..
}
| AggregateFieldSelection::AggregationFunction {
graphql_field_path, ..
} => graphql_field_path,
}
}
}
pub fn generate_aggregate_selection_set_ir<'s>(
selection_set: &normalized_ast::SelectionSet<'s, GDS>,
data_connector: &'s metadata_resolve::DataConnectorLink,
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, TypeMapping>,
field_mappings: &'s BTreeMap<FieldName, metadata_resolve::FieldMapping>,
aggregate_operand_type: &QualifiedTypeName,
) -> Result<AggregateSelectionSet<'s>, error::Error> {
let mut aggregate_field_selections = IndexMap::new();
add_aggregate_selections(
&mut aggregate_field_selections,
selection_set,
aggregate_operand_type,
&data_connector.name,
&[], // column_path
&[], // graphql_field_path
type_mappings,
Some(field_mappings),
)?;
Ok(AggregateSelectionSet {
fields: aggregate_field_selections,
})
}
fn add_aggregate_selections<'s>(
aggregate_field_selections: &mut IndexMap<String, AggregateFieldSelection<'s>>,
selection_set: &normalized_ast::SelectionSet<'s, GDS>,
aggregate_operand_type: &QualifiedTypeName,
data_connector_name: &Qualified<DataConnectorName>,
column_path: &[&'s metadata_resolve::FieldMapping],
graphql_field_path: &[Alias],
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, TypeMapping>,
field_mappings: Option<&'s BTreeMap<FieldName, metadata_resolve::FieldMapping>>,
) -> Result<(), error::Error> {
for field in selection_set.fields.values() {
let graphql_field_path = graphql_field_path
.iter()
.chain(std::iter::once(&field.alias))
.cloned()
.collect::<Vec<Alias>>();
let field_call = field.field_call()?;
match field_call.info.generic {
Annotation::Output(OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregationFunctionField(aggregate_function),
)) => match aggregate_function {
AggregationFunctionAnnotation::Count => {
let selection_field_name =
mk_alias_from_graphql_field_path(&graphql_field_path);
let selection = AggregateFieldSelection::Count {
column_path: column_path.iter().map(|m| m.column.0.as_str()).collect(),
graphql_field_path,
};
aggregate_field_selections.insert(selection_field_name, selection);
}
AggregationFunctionAnnotation::CountDistinct => {
let selection_field_name =
mk_alias_from_graphql_field_path(&graphql_field_path);
let selection = AggregateFieldSelection::CountDistinct {
column_path: column_path.iter().map(|m| m.column.0.as_str()).collect(),
graphql_field_path,
};
aggregate_field_selections.insert(selection_field_name, selection);
}
AggregationFunctionAnnotation::Function {
function_name,
data_connector_functions,
} => {
let selection_field_name =
mk_alias_from_graphql_field_path(&graphql_field_path);
let column_path = nonempty::NonEmpty::from_slice(column_path)
.ok_or_else(|| error::InternalDeveloperError::ColumnAggregationFunctionUsedOnModelObjectType {
aggregate_operand_type: aggregate_operand_type.clone(),
aggregation_function: function_name.clone(),
})?;
let column_scalar_type =
get_ndc_underlying_type_name(&column_path.last().column_type);
let data_connector_function_info = data_connector_functions
.iter()
.find(|fn_info| {
fn_info.data_connector_name == *data_connector_name &&
fn_info.operand_scalar_type.0 == *column_scalar_type
})
.ok_or_else(|| {
error::InternalDeveloperError::DataConnectorAggregationFunctionNotFound {
aggregate_operand_type: aggregate_operand_type.clone(),
aggregation_function: function_name.clone(),
data_connector_name: data_connector_name.clone(),
}
})?;
let selection = AggregateFieldSelection::AggregationFunction {
function_name: &data_connector_function_info.function_name,
column_path: column_path.map(|m| m.column.0.as_str()),
graphql_field_path,
};
aggregate_field_selections.insert(selection_field_name, selection);
}
},
Annotation::Output(OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregatableField {
field_name,
aggregate_operand_type: field_aggregate_operand_type,
},
)) => {
let field_mapping = field_mappings
.ok_or_else(|| {
error::InternalDeveloperError::AggregatableFieldFoundOnScalarTypedOperand {
field_name: field_name.clone(),
aggregate_operand_type: aggregate_operand_type.clone(),
}
})?
.get(field_name)
.ok_or_else(|| error::InternalEngineError::InternalGeneric {
description: format!("invalid field in annotation: {field_name}"),
})?;
let column_path = column_path
.iter()
.copied() // This just dereferences the double reference: &&FieldMapping -> &FieldMapping
.chain(std::iter::once(field_mapping))
.collect::<Vec<&metadata_resolve::FieldMapping>>();
// If the type name is not in the object type mappings or is inbuilt, it is a scalar type
// and therefore does not have field mappings
let field_operand_field_mappings = match field_aggregate_operand_type {
QualifiedTypeName::Custom(custom_type_name) => {
type_mappings.get(custom_type_name).map(|type_mapping| {
let metadata_resolve::TypeMapping::Object { field_mappings, .. } =
type_mapping;
field_mappings
})
}
QualifiedTypeName::Inbuilt(_) => None,
};
add_aggregate_selections(
aggregate_field_selections,
&field.selection_set,
aggregate_operand_type,
data_connector_name,
&column_path,
&graphql_field_path,
type_mappings,
field_operand_field_mappings,
)?;
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
}
Ok(())
}
fn mk_alias_from_graphql_field_path(graphql_field_path: &[Alias]) -> String {
graphql_field_path
.iter()
.map(|alias| alias.0.as_str())
.collect::<Vec<_>>()
.join("_")
}
fn get_ndc_underlying_type_name(result_type: &ndc_models::Type) -> &String {
match result_type {
ndc_models::Type::Named { name } => name,
ndc_models::Type::Array { element_type } => get_ndc_underlying_type_name(element_type),
ndc_models::Type::Nullable { underlying_type } => {
get_ndc_underlying_type_name(underlying_type)
}
ndc_models::Type::Predicate { object_type_name } => object_type_name,
}
}

View File

@ -1,6 +1,7 @@
use gql::ast::common as ast;
use lang_graphql as gql;
use open_dds::{
aggregates::AggregationFunctionName,
arguments::ArgumentName,
data_connector::{DataConnectorColumnName, DataConnectorName},
relationships::RelationshipName,
@ -12,7 +13,7 @@ use thiserror::Error;
use tracing_util::{ErrorVisibility, TraceableError};
use transitive::Transitive;
use metadata_resolve::Qualified;
use metadata_resolve::{Qualified, QualifiedTypeName};
use schema::{Annotation, NamespaceAnnotation};
#[derive(Error, Debug, Transitive)]
@ -153,6 +154,25 @@ pub enum InternalDeveloperError {
#[error("The value expression could not be converted to header value. Error: ")]
UnableToConvertValueExpressionToHeaderValue,
#[error("The aggregation function {aggregation_function} operating over the {aggregate_operand_type} type is missing a data connector mapping for {data_connector_name}")]
DataConnectorAggregationFunctionNotFound {
aggregate_operand_type: QualifiedTypeName,
aggregation_function: AggregationFunctionName,
data_connector_name: Qualified<DataConnectorName>,
},
#[error("A field ({field_name}) with an AggregatableField annotation was found on a scalar-typed ({aggregate_operand_type}) operand's selection set")]
AggregatableFieldFoundOnScalarTypedOperand {
field_name: FieldName,
aggregate_operand_type: QualifiedTypeName,
},
#[error("The aggregation function {aggregation_function} was used on the model object type and not on a model field. Aggregation functions operate on columns, not rows")]
ColumnAggregationFunctionUsedOnModelObjectType {
aggregate_operand_type: QualifiedTypeName,
aggregation_function: AggregationFunctionName,
},
// we'll be adding them shortly, and not advertising the feature until they are complete
// however temporarily emitting the error allows merging the work in chunks
#[error("boolean expressions not implemented")]

View File

@ -2,11 +2,13 @@
use hasura_authn_core::SessionVariables;
use lang_graphql::normalized_ast;
use metadata_resolve::QualifiedTypeName;
use ndc_models;
use open_dds::types::CustomTypeName;
use serde::Serialize;
use std::collections::BTreeMap;
use super::aggregates;
use super::filter::ResolvedFilterExpression;
use super::order_by::ResolvedOrderBy;
use super::permissions;
@ -42,7 +44,10 @@ pub struct ModelSelection<'s> {
pub(crate) order_by: Option<ResolvedOrderBy<'s>>,
// Fields requested from the model
pub(crate) selection: selection_set::ResultSelectionSet<'s>,
pub(crate) selection: Option<selection_set::ResultSelectionSet<'s>>,
// Aggregates requested of the model
pub(crate) aggregate_selection: Option<aggregates::AggregateSelectionSet<'s>>,
}
/// Generates the IR fragment for selecting from a model.
@ -52,7 +57,7 @@ pub(crate) fn model_selection_ir<'s>(
data_type: &Qualified<CustomTypeName>,
model_source: &'s metadata_resolve::ModelSource,
arguments: BTreeMap<ConnectorArgumentName, ndc_models::Argument>,
mut filter_clauses: ResolvedFilterExpression<'s>,
filter_clauses: ResolvedFilterExpression<'s>,
permissions_predicate: &'s metadata_resolve::FilterPermission,
limit: Option<u32>,
offset: Option<u32>,
@ -61,38 +66,14 @@ pub(crate) fn model_selection_ir<'s>(
request_headers: &reqwest::header::HeaderMap,
usage_counts: &mut UsagesCounts,
) -> Result<ModelSelection<'s>, error::Error> {
match permissions_predicate {
metadata_resolve::FilterPermission::AllowAll => {}
metadata_resolve::FilterPermission::Filter(predicate) => {
let mut permissions_predicate_relationships = BTreeMap::new();
let processed_model_perdicate = permissions::process_model_predicate(
predicate,
session_variables,
&mut permissions_predicate_relationships,
usage_counts,
)?;
filter_clauses.expression = match filter_clauses.expression {
Some(existing) => Some(ndc_models::Expression::And {
expressions: vec![existing, processed_model_perdicate],
}),
None => Some(processed_model_perdicate),
};
for (rel_name, rel_info) in permissions_predicate_relationships {
filter_clauses.relationships.insert(rel_name, rel_info);
}
}
};
let field_mappings = model_source
.type_mappings
.get(data_type)
.map(|type_mapping| {
let metadata_resolve::TypeMapping::Object { field_mappings, .. } = type_mapping;
field_mappings
})
.ok_or_else(|| error::InternalEngineError::InternalGeneric {
description: format!("type '{data_type}' not found in model source type_mappings"),
})?;
let filter_clauses = apply_permissions_predicate(
filter_clauses,
permissions_predicate,
session_variables,
usage_counts,
)?;
let field_mappings = get_field_mappings_for_object_type(model_source, data_type)?;
let selection = selection_set::generate_selection_set_ir(
selection_set,
&model_source.data_connector,
@ -111,6 +92,102 @@ pub(crate) fn model_selection_ir<'s>(
limit,
offset,
order_by,
selection,
selection: Some(selection),
aggregate_selection: None,
})
}
fn apply_permissions_predicate<'s>(
mut filter_clauses: ResolvedFilterExpression<'s>,
permissions_predicate: &'s metadata_resolve::FilterPermission,
session_variables: &SessionVariables,
usage_counts: &mut UsagesCounts,
) -> Result<ResolvedFilterExpression<'s>, error::Error> {
match permissions_predicate {
metadata_resolve::FilterPermission::AllowAll => {}
metadata_resolve::FilterPermission::Filter(predicate) => {
let mut permissions_predicate_relationships = BTreeMap::new();
let processed_model_predicate = permissions::process_model_predicate(
predicate,
session_variables,
&mut permissions_predicate_relationships,
usage_counts,
)?;
filter_clauses.expression = match filter_clauses.expression {
Some(existing) => Some(ndc_models::Expression::And {
expressions: vec![existing, processed_model_predicate],
}),
None => Some(processed_model_predicate),
};
for (rel_name, rel_info) in permissions_predicate_relationships {
filter_clauses.relationships.insert(rel_name, rel_info);
}
}
};
Ok(filter_clauses)
}
/// Generates the IR fragment for selecting an aggregate of a model.
#[allow(clippy::too_many_arguments)]
pub(crate) fn model_aggregate_selection_ir<'s>(
aggregate_selection_set: &normalized_ast::SelectionSet<'s, GDS>,
data_type: &Qualified<CustomTypeName>,
model_source: &'s metadata_resolve::ModelSource,
arguments: BTreeMap<ConnectorArgumentName, ndc_models::Argument>,
filter_clauses: ResolvedFilterExpression<'s>,
permissions_predicate: &'s metadata_resolve::FilterPermission,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ResolvedOrderBy<'s>>,
session_variables: &SessionVariables,
usage_counts: &mut UsagesCounts,
) -> Result<ModelSelection<'s>, error::Error> {
let filter_clauses = apply_permissions_predicate(
filter_clauses,
permissions_predicate,
session_variables,
usage_counts,
)?;
let field_mappings = get_field_mappings_for_object_type(model_source, data_type)?;
let aggregate_selection = aggregates::generate_aggregate_selection_set_ir(
aggregate_selection_set,
&model_source.data_connector,
&model_source.type_mappings,
field_mappings,
&QualifiedTypeName::Custom(data_type.clone()),
)?;
Ok(ModelSelection {
data_connector: &model_source.data_connector,
collection: &model_source.collection,
arguments,
filter_clause: filter_clauses,
limit,
offset,
order_by,
selection: None,
aggregate_selection: Some(aggregate_selection),
})
}
fn get_field_mappings_for_object_type<'s>(
model_source: &'s metadata_resolve::ModelSource,
data_type: &Qualified<CustomTypeName>,
) -> Result<&'s BTreeMap<open_dds::types::FieldName, metadata_resolve::FieldMapping>, error::Error>
{
model_source
.type_mappings
.get(data_type)
.map(|type_mapping| {
let metadata_resolve::TypeMapping::Object { field_mappings, .. } = type_mapping;
field_mappings
})
.ok_or_else(|| {
error::InternalEngineError::InternalGeneric {
description: format!("type '{data_type}' not found in model source type_mappings"),
}
.into()
})
}

View File

@ -23,6 +23,7 @@ use schema::{Annotation, NodeFieldTypeNameMapping, OutputAnnotation, RootFieldAn
pub mod apollo_federation;
pub mod node_field;
pub mod select_aggregate;
pub mod select_many;
pub mod select_one;
@ -216,6 +217,17 @@ fn generate_model_rootfield_ir<'n, 's>(
model_name,
)?,
},
RootFieldKind::SelectAggregate => root_field::QueryRootField::ModelSelectAggregate {
selection_set: &field.selection_set,
ir: select_aggregate::select_aggregate_generate_ir(
field,
field_call,
data_type,
source,
&session.variables,
model_name,
)?,
},
};
Ok(ir)
}

View File

@ -0,0 +1,283 @@
//! model_source IR for 'select_aggregate' operation
//!
//! A 'select_aggregate' operation fetches a set of aggregates over rows of a model
/// Generates the IR for a 'select_aggregate' operation
use hasura_authn_core::SessionVariables;
use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use lang_graphql::normalized_ast;
use open_dds;
use schema::InputAnnotation;
use serde::Serialize;
use std::collections::BTreeMap;
use crate::ir::arguments;
use crate::ir::error;
use crate::ir::filter;
use crate::ir::filter::ResolvedFilterExpression;
use crate::ir::model_selection;
use crate::ir::order_by::build_ndc_order_by;
use crate::ir::order_by::ResolvedOrderBy;
use crate::ir::permissions;
use crate::model_tracking::{count_model, UsagesCounts};
use metadata_resolve;
use metadata_resolve::Qualified;
use schema::GDS;
use schema::{self, Annotation, BooleanExpressionAnnotation, ModelInputAnnotation};
/// IR for the 'select_many' operation on a model
#[derive(Debug, Serialize)]
pub struct ModelSelectAggregate<'n, 's> {
// The name of the field as published in the schema
pub field_name: ast::Name,
pub model_selection: model_selection::ModelSelection<'s>,
// The Graphql output type of the operation
pub(crate) type_container: &'n ast::TypeContainer<ast::TypeName>,
// All the models/commands used in this operation. This includes the models/commands
// used via relationships. And in future, the models/commands used in the filter clause
pub(crate) usage_counts: UsagesCounts,
}
struct ModelSelectAggregateArguments<'s> {
model_arguments: BTreeMap<metadata_resolve::ConnectorArgumentName, ndc_models::Argument>,
filter_input_arguments: FilterInputArguments<'s>,
}
struct FilterInputArguments<'s> {
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ResolvedOrderBy<'s>>,
filter_clause: ResolvedFilterExpression<'s>,
}
/// Generates the IR for a 'select_aggregate' operation
#[allow(irrefutable_let_patterns)]
pub(crate) fn select_aggregate_generate_ir<'n, 's>(
field: &'n normalized_ast::Field<'s, GDS>,
field_call: &'n normalized_ast::FieldCall<'s, GDS>,
data_type: &Qualified<open_dds::types::CustomTypeName>,
model_source: &'s metadata_resolve::ModelSource,
session_variables: &SessionVariables,
model_name: &'s Qualified<open_dds::models::ModelName>,
) -> Result<ModelSelectAggregate<'n, 's>, error::Error> {
let mut usage_counts = UsagesCounts::new();
count_model(model_name, &mut usage_counts);
let mut arguments =
read_model_select_aggregate_arguments(field_call, model_source, &mut usage_counts)?;
// If there are model arguments presets from permissions, apply them
if let Some(model_argument_presets) =
permissions::get_argument_presets(field_call.info.namespaced)?
{
arguments::process_model_arguments_presets(
model_argument_presets,
session_variables,
&mut arguments.model_arguments,
&mut usage_counts,
)?;
}
let model_selection = model_selection::model_aggregate_selection_ir(
&field.selection_set,
data_type,
model_source,
arguments.model_arguments,
arguments.filter_input_arguments.filter_clause,
permissions::get_select_filter_predicate(field_call)?,
arguments.filter_input_arguments.limit,
arguments.filter_input_arguments.offset,
arguments.filter_input_arguments.order_by,
session_variables,
// Get all the models/commands that were used as relationships
&mut usage_counts,
)?;
Ok(ModelSelectAggregate {
field_name: field_call.name.clone(),
model_selection,
type_container: &field.type_container,
usage_counts,
})
}
fn read_model_select_aggregate_arguments<'s>(
field_call: &normalized_ast::FieldCall<'s, GDS>,
model_source: &'s metadata_resolve::ModelSource,
usage_counts: &mut UsagesCounts,
) -> Result<ModelSelectAggregateArguments<'s>, error::Error> {
let mut model_arguments = None;
let mut filter_input_props = None;
for field_call_argument in field_call.arguments.values() {
match field_call_argument.info.generic {
// Model arguments
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelArgumentsExpression,
)) => {
if model_arguments.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: field_call_argument.info.generic.clone(),
}
.into());
}
model_arguments = Some(match &field_call_argument.value {
normalized_ast::Value::Object(model_args_input_props) => {
arguments::build_ndc_model_arguments(
&field_call.name,
model_args_input_props.values(),
&model_source.type_mappings,
)
}
_ => Err(error::InternalEngineError::InternalGeneric {
description: "Expected object value for model arguments".into(),
}
.into()),
}?);
}
// Filter input arguments
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterInputArgument,
)) => {
if filter_input_props.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: field_call_argument.info.generic.clone(),
}
.into());
}
filter_input_props = Some(match &field_call_argument.value {
normalized_ast::Value::Object(model_args_input_props) => {
Ok(model_args_input_props)
}
_ => Err(error::Error::Internal(error::InternalError::Engine(
error::InternalEngineError::InternalGeneric {
description: "Expected object value for model arguments".into(),
},
))),
}?);
}
_ => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: field_call_argument.info.generic.clone(),
}
.into())
}
}
}
let filter_input_arguments =
read_filter_input_arguments(filter_input_props, model_source, usage_counts)?;
Ok(ModelSelectAggregateArguments {
model_arguments: model_arguments.unwrap_or_else(BTreeMap::new),
filter_input_arguments,
})
}
fn read_filter_input_arguments<'s>(
filter_input_field_props: Option<&IndexMap<ast::Name, normalized_ast::InputField<'s, GDS>>>,
model_source: &'s metadata_resolve::ModelSource,
usage_counts: &mut UsagesCounts,
) -> Result<FilterInputArguments<'s>, error::Error> {
let mut limit = None;
let mut offset = None;
let mut order_by = None;
let mut filter_clause = None;
if let Some(filter_input_field_props) = filter_input_field_props {
for filter_input_field_arg in filter_input_field_props.values() {
match filter_input_field_arg.info.generic {
// Limit argument
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelLimitArgument,
)) => {
if limit.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: filter_input_field_arg.info.generic.clone(),
}
.into());
}
limit = Some(
filter_input_field_arg
.value
.as_int_u32()
.map_err(error::Error::map_unexpected_value_to_external_error)?,
);
}
// Offset argument
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelOffsetArgument,
)) => {
if offset.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: filter_input_field_arg.info.generic.clone(),
}
.into());
}
offset = Some(
filter_input_field_arg
.value
.as_int_u32()
.map_err(error::Error::map_unexpected_value_to_external_error)?,
);
}
// Order By argument
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelOrderByExpression,
)) => {
if order_by.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: filter_input_field_arg.info.generic.clone(),
}
.into());
}
order_by = Some(build_ndc_order_by(filter_input_field_arg, usage_counts)?);
}
// Where argument
Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpression,
)) => {
if filter_clause.is_some() {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: filter_input_field_arg.info.generic.clone(),
}
.into());
}
filter_clause = Some(filter::resolve_filter_expression(
filter_input_field_arg.value.as_object()?,
&model_source.data_connector,
&model_source.type_mappings,
usage_counts,
)?);
}
_ => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: filter_input_field_arg.info.generic.clone(),
}
.into())
}
}
}
}
Ok(FilterInputArguments {
limit,
offset,
order_by,
filter_clause: filter_clause.unwrap_or_else(|| ResolvedFilterExpression {
expression: None,
relationships: BTreeMap::new(),
}),
})
}

View File

@ -6,7 +6,7 @@ use serde::Serialize;
use super::{
commands,
query_root::{apollo_federation, node_field, select_many, select_one},
query_root::{apollo_federation, node_field, select_aggregate, select_many, select_one},
};
use schema::GDS;
@ -40,6 +40,11 @@ pub enum QueryRootField<'n, 's> {
selection_set: &'n gql::normalized_ast::SelectionSet<'s, GDS>,
ir: select_many::ModelSelectMany<'n, 's>,
},
// Operation that selects an aggregate of rows from a model
ModelSelectAggregate {
selection_set: &'n gql::normalized_ast::SelectionSet<'s, GDS>,
ir: select_aggregate::ModelSelectAggregate<'n, 's>,
},
// Operation that selects a single row from the model corresponding
// to the Global Id input.
NodeSelect(Option<node_field::NodeSelect<'n, 's>>),

View File

@ -89,7 +89,7 @@ impl NDCRelationshipName {
}
/// IR that represents the selected fields of an output type.
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Default)]
pub(crate) struct ResultSelectionSet<'s> {
// The fields in the selection set. They are stored in the form that would
// be converted and sent over the wire. Serialized the map as ordered to

View File

@ -66,6 +66,10 @@ pub fn get_all_usage_counts_in_query(ir: &ir::IR<'_, '_>) -> UsagesCounts {
let usage_counts = ir.usage_counts.clone();
extend_usage_count(usage_counts, &mut all_usage_counts);
}
root_field::QueryRootField::ModelSelectAggregate { ir, .. } => {
let usage_counts = ir.usage_counts.clone();
extend_usage_count(usage_counts, &mut all_usage_counts);
}
root_field::QueryRootField::NodeSelect(ir1) => match ir1 {
None => {}
Some(ir2) => {

View File

@ -119,7 +119,7 @@ pub(crate) async fn execute_ndc_mutation<'n, 's, 'ir>(
selection_set: &'n normalized_ast::SelectionSet<'s, GDS>,
execution_span_attribute: String,
field_span_attribute: String,
process_response_as: ProcessResponseAs<'ir>,
process_response_as: ProcessResponseAs<'s, 'ir>,
project_id: Option<&ProjectId>,
) -> Result<json::Value, error::FieldError> {
let tracer = tracing_util::global_tracer();

View File

@ -14,6 +14,7 @@ use serde_json as json;
use tracing_util::{set_attribute_on_active_span, AttributeVisibility, Traceable};
use super::ir;
use super::ir::aggregates::AggregateFieldSelection;
use super::ir::model_selection::ModelSelection;
use super::ir::root_field;
use super::ndc;
@ -81,7 +82,7 @@ pub struct NDCQueryExecution<'s, 'ir> {
pub execution_tree: ExecutionTree<'s, 'ir>,
pub execution_span_attribute: &'static str,
pub field_span_attribute: String,
pub process_response_as: ProcessResponseAs<'ir>,
pub process_response_as: ProcessResponseAs<'s, 'ir>,
// This selection set can either be owned by the IR structures or by the normalized query request itself.
// We use the more restrictive lifetime `'ir` here which allows us to construct this struct using the selection
// set either from the IR or from the normalized query request.
@ -105,7 +106,7 @@ pub struct NDCMutationExecution<'n, 's, 'ir> {
pub data_connector: &'s metadata_resolve::DataConnectorLink,
pub execution_span_attribute: String,
pub field_span_attribute: String,
pub process_response_as: ProcessResponseAs<'ir>,
pub process_response_as: ProcessResponseAs<'s, 'ir>,
pub selection_set: &'n normalized_ast::SelectionSet<'s, GDS>,
}
@ -122,7 +123,7 @@ pub struct ExecutionNode<'s> {
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ProcessResponseAs<'ir> {
pub enum ProcessResponseAs<'s, 'ir> {
Object {
is_nullable: bool,
},
@ -133,14 +134,18 @@ pub enum ProcessResponseAs<'ir> {
command_name: &'ir metadata_resolve::Qualified<open_dds::commands::CommandName>,
type_container: &'ir ast::TypeContainer<ast::TypeName>,
},
Aggregates {
requested_fields: &'ir IndexMap<String, AggregateFieldSelection<'s>>,
},
}
impl<'ir> ProcessResponseAs<'ir> {
impl<'s, 'ir> ProcessResponseAs<'s, 'ir> {
pub fn is_nullable(&self) -> bool {
match self {
ProcessResponseAs::Object { is_nullable }
| ProcessResponseAs::Array { is_nullable } => *is_nullable,
ProcessResponseAs::CommandResponse { type_container, .. } => type_container.nullable,
ProcessResponseAs::Aggregates { .. } => false,
}
}
}
@ -263,6 +268,25 @@ fn plan_query<'n, 's, 'ir>(
},
})
}
root_field::QueryRootField::ModelSelectAggregate { ir, selection_set } => {
let execution_tree = generate_execution_tree(&ir.model_selection)?;
let requested_fields = ir
.model_selection
.aggregate_selection
.as_ref()
.map(|selection| &selection.fields)
.ok_or_else(|| error::InternalError::InternalGeneric {
description: "Found a ModelSelectAggregate without an aggregate selection"
.to_owned(),
})?;
NodeQueryPlan::NDCQueryExecution(NDCQueryExecution {
execution_tree,
selection_set,
execution_span_attribute: "execute_model_select_aggregate",
field_span_attribute: ir.field_name.to_string(),
process_response_as: ProcessResponseAs::Aggregates { requested_fields },
})
}
root_field::QueryRootField::NodeSelect(optional_ir) => match optional_ir {
Some(ir) => {
let execution_tree = generate_execution_tree(&ir.model_selection)?;

View File

@ -2,29 +2,112 @@
use std::collections::BTreeMap;
use indexmap::IndexMap;
use super::error;
use super::relationships;
use super::selection_set;
use crate::ir::aggregates::{AggregateFieldSelection, AggregateSelectionSet};
use crate::ir::model_selection::ModelSelection;
use crate::remote_joins::types::{JoinLocations, MonotonicCounter, RemoteJoin};
/// Create an NDC `Query` based on the internal IR `ModelSelection` settings
pub(crate) fn ndc_query<'s, 'ir>(
ir: &'ir ModelSelection<'s>,
join_id_counter: &mut MonotonicCounter,
) -> Result<(ndc_models::Query, JoinLocations<RemoteJoin<'s, 'ir>>), error::Error> {
let (ndc_fields, join_locations) =
selection_set::process_selection_set_ir(&ir.selection, join_id_counter)?;
let (ndc_fields, join_locations) = ir
.selection
.as_ref()
.map(|selection| -> Result<_, error::Error> {
let (ndc_fields, join_locations) =
selection_set::process_selection_set_ir(selection, join_id_counter)?;
Ok((Some(ndc_fields), join_locations))
})
.transpose()?
.unwrap_or_else(|| (None, JoinLocations::new()));
let aggregates = ir.aggregate_selection.as_ref().map(ndc_aggregates);
let ndc_query = ndc_models::Query {
aggregates: None,
fields: Some(ndc_fields),
aggregates,
fields: ndc_fields,
limit: ir.limit,
offset: ir.offset,
order_by: ir.order_by.as_ref().map(|x| x.order_by.clone()),
predicate: ir.filter_clause.expression.clone(),
};
Ok((ndc_query, join_locations))
}
/// Translates the internal IR 'AggregateSelectionSet' into an NDC query aggregates selection
fn ndc_aggregates(
aggregate_selection_set: &AggregateSelectionSet,
) -> IndexMap<String, ndc_models::Aggregate> {
aggregate_selection_set
.fields
.iter()
.map(|(field_name, aggregate_selection)| {
let aggregate = match aggregate_selection {
AggregateFieldSelection::Count { column_path, .. } => {
ndc_count_aggregate(column_path, false)
}
AggregateFieldSelection::CountDistinct { column_path, .. } => {
ndc_count_aggregate(column_path, true)
}
AggregateFieldSelection::AggregationFunction {
function_name,
column_path,
..
} => {
let nonempty::NonEmpty {
head: column,
tail: field_path,
} = column_path;
let nested_field_path = field_path
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>();
ndc_models::Aggregate::SingleColumn {
column: (*column).to_string(),
field_path: if nested_field_path.is_empty() {
None
} else {
Some(nested_field_path)
},
function: function_name.0.clone(),
}
}
};
(field_name.clone(), aggregate)
})
.collect()
}
/// Creates the appropriate NDC count aggregation based on whether we're selecting
/// a column (nested or otherwise) or not
fn ndc_count_aggregate(column_path: &[&str], distinct: bool) -> ndc_models::Aggregate {
let mut column_path_iter = column_path.iter();
if let Some(first_path_element) = column_path_iter.next() {
let remaining_path = column_path_iter
.map(std::string::ToString::to_string)
.collect::<Vec<_>>();
let nested_field_path = if remaining_path.is_empty() {
None
} else {
Some(remaining_path)
};
ndc_models::Aggregate::ColumnCount {
column: (*first_path_element).to_string(),
field_path: nested_field_path,
distinct,
}
} else {
ndc_models::Aggregate::StarCount {}
}
}
/// Convert the internal IR (`ModelSelection`) into NDC IR (`ndc::models::QueryRequest`)
pub(crate) fn ndc_ir<'s, 'ir>(
ir: &'ir ModelSelection<'s>,

View File

@ -17,41 +17,43 @@ pub(crate) fn collect_relationships(
relationships: &mut BTreeMap<String, ndc_models::Relationship>,
) -> Result<(), error::Error> {
// from selection fields
for field in ir.selection.fields.values() {
match field {
FieldSelection::ModelRelationshipLocal {
query,
name,
relationship_info,
} => {
relationships.insert(
name.to_string(),
process_model_relationship_definition(relationship_info)?,
);
collect_relationships(query, relationships)?;
}
FieldSelection::CommandRelationshipLocal {
ir,
name,
relationship_info,
} => {
relationships.insert(
name.to_string(),
process_command_relationship_definition(relationship_info)?,
);
if let Some(nested_selection) = &ir.command_info.selection {
selection_set::collect_relationships_from_nested_selection(
nested_selection,
relationships,
)?;
if let Some(selection) = &ir.selection {
for field in selection.fields.values() {
match field {
FieldSelection::ModelRelationshipLocal {
query,
name,
relationship_info,
} => {
relationships.insert(
name.to_string(),
process_model_relationship_definition(relationship_info)?,
);
collect_relationships(query, relationships)?;
}
}
FieldSelection::Column { .. }
// we ignore remote relationships as we are generating relationship
// definition for one data connector
| FieldSelection::ModelRelationshipRemote { .. }
| FieldSelection::CommandRelationshipRemote { .. } => (),
};
FieldSelection::CommandRelationshipLocal {
ir,
name,
relationship_info,
} => {
relationships.insert(
name.to_string(),
process_command_relationship_definition(relationship_info)?,
);
if let Some(nested_selection) = &ir.command_info.selection {
selection_set::collect_relationships_from_nested_selection(
nested_selection,
relationships,
)?;
}
}
FieldSelection::Column { .. }
// we ignore remote relationships as we are generating relationship
// definition for one data connector
| FieldSelection::ModelRelationshipRemote { .. }
| FieldSelection::CommandRelationshipRemote { .. } => (),
};
}
}
// from filter clause

View File

@ -14,7 +14,7 @@ use open_dds::types::FieldName;
use super::global_id::{global_id_col_format, GLOBAL_ID_VERSION};
use super::ndc::FUNCTION_IR_VALUE_COLUMN_NAME;
use super::plan::ProcessResponseAs;
use crate::error;
use crate::error::{self, FieldInternalError};
use metadata_resolve::Qualified;
use schema::{Annotation, GlobalID, OutputAnnotation, GDS};
@ -378,6 +378,62 @@ fn process_command_field_value(
}
}
fn process_aggregate_requested_fields(
row_set: ndc_models::RowSet,
requested_fields: &IndexMap<String, crate::ir::aggregates::AggregateFieldSelection>,
) -> Result<json::Map<String, json::Value>, error::FieldError> {
let mut json_object = json::Map::<String, json::Value>::new();
let mut aggregate_results = row_set.aggregates
.ok_or_else(|| error::NDCUnexpectedError::BadNDCResponse {
summary:
"Unable to parse response from NDC, RowSet aggregates property was null when it was expected to be an object"
.to_owned(),
})?;
for (field_name, aggregate_field_selection) in requested_fields {
let aggregate_value = aggregate_results.swap_remove(field_name).ok_or_else(|| {
error::NDCUnexpectedError::BadNDCResponse {
summary: format!("missing aggregate field: {}", field_name),
}
})?;
let graphql_field_path = aggregate_field_selection.get_graphql_field_path();
set_value_at_json_path(&mut json_object, graphql_field_path, aggregate_value)?;
}
Ok(json_object)
}
fn set_value_at_json_path(
mut json_object: &mut json::Map<String, json::Value>,
path: &[Alias],
value: json::Value,
) -> Result<(), error::FieldError> {
let mut path_iterator = path.iter();
let mut path_element =
path_iterator
.next()
.ok_or_else(|| FieldInternalError::InternalGeneric {
description: "Path to set value in JSON was empty".to_owned(),
})?;
for next_path_element in path_iterator {
let field_value = json_object
.entry(path_element.0.as_str())
.or_insert_with(|| json::Value::Object(json::Map::new()));
json_object =
field_value
.as_object_mut()
.ok_or_else(|| FieldInternalError::InternalGeneric {
description: "Encountered non-object type in JSON along path".to_owned(),
})?;
path_element = next_path_element;
}
json_object.insert(path_element.to_string(), value);
Ok(())
}
pub fn process_response(
selection_set: &normalized_ast::SelectionSet<'_, GDS>,
rows_sets: Vec<ndc_models::RowSet>,
@ -412,6 +468,10 @@ pub fn process_response(
)?;
json::to_value(result).map_err(error::FieldError::from)
}
ProcessResponseAs::Aggregates { requested_fields } => {
let result = process_aggregate_requested_fields(row_set, requested_fields)?;
Ok(json::Value::Object(result))
}
}
},
)

View File

@ -106,6 +106,14 @@ fn collect_argument_from_rows(
ProcessResponseAs::Array { .. } | ProcessResponseAs::Object { .. } => {
collect_argument_from_row(row, join_fields, path, &mut arguments)?;
}
ProcessResponseAs::Aggregates { .. } => {
return Err(error::FieldInternalError::InternalGeneric {
description:
"Unexpected aggregate response on the LHS of a remote join"
.to_owned(),
}
.into())
}
ProcessResponseAs::CommandResponse {
command_name: _,
type_container,

View File

@ -120,7 +120,7 @@ pub struct RemoteJoin<'s, 'ir> {
/// field or an argument name.
pub join_mapping: HashMap<SourceFieldName, (SourceFieldAlias, TargetField)>,
/// Represents how to process the join response.
pub process_response_as: ProcessResponseAs<'ir>,
pub process_response_as: ProcessResponseAs<'s, 'ir>,
/// Represents the type of the remote join
pub remote_join_type: RemoteJoinType,
}

View File

@ -152,7 +152,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"type_container": {
"base": {

View File

@ -126,7 +126,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"type_container": {
"base": {

View File

@ -611,7 +611,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"name": "[{\"subgraph\":\"default\",\"name\":\"author\"},\"Articles\"]",
"relationship_info": {
@ -755,7 +756,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"name": "[{\"subgraph\":\"default\",\"name\":\"article\"},\"Author\"]",
"relationship_info": {
@ -899,7 +901,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"name": "[{\"subgraph\":\"default\",\"name\":\"commandArticle\"},\"article\"]",
"relationship_info": {

View File

@ -164,7 +164,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"type_container": {
"base": {

View File

@ -148,7 +148,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"type_container": {
"base": {
@ -335,7 +336,8 @@
}
}
}
}
},
"aggregate_selection": null
},
"type_container": {
"base": {

View File

@ -0,0 +1,323 @@
use hasura_authn_core::Role;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use strum_macros::Display;
use lang_graphql::{
ast::common::{self as ast, TypeContainer},
schema as gql_schema,
};
use metadata_resolve::{
mk_name, AggregateExpression, DataConnectorAggregationFunctionInfo,
ObjectTypeWithRelationships, Qualified, QualifiedTypeName,
};
use open_dds::{
aggregates::{AggregateExpressionName, AggregationFunctionName},
types::{CustomTypeName, FieldName},
};
use crate::{
mk_deprecation_status,
types::{output_type, TypeId},
Annotation, Error, NamespaceAnnotation, GDS,
};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
pub enum AggregateOutputAnnotation {
AggregationFunctionField(AggregationFunctionAnnotation),
AggregatableField {
field_name: FieldName,
aggregate_operand_type: QualifiedTypeName,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
pub enum AggregationFunctionAnnotation {
Count,
CountDistinct,
Function {
function_name: AggregationFunctionName,
data_connector_functions: Vec<DataConnectorAggregationFunctionInfo>,
},
}
pub fn get_aggregate_select_output_type(
builder: &mut gql_schema::Builder<GDS>,
aggregate_expression: &metadata_resolve::AggregateExpression,
) -> Result<gql_schema::RegisteredTypeName, Error> {
Ok(builder.register_type(TypeId::AggregateSelectOutputType {
aggregate_expression_name: aggregate_expression.name.clone(),
graphql_type_name: aggregate_expression
.graphql
.as_ref()
.map(|graphql| &graphql.select_output_type_name)
.ok_or_else(|| Error::NoGraphQlSelectTypeNameForAggregateExpression {
aggregate_expression: aggregate_expression.name.clone(),
})?
.clone(),
}))
}
pub fn build_aggregate_select_output_type(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
aggregate_expression_name: &Qualified<AggregateExpressionName>,
graphql_type_name: &ast::TypeName,
) -> Result<gql_schema::TypeInfo<GDS>, Error> {
let aggregate_expression = gds
.metadata
.aggregate_expressions
.get(aggregate_expression_name)
.ok_or_else(|| Error::InternalAggregateExpressionNotFound {
aggregate_expression: aggregate_expression_name.clone(),
})?;
let mut aggregate_select_output_type_fields = BTreeMap::new();
if let Some((object_type_name, object_type)) =
get_object_type(gds, &aggregate_expression.operand.aggregated_type)
{
add_aggregatable_fields(
&mut aggregate_select_output_type_fields,
gds,
builder,
aggregate_expression,
object_type_name,
object_type,
)?;
}
add_count_aggregation_fields(
&mut aggregate_select_output_type_fields,
builder,
aggregate_expression,
)?;
add_aggregation_functions(
&mut aggregate_select_output_type_fields,
gds,
builder,
aggregate_expression,
)?;
Ok(gql_schema::TypeInfo::Object(gql_schema::Object::new(
builder,
graphql_type_name.clone(),
aggregate_expression.description.clone(),
aggregate_select_output_type_fields,
BTreeMap::new(), // Interfaces
vec![], // Directives
)))
}
fn add_aggregatable_fields(
type_fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>>,
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
aggregate_expression: &AggregateExpression,
object_type_name: &Qualified<CustomTypeName>,
object_type: &ObjectTypeWithRelationships,
) -> Result<(), Error> {
for aggregatable_field_info in &aggregate_expression.operand.aggregatable_fields {
let field_def = object_type
.object_type
.fields
.get(&aggregatable_field_info.field_name)
.ok_or_else(|| Error::InternalObjectTypeFieldNotFound {
type_name: object_type_name.clone(),
field_name: aggregatable_field_info.field_name.clone(),
})?;
let field_aggregate_expression = gds
.metadata
.aggregate_expressions
.get(&aggregatable_field_info.aggregate_expression)
.ok_or_else(|| Error::InternalAggregateExpressionNotFound {
aggregate_expression: aggregatable_field_info.aggregate_expression.clone(),
})?;
let field_graphql_name = mk_name(aggregatable_field_info.field_name.0.as_str())?;
let field = gql_schema::Field::<GDS>::new(
field_graphql_name.clone(),
aggregatable_field_info.description.clone(),
Annotation::Output(super::OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregatableField {
field_name: aggregatable_field_info.field_name.clone(),
aggregate_operand_type: field_aggregate_expression
.operand
.aggregated_type
.clone(),
},
)),
TypeContainer::named_non_null(get_aggregate_select_output_type(
builder,
field_aggregate_expression,
)?),
BTreeMap::new(), // Arguments
mk_deprecation_status(&field_def.deprecated), // Use the field's deprecated status; if the field is deprecated the aggregation of it should be too
);
// Only allow access to aggregations of the field if the type permissions allow it
let allowed_roles = object_type
.type_output_permissions
.iter()
.filter(|(_role, perms)| {
perms
.allowed_fields
.contains(&aggregatable_field_info.field_name)
})
.map(|(role, _perms)| (role.clone(), None))
.collect::<HashMap<Role, Option<NamespaceAnnotation>>>();
let namespaced_field = builder.conditional_namespaced(field, allowed_roles);
if type_fields
.insert(field_graphql_name.clone(), namespaced_field)
.is_some()
{
return Err(Error::InternalDuplicateAggregatableField {
aggregate_expression: aggregate_expression.name.clone(),
field_name: field_graphql_name.clone(),
});
}
}
Ok(())
}
fn add_count_aggregation_fields(
type_fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>>,
builder: &mut gql_schema::Builder<GDS>,
aggregate_expression: &AggregateExpression,
) -> Result<(), Error> {
// Add the _count aggregation, if enabled and a graphql name has been specified
if aggregate_expression.count.enable {
if let Some(count_field_name) = aggregate_expression
.graphql
.as_ref()
.map(|graphql| &graphql.count_field_name)
{
let field = gql_schema::Field::<GDS>::new(
count_field_name.clone(),
aggregate_expression.count.description.clone(),
Annotation::Output(super::OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregationFunctionField(
AggregationFunctionAnnotation::Count,
),
)),
TypeContainer::named_non_null(gql_schema::RegisteredTypeName::int()),
BTreeMap::new(), // Arguments
mk_deprecation_status(&None),
);
// All roles can use the count aggregation
let namespaced_field = builder.allow_all_namespaced(field);
if type_fields
.insert(count_field_name.clone(), namespaced_field)
.is_some()
{
return Err(Error::AggregationFunctionFieldNameConflict {
aggregate_expression: aggregate_expression.name.clone(),
field_name: count_field_name.clone(),
});
}
}
}
// Add the _count_distinct aggregation, if enabled and a graphql name has been specified
if aggregate_expression.count_distinct.enable {
if let Some(count_distinct_field_name) = aggregate_expression
.graphql
.as_ref()
.map(|graphql| &graphql.count_distinct_field_name)
{
let field = gql_schema::Field::<GDS>::new(
count_distinct_field_name.clone(),
aggregate_expression.count_distinct.description.clone(),
Annotation::Output(super::OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregationFunctionField(
AggregationFunctionAnnotation::CountDistinct,
),
)),
TypeContainer::named_non_null(gql_schema::RegisteredTypeName::int()),
BTreeMap::new(), // Arguments
mk_deprecation_status(&None),
);
// All roles can use the count distinct aggregation
let namespaced_field = builder.allow_all_namespaced(field);
if type_fields
.insert(count_distinct_field_name.clone(), namespaced_field)
.is_some()
{
return Err(Error::AggregationFunctionFieldNameConflict {
aggregate_expression: aggregate_expression.name.clone(),
field_name: count_distinct_field_name.clone(),
});
}
}
}
Ok(())
}
fn add_aggregation_functions(
type_fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>>,
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
aggregate_expression: &AggregateExpression,
) -> Result<(), Error> {
for aggregatable_function_info in &aggregate_expression.operand.aggregation_functions {
let field_graphql_name = mk_name(aggregatable_function_info.name.0.as_str())?;
let field = gql_schema::Field::<GDS>::new(
field_graphql_name.clone(),
aggregatable_function_info.description.clone(),
Annotation::Output(super::OutputAnnotation::Aggregate(
AggregateOutputAnnotation::AggregationFunctionField(
AggregationFunctionAnnotation::Function {
function_name: aggregatable_function_info.name.clone(),
data_connector_functions: aggregatable_function_info
.data_connector_functions
.clone(),
},
),
)),
output_type::get_output_type(gds, builder, &aggregatable_function_info.return_type)?,
BTreeMap::new(), // Arguments
mk_deprecation_status(&None),
);
// All roles can access all functions
let namespaced_field = builder.allow_all_namespaced(field);
if type_fields
.insert(field_graphql_name.clone(), namespaced_field)
.is_some()
{
return Err(Error::AggregationFunctionFieldNameConflict {
aggregate_expression: aggregate_expression.name.clone(),
field_name: field_graphql_name.clone(),
});
}
}
Ok(())
}
fn get_object_type<'a>(
gds: &'a GDS,
type_name: &'a QualifiedTypeName,
) -> Option<(
&'a Qualified<CustomTypeName>,
&'a ObjectTypeWithRelationships,
)> {
match type_name {
QualifiedTypeName::Inbuilt(_) => None,
QualifiedTypeName::Custom(custom_type_name) => gds
.metadata
.object_types
.get(custom_type_name)
.map(|obj_type| (custom_type_name, obj_type)),
}
}

View File

@ -1,5 +1,6 @@
use lang_graphql::schema::{self as gql_schema, SchemaContext};
use lang_graphql::{ast::common as ast, mk_name};
use open_dds::aggregates::AggregateExpressionName;
use open_dds::types::FieldName;
use open_dds::{
commands::CommandName,
@ -19,11 +20,13 @@ use metadata_resolve::{
use self::types::PossibleApolloFederationTypes;
// we deliberately do not export these entire modules and instead explicitly export types below
mod aggregates;
mod apollo_federation;
mod boolean_expression;
mod commands;
mod model_arguments;
mod model_filter;
mod model_filter_input;
mod model_order_by;
mod mutation_root;
mod permissions;
@ -31,6 +34,7 @@ mod query_root;
mod relay;
mod types;
pub use aggregates::{AggregateOutputAnnotation, AggregationFunctionAnnotation};
pub use types::output_type::relationship::{
CommandRelationshipAnnotation, CommandTargetSource, FilterRelationshipAnnotation,
ModelRelationshipAnnotation, OrderByRelationshipAnnotation,
@ -221,6 +225,24 @@ impl gql_schema::SchemaContext for GDS {
apollo_federation::apollo_federation_service_schema(builder)?,
))
}
types::TypeId::AggregateSelectOutputType {
aggregate_expression_name,
graphql_type_name,
} => aggregates::build_aggregate_select_output_type(
self,
builder,
aggregate_expression_name,
graphql_type_name,
),
types::TypeId::ModelFilterInputType {
model_name,
graphql_type_name,
} => model_filter_input::build_model_filter_input_type(
self,
builder,
model_name,
graphql_type_name,
),
}
}
@ -255,6 +277,13 @@ pub enum Error {
InternalTypeNotFound {
type_name: Qualified<CustomTypeName>,
},
#[error(
"internal error while building schema, field {field_name} not found in type {type_name}"
)]
InternalObjectTypeFieldNotFound {
type_name: Qualified<CustomTypeName>,
field_name: FieldName,
},
#[error("duplicate field name {field_name} generated while building object type {type_name}")]
DuplicateFieldNameGeneratedInObjectType {
field_name: ast::Name,
@ -266,6 +295,16 @@ pub enum Error {
field_name: ast::Name,
type_name: Qualified<CustomTypeName>,
},
#[error("the aggregation function {field_name} conflicts with the aggregatable field {field_name} in the aggregate expression {aggregate_expression} is named {field_name}. Either rename the aggregation function or the field")]
AggregationFunctionFieldNameConflict {
aggregate_expression: Qualified<AggregateExpressionName>,
field_name: ast::Name,
},
#[error("internal error: duplicate aggregatable field {field_name} in the aggregate expression {aggregate_expression} is named {field_name}")]
InternalDuplicateAggregatableField {
aggregate_expression: Qualified<AggregateExpressionName>,
field_name: ast::Name,
},
#[error(
"internal error: duplicate models with global id implementing the same type {type_name} are found"
)]
@ -296,6 +335,10 @@ pub enum Error {
InternalCommandNotFound {
command_name: Qualified<CommandName>,
},
#[error("internal error while building schema, aggregate expression not found: {aggregate_expression}")]
InternalAggregateExpressionNotFound {
aggregate_expression: Qualified<AggregateExpressionName>,
},
#[error("Cannot generate select_many API for model {model_name} since order_by_expression isn't defined")]
NoOrderByExpression { model_name: Qualified<ModelName> },
#[error("No graphql type name has been defined for scalar type: {type_name}")]
@ -306,6 +349,10 @@ pub enum Error {
NoGraphQlOutputTypeNameForObject {
type_name: Qualified<CustomTypeName>,
},
#[error("No graphql select type name has been defined for aggregate expression: {aggregate_expression}")]
NoGraphQlSelectTypeNameForAggregateExpression {
aggregate_expression: Qualified<AggregateExpressionName>,
},
#[error("No graphql input type name has been defined for object type: {type_name}")]
NoGraphQlInputTypeNameForObject {
type_name: Qualified<CustomTypeName>,
@ -316,6 +363,10 @@ pub enum Error {
"Cannot generate arguments for model {model_name} since argumentsInputType and it's corresponding graphql config argumentsInput isn't defined"
)]
NoArgumentsInputConfigForSelectMany { model_name: Qualified<ModelName> },
#[error(
"Cannot generate the filter input type for model {model_name} since filterInputTypeName isn't defined in the graphql config"
)]
NoFilterInputTypeNameConfigNameForModel { model_name: Qualified<ModelName> },
#[error("Internal error: Relationship capabilities are missing for {relationship} on type {type_name}")]
InternalMissingRelationshipCapabilities {
type_name: Qualified<CustomTypeName>,

View File

@ -0,0 +1,240 @@
use std::collections::BTreeMap;
use lang_graphql::{ast::common as ast, schema as gql_schema};
use metadata_resolve::Qualified;
use crate::{
model_filter::get_where_expression_input_field,
model_order_by::get_order_by_expression_input_field,
types::{self, TypeId},
Annotation, Error, ModelInputAnnotation, GDS,
};
pub fn add_filter_input_argument_field(
fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
field_name: &ast::Name,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<(), Error> {
let filter_input_type_name = get_model_filter_input_type(builder, model)?;
let filter_input_argument = gql_schema::InputField::new(
field_name.clone(),
None,
Annotation::Input(types::InputAnnotation::Model(
ModelInputAnnotation::ModelFilterInputArgument,
)),
ast::TypeContainer::named_null(filter_input_type_name),
None,
gql_schema::DeprecationStatus::NotDeprecated,
);
fields.insert(
field_name.clone(),
builder.allow_all_namespaced(filter_input_argument),
);
Ok(())
}
pub fn get_model_filter_input_type(
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<gql_schema::RegisteredTypeName, Error> {
model
.model
.graphql_api
.filter_input_type_name
.as_ref()
.ok_or(crate::Error::NoFilterInputTypeNameConfigNameForModel {
model_name: model.model.name.clone(),
})
.map(|filter_input_type_name| {
builder.register_type(TypeId::ModelFilterInputType {
model_name: model.model.name.clone(),
graphql_type_name: filter_input_type_name.clone(),
})
})
}
pub fn build_model_filter_input_type(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
model_name: &Qualified<open_dds::models::ModelName>,
graphql_type_name: &ast::TypeName,
) -> Result<gql_schema::TypeInfo<GDS>, Error> {
let model_with_permissions =
gds.metadata
.models
.get(model_name)
.ok_or_else(|| Error::InternalModelNotFound {
model_name: model_name.clone(),
})?;
let mut filter_input_type_fields = BTreeMap::new();
add_limit_input_field(
&mut filter_input_type_fields,
builder,
model_with_permissions,
)?;
add_offset_input_field(
&mut filter_input_type_fields,
builder,
model_with_permissions,
)?;
add_order_by_input_field(
&mut filter_input_type_fields,
builder,
model_with_permissions,
)?;
add_where_input_field(
&mut filter_input_type_fields,
builder,
model_with_permissions,
)?;
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(
graphql_type_name.clone(),
None,
filter_input_type_fields,
vec![], // Directives
),
))
}
pub fn add_limit_input_field(
fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<(), Error> {
if let Some(limit_field) = &model.model.graphql_api.limit_field {
let limit_argument = generate_int_input_argument(
limit_field.field_name.as_str(),
Annotation::Input(types::InputAnnotation::Model(
ModelInputAnnotation::ModelLimitArgument,
)),
)?;
fields.insert(
limit_argument.name.clone(),
builder.allow_all_namespaced(limit_argument),
);
}
Ok(())
}
pub fn add_offset_input_field(
fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<(), Error> {
if let Some(offset_field) = &model.model.graphql_api.offset_field {
let offset_argument = generate_int_input_argument(
offset_field.field_name.as_str(),
Annotation::Input(types::InputAnnotation::Model(
ModelInputAnnotation::ModelOffsetArgument,
)),
)?;
fields.insert(
offset_argument.name.clone(),
builder.allow_all_namespaced(offset_argument),
);
}
Ok(())
}
pub fn add_order_by_input_field(
fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<(), Error> {
if let Some(order_by_expression_info) = &model.model.graphql_api.order_by_expression {
let order_by_argument = {
get_order_by_expression_input_field(
builder,
model.model.name.clone(),
order_by_expression_info,
)
};
fields.insert(
order_by_argument.name.clone(),
builder.allow_all_namespaced(order_by_argument),
);
}
Ok(())
}
pub fn add_where_input_field(
fields: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
) -> Result<(), Error> {
let boolean_expression_filter_type =
&model
.model
.filter_expression_type
.as_ref()
.and_then(|bool_exp| match bool_exp {
metadata_resolve::ModelExpressionType::ObjectBooleanExpressionType(
object_boolean_expression_type,
) => object_boolean_expression_type
.graphql
.as_ref()
.map(|graphql_config| {
(
object_boolean_expression_type.name.clone(),
graphql_config.clone(),
)
}),
metadata_resolve::ModelExpressionType::BooleanExpressionType(
boolean_expression_object_type,
) => boolean_expression_object_type
.graphql
.as_ref()
.map(|graphql_config| {
(
boolean_expression_object_type.name.clone(),
graphql_config.clone(),
)
}),
});
if let Some((boolean_expression_type_name, boolean_expression_graphql_config)) =
boolean_expression_filter_type
{
let where_argument = get_where_expression_input_field(
builder,
boolean_expression_type_name.clone(),
boolean_expression_graphql_config,
);
fields.insert(
where_argument.name.clone(),
builder.allow_all_namespaced(where_argument),
);
}
Ok(())
}
/// Generates the input field for the arguments which are of type int.
fn generate_int_input_argument(
name: &str,
annotation: Annotation,
) -> Result<gql_schema::InputField<GDS>, crate::Error> {
let input_field_name = metadata_resolve::mk_name(name)?;
Ok(gql_schema::InputField::new(
input_field_name,
None,
annotation,
ast::TypeContainer::named_null(gql_schema::RegisteredTypeName::int()),
None,
gql_schema::DeprecationStatus::NotDeprecated,
))
}

View File

@ -13,6 +13,7 @@ use self::node_field::RelayNodeFieldOutput;
pub mod apollo_federation;
pub mod node_field;
pub mod select_aggregate;
pub mod select_many;
pub mod select_one;
@ -44,6 +45,16 @@ pub fn query_root_schema(
)?;
fields.insert(field_name, field);
}
if let Some(select_aggregate) = &model.model.graphql_api.select_aggregate {
let (field_name, field) = select_aggregate::select_aggregate_field(
gds,
builder,
model,
select_aggregate,
query_root_type_name,
)?;
fields.insert(field_name, field);
}
}
// Add node field for only the commands which have a query root field

View File

@ -0,0 +1,118 @@
//! model_source.Schema for 'select_aggregate' operation
//!
//! A 'select_aggregate' operation fetches aggregations over the model's data
//!
use std::collections::BTreeMap;
use lang_graphql::{ast::common as ast, schema as gql_schema};
use crate::aggregates::get_aggregate_select_output_type;
use crate::types::{self, Annotation};
use crate::{
mk_deprecation_status, model_arguments, model_filter_input, permissions, Error,
RootFieldAnnotation, RootFieldKind, GDS,
};
use metadata_resolve;
pub(crate) fn select_aggregate_field(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
select_aggregate: &metadata_resolve::SelectAggregateGraphQlDefinition,
parent_type: &ast::TypeName,
) -> Result<
(
ast::Name,
gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>,
),
Error,
> {
let aggregate_expression = gds
.metadata
.aggregate_expressions
.get(&select_aggregate.aggregate_expression_name)
.ok_or_else(|| Error::InternalAggregateExpressionNotFound {
aggregate_expression: select_aggregate.aggregate_expression_name.clone(),
})?;
let root_field_name = select_aggregate.query_root_field.clone();
let mut arguments =
BTreeMap::<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>::new();
add_model_arguments_field(
&mut arguments,
gds,
builder,
model,
&root_field_name,
parent_type,
)?;
model_filter_input::add_filter_input_argument_field(
&mut arguments,
&select_aggregate.filter_input_field_name,
builder,
model,
)?;
let field_permissions = permissions::get_select_permissions_namespace_annotations(
model,
&gds.metadata.object_types,
)?;
let output_typename = get_aggregate_select_output_type(builder, aggregate_expression)?;
let field = builder.conditional_namespaced(
gql_schema::Field::new(
root_field_name.clone(),
select_aggregate.description.clone(),
Annotation::Output(types::OutputAnnotation::RootField(
RootFieldAnnotation::Model {
data_type: model.model.data_type.clone(),
source: model.model.source.clone(),
kind: RootFieldKind::SelectAggregate,
name: model.model.name.clone(),
},
)),
ast::TypeContainer::named_null(output_typename),
arguments,
mk_deprecation_status(&select_aggregate.deprecated),
),
field_permissions,
);
Ok((root_field_name, field))
}
fn add_model_arguments_field(
arguments: &mut BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
model: &metadata_resolve::ModelWithPermissions,
root_field_name: &ast::Name,
parent_type: &ast::TypeName,
) -> Result<(), Error> {
if !model.model.arguments.is_empty() {
let model_arguments_input =
model_arguments::get_model_arguments_input_field(builder, model)?;
let name = model_arguments_input.name.clone();
let model_arguments = builder.conditional_namespaced(
model_arguments_input,
permissions::get_select_permissions_namespace_annotations(
model,
&gds.metadata.object_types,
)?,
);
if arguments.insert(name.clone(), model_arguments).is_some() {
return Err(crate::Error::GraphQlArgumentConflict {
argument_name: name,
field_name: root_field_name.clone(),
type_name: parent_type.clone(),
});
}
}
Ok(())
}

View File

@ -8,12 +8,12 @@ use lang_graphql::schema as gql_schema;
use std::collections::BTreeMap;
use crate::mk_deprecation_status;
use crate::model_filter_input::{
add_limit_input_field, add_offset_input_field, add_order_by_input_field, add_where_input_field,
};
use crate::{
model_arguments,
model_filter::get_where_expression_input_field,
model_order_by::get_order_by_expression_input_field,
permissions,
types::{self, output_type::get_custom_output_type, Annotation, ModelInputAnnotation},
model_arguments, permissions,
types::{self, output_type::get_custom_output_type, Annotation},
GDS,
};
use metadata_resolve;
@ -27,96 +27,10 @@ pub(crate) fn generate_select_many_arguments(
{
let mut arguments = BTreeMap::new();
// insert limit argument
if let Some(limit_field) = &model.model.graphql_api.limit_field {
let limit_argument = generate_int_input_argument(
limit_field.field_name.as_str(),
Annotation::Input(types::InputAnnotation::Model(
ModelInputAnnotation::ModelLimitArgument,
)),
)?;
arguments.insert(
limit_argument.name.clone(),
builder.allow_all_namespaced(limit_argument),
);
}
// insert offset argument
if let Some(offset_field) = &model.model.graphql_api.offset_field {
let offset_argument = generate_int_input_argument(
offset_field.field_name.as_str(),
Annotation::Input(types::InputAnnotation::Model(
ModelInputAnnotation::ModelOffsetArgument,
)),
)?;
arguments.insert(
offset_argument.name.clone(),
builder.allow_all_namespaced(offset_argument),
);
}
// generate and insert order_by argument
if let Some(order_by_expression_info) = &model.model.graphql_api.order_by_expression {
let order_by_argument = {
get_order_by_expression_input_field(
builder,
model.model.name.clone(),
order_by_expression_info,
)
};
arguments.insert(
order_by_argument.name.clone(),
builder.allow_all_namespaced(order_by_argument),
);
}
// generate and insert where argument
let boolean_expression_filter_type =
&model
.model
.filter_expression_type
.as_ref()
.and_then(|bool_exp| match bool_exp {
metadata_resolve::ModelExpressionType::ObjectBooleanExpressionType(
object_boolean_expression_type,
) => object_boolean_expression_type
.graphql
.as_ref()
.map(|graphql_config| {
(
object_boolean_expression_type.name.clone(),
graphql_config.clone(),
)
}),
metadata_resolve::ModelExpressionType::BooleanExpressionType(
boolean_expression_object_type,
) => boolean_expression_object_type
.graphql
.as_ref()
.map(|graphql_config| {
(
boolean_expression_object_type.name.clone(),
graphql_config.clone(),
)
}),
});
if let Some((boolean_expression_type_name, boolean_expression_graphql_config)) =
boolean_expression_filter_type
{
let where_argument = get_where_expression_input_field(
builder,
boolean_expression_type_name.clone(),
boolean_expression_graphql_config,
);
arguments.insert(
where_argument.name.clone(),
builder.allow_all_namespaced(where_argument),
);
}
add_limit_input_field(&mut arguments, builder, model)?;
add_offset_input_field(&mut arguments, builder, model)?;
add_order_by_input_field(&mut arguments, builder, model)?;
add_where_input_field(&mut arguments, builder, model)?;
Ok(arguments)
}
@ -190,19 +104,3 @@ pub(crate) fn select_many_field(
);
Ok((query_root_field, field))
}
/// Generates the input field for the arguments which are of type int.
pub(crate) fn generate_int_input_argument(
name: &str,
annotation: Annotation,
) -> Result<gql_schema::InputField<GDS>, crate::Error> {
let input_field_name = metadata_resolve::mk_name(name)?;
Ok(gql_schema::InputField::new(
input_field_name,
None,
annotation,
ast::TypeContainer::named_null(gql_schema::RegisteredTypeName::int()),
None,
gql_schema::DeprecationStatus::NotDeprecated,
))
}

View File

@ -10,6 +10,7 @@ use std::{
};
use open_dds::{
aggregates,
arguments::ArgumentName,
commands,
data_connector::{DataConnectorColumnName, DataConnectorName, DataConnectorOperatorName},
@ -74,6 +75,7 @@ pub struct EntityFieldTypeNameMapping {
pub enum RootFieldKind {
SelectOne,
SelectMany,
SelectAggregate,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@ -186,6 +188,7 @@ pub enum OutputAnnotation {
typename_mappings: HashMap<ast::TypeName, Vec<types::FieldName>>,
},
SDL,
Aggregate(crate::aggregates::AggregateOutputAnnotation),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
@ -219,6 +222,7 @@ pub enum ModelInputAnnotation {
// Optional because we allow building schema without specifying a data source
ndc_column: Option<NdcColumnForComparison>,
},
ModelFilterInputArgument,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
@ -371,6 +375,14 @@ pub enum TypeId {
graphql_type_name: ast::TypeName,
},
ApolloFederationType(PossibleApolloFederationTypes),
AggregateSelectOutputType {
aggregate_expression_name: Qualified<aggregates::AggregateExpressionName>,
graphql_type_name: ast::TypeName,
},
ModelFilterInputType {
model_name: Qualified<models::ModelName>,
graphql_type_name: ast::TypeName,
},
}
#[derive(Serialize, Clone, Debug, Hash, PartialEq, Eq)]
@ -412,6 +424,12 @@ impl TypeId {
}
| TypeId::OrderByEnumType {
graphql_type_name, ..
}
| TypeId::AggregateSelectOutputType {
graphql_type_name, ..
}
| TypeId::ModelFilterInputType {
graphql_type_name, ..
} => graphql_type_name.clone(),
TypeId::NodeRoot => ast::TypeName(mk_name!("Node")),
TypeId::ModelArgumentsInput { type_name, .. } => type_name.clone(),