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

Structured errors for parsing overflowing ints #2921

Merged
merged 6 commits into from
Jun 1, 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
7 changes: 7 additions & 0 deletions libs/user-facing-errors/src/query_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,10 @@ pub struct MissingFieldsInModel {
pub expected_type: String,
pub found: String,
}

#[derive(Debug, UserFacingError, Serialize)]
#[user_facing(code = "P2033", message = "{details}")]

pub struct ValueFitError {
pub details: String,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use query_engine_tests::*;

#[test_suite(schema(schema))]
mod max_integer {
fn schema() -> String {
let schema = indoc! {r#"
model Test {
#id(id, Int, @id)
int Int
}
"#};

schema.to_string()
}

#[connector_test]
async fn transform_gql_parser_too_large(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, int: 100000000000000000000 }) { id int } }",
2033,
"A number used in the query does not fit into a 64 bit signed integer. Consider using `BigInt` as field type if you're trying to store large integers."
);

Ok(())
}

#[connector_test]
async fn transform_gql_parser_too_small(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, int: -100000000000000000000 }) { id int } }",
2033,
"A number used in the query does not fit into a 64 bit signed integer. Consider using `BigInt` as field type if you're trying to store large integers."
);

Ok(())
}

// The document parser does not crash on encountering an exponent-notation-serialized int.
// This triggers a 2009 instead of 2033 as this is in the document parser.
#[connector_test]
async fn document_parser_no_crash_too_large(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, int: 1e20 }) { id int } }",
2009,
"Unable to fit float value (or large JS integer serialized in exponent notation) '100000000000000000000' into a 64 Bit signed integer for field 'int'. If you're trying to store large integers, consider using `BigInt`"
);

Ok(())
}

#[connector_test]
async fn document_parser_no_crash_too_small(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, int: -1e20 }) { id int } }",
2009,
"Unable to fit float value (or large JS integer serialized in exponent notation) '-100000000000000000000' into a 64 Bit signed integer for field 'int'. If you're trying to store large integers, consider using `BigInt`"
);

Ok(())
}

// This will not work anymore in the future as we'll redo the float / decimal story. Right now this "works" because floats are deserialized as BigDecimal.
#[connector_test]
async fn document_parser_no_crash_ridiculously_big(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, int: 1e100 }) { id int } }",
2009,
"Unable to fit float value (or large JS integer serialized in exponent notation) '10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' into a 64 Bit signed integer for field 'int'. If you're trying to store large integers, consider using `BigInt`"
);

Ok(())
}
}

#[test_suite(schema(schema))]
mod float_serialization_issues {
fn schema() -> String {
let schema = indoc! {r#"
model Test {
#id(id, Int, @id)
float Float
}
"#};

schema.to_string()
}

#[connector_test(exclude(SqlServer))]
async fn int_range_overlap_works(runner: Runner) -> TestResult<()> {
runner
.query("mutation { createOneTest(data: { id: 1, float: 1e20 }) { id float } }")
.await?
.assert_success();

Ok(())
}

// The same number as above, just not in the exponent notation. That one fails, because f64 can represent the number, i64 can't.
#[connector_test]
async fn int_range_overlap_fails(runner: Runner) -> TestResult<()> {
assert_error!(
runner,
"mutation { createOneTest(data: { id: 1, float: 100000000000000000000 }) { id float } }",
2033,
"A number used in the query does not fit into a 64 bit signed integer. Consider using `BigInt` as field type if you're trying to store large integers."
);

Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod max_integer;
mod prisma_10098;
mod prisma_12929;
mod prisma_6173;
Expand Down
6 changes: 6 additions & 0 deletions query-engine/core/src/query_document/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ impl QueryPath {
path.segments.push(segment);
path
}

pub fn last(&self) -> Option<&str> {
self.segments.last().map(|s| s.as_str())
}
}

impl fmt::Display for QueryPath {
Expand All @@ -60,6 +64,7 @@ pub enum QueryParserErrorKind {
ValueParseError(String),
ValueTypeMismatchError { have: QueryValue, want: InputType },
InputUnionParseError { parsing_errors: Vec<QueryParserError> },
ValueFitError(String),
}

impl Display for QueryParserErrorKind {
Expand All @@ -83,6 +88,7 @@ impl Display for QueryParserErrorKind {
Self::ValueTypeMismatchError { have, want } => {
write!(f, "Value types mismatch. Have: {:?}, want: {:?}", have, want)
}
Self::ValueFitError(s) => write!(f, "{}", s),
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions query-engine/core/src/query_document/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,12 @@ impl QueryDocumentParser {
(QueryValue::Int(i), ScalarType::BigInt) => Ok(PrismaValue::BigInt(i)),

(QueryValue::Float(f), ScalarType::Float) => Ok(PrismaValue::Float(f)),
(QueryValue::Float(f), ScalarType::Int) => Ok(PrismaValue::Int(f.to_i64().unwrap())),
(QueryValue::Float(d), ScalarType::Decimal) => Ok(PrismaValue::Float(d)),
(QueryValue::Float(f), ScalarType::Decimal) => Ok(PrismaValue::Float(f)),
(QueryValue::Float(f), ScalarType::Int) => match f.to_i64() {
Some(converted) => Ok(PrismaValue::Int(converted)),
None => Err(QueryParserError::new(parent_path.clone(), QueryParserErrorKind::ValueFitError(
format!("Unable to fit float value (or large JS integer serialized in exponent notation) '{}' into a 64 Bit signed integer for field '{}'. If you're trying to store large integers, consider using `BigInt`.", f, parent_path.last().unwrap())))),
},

(QueryValue::Boolean(b), ScalarType::Boolean) => Ok(PrismaValue::Boolean(b)),

Expand Down
19 changes: 19 additions & 0 deletions query-engine/request-handlers/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use graphql_parser::query::ParseError;
use query_core::CoreError;
use thiserror::Error;
use user_facing_errors::KnownError;

#[derive(Debug, Error)]
#[allow(clippy::large_enum_variant)]
Expand All @@ -19,6 +20,9 @@ pub enum HandlerError {
feature_name: &'static str,
message: String,
},

#[error("{}", _0)]
ValueFitError(String),
}

impl HandlerError {
Expand All @@ -35,6 +39,21 @@ impl HandlerError {

Self::UnsupportedFeature { feature_name, message }
}

pub fn value_fit(details: impl ToString) -> Self {
Self::ValueFitError(details.to_string())
}

pub fn as_known_error(&self) -> Option<KnownError> {
match self {
HandlerError::ValueFitError(details) => {
Some(KnownError::new(user_facing_errors::query_engine::ValueFitError {
details: details.clone(),
}))
}
_ => None,
}
}
}

impl From<url::ParseError> for HandlerError {
Expand Down
15 changes: 12 additions & 3 deletions query-engine/request-handlers/src/graphql/body.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use super::GraphQLProtocolAdapter;
use crate::HandlerError;
use graphql_parser as gql;
use query_core::{BatchDocument, Operation, QueryDocument};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::GraphQLProtocolAdapter;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum GraphQlBody {
Expand Down Expand Up @@ -54,7 +54,16 @@ impl GraphQlBody {
pub(crate) fn into_doc(self) -> crate::Result<QueryDocument> {
match self {
GraphQlBody::Single(body) => {
let gql_doc = gql::parse_query(&body.query)?;
let gql_doc = match gql::parse_query(&body.query) {
Ok(doc) => doc,
Err(err)
if err.to_string().contains("number too large to fit in target type")
| err.to_string().contains("number too small to fit in target type") =>
{
return Err(HandlerError::ValueFitError("Query parsing failure: A number used in the query does not fit into a 64 bit signed integer. Consider using `BigInt` as field type if you're trying to store large integers.".to_owned()));
}
err @ Err(_) => err?,
};
let operation = GraphQLProtocolAdapter::convert(gql_doc, body.operation_name)?;

Ok(QueryDocument::Single(operation))
Expand Down
5 changes: 4 additions & 1 deletion query-engine/request-handlers/src/graphql/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ impl<'a> GraphQlHandler<'a> {
}
BatchDocument::Compact(compacted) => self.handle_compacted(compacted, tx_id, trace_id).await,
},
Err(err) => PrismaResponse::Single(err.into()),
Err(err) => match err.as_known_error() {
Some(transformed) => PrismaResponse::Single(user_facing_errors::Error::new_known(transformed).into()),
None => PrismaResponse::Single(err.into()),
},
}
}

Expand Down