Skip to content

Commit e3dc896

Browse files
rakeshkkydaniel-chambers
authored andcommitted
metadata-resolve: disallow relationship comparisons in nested object filter expressions for model filters (#960)
<!-- The PR description should answer 2 important questions: --> ### What We cannot support having relationship comparisons in nested object field filters of a boolean expression. NDC does not support this natively ([slack thread](https://hasurahq.slack.com/archives/C05HND0F6LB/p1722845028319099)). This PR adds a metadata build check for this case and reject such boolean expressions. Tests in the PR is partially based on hasura/v3-engine#935. **Note:** This might break older builds having relationship comparisons in the nested object field filter expressions. This is a rare scenario. We will check through our schema-diff job for any failing builds. If there are significant, we might hold this PR. Full context in this slack thread - https://hasurahq.slack.com/archives/C06P2U8U55G/p1723121764332539. ### How - Add a metadata build check for boolean expressions that restricts them to have nested object filters with relationship comparisons. - Raise GraphQL API runtime internal error while building the filter IR when a relationship comparison found within a nested field filter. --------- Co-authored-by: Daniel Chambers <[email protected]> V3_GIT_ORIGIN_REV_ID: 86dce0b784c77e57bb1888125dba5b599e40f741
1 parent 7c4245f commit e3dc896

File tree

11 files changed

+1976
-18
lines changed

11 files changed

+1976
-18
lines changed

v3/changelog.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
### Fixed
88

9+
- Disallow model filter boolean expressions having relationship comparisons in
10+
their nested object filters.
11+
912
### Changed
1013

1114
## [v2024.08.07]

v3/crates/engine/tests/db_definition.sql

+19-4
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,22 @@ CREATE TYPE public.staff AS (
114114
favourite_artist_id int
115115
);
116116

117+
CREATE TYPE public.institution_songs AS (
118+
primary_anthem_track_id int,
119+
secondary_anthem_track_id int
120+
);
121+
117122
CREATE TABLE public.institution (
118123
id integer NOT NULL,
119124
name text,
120125
location location,
121126
staff staff [],
122127
departments text [],
128+
songs institution_songs,
123129
CONSTRAINT institution_pkey PRIMARY KEY (id)
124130
);
125131

126-
INSERT INTO public.institution (id, name, location, staff, departments)
132+
INSERT INTO public.institution (id, name, location, staff, departments, songs)
127133
VALUES (
128134
1,
129135
'Queen Mary University of London',
@@ -135,7 +141,11 @@ VALUES (
135141
ARRAY [ROW('Peter','Landin',ARRAY['Computer Science','Education'],
136142
1
137143
)::staff ],
138-
ARRAY ['Humanities and Social Sciences','Science and Engineering','Medicine and Dentistry']
144+
ARRAY ['Humanities and Social Sciences','Science and Engineering','Medicine and Dentistry'],
145+
ROW(
146+
2270,
147+
2271
148+
)::institution_songs
139149
),
140150
(
141151
2,
@@ -154,12 +164,17 @@ ROW(
154164
ARRAY ['Computer Science','Functional Programming','Automated Reasoning'],
155165
3
156166
)::staff ],
157-
ARRAY ['Architecture and Civil Engineering','Computer Science and Engineering','Electrical Engineering','Physics','Industrial and Materials Science']
167+
ARRAY ['Architecture and Civil Engineering','Computer Science and Engineering','Electrical Engineering','Physics','Industrial and Materials Science'],
168+
ROW(
169+
3421,
170+
NULL
171+
)::institution_songs
158172
),
159173
(
160174
3,
161175
'University of Nowhere',
162176
null,
163177
null,
164-
ARRAY ['nothing',null]
178+
ARRAY ['nothing',null],
179+
NULL
165180
);

v3/crates/engine/tests/execute/common_metadata/postgres_connector_schema.json

+25
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,15 @@
499499
"element_type": { "type": "named", "name": "staff" }
500500
}
501501
},
502+
"songs": {
503+
"type": {
504+
"type": "nullable",
505+
"underlying_type": {
506+
"type": "named",
507+
"name": "institution_songs"
508+
}
509+
}
510+
},
502511
"departments": {
503512
"description": "The institution's departments",
504513
"arguments": {},
@@ -517,6 +526,22 @@
517526
}
518527
}
519528
},
529+
"institution_songs": {
530+
"fields": {
531+
"primary_anthem_track_id": {
532+
"type": {
533+
"type": "named",
534+
"name": "int4"
535+
}
536+
},
537+
"secondary_anthem_track_id": {
538+
"type": {
539+
"type": "named",
540+
"name": "int4"
541+
}
542+
}
543+
}
544+
},
520545

521546
"test_nullable_and_array": {
522547
"description": "Testing nullable and array types",

v3/crates/engine/tests/ndc-postgres-configuration/configuration.json

+29
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,14 @@
841841
"nullable": "nullable",
842842
"description": null
843843
},
844+
"songs": {
845+
"name": "songs",
846+
"type": {
847+
"compositeType": "institution_songs"
848+
},
849+
"nullable": "nullable",
850+
"description": null
851+
},
844852
"staff": {
845853
"name": "staff",
846854
"type": {
@@ -2578,6 +2586,27 @@
25782586
},
25792587
"description": null
25802588
},
2589+
"institution_songs": {
2590+
"typeName": "institution_songs",
2591+
"schemaName": "public",
2592+
"fields": {
2593+
"primary_anthem_track_id": {
2594+
"fieldName": "primary_anthem_track_id",
2595+
"type": {
2596+
"scalarType": "int4"
2597+
},
2598+
"description": null
2599+
},
2600+
"secondary_anthem_track_id": {
2601+
"fieldName": "secondary_anthem_track_id",
2602+
"type": {
2603+
"scalarType": "int4"
2604+
},
2605+
"description": null
2606+
}
2607+
},
2608+
"description": null
2609+
},
25812610
"location": {
25822611
"typeName": "location",
25832612
"schemaName": "public",

v3/crates/engine/tests/ndc-postgres-configuration/schema.json

+15-13
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
"exposedName": "_lt",
9292
"operatorKind": "custom"
9393
},
94+
{
95+
"operatorName": "<>",
96+
"exposedName": "_neq",
97+
"operatorKind": "custom"
98+
},
9499
{
95100
"operatorName": "!=",
96101
"exposedName": "_neq",
@@ -589,7 +594,7 @@
589594
"required": ["scalarType"],
590595
"properties": {
591596
"scalarType": {
592-
"$ref": "#/definitions/ScalarTypeName"
597+
"type": "string"
593598
}
594599
},
595600
"additionalProperties": false
@@ -599,7 +604,7 @@
599604
"required": ["compositeType"],
600605
"properties": {
601606
"compositeType": {
602-
"$ref": "#/definitions/CompositeTypeName"
607+
"type": "string"
603608
}
604609
},
605610
"additionalProperties": false
@@ -616,14 +621,6 @@
616621
}
617622
]
618623
},
619-
"ScalarTypeName": {
620-
"description": "A name of a Scalar Type, as it appears in the NDC scheme.",
621-
"type": "string"
622-
},
623-
"CompositeTypeName": {
624-
"description": "The name of a Composite Type, as it appears in the NDC schema",
625-
"type": "string"
626-
},
627624
"Nullable": {
628625
"description": "Can this column contain null values",
629626
"type": "string",
@@ -652,7 +649,7 @@
652649
}
653650
},
654651
"UniquenessConstraint": {
655-
"description": "The set of columns that make up a uniqueness constraint.",
652+
"description": "The set of columns that make up a uniqueness constraint. We map each table column to their ndc field names.",
656653
"type": "array",
657654
"items": {
658655
"type": "string"
@@ -740,7 +737,7 @@
740737
"required": ["returnType"],
741738
"properties": {
742739
"returnType": {
743-
"$ref": "#/definitions/ScalarTypeName"
740+
"type": "string"
744741
}
745742
}
746743
},
@@ -756,7 +753,7 @@
756753
"$ref": "#/definitions/OperatorKind"
757754
},
758755
"argumentType": {
759-
"$ref": "#/definitions/ScalarTypeName"
756+
"type": "string"
760757
},
761758
"isInfix": {
762759
"default": true,
@@ -1110,6 +1107,11 @@
11101107
"exposedName": "_lt",
11111108
"operatorKind": "custom"
11121109
},
1110+
{
1111+
"operatorName": "<>",
1112+
"exposedName": "_neq",
1113+
"operatorKind": "custom"
1114+
},
11131115
{
11141116
"operatorName": "!=",
11151117
"exposedName": "_neq",

v3/crates/ir/src/error.rs

+3
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ pub enum InternalDeveloperError {
180180
aggregate_operand_type: QualifiedTypeName,
181181
aggregation_function: AggregationFunctionName,
182182
},
183+
184+
#[error("The relationship '{relationship_name}' is from a nested object and cannot be used in a predicate")]
185+
NestedObjectRelationshipInPredicate { relationship_name: RelationshipName },
183186
}
184187

185188
#[derive(Debug, thiserror::Error)]

v3/crates/ir/src/filter.rs

+15
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ fn build_comparison_expression<'s>(
358358
);
359359
expressions.push(expression);
360360
}
361+
// Nested field comparison
361362
schema::Annotation::Input(InputAnnotation::BooleanExpression(
362363
BooleanExpressionAnnotation::BooleanExpressionArgument {
363364
field:
@@ -391,6 +392,20 @@ fn build_comparison_expression<'s>(
391392

392393
expressions.push(inner_expression);
393394
}
395+
// Nested relationship comparison
396+
schema::Annotation::Input(InputAnnotation::BooleanExpression(
397+
BooleanExpressionAnnotation::BooleanExpressionArgument {
398+
field:
399+
schema::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation {
400+
relationship_name,
401+
..
402+
}),
403+
},
404+
)) => Err(
405+
error::InternalDeveloperError::NestedObjectRelationshipInPredicate {
406+
relationship_name: relationship_name.clone(),
407+
},
408+
)?,
394409

395410
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
396411
annotation: annotation.clone(),

v3/crates/metadata-resolve/src/helpers/boolean_expression.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub(crate) fn validate_data_connector_with_object_boolean_expression_type(
2727
models: &IndexMap<Qualified<ModelName>, models::Model>,
2828
) -> Result<(), Error> {
2929
if let Some(graphql_config) = &object_boolean_expression_type.graphql {
30-
for object_comparison_expression_info in graphql_config.object_fields.values() {
30+
for (field_name, object_comparison_expression_info) in &graphql_config.object_fields {
3131
// look up the leaf boolean expression type
3232
let leaf_boolean_expression = boolean_expression_types
3333
.objects
@@ -53,6 +53,21 @@ pub(crate) fn validate_data_connector_with_object_boolean_expression_type(
5353
}));
5454
}
5555

56+
// This nested object field should not contain any relationship comparisons
57+
if leaf_boolean_expression
58+
.graphql
59+
.as_ref()
60+
.is_some_and(|leaf_graphql_config| {
61+
!leaf_graphql_config.relationship_fields.is_empty()
62+
})
63+
{
64+
return Err(Error::from(boolean_expressions::BooleanExpressionError::NestedObjectFieldContainsRelationshipComparison {
65+
parent_boolean_expression_type_name: object_boolean_expression_type.name.clone(),
66+
field_name: field_name.clone(),
67+
nested_boolean_expression_type_name: object_comparison_expression_info.object_type_name.clone(),
68+
}));
69+
}
70+
5671
// continue checking the nested object...
5772
validate_data_connector_with_object_boolean_expression_type(
5873
data_connector,

v3/crates/metadata-resolve/src/stages/boolean_expressions/error.rs

+9
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ pub enum BooleanExpressionError {
7676
nested_type_name: Qualified<CustomTypeName>,
7777
data_connector_name: Qualified<DataConnectorName>,
7878
},
79+
#[error(
80+
"The nested object field '{field_name}' within '{parent_boolean_expression_type_name}' cannot be used for comparison \
81+
because its boolean expression type '{nested_boolean_expression_type_name}' involves a relationship comparison field."
82+
)]
83+
NestedObjectFieldContainsRelationshipComparison {
84+
field_name: FieldName,
85+
parent_boolean_expression_type_name: Qualified<CustomTypeName>,
86+
nested_boolean_expression_type_name: Qualified<CustomTypeName>,
87+
},
7988
#[error("The field {field_name:} has type {field_type:} but the field's boolean expression type {field_boolean_expression_type_name:} has type {underlying_type:}")]
8089
FieldTypeMismatch {
8190
field_name: FieldName,

0 commit comments

Comments
 (0)