From 0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Mon, 18 Dec 2023 13:21:52 +0100 Subject: [PATCH] fix: handle native types for joined queries (#4546) --- Cargo.lock | 1 + psl/builtin-connectors/Cargo.toml | 2 + .../src/cockroach_datamodel_connector.rs | 24 ++ psl/builtin-connectors/src/lib.rs | 1 + .../src/postgres_datamodel_connector.rs | 24 ++ psl/builtin-connectors/src/utils.rs | 37 ++ psl/psl-core/src/datamodel_connector.rs | 9 + quaint/src/ast/column.rs | 9 +- quaint/src/visitor/postgres.rs | 34 +- .../tests/queries/data_types/mod.rs | 1 + .../tests/queries/data_types/native/mod.rs | 1 + .../queries/data_types/native/postgres.rs | 325 ++++++++++++++++++ .../src/database/operations/coerce.rs | 58 +++- .../src/model_extensions/column.rs | 1 + .../query-structure/src/field/scalar.rs | 8 + 15 files changed, 518 insertions(+), 17 deletions(-) create mode 100644 psl/builtin-connectors/src/utils.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mod.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs diff --git a/Cargo.lock b/Cargo.lock index 8c48c0dcc3dd..ec1df3dd76b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,7 @@ dependencies = [ name = "builtin-psl-connectors" version = "0.1.0" dependencies = [ + "chrono", "connection-string", "either", "enumflags2", diff --git a/psl/builtin-connectors/Cargo.toml b/psl/builtin-connectors/Cargo.toml index 218fcf8b81a5..ef9e810b8aba 100644 --- a/psl/builtin-connectors/Cargo.toml +++ b/psl/builtin-connectors/Cargo.toml @@ -13,3 +13,5 @@ indoc.workspace = true lsp-types = "0.91.1" once_cell = "1.3" regex = "1" +chrono = { version = "0.4.6", default-features = false } + diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index 4ab77cf45639..99f94c953971 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -3,6 +3,7 @@ mod validations; pub use native_types::CockroachType; +use chrono::*; use enumflags2::BitFlags; use lsp_types::{CompletionItem, CompletionItemKind, CompletionList}; use psl_core::{ @@ -307,6 +308,29 @@ impl Connector for CockroachDatamodelConnector { fn flavour(&self) -> Flavour { Flavour::Cockroach } + + fn parse_json_datetime( + &self, + str: &str, + nt: Option, + ) -> chrono::ParseResult> { + let native_type: Option<&CockroachType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(ct) => match ct { + CockroachType::Timestamptz(_) => crate::utils::parse_timestamptz(str), + CockroachType::Timestamp(_) => crate::utils::parse_timestamp(str), + CockroachType::Date => crate::utils::parse_date(str), + CockroachType::Time(_) => crate::utils::parse_time(str), + CockroachType::Timetz(_) => crate::utils::parse_timetz(str), + _ => unreachable!(), + }, + None => self.parse_json_datetime( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::DateTime)), + ), + } + } } /// An `@default(sequence())` function. diff --git a/psl/builtin-connectors/src/lib.rs b/psl/builtin-connectors/src/lib.rs index c477386a23ed..4f8d26801213 100644 --- a/psl/builtin-connectors/src/lib.rs +++ b/psl/builtin-connectors/src/lib.rs @@ -16,6 +16,7 @@ mod mysql_datamodel_connector; mod native_type_definition; mod postgres_datamodel_connector; mod sqlite_datamodel_connector; +mod utils; use psl_core::{datamodel_connector::Connector, ConnectorRegistry}; diff --git a/psl/builtin-connectors/src/postgres_datamodel_connector.rs b/psl/builtin-connectors/src/postgres_datamodel_connector.rs index 697b4b9c12bb..fa3325d1403f 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -4,6 +4,7 @@ mod validations; pub use native_types::PostgresType; +use chrono::*; use enumflags2::BitFlags; use lsp_types::{CompletionItem, CompletionItemKind, CompletionList, InsertTextFormat}; use psl_core::{ @@ -567,6 +568,29 @@ impl Connector for PostgresDatamodelConnector { fn flavour(&self) -> Flavour { Flavour::Postgres } + + fn parse_json_datetime( + &self, + str: &str, + nt: Option, + ) -> chrono::ParseResult> { + let native_type: Option<&PostgresType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(pt) => match pt { + Timestamptz(_) => crate::utils::parse_timestamptz(str), + Timestamp(_) => crate::utils::parse_timestamp(str), + Date => crate::utils::parse_date(str), + Time(_) => crate::utils::parse_time(str), + Timetz(_) => crate::utils::parse_timetz(str), + _ => unreachable!(), + }, + None => self.parse_json_datetime( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::DateTime)), + ), + } + } } fn allowed_index_operator_classes(algo: IndexAlgorithm, field: walkers::ScalarFieldWalker<'_>) -> Vec { diff --git a/psl/builtin-connectors/src/utils.rs b/psl/builtin-connectors/src/utils.rs new file mode 100644 index 000000000000..3ef9f55cd80a --- /dev/null +++ b/psl/builtin-connectors/src/utils.rs @@ -0,0 +1,37 @@ +use chrono::*; + +pub(crate) fn parse_date(str: &str) -> Result, chrono::ParseError> { + chrono::NaiveDate::parse_from_str(str, "%Y-%m-%d") + .map(|date| DateTime::::from_utc(date.and_hms_opt(0, 0, 0).unwrap(), Utc)) + .map(DateTime::::from) +} + +pub(crate) fn parse_timestamptz(str: &str) -> Result, chrono::ParseError> { + DateTime::parse_from_rfc3339(str) +} + +pub(crate) fn parse_timestamp(str: &str) -> Result, chrono::ParseError> { + NaiveDateTime::parse_from_str(str, "%Y-%m-%dT%H:%M:%S%.f") + .map(|dt| DateTime::from_utc(dt, Utc)) + .or_else(|_| DateTime::parse_from_rfc3339(str).map(DateTime::::from)) + .map(DateTime::::from) +} + +pub(crate) fn parse_time(str: &str) -> Result, chrono::ParseError> { + chrono::NaiveTime::parse_from_str(str, "%H:%M:%S%.f") + .map(|time| { + let base_date = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + DateTime::::from_utc(base_date.and_time(time), Utc) + }) + .map(DateTime::::from) +} + +pub(crate) fn parse_timetz(str: &str) -> Result, chrono::ParseError> { + // We currently don't support time with timezone. + // We strip the timezone information and parse it as a time. + // This is inline with what Quaint does already. + let time_without_tz = str.split('+').next().unwrap(); + + parse_time(time_without_tz) +} diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index 72671e06688f..751b03ac9da1 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -25,6 +25,7 @@ pub use self::{ }; use crate::{configuration::DatasourceConnectorData, Configuration, Datasource, PreviewFeature}; +use chrono::{DateTime, FixedOffset}; use diagnostics::{DatamodelError, Diagnostics, NativeTypeErrorFactory, Span}; use enumflags2::BitFlags; use lsp_types::CompletionList; @@ -359,6 +360,14 @@ pub trait Connector: Send + Sync { ) -> DatasourceConnectorData { Default::default() } + + fn parse_json_datetime( + &self, + _str: &str, + _nt: Option, + ) -> chrono::ParseResult> { + unreachable!("This method is only implemented on connectors with lateral join support.") + } } #[derive(Copy, Clone, Debug, PartialEq)] diff --git a/quaint/src/ast/column.rs b/quaint/src/ast/column.rs index 836b4ce96527..cf2d157be085 100644 --- a/quaint/src/ast/column.rs +++ b/quaint/src/ast/column.rs @@ -1,4 +1,4 @@ -use super::Aliasable; +use super::{values::NativeColumnType, Aliasable}; use crate::{ ast::{Expression, ExpressionKind, Table}, Value, @@ -32,6 +32,8 @@ pub struct Column<'a> { pub(crate) alias: Option>, pub(crate) default: Option>, pub(crate) type_family: Option, + /// The underlying native type of the column. + pub(crate) native_type: Option>, /// Whether the column is an enum. pub(crate) is_enum: bool, /// Whether the column is a (scalar) list. @@ -130,6 +132,11 @@ impl<'a> Column<'a> { .map(|d| d == &DefaultValue::Generated) .unwrap_or(false) } + + pub fn native_column_type>>(mut self, native_type: Option) -> Column<'a> { + self.native_type = native_type.map(|nt| nt.into()); + self + } } impl<'a> From> for Expression<'a> { diff --git a/quaint/src/visitor/postgres.rs b/quaint/src/visitor/postgres.rs index da02c26c3353..40c80d330c14 100644 --- a/quaint/src/visitor/postgres.rs +++ b/quaint/src/visitor/postgres.rs @@ -17,6 +17,23 @@ pub struct Postgres<'a> { parameters: Vec>, } +impl<'a> Postgres<'a> { + fn visit_json_build_obj_expr(&mut self, expr: Expression<'a>) -> crate::Result<()> { + match expr.kind() { + ExpressionKind::Column(col) => match (col.type_family.as_ref(), col.native_type.as_deref()) { + (Some(TypeFamily::Decimal(_)), Some("MONEY")) => { + self.visit_expression(expr)?; + self.write("::numeric")?; + + Ok(()) + } + _ => self.visit_expression(expr), + }, + _ => self.visit_expression(expr), + } + } +} + impl<'a> Visitor<'a> for Postgres<'a> { const C_BACKTICK_OPEN: &'static str = "\""; const C_BACKTICK_CLOSE: &'static str = "\""; @@ -534,7 +551,7 @@ impl<'a> Visitor<'a> for Postgres<'a> { while let Some((name, expr)) = chunk.next() { s.visit_raw_value(Value::text(name))?; s.write(", ")?; - s.visit_expression(expr)?; + s.visit_json_build_obj_expr(expr)?; if chunk.peek().is_some() { s.write(", ")?; } @@ -1290,6 +1307,21 @@ mod tests { ); } + #[test] + fn money() { + let build_json = json_build_object(vec![( + "money".into(), + Column::from("money") + .native_column_type(Some("money")) + .type_family(TypeFamily::Decimal(None)) + .into(), + )]); + let query = Select::default().value(build_json); + let (sql, _) = Postgres::build(query).unwrap(); + + assert_eq!(sql, "SELECT JSONB_BUILD_OBJECT('money', \"money\"::numeric)"); + } + fn build_json_object(num_fields: u32) -> JsonBuildObject<'static> { let fields = (1..=num_fields) .map(|i| (format!("f{i}").into(), Expression::from(i as i64))) 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 09ed6668f619..127e5e23c29a 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 @@ -7,5 +7,6 @@ mod enum_type; mod float; mod int; mod json; +mod native; mod string; mod through_relation; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mod.rs new file mode 100644 index 000000000000..70faf80832c5 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mod.rs @@ -0,0 +1 @@ +mod postgres; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs new file mode 100644 index 000000000000..e25d57e854de --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs @@ -0,0 +1,325 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(only(Postgres, CockroachDb))] +mod datetime { + fn schema_date() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + + childId Int? @unique + child Child? @relation(fields: [childId], references: [id]) + } + + model Child { + #id(id, Int, @id) + date DateTime @test.Date + date_2 DateTime @test.Date + time DateTime @test.Time(3) + time_2 DateTime @test.Time(3) + time_tz DateTime @test.Timetz(3) + time_tz_2 DateTime @test.Timetz(3) + ts DateTime @test.Timestamp(3) + ts_2 DateTime @test.Timestamp(3) + ts_tz DateTime @test.Timestamptz(3) + ts_tz_2 DateTime @test.Timestamptz(3) + + parent Parent? + }"# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_date))] + async fn dt_native(runner: Runner) -> TestResult<()> { + create_row( + &runner, + r#"{ + id: 1, + child: { create: { + id: 1, + date: "2016-09-24T00:00:00.000Z" + date_2: "2016-09-24T00:00:00.000+03:00" + time: "1111-11-11T13:02:20.321Z" + time_2: "1111-11-11T13:02:20.321+03:00" + time_tz: "1111-11-11T13:02:20.321Z" + time_tz_2: "1111-11-11T13:02:20.321+03:00" + ts: "2016-09-24T14:01:30.213Z" + ts_2: "2016-09-24T14:01:30.213+03:00" + ts_tz: "2016-09-24T14:01:30.213Z" + ts_tz_2: "2016-09-24T14:01:30.213+03:00" + }} + }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(runner, r#"{ findManyParent { id child { date date_2 time time_2 time_tz time_tz_2 ts ts_2 ts_tz ts_tz_2 } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"child":{"date":"2016-09-24T00:00:00.000Z","date_2":"2016-09-23T00:00:00.000Z","time":"1970-01-01T13:02:20.321Z","time_2":"1970-01-01T10:02:20.321Z","time_tz":"1970-01-01T13:02:20.321Z","time_tz_2":"1970-01-01T10:02:20.321Z","ts":"2016-09-24T14:01:30.213Z","ts_2":"2016-09-24T11:01:30.213Z","ts_tz":"2016-09-24T14:01:30.213Z","ts_tz_2":"2016-09-24T11:01:30.213Z"}}]}}"### + ); + + Ok(()) + } + + async fn create_row(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneParent(data: {}) {{ id }} }}", data)) + .await? + .assert_success(); + Ok(()) + } +} + +#[test_suite(only(Postgres))] +mod decimal { + fn schema_decimal() -> String { + let schema = indoc! { + r#" + model Parent { + #id(id, Int, @id) + + childId Int? @unique + child Child? @relation(fields: [childId], references: [id]) + } + + model Child { + #id(id, Int, @id) + + float Float @test.Real + dfloat Float @test.DoublePrecision + decFloat Decimal @test.Decimal(2, 1) + money Decimal @test.Money + + parent Parent? + }"# + }; + + schema.to_owned() + } + + // "Postgres native decimal types" should "work" + #[connector_test(schema(schema_decimal))] + async fn native_decimal_types(runner: Runner) -> TestResult<()> { + create_row( + &runner, + r#"{ + id: 1, + child: { create: { + id: 1, + float: 1.1, + dfloat: 2.2, + decFloat: 3.1234, + money: 3.51, + }} + }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { id child { float dfloat decFloat money } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"child":{"float":1.1,"dfloat":2.2,"decFloat":"3.1","money":"3.51"}}]}}"### + ); + + Ok(()) + } + + async fn create_row(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneParent(data: {}) {{ id }} }}", data)) + .await? + .assert_success(); + Ok(()) + } +} + +#[test_suite(only(Postgres))] +mod string { + fn schema_string() -> String { + let schema = indoc! { + r#" + model Parent { + #id(id, Int, @id) + + childId Int? @unique + child Child? @relation(fields: [childId], references: [id]) + } + + model Child { + #id(id, Int, @id) + char String @test.Char(10) + vChar String @test.VarChar(11) + text String @test.Text + bit String @test.Bit(4) + vBit String @test.VarBit(5) + uuid String @test.Uuid + ip String @test.Inet + + parent Parent? + }"# + }; + + schema.to_owned() + } + + // "Postgres native string types" should "work" + #[connector_test(schema(schema_string))] + async fn native_string(runner: Runner) -> TestResult<()> { + create_row( + &runner, + r#"{ + id: 1, + child: { create: { + id: 1, + char: "1234567890" + vChar: "12345678910" + text: "text" + bit: "1010" + vBit: "00110" + uuid: "123e4567-e89b-12d3-a456-426614174000" + ip: "127.0.0.1" + }} + }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { + id + child { + char + vChar + text + bit + vBit + uuid + ip + } + }}"#), + @r###"{"data":{"findManyParent":[{"id":1,"child":{"char":"1234567890","vChar":"12345678910","text":"text","bit":"1010","vBit":"00110","uuid":"123e4567-e89b-12d3-a456-426614174000","ip":"127.0.0.1"}}]}}"### + ); + + Ok(()) + } + + async fn create_row(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneParent(data: {}) {{ id }} }}", data)) + .await? + .assert_success(); + Ok(()) + } +} + +// Napi & Wasm DAs excluded because of a bytes bug +#[test_suite( + schema(schema), + only(Postgres("9", "10", "11", "12", "13", "14", "15", "pg.js", "neon.js")) +)] +mod others { + fn schema_other_types() -> String { + let schema = indoc! { + r#" + model Parent { + #id(id, Int, @id) + + childId Int? @unique + child Child? @relation(fields: [childId], references: [id]) + } + + model Child { + #id(id, Int, @id) + bool Boolean @test.Boolean + byteA Bytes @test.ByteA + json Json @test.Json + jsonb Json @test.JsonB + + parent Parent? + }"# + }; + + schema.to_owned() + } + + // "Other Postgres native types" should "work" + #[connector_test(schema(schema_other_types))] + async fn native_other_types(runner: Runner) -> TestResult<()> { + create_row( + &runner, + r#"{ + id: 1, + child: { + create: { + id: 1, + bool: true + byteA: "dGVzdA==" + json: "{}" + jsonb: "{\"a\": \"b\"}" + } + } + }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { id child { id bool byteA json jsonb } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"child":{"id":1,"bool":true,"byteA":"dGVzdA==","json":"{}","jsonb":"{\"a\":\"b\"}"}}]}}"### + ); + + Ok(()) + } + + fn schema_xml() -> String { + let schema = indoc! { + r#" + model Parent { + #id(id, Int, @id) + + childId Int? @unique + child Child? @relation(fields: [childId], references: [id]) + } + + model Child { + #id(id, Int, @id) + xml String @test.Xml + + parent Parent? + }"# + }; + + schema.to_owned() + } + + #[connector_test(schema(schema_xml), only(Postgres))] + async fn native_xml(runner: Runner) -> TestResult<()> { + create_row( + &runner, + r#"{ + id: 1, + child: { + create: { + id: 1, + xml: "wurst" + } + } + }"#, + ) + .await?; + + insta::assert_snapshot!( + run_query!(&runner, r#"{ findManyParent { id child { xml } } }"#), + @r###"{"data":{"findManyParent":[{"id":1,"child":{"xml":"wurst"}}]}}"### + ); + + Ok(()) + } + + async fn create_row(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/src/database/operations/coerce.rs b/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs index daf947e2ffea..d42dc627bf62 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,8 +1,7 @@ -use std::io; - -use bigdecimal::{BigDecimal, FromPrimitive}; +use bigdecimal::{BigDecimal, FromPrimitive, ParseBigDecimalError}; use itertools::{Either, Itertools}; use query_structure::*; +use std::{io, str::FromStr}; use crate::{query_arguments_ext::QueryArgumentsExt, SqlError}; @@ -106,10 +105,10 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel serde_json::Value::Bool(b) => Ok(PrismaValue::Boolean(b)), serde_json::Value::Number(n) => match sf.type_identifier() { TypeIdentifier::Int => Ok(PrismaValue::Int(n.as_i64().ok_or_else(|| { - build_conversion_error(&format!("Number({n})"), &format!("{:?}", sf.type_identifier())) + build_conversion_error(sf, &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())) + build_conversion_error(sf, &format!("Number({n})"), &format!("{:?}", sf.type_identifier())) })?)), TypeIdentifier::Float | TypeIdentifier::Decimal => { let bd = n @@ -117,12 +116,13 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel .and_then(BigDecimal::from_f64) .map(|bd| bd.normalized()) .ok_or_else(|| { - build_conversion_error(&format!("Number({n})"), &format!("{:?}", sf.type_identifier())) + build_conversion_error(sf, &format!("Number({n})"), &format!("{:?}", sf.type_identifier())) })?; Ok(PrismaValue::Float(bd)) } _ => Err(build_conversion_error( + sf, &format!("Number({n})"), &format!("{:?}", sf.type_identifier()), )), @@ -130,26 +130,43 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel serde_json::Value::String(s) => match sf.type_identifier() { 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| { + TypeIdentifier::DateTime => { + let res = sf.parse_json_datetime(&s).map_err(|err| { + build_conversion_error_with_reason( + sf, + &format!("String({s})"), + &format!("{:?}", sf.type_identifier()), + &err.to_string(), + ) + })?; + + Ok(PrismaValue::DateTime(res)) + } + TypeIdentifier::Decimal => { + let res = parse_decimal(&s).map_err(|err| { build_conversion_error_with_reason( + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), ) - }, - )?)), + })?; + + Ok(PrismaValue::Float(res)) + } TypeIdentifier::UUID => Ok(PrismaValue::Uuid(uuid::Uuid::parse_str(&s).map_err(|err| { build_conversion_error_with_reason( + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), ) })?)), TypeIdentifier::Bytes => { - // We skip the first two characters because they are the \x prefix. + // We skip the first two characters because there's the \x prefix. let bytes = hex::decode(&s[2..]).map_err(|err| { build_conversion_error_with_reason( + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), @@ -159,6 +176,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel Ok(PrismaValue::Bytes(bytes)) } _ => Err(build_conversion_error( + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), )), @@ -173,20 +191,30 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel } } -fn build_conversion_error(from: &str, to: &str) -> SqlError { +fn build_conversion_error(sf: &ScalarField, from: &str, to: &str) -> SqlError { + let container_name = sf.container().name(); + let field_name = sf.name(); + let error = io::Error::new( io::ErrorKind::InvalidData, - format!("Unexpected conversion failure from {from} to {to}."), + format!("Unexpected conversion failure for field {container_name}.{field_name} from {from} to {to}."), ); SqlError::ConversionError(error.into()) } -fn build_conversion_error_with_reason(from: &str, to: &str, reason: &str) -> SqlError { +fn build_conversion_error_with_reason(sf: &ScalarField, from: &str, to: &str, reason: &str) -> SqlError { + let container_name = sf.container().name(); + let field_name = sf.name(); + let error = io::Error::new( io::ErrorKind::InvalidData, - format!("Unexpected conversion failure from {from} to {to}. Reason: ${reason}"), + format!("Unexpected conversion failure for field {container_name}.{field_name} from {from} to {to}. Reason: ${reason}"), ); SqlError::ConversionError(error.into()) } + +fn parse_decimal(str: &str) -> std::result::Result { + BigDecimal::from_str(str).map(|bd| bd.normalized()) +} diff --git a/query-engine/connectors/sql-query-connector/src/model_extensions/column.rs b/query-engine/connectors/sql-query-connector/src/model_extensions/column.rs index c2eb84435d5b..81b424ca5902 100644 --- a/query-engine/connectors/sql-query-connector/src/model_extensions/column.rs +++ b/query-engine/connectors/sql-query-connector/src/model_extensions/column.rs @@ -107,6 +107,7 @@ impl AsColumn for ScalarField { Column::from((full_table_name, col)) .type_family(self.type_family()) + .native_column_type(self.native_type().map(|nt| nt.name())) .set_is_enum(self.type_identifier().is_enum()) .set_is_list(self.is_list()) .default(quaint::ast::DefaultValue::Generated) diff --git a/query-engine/query-structure/src/field/scalar.rs b/query-engine/query-structure/src/field/scalar.rs index b8ef8ab204e2..52d59686fdda 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -1,4 +1,5 @@ use crate::{ast, parent_container::ParentContainer, prelude::*, DefaultKind, NativeTypeInstance, ValueGenerator}; +use chrono::{DateTime, FixedOffset}; use psl::{ parser_database::{walkers, ScalarFieldType, ScalarType}, schema_ast::ast::FieldArity, @@ -170,6 +171,13 @@ impl ScalarField { }) } + pub fn parse_json_datetime(&self, value: &str) -> chrono::ParseResult> { + let nt = self.native_type().map(|nt| nt.native_type); + let connector = self.dm.schema.connector; + + connector.parse_json_datetime(value, nt) + } + pub fn is_autoincrement(&self) -> bool { match self.id { ScalarFieldId::InModel(id) => self.dm.walk(id).is_autoincrement(),