Skip to content

Commit 94e2a71

Browse files
authored
feat(schema): enable client APIs that require unique for views (#5582)
1 parent 0c162af commit 94e2a71

5 files changed

Lines changed: 190 additions & 30 deletions

File tree

query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/views.rs

Lines changed: 176 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ mod views {
1919
lastName String
2020
fullName String
2121
}
22+
23+
view TestViewWithUnique {
24+
#id(id, Int, @unique)
25+
firstName String
26+
lastName String
27+
fullName String
28+
}
2229
"#
2330
};
2431

@@ -84,11 +91,35 @@ mod views {
8491
Ok(())
8592
}
8693

94+
#[connector_test]
95+
async fn cursor(runner: Runner) -> TestResult<()> {
96+
create_test_data(&runner, "cursor").await?;
97+
98+
insta::assert_snapshot!(
99+
run_query!(&runner, r#"{ findManyTestViewWithUnique(cursor: { id: 1 }) { fullName } }"#),
100+
@r#"{"data":{"findManyTestViewWithUnique":[{"fullName":"John Doe"},{"fullName":"Jane Doe"},{"fullName":"Bob Maurane"}]}}"#
101+
);
102+
103+
Ok(())
104+
}
105+
87106
#[connector_test]
88107
async fn no_find_unique(runner: Runner) -> TestResult<()> {
89108
test_no_toplevel_query(runner, r#"{ findUniqueTestView(where: { id: 1 }) { fullName } }"#).await
90109
}
91110

111+
#[connector_test]
112+
async fn find_unique(runner: Runner) -> TestResult<()> {
113+
create_test_data(&runner, "find_unique").await?;
114+
115+
insta::assert_snapshot!(
116+
run_query!(&runner, r#"{ findUniqueTestViewWithUnique(where: { id: 1 }) { fullName } }"#),
117+
@r#"{"data":{"findUniqueTestViewWithUnique":{"fullName":"John Doe"}}}"#
118+
);
119+
120+
Ok(())
121+
}
122+
92123
#[connector_test]
93124
async fn no_find_unique_or_throw(runner: Runner) -> TestResult<()> {
94125
test_no_toplevel_query(
@@ -98,6 +129,18 @@ mod views {
98129
.await
99130
}
100131

132+
#[connector_test]
133+
async fn find_unique_or_throw(runner: Runner) -> TestResult<()> {
134+
create_test_data(&runner, "find_unique_or_throw").await?;
135+
136+
insta::assert_snapshot!(
137+
run_query!(&runner, r#"{ findUniqueTestViewWithUniqueOrThrow(where: { id: 1 }) { fullName } }"#),
138+
@r#"{"data":{"findUniqueTestViewWithUniqueOrThrow":{"fullName":"John Doe"}}}"#
139+
);
140+
141+
Ok(())
142+
}
143+
101144
#[connector_test]
102145
async fn take_with_order_by(runner: Runner) -> TestResult<()> {
103146
create_test_data(&runner, "take_with_order_by").await?;
@@ -124,6 +167,18 @@ mod views {
124167
Ok(())
125168
}
126169

170+
#[connector_test]
171+
async fn take_without_order_by_with_unique(runner: Runner) -> TestResult<()> {
172+
create_test_data(&runner, "take_without_order_by_with_unique").await?;
173+
174+
insta::assert_snapshot!(
175+
run_query!(runner, r#"{ findManyTestViewWithUnique(take: 2) { id fullName } }"#),
176+
@r#"{"data":{"findManyTestViewWithUnique":[{"id":1,"fullName":"John Doe"},{"id":2,"fullName":"Jane Doe"}]}}"#
177+
);
178+
179+
Ok(())
180+
}
181+
127182
#[connector_test]
128183
async fn skip_with_order_by(runner: Runner) -> TestResult<()> {
129184
create_test_data(&runner, "skip_with_order_by").await?;
@@ -150,6 +205,18 @@ mod views {
150205
Ok(())
151206
}
152207

208+
#[connector_test]
209+
async fn skip_without_order_by_with_unique(runner: Runner) -> TestResult<()> {
210+
create_test_data(&runner, "skip_without_order_by_with_unique").await?;
211+
212+
insta::assert_snapshot!(
213+
run_query!(runner, r#"{ findManyTestViewWithUnique(skip: 2) { id fullName } }"#),
214+
@r#"{"data":{"findManyTestViewWithUnique":[{"id":3,"fullName":"Bob Maurane"}]}}"#
215+
);
216+
217+
Ok(())
218+
}
219+
153220
#[connector_test]
154221
async fn take_skip_with_order_by(runner: Runner) -> TestResult<()> {
155222
create_test_data(&runner, "take_skip_with_order_by").await?;
@@ -175,6 +242,17 @@ mod views {
175242

176243
Ok(())
177244
}
245+
#[connector_test]
246+
async fn take_skip_without_order_by_with_unique(runner: Runner) -> TestResult<()> {
247+
create_test_data(&runner, "take_skip_without_order_by_with_unique").await?;
248+
249+
insta::assert_snapshot!(
250+
run_query!(runner, r#"{ findManyTestViewWithUnique(take: 1, skip: 1) { id fullName } }"#),
251+
@r#"{"data":{"findManyTestViewWithUnique":[{"id":2,"fullName":"Jane Doe"}]}}"#
252+
);
253+
254+
Ok(())
255+
}
178256

179257
#[connector_test]
180258
async fn take_with_empty_order_by(runner: Runner) -> TestResult<()> {
@@ -184,7 +262,19 @@ mod views {
184262
runner,
185263
r#"{ findManyTestView(take: 1, orderBy: {}) { id fullName } }"#,
186264
2019,
187-
"`orderBy` definition must not be empty when querying views"
265+
"`orderBy` definition must not be empty when querying views without unique fields"
266+
);
267+
268+
Ok(())
269+
}
270+
271+
#[connector_test]
272+
async fn take_with_empty_order_by_with_unique(runner: Runner) -> TestResult<()> {
273+
create_test_data(&runner, "take_with_empty_order_by_with_unique").await?;
274+
275+
insta::assert_snapshot!(
276+
run_query!(runner, r#"{ findManyTestViewWithUnique(take: 1, orderBy: {}) { id fullName } }"#),
277+
@r#"{"data":{"findManyTestViewWithUnique":[{"id":1,"fullName":"John Doe"}]}}"#
188278
);
189279

190280
Ok(())
@@ -198,7 +288,19 @@ mod views {
198288
runner,
199289
r#"{ findManyTestView(skip: 1, orderBy: {}) { id fullName } }"#,
200290
2019,
201-
"`orderBy` definition must not be empty when querying views"
291+
"`orderBy` definition must not be empty when querying views without unique fields"
292+
);
293+
294+
Ok(())
295+
}
296+
297+
#[connector_test]
298+
async fn skip_with_empty_order_by_with_unique(runner: Runner) -> TestResult<()> {
299+
create_test_data(&runner, "skip_with_empty_order_by_with_unique").await?;
300+
301+
insta::assert_snapshot!(
302+
run_query!(runner, r#"{ findManyTestViewWithUnique(skip: 1, orderBy: {}) { id fullName } }"#),
303+
@r#"{"data":{"findManyTestViewWithUnique":[{"id":2,"fullName":"Jane Doe"},{"id":3,"fullName":"Bob Maurane"}]}}"#
202304
);
203305

204306
Ok(())
@@ -230,6 +332,18 @@ mod views {
230332
Ok(())
231333
}
232334

335+
#[connector_test]
336+
async fn group_by_take_without_order_by_with_unique(runner: Runner) -> TestResult<()> {
337+
create_test_data(&runner, "group_by_take_without_order_by_with_unique").await?;
338+
339+
insta::assert_snapshot!(
340+
run_query!(runner, r#"{ groupByTestViewWithUnique(by: [lastName, id], take: 1) { lastName } }"#),
341+
@r#"{"data":{"groupByTestViewWithUnique":[{"lastName":"Doe"}]}}"#
342+
);
343+
344+
Ok(())
345+
}
346+
233347
#[connector_test]
234348
async fn group_by_take_with_empty_order_by(runner: Runner) -> TestResult<()> {
235349
create_test_data(&runner, "group_by_take_with_empty_order_by").await?;
@@ -238,7 +352,19 @@ mod views {
238352
runner,
239353
r#"{ groupByTestView(by: lastName, take: 1, orderBy: {}) { lastName } }"#,
240354
2019,
241-
"`orderBy` definition must not be empty when querying views"
355+
"`orderBy` definition must not be empty when querying views without unique fields"
356+
);
357+
358+
Ok(())
359+
}
360+
361+
#[connector_test]
362+
async fn group_by_take_with_empty_order_by_with_unique(runner: Runner) -> TestResult<()> {
363+
create_test_data(&runner, "group_by_take_with_empty_order_by_with_unique").await?;
364+
365+
insta::assert_snapshot!(
366+
run_query!(runner, r#"{ groupByTestViewWithUnique(by: [lastName, id], take: 1, orderBy: {}) { lastName } }"#),
367+
@r#"{"data":{"groupByTestViewWithUnique":[{"lastName":"Doe"}]}}"#
242368
);
243369

244370
Ok(())
@@ -270,6 +396,18 @@ mod views {
270396
Ok(())
271397
}
272398

399+
#[connector_test]
400+
async fn group_by_skip_without_order_by_with_unique(runner: Runner) -> TestResult<()> {
401+
create_test_data(&runner, "group_by_skip_without_order_by_with_unique").await?;
402+
403+
insta::assert_snapshot!(
404+
run_query!(runner, r#"{ groupByTestViewWithUnique(by: [lastName, id], skip: 1) { lastName } }"#),
405+
@r#"{"data":{"groupByTestViewWithUnique":[{"lastName":"Doe"},{"lastName":"Maurane"}]}}"#
406+
);
407+
408+
Ok(())
409+
}
410+
273411
#[connector_test]
274412
async fn group_by_skip_with_empty_order_by(runner: Runner) -> TestResult<()> {
275413
create_test_data(&runner, "group_by_skip_with_empty_order_by").await?;
@@ -278,14 +416,27 @@ mod views {
278416
runner,
279417
r#"{ groupByTestView(by: lastName, skip: 1, orderBy: {}) { lastName } }"#,
280418
2019,
281-
"`orderBy` definition must not be empty when querying views"
419+
"`orderBy` definition must not be empty when querying views without unique fields"
420+
);
421+
422+
Ok(())
423+
}
424+
425+
#[connector_test]
426+
async fn group_by_skip_with_empty_order_by_with_unique(runner: Runner) -> TestResult<()> {
427+
create_test_data(&runner, "group_by_skip_with_empty_order_by_with_unique").await?;
428+
429+
insta::assert_snapshot!(
430+
run_query!(runner, r#"{ groupByTestViewWithUnique(by: [lastName, id], skip: 1, orderBy: {}) { lastName } }"#),
431+
@r#"{"data":{"groupByTestViewWithUnique":[{"lastName":"Doe"},{"lastName":"Maurane"}]}}"#
282432
);
283433

284434
Ok(())
285435
}
286436

287437
async fn create_test_data(runner: &Runner, schema_name: &str) -> TestResult<()> {
288-
migrate_view(runner, schema_name).await?;
438+
migrate_view(runner, schema_name, "TestView").await?;
439+
migrate_view(runner, schema_name, "TestViewWithUnique").await?;
289440

290441
create_test_model(runner, r#"{ id: 1, firstName: "John", lastName: "Doe" }"#).await?;
291442
create_test_model(runner, r#"{ id: 2, firstName: "Jane", lastName: "Doe" }"#).await?;
@@ -294,32 +445,34 @@ mod views {
294445
Ok(())
295446
}
296447

297-
async fn migrate_view(runner: &Runner, schema_name: &str) -> TestResult<()> {
298-
let sql = migrate_view_sql(runner, schema_name).await;
299-
300-
runner.raw_execute(sql).await?;
301-
302-
Ok(())
448+
async fn migrate_view(runner: &Runner, schema_name: &str, view_name: &str) -> TestResult<()> {
449+
let sql = migrate_view_sql(runner, schema_name, view_name).await;
450+
runner.raw_execute(sql).await
303451
}
304452

305453
// schema name must be the name of the test in which it's called.
306-
async fn migrate_view_sql(runner: &Runner, schema_name: &str) -> String {
454+
async fn migrate_view_sql(runner: &Runner, schema_name: &str, view_name: &str) -> String {
307455
match runner.connector_version() {
308-
ConnectorVersion::Postgres(_)
309-
| ConnectorVersion::CockroachDb(_)
310-
=> {
311-
r#"CREATE VIEW "TestView" AS SELECT "TestModel".id, "TestModel"."firstName", "TestModel"."lastName", CONCAT("TestModel"."firstName", ' ', "TestModel"."lastName") as "fullName" From "TestModel""#.to_owned()
456+
ConnectorVersion::Postgres(_) | ConnectorVersion::CockroachDb(_) => {
457+
format!(
458+
r#"CREATE VIEW "{view_name}" AS SELECT "TestModel".id, "TestModel"."firstName", "TestModel"."lastName", CONCAT("TestModel"."firstName", ' ', "TestModel"."lastName") as "fullName" From "TestModel""#
459+
)
460+
}
461+
ConnectorVersion::MySql(_) | ConnectorVersion::Vitess(_) => {
462+
format!(
463+
r#"CREATE VIEW {view_name} AS SELECT TestModel.*, CONCAT(TestModel.firstName, ' ', TestModel.lastName) AS "fullName" FROM TestModel"#
464+
)
312465
}
313-
ConnectorVersion::MySql(_) | ConnectorVersion::Vitess(_)
314-
=> {
315-
r#"CREATE VIEW TestView AS SELECT TestModel.*, CONCAT(TestModel.firstName, ' ', TestModel.lastName) AS "fullName" FROM TestModel"#.to_owned()
316-
},
317466
ConnectorVersion::Sqlite(_) => {
318-
r#"CREATE VIEW TestView AS SELECT TestModel.*, TestModel.firstName || ' ' || TestModel.lastName AS "fullName" FROM TestModel"#.to_owned()
467+
format!(
468+
r#"CREATE VIEW {view_name} AS SELECT TestModel.*, TestModel.firstName || ' ' || TestModel.lastName AS "fullName" FROM TestModel"#
469+
)
319470
}
320471
ConnectorVersion::SqlServer(_) => {
321-
format!(r#"CREATE VIEW [views_{schema_name}].[TestView] AS SELECT [views_{schema_name}].[TestModel].[id], [views_{schema_name}].[TestModel].[firstName], [views_{schema_name}].[TestModel].[lastName], CONCAT([views_{schema_name}].[TestModel].[firstName], ' ', [views_{schema_name}].[TestModel].[lastName]) as "fullName" FROM [views_{schema_name}].[TestModel];"#)
322-
},
472+
format!(
473+
r#"CREATE VIEW [views_{schema_name}].[{view_name}] AS SELECT [views_{schema_name}].[TestModel].[id], [views_{schema_name}].[TestModel].[firstName], [views_{schema_name}].[TestModel].[lastName], CONCAT([views_{schema_name}].[TestModel].[firstName], ' ', [views_{schema_name}].[TestModel].[lastName]) as "fullName" FROM [views_{schema_name}].[TestModel];"#
474+
)
475+
}
323476
ConnectorVersion::MongoDb(_) => unreachable!(),
324477
}
325478
}

query-engine/core/src/query_graph_builder/extractors/query_arguments.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ fn finalize_arguments(mut args: QueryArguments, model: &Model) -> QueryGraphBuil
345345
&& args.order_by.is_empty();
346346

347347
if add_implicit_ordering {
348-
if model.is_view() {
348+
if !model.has_unique_identifier() {
349349
return Err(QueryGraphBuilderError::InputError(
350-
"`orderBy` definition must not be empty when querying views".into(),
350+
"`orderBy` definition must not be empty when querying views without unique fields".into(),
351351
));
352352
}
353353

query-engine/query-structure/src/model.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ impl Model {
6464
!has_unsupported_field && !self.is_view()
6565
}
6666

67+
/// Checks if the model has a true unique identifier defined in the schema.
68+
/// This can only be false if the "model" is actually a view.
69+
pub fn has_unique_identifier(&self) -> bool {
70+
self.walker().required_unique_criterias().next().is_some()
71+
}
72+
6773
/// The name of the model in the database
6874
/// For a sql database this will be the Table name for this model
6975
pub fn db_name(&self) -> &str {

query-engine/schema/src/build/input_types/fields/arguments.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ pub(crate) fn skip_argument<'a>(model: &Model) -> InputField<'a> {
161161

162162
fn pagination_argument<'a>(arg: &'static str, model: &Model) -> InputField<'a> {
163163
let arg = input_field(arg, vec![InputType::int()], None).optional();
164-
if model.is_view() {
165-
arg.with_requires_other_fields([args::ORDER_BY])
166-
} else {
164+
if model.has_unique_identifier() {
167165
arg
166+
} else {
167+
arg.with_requires_other_fields([args::ORDER_BY])
168168
}
169169
}
170170

@@ -227,7 +227,7 @@ impl<'a> ManyRecordsSelectionArgumentsBuilder<'a> {
227227
),
228228
]
229229
.into_iter()
230-
.chain((!self.model.is_view()).then(|| {
230+
.chain(self.model.has_unique_identifier().then(|| {
231231
let unique_input_type =
232232
InputType::object(filter_objects::where_unique_object_type(self.ctx, self.model.clone()));
233233
input_field(args::CURSOR, vec![unique_input_type], None).optional()

query-engine/schema/src/build/output_types/query_type.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pub(crate) fn query_fields(ctx: &QuerySchema) -> Vec<FieldFn> {
1818
field!(find_many_field, model);
1919
field!(plain_aggregation_field, model);
2020
field!(group_by_aggregation_field, model);
21-
if !model.is_view() {
21+
22+
if model.has_unique_identifier() {
2223
field!(find_unique_field, model);
2324
field!(find_unique_or_throw_field, model);
2425
}

0 commit comments

Comments
 (0)