Skip to content

Commit

Permalink
Structured errors for parsing overflowing ints (#2921)
Browse files Browse the repository at this point in the history
  • Loading branch information
dpetrick committed Jun 1, 2022
1 parent be3dd4d commit b3b91ef
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 6 deletions.
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

0 comments on commit b3b91ef

Please sign in to comment.