Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(qe): Implement findFirstOrThrow and findUniqeOrThrow in the engine #3356

Merged
merged 6 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod find_first_query {
use query_engine_tests::assert_query;

#[connector_test]
async fn fetch_first_matching(runner: Runner) -> TestResult<()> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a small replacement of fetch -> find on the test cases

async fn find_first_matching(runner: Runner) -> TestResult<()> {
test_data(&runner).await?;

assert_query!(
Expand Down Expand Up @@ -35,6 +35,19 @@ mod find_first_query {
Ok(())
}

#[connector_test]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a failure case for findFirst to check that it actually behaves differently than findFirstOrThrow

async fn find_first_not_matching(runner: Runner) -> TestResult<()> {
test_data(&runner).await?;

assert_query!(
runner,
"query { findFirstTestModel(where: { id: 6 }) { id }}",
r#"{"data":{"findFirstTestModel":null}}"#
);

Ok(())
}

async fn test_data(runner: &Runner) -> TestResult<()> {
test_row(runner, r#"{ id: 1, field: "test1" }"#).await?;
test_row(runner, r#"{ id: 2, field: "test2" }"#).await?;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use query_engine_tests::*;

#[test_suite(schema(schemas::generic))]
mod find_first_or_throw_query {
use query_engine_tests::assert_query;

#[connector_test]
async fn find_first_or_throw_matching(runner: Runner) -> TestResult<()> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test case to ensure that findFirstOrThrow behaves like findFirst when a record is found

test_data(&runner).await?;

assert_query!(
runner,
"query { findFirstTestModelOrThrow(where: { id: 1 }) { id }}",
r#"{"data":{"findFirstTestModelOrThrow":{"id":1}}}"#
);

assert_query!(
runner,
"query { findFirstTestModelOrThrow(where: { field: { not: null }}) { id }}",
r#"{"data":{"findFirstTestModelOrThrow":{"id":1}}}"#
);

assert_query!(
runner,
"query { findFirstTestModelOrThrow(where: { field: { not: null }}, orderBy: { id: desc }) { id }}",
r#"{"data":{"findFirstTestModelOrThrow":{"id":5}}}"#
);

assert_query!(
runner,
"query { findFirstTestModelOrThrow(where: { field: { not: null }}, cursor: { id: 1 }, take: 1, skip: 1, orderBy: { id: asc }) { id }}",
r#"{"data":{"findFirstTestModelOrThrow":{"id":2}}}"#
);

Ok(())
}

#[connector_test]
async fn find_first_or_throw_not_matching(runner: Runner) -> TestResult<()> {
test_data(&runner).await?;

assert_error!(
&runner,
"query { findFirstTestModelOrThrow(where: { id: 6 }) { id }}",
2025,
"An operation failed because it depends on one or more records that were required but not found. Expected a record, found none."
);

Ok(())
}

async fn test_data(runner: &Runner) -> TestResult<()> {
test_row(runner, r#"{ id: 1, field: "test1" }"#).await?;
test_row(runner, r#"{ id: 2, field: "test2" }"#).await?;
test_row(runner, r#"{ id: 3 }"#).await?;
test_row(runner, r#"{ id: 4 }"#).await?;
test_row(runner, r#"{ id: 5, field: "test3" }"#).await?;

Ok(())
}

async fn test_row(runner: &Runner, data: &str) -> TestResult<()> {
runner
.query(format!("mutation {{ createOneTestModel(data: {}) {{ id }} }}", data))
.await?
.assert_success();
Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod find_unique {
use query_engine_tests::assert_query;

#[connector_test]
async fn fetch_unique_by_id(runner: Runner) -> TestResult<()> {
async fn find_unique_by_id(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand All @@ -18,7 +18,7 @@ mod find_unique {
}

#[connector_test]
async fn fetch_unique_by_single_unique(runner: Runner) -> TestResult<()> {
async fn find_unique_by_single_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand All @@ -31,7 +31,7 @@ mod find_unique {
}

#[connector_test]
async fn fetch_unique_by_multi_unique(runner: Runner) -> TestResult<()> {
async fn find_unique_by_multi_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand All @@ -44,7 +44,7 @@ mod find_unique {
}

#[connector_test]
async fn no_result_fetch_unique_by_id(runner: Runner) -> TestResult<()> {
async fn no_result_find_unique_by_id(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand All @@ -57,7 +57,7 @@ mod find_unique {
}

#[connector_test]
async fn no_result_fetch_unique_by_single_unique(runner: Runner) -> TestResult<()> {
async fn no_result_find_unique_by_single_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand All @@ -70,7 +70,7 @@ mod find_unique {
}

#[connector_test]
async fn no_result_fetch_unique_by_multi_unique(runner: Runner) -> TestResult<()> {
async fn no_result_find_unique_by_multi_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use query_engine_tests::*;

#[test_suite(schema(schemas::user))]
mod find_unique_or_throw {
use query_engine_tests::assert_query;

#[connector_test]
async fn find_unique_or_throw_when_record_is_found(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_query!(
&runner,
r#"query { findUniqueUserOrThrow(where: { email: "a@b.com" }) { id } }"#,
r#"{"data":{"findUniqueUserOrThrow":{"id":1}}}"#
);

assert_query!(
&runner,
r#"query { findUniqueUserOrThrow(where: { first_name_last_name: { first_name: "Elongated", last_name: "Muskrat" } }) { id } }"#,
r#"{"data":{"findUniqueUserOrThrow":{"id":1}}}"#
);

assert_query!(
runner,
"query { findUniqueUserOrThrow(where: { id: 1 }) { id } }",
r#"{"data":{"findUniqueUserOrThrow":{"id":1}}}"#
);

Ok(())
}

#[connector_test]
async fn no_result_find_unique_by_id(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_error!(
&runner,
"query { findUniqueUserOrThrow(where: { id: 2 }) { id } }",
2025,
"An operation failed because it depends on one or more records that were required but not found. Expected a record, found none."
Comment on lines +39 to +40
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the error returned to the user, which as per this method is a ConnectorError rendering this

);

Ok(())
}

#[connector_test]
async fn no_result_find_unique_by_single_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_error!(
&runner,
r#"query { findUniqueUserOrThrow(where: { email: "b@a.com" }) { id } }"#,
2025,
"An operation failed because it depends on one or more records that were required but not found. Expected a record, found none."
);

Ok(())
}

#[connector_test]
async fn no_result_find_unique_by_multi_unique(runner: Runner) -> TestResult<()> {
test_user(&runner).await?;

assert_error!(
&runner,
r#"query { findUniqueUserOrThrow(where: { first_name_last_name: { first_name: "Doesn't", last_name: "Exist" } }) { id } }"#,
2025,
"An operation failed because it depends on one or more records that were required but not found. Expected a record, found none."
);

Ok(())
}

async fn test_user(runner: &Runner) -> TestResult<()> {
runner
.query(r#"mutation { createOneUser(data: { id: 1, email: "a@b.com", first_name: "Elongated", last_name: "Muskrat" }) { id } }"#)
.await?.assert_success();

Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod composite_default_value;
mod find_first;
mod find_first_or_throw;
mod find_many;
mod find_unique;
mod find_unique_or_throw;
mod json_result;
mod m2m;
mod mongo_incorrect_fields;
Expand Down
1 change: 1 addition & 0 deletions query-engine/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ schema = { path = "../schema" }
schema-builder = { path = "../schema-builder" }
parking_lot = "0.12"
lru = "0.7.7"
enumflags2 = "0.7"
94 changes: 54 additions & 40 deletions query-engine/core/src/interpreter/query_interpreters/read.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use super::*;
use crate::{interpreter::InterpretationResult, query_ast::*, result_ast::*};
use connector::{self, ConnectionLike, QueryArguments, RelAggregationRow, RelAggregationSelection};
use connector::{
self, error::ConnectorError, ConnectionLike, QueryArguments, RelAggregationRow, RelAggregationSelection,
};
use futures::future::{BoxFuture, FutureExt};
use inmemory_record_processor::InMemoryRecordProcessor;
use prisma_models::ManyRecords;
use std::collections::HashMap;
use user_facing_errors::KnownError;

pub fn execute<'conn>(
tx: &'conn mut dyn ConnectionLike,
Expand Down Expand Up @@ -62,6 +65,8 @@ fn read_one(
.into())
}

None if query.options.contains(QueryOption::ThrowOnEmpty) => record_not_found(),
miguelff marked this conversation as resolved.
Show resolved Hide resolved

None => Ok(QueryResult::RecordSelection(Box::new(RecordSelection {
name: query.name,
fields: query.selection_order,
Expand All @@ -88,50 +93,46 @@ fn read_many(
mut query: ManyRecordsQuery,
trace_id: Option<String>,
) -> BoxFuture<'_, InterpretationResult<QueryResult>> {
let processor = if query.args.requires_inmemory_processing() {
Some(InMemoryRecordProcessor::new_from_query_args(&mut query.args))
} else {
None
};

let fut = async move {
let (scalars, aggregation_rows) = if query.args.requires_inmemory_processing() {
let processor = InMemoryRecordProcessor::new_from_query_args(&mut query.args);
let scalars = tx
.get_many_records(
&query.model,
query.args.clone(),
&query.selected_fields,
&query.aggregation_selections,
trace_id,
)
.await?;
let scalars = processor.apply(scalars);
let (scalars, aggregation_rows) =
extract_aggregation_rows_from_scalars(scalars, query.aggregation_selections);

(scalars, aggregation_rows)
let scalars = tx
.get_many_records(
&query.model,
query.args.clone(),
&query.selected_fields,
&query.aggregation_selections,
trace_id,
)
.await?;

let scalars = if let Some(p) = processor {
p.apply(scalars)
} else {
let scalars = tx
.get_many_records(
&query.model,
query.args.clone(),
&query.selected_fields,
&query.aggregation_selections,
trace_id,
)
.await?;
let (scalars, aggregation_rows) =
extract_aggregation_rows_from_scalars(scalars, query.aggregation_selections);
(scalars, aggregation_rows)
scalars
};

let nested: Vec<QueryResult> = process_nested(tx, query.nested, Some(&scalars)).await?;
let (scalars, aggregation_rows) = extract_aggregation_rows_from_scalars(scalars, query.aggregation_selections);

Ok(RecordSelection {
name: query.name,
fields: query.selection_order,
scalars,
nested,
query_arguments: query.args,
model: query.model,
aggregation_rows,
if scalars.records.is_empty() && query.options.contains(QueryOption::ThrowOnEmpty) {
miguelff marked this conversation as resolved.
Show resolved Hide resolved
record_not_found()
} else {
let nested: Vec<QueryResult> = process_nested(tx, query.nested, Some(&scalars)).await?;
Ok(RecordSelection {
name: query.name,
fields: query.selection_order,
scalars,
nested,
query_arguments: query.args,
model: query.model,
aggregation_rows,
}
.into())
}
.into())
};

fut.boxed()
Expand Down Expand Up @@ -165,7 +166,6 @@ fn read_related<'conn>(
)
.await?
};

let model = query.parent_field.related_model();
let nested: Vec<QueryResult> = process_nested(tx, query.nested, Some(&scalars)).await?;

Expand Down Expand Up @@ -288,3 +288,17 @@ pub fn extract_aggregation_rows_from_scalars(

(scalars, Some(aggregation_rows))
}

// Custom error built for findXOrThrow queries, when a record is not found and it needs to throw an error
#[inline]
fn record_not_found() -> InterpretationResult<QueryResult> {
Err(ConnectorError {
user_facing_error: Some(KnownError::new(
user_facing_errors::query_engine::RecordRequiredButNotFound {
cause: "Expected a record, found none.".to_owned(),
},
)),
kind: connector::error::ErrorKind::RecordDoesNotExist,
}
.into())
}