diff --git a/Cargo.lock b/Cargo.lock index 74f0b840d4f..93d70a3bae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4939,6 +4939,7 @@ dependencies = [ "chrono", "cuid", "futures", + "hex", "itertools", "once_cell", "opentelemetry", diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/mod.rs index ae1fe75b883..09ed6668f61 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/mod.rs @@ -8,3 +8,4 @@ mod float; mod int; mod json; mod string; +mod through_relation; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/through_relation.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/through_relation.rs new file mode 100644 index 00000000000..6b2a6fd6c14 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/through_relation.rs @@ -0,0 +1,286 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite] +mod scalar_relations { + fn schema_common() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + children Child[] + } + + model Child { + #id(childId, Int, @id) + + parentId Int? + parent Parent? @relation(fields: [parentId], references: [id]) + + string String + int Int + bInt BigInt + float Float + bytes Bytes + bool Boolean + dt DateTime + } + "# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_common))] + async fn common_types(runner: Runner) -> TestResult<()> { + create_common_children(&runner).await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { id children { childId string int bInt float bytes bool dt } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"children":[{"childId":1,"string":"abc","int":1,"bInt":"1","float":1.5,"bytes":"AQID","bool":false,"dt":"1900-10-10T01:10:10.001Z"},{"childId":2,"string":"def","int":-4234234,"bInt":"14324324234324","float":-2.54367,"bytes":"FDSF","bool":true,"dt":"1999-12-12T21:12:12.121Z"}]}]}}"### + ); + + Ok(()) + } + + fn schema_json() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + children Child[] + } + + model Child { + #id(childId, Int, @id) + + parentId Int? + parent Parent? @relation(fields: [parentId], references: [id]) + + json Json + } + "# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_json), capabilities(Json), exclude(Mysql(5.6)))] + async fn json_type(runner: Runner) -> TestResult<()> { + create_child(&runner, r#"{ childId: 1, json: "1" }"#).await?; + create_child(&runner, r#"{ childId: 2, json: "{}" }"#).await?; + create_child(&runner, r#"{ childId: 3, json: "{\"a\": \"b\"}" }"#).await?; + create_child(&runner, r#"{ childId: 4, json: "[]" }"#).await?; + create_child(&runner, r#"{ childId: 5, json: "[1, -1, true, {\"a\": \"b\"}]" }"#).await?; + create_parent( + &runner, + r#"{ id: 1, children: { connect: [{ childId: 1 }, { childId: 2 }, { childId: 3 }, { childId: 4 }, { childId: 5 }] } }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent(orderBy: { id: asc }) { id children { childId json } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"children":[{"childId":1,"json":"1"},{"childId":2,"json":"{}"},{"childId":3,"json":"{\"a\":\"b\"}"},{"childId":4,"json":"[]"},{"childId":5,"json":"[1,-1,true,{\"a\":\"b\"}]"}]}]}}"### + ); + + Ok(()) + } + + fn schema_enum() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + children Child[] + } + + model Child { + #id(childId, Int, @id) + + parentId Int? + parent Parent? @relation(fields: [parentId], references: [id]) + + enum Color + } + + enum Color { + Red + Green + Blue + } + "# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_enum), capabilities(Enums))] + async fn enum_type(runner: Runner) -> TestResult<()> { + create_child(&runner, r#"{ childId: 1, enum: Red }"#).await?; + create_child(&runner, r#"{ childId: 2, enum: Green }"#).await?; + create_child(&runner, r#"{ childId: 3, enum: Blue }"#).await?; + create_parent( + &runner, + r#"{ id: 1, children: { connect: [{ childId: 1 }, { childId: 2 }, { childId: 3 }] } }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent(orderBy: { id :asc }) { id children { childId enum } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"children":[{"childId":1,"enum":"Red"},{"childId":2,"enum":"Green"},{"childId":3,"enum":"Blue"}]}]}}"### + ); + + Ok(()) + } + + fn schema_decimal() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + children Child[] + } + + model Child { + #id(childId, Int, @id) + + parentId Int? + parent Parent? @relation(fields: [parentId], references: [id]) + + dec Decimal + } + "# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_decimal), capabilities(DecimalType))] + async fn decimal_type(runner: Runner) -> TestResult<()> { + create_child(&runner, r#"{ childId: 1, dec: "1" }"#).await?; + create_child(&runner, r#"{ childId: 2, dec: "-1" }"#).await?; + create_child(&runner, r#"{ childId: 3, dec: "123.45678910" }"#).await?; + create_parent( + &runner, + r#"{ id: 1, children: { connect: [{ childId: 1 }, { childId: 2 }, { childId: 3 }] } }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent(orderBy: { id: asc }) { id children { childId dec } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"children":[{"childId":1,"dec":"1"},{"childId":2,"dec":"-1"},{"childId":3,"dec":"123.4567891"}]}]}}"### + ); + + Ok(()) + } + + fn schema_scalar_lists() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + children Child[] + } + + model Child { + #id(childId, Int, @id) + + parentId Int? + parent Parent? @relation(fields: [parentId], references: [id]) + + string String[] + int Int[] + bInt BigInt[] + float Float[] + bytes Bytes[] + bool Boolean[] + dt DateTime[] + } + "# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_scalar_lists), capabilities(ScalarLists))] + async fn scalar_lists(runner: Runner) -> TestResult<()> { + create_child( + &runner, + r#"{ + childId: 1, + string: ["abc", "def"], + int: [1, -1, 1234567], + bInt: [1, -1, 9223372036854775807, -9223372036854775807], + float: [1.5, -1.5, 1.234567], + bytes: ["AQID", "Qk9OSk9VUg=="], + bool: [false, true], + dt: ["1900-10-10T01:10:10.001Z", "1999-12-12T21:12:12.121Z"], + }"#, + ) + .await?; + create_parent(&runner, r#"{ id: 1, children: { connect: [{ childId: 1 }] } }"#).await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { id children { childId string int bInt float bytes bool dt } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"children":[{"childId":1,"string":["abc","def"],"int":[1,-1,1234567],"bInt":["1","-1","9223372036854775807","-9223372036854775807"],"float":[1.5,-1.5,1.234567],"bytes":["AQID","Qk9OSk9VUg=="],"bool":[false,true],"dt":["1900-10-10T01:10:10.001Z","1999-12-12T21:12:12.121Z"]}]}]}}"### + ); + + Ok(()) + } + + async fn create_common_children(runner: &Runner) -> TestResult<()> { + create_child( + &runner, + r#"{ + childId: 1, + string: "abc", + int: 1, + bInt: 1, + float: 1.5, + bytes: "AQID", + bool: false, + dt: "1900-10-10T01:10:10.001Z", + }"#, + ) + .await?; + + create_child( + &runner, + r#"{ + childId: 2, + string: "def", + int: -4234234, + bInt: 14324324234324, + float: -2.54367, + bytes: "FDSF", + bool: true, + dt: "1999-12-12T21:12:12.121Z", + }"#, + ) + .await?; + + create_parent( + &runner, + r#"{ id: 1, children: { connect: [{ childId: 1 }, { childId: 2 }] } }"#, + ) + .await?; + + Ok(()) + } + + async fn create_child(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneChild(data: {}) {{ childId }} }}", data)) + .await? + .assert_success(); + Ok(()) + } + + async fn create_parent(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneParent(data: {}) {{ id }} }}", data)) + .await? + .assert_success(); + Ok(()) + } +} diff --git a/query-engine/connectors/sql-query-connector/Cargo.toml b/query-engine/connectors/sql-query-connector/Cargo.toml index fbe04850164..9ba23da469c 100644 --- a/query-engine/connectors/sql-query-connector/Cargo.toml +++ b/query-engine/connectors/sql-query-connector/Cargo.toml @@ -27,6 +27,7 @@ uuid.workspace = true opentelemetry = { version = "0.17", features = ["tokio"] } tracing-opentelemetry = "0.17.3" cuid = { git = "https://github.com/prisma/cuid-rust", branch = "wasm32-support" } +hex = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] quaint.workspace = true diff --git a/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs b/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs index 22ec980a0b4..8e8820b29e1 100644 --- a/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs +++ b/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs @@ -1,21 +1,29 @@ +use std::io; + +use bigdecimal::{BigDecimal, FromPrimitive}; use itertools::{Either, Itertools}; use query_structure::*; -use crate::query_arguments_ext::QueryArgumentsExt; +use crate::{query_arguments_ext::QueryArgumentsExt, SqlError}; /// Coerces relations resolved as JSON to PrismaValues. /// Note: Some in-memory processing is baked into this function too for performance reasons. -pub(crate) fn coerce_record_with_json_relation(record: &mut Record, rq_indexes: Vec<(usize, &RelationSelection)>) { +pub(crate) fn coerce_record_with_json_relation( + record: &mut Record, + rq_indexes: Vec<(usize, &RelationSelection)>, +) -> crate::Result<()> { for (val_idx, rs) in rq_indexes { let val = record.values.get_mut(val_idx).unwrap(); // TODO(perf): Find ways to avoid serializing and deserializing multiple times. let json_val: serde_json::Value = serde_json::from_str(val.as_json().unwrap()).unwrap(); - *val = coerce_json_relation_to_pv(json_val, rs); + *val = coerce_json_relation_to_pv(json_val, rs)?; } + + Ok(()) } -fn coerce_json_relation_to_pv(value: serde_json::Value, rs: &RelationSelection) -> PrismaValue { +fn coerce_json_relation_to_pv(value: serde_json::Value, rs: &RelationSelection) -> crate::Result { let relations = rs.relations().collect_vec(); match value { @@ -29,7 +37,7 @@ fn coerce_json_relation_to_pv(value: serde_json::Value, rs: &RelationSelection) false => Either::Right(iter), }; - PrismaValue::List(iter.collect()) + Ok(PrismaValue::List(iter.collect::>>()?)) } // to-one serde_json::Value::Array(values) => { @@ -43,7 +51,7 @@ fn coerce_json_relation_to_pv(value: serde_json::Value, rs: &RelationSelection) if let Some(val) = coerced { val } else { - PrismaValue::Null + Ok(PrismaValue::Null) } } serde_json::Value::Object(obj) => { @@ -53,44 +61,115 @@ fn coerce_json_relation_to_pv(value: serde_json::Value, rs: &RelationSelection) for (key, value) in obj { match related_model.fields().all().find(|f| f.db_name() == key).unwrap() { Field::Scalar(sf) => { - map.push((key, coerce_json_scalar_to_pv(value, &sf))); + map.push((key, coerce_json_scalar_to_pv(value, &sf)?)); } Field::Relation(rf) => { // TODO: optimize this if let Some(nested_selection) = relations.iter().find(|rs| rs.field == rf) { - map.push((key, coerce_json_relation_to_pv(value, nested_selection))); + map.push((key, coerce_json_relation_to_pv(value, nested_selection)?)); } } _ => (), } } - PrismaValue::Object(map) + Ok(PrismaValue::Object(map)) } _ => unreachable!(), } } -pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarField) -> PrismaValue { +pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarField) -> crate::Result { + if sf.type_identifier().is_json() { + return Ok(PrismaValue::Json(serde_json::to_string(&value)?)); + } + match value { - serde_json::Value::Null => PrismaValue::Null, - serde_json::Value::Bool(b) => PrismaValue::Boolean(b), + serde_json::Value::Null => Ok(PrismaValue::Null), + serde_json::Value::Bool(b) => Ok(PrismaValue::Boolean(b)), serde_json::Value::Number(n) => match sf.type_identifier() { - TypeIdentifier::Int => PrismaValue::Int(n.as_i64().unwrap()), - TypeIdentifier::BigInt => PrismaValue::BigInt(n.as_i64().unwrap()), - TypeIdentifier::Float => todo!(), - TypeIdentifier::Decimal => todo!(), - _ => unreachable!(), + TypeIdentifier::Int => Ok(PrismaValue::Int(n.as_i64().ok_or_else(|| { + build_conversion_error(&format!("Number({n})"), &format!("{:?}", sf.type_identifier())) + })?)), + TypeIdentifier::BigInt => Ok(PrismaValue::BigInt(n.as_i64().ok_or_else(|| { + build_conversion_error(&format!("Number({n})"), &format!("{:?}", sf.type_identifier())) + })?)), + TypeIdentifier::Float | TypeIdentifier::Decimal => { + let bd = n + .as_f64() + .and_then(BigDecimal::from_f64) + .map(|bd| bd.normalized()) + .ok_or_else(|| { + build_conversion_error(&format!("Number({n})"), &format!("{:?}", sf.type_identifier())) + })?; + + Ok(PrismaValue::Float(bd)) + } + _ => Err(build_conversion_error( + &format!("Number({n})"), + &format!("{:?}", sf.type_identifier()), + )), }, serde_json::Value::String(s) => match sf.type_identifier() { - TypeIdentifier::String => PrismaValue::String(s), - TypeIdentifier::Enum(_) => PrismaValue::Enum(s), - TypeIdentifier::DateTime => PrismaValue::DateTime(parse_datetime(&s).unwrap()), - TypeIdentifier::UUID => PrismaValue::Uuid(uuid::Uuid::parse_str(&s).unwrap()), - TypeIdentifier::Bytes => PrismaValue::Bytes(decode_bytes(&s).unwrap()), - _ => unreachable!(), + TypeIdentifier::String => Ok(PrismaValue::String(s)), + TypeIdentifier::Enum(_) => Ok(PrismaValue::Enum(s)), + TypeIdentifier::DateTime => Ok(PrismaValue::DateTime(parse_datetime(&format!("{s}Z")).map_err( + |err| { + build_conversion_error_with_reason( + &format!("String({s})"), + &format!("{:?}", sf.type_identifier()), + &err.to_string(), + ) + }, + )?)), + TypeIdentifier::UUID => Ok(PrismaValue::Uuid(uuid::Uuid::parse_str(&s).map_err(|err| { + build_conversion_error_with_reason( + &format!("String({s})"), + &format!("{:?}", sf.type_identifier()), + &err.to_string(), + ) + })?)), + TypeIdentifier::Bytes => { + // We skip the first two characters because they are the \x prefix. + let bytes = hex::decode(&s[2..]).map_err(|err| { + build_conversion_error_with_reason( + &format!("String({s})"), + &format!("{:?}", sf.type_identifier()), + &err.to_string(), + ) + })?; + + Ok(PrismaValue::Bytes(bytes)) + } + _ => Err(build_conversion_error( + &format!("String({s})"), + &format!("{:?}", sf.type_identifier()), + )), }, - serde_json::Value::Array(_) => todo!(), - serde_json::Value::Object(_) => todo!(), + serde_json::Value::Array(values) => Ok(PrismaValue::List( + values + .into_iter() + .map(|v| coerce_json_scalar_to_pv(v, sf)) + .collect::>>()?, + )), + serde_json::Value::Object(_) => unreachable!("Objects should be caught by the json catch-all above."), } } + +fn build_conversion_error(from: &str, to: &str) -> SqlError { + let error = io::Error::new( + io::ErrorKind::InvalidData, + format!("Unexpected conversion failure from {from} to {to}."), + ); + + SqlError::ConversionError(error.into()) +} + +fn build_conversion_error_with_reason(from: &str, to: &str, reason: &str) -> SqlError { + let error = io::Error::new( + io::ErrorKind::InvalidData, + format!("Unexpected conversion failure from {from} to {to}. Reason: ${reason}"), + ); + + SqlError::ConversionError(error.into()) +} diff --git a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs index dd90ad08476..3a77bf05743 100644 --- a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs +++ b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs @@ -117,7 +117,7 @@ pub(crate) async fn get_many_records_joins( let mut record = Record::from(item); // Coerces json values to prisma values - coerce_record_with_json_relation(&mut record, rs_indexes.clone()); + coerce_record_with_json_relation(&mut record, rs_indexes.clone())?; records.push(record) } diff --git a/query-engine/query-structure/src/field/mod.rs b/query-engine/query-structure/src/field/mod.rs index 45d529c56ab..39e43f186c1 100644 --- a/query-engine/query-structure/src/field/mod.rs +++ b/query-engine/query-structure/src/field/mod.rs @@ -183,6 +183,11 @@ impl TypeIdentifier { pub fn is_enum(&self) -> bool { matches!(self, Self::Enum(..)) } + + /// Returns `true` if the type identifier is [`Json`]. + pub fn is_json(&self) -> bool { + matches!(self, Self::Json) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)]