From ae9c19087ef1d96ca4c11a45f80a36d8d6a3c6a5 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Fri, 8 Dec 2023 12:06:49 +0100 Subject: [PATCH 1/6] fix: handle native types for joined queries --- Cargo.lock | 2 + psl/builtin-connectors/Cargo.toml | 2 + .../src/cockroach_datamodel_connector.rs | 45 +++ psl/builtin-connectors/src/lib.rs | 1 + .../src/postgres_datamodel_connector.rs | 43 +++ psl/builtin-connectors/src/utils.rs | 39 +++ psl/psl-core/src/datamodel_connector.rs | 18 + .../tests/queries/data_types/mod.rs | 1 + .../tests/queries/data_types/native/mod.rs | 1 + .../queries/data_types/native/postgres.rs | 321 ++++++++++++++++++ .../src/database/operations/coerce.rs | 49 ++- .../query-structure/src/field/scalar.rs | 16 + 12 files changed, 526 insertions(+), 12 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 e3a56425f0ce..db5912b6377e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,6 +432,8 @@ dependencies = [ name = "builtin-psl-connectors" version = "0.1.0" dependencies = [ + "bigdecimal", + "chrono", "connection-string", "either", "enumflags2", diff --git a/psl/builtin-connectors/Cargo.toml b/psl/builtin-connectors/Cargo.toml index 218fcf8b81a5..7e8db138bb5c 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 } +bigdecimal = "0.3" diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index 4ab77cf45639..aa9d9ae27446 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -1,8 +1,10 @@ mod native_types; mod validations; +use bigdecimal::{BigDecimal, ParseBigDecimalError}; pub use native_types::CockroachType; +use chrono::*; use enumflags2::BitFlags; use lsp_types::{CompletionItem, CompletionItemKind, CompletionList}; use psl_core::{ @@ -307,6 +309,49 @@ impl Connector for CockroachDatamodelConnector { fn flavour(&self) -> Flavour { Flavour::Cockroach } + + fn coerce_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(_) => { + // 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(); + + crate::utils::parse_time(time_without_tz) + } + _ => unreachable!(), + }, + None => crate::utils::parse_timestamptz(str), + } + } + + fn coerce_json_decimal( + &self, + str: &str, + nt: Option, + ) -> Result { + let native_type: Option<&CockroachType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(pt) => match pt { + CockroachType::Decimal(_) => crate::utils::parse_decimal(str), + _ => unreachable!(), + }, + None => crate::utils::parse_decimal(str), + } + } } /// 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..c16b559a8253 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -4,6 +4,8 @@ mod validations; pub use native_types::PostgresType; +use bigdecimal::{BigDecimal, ParseBigDecimalError}; +use chrono::*; use enumflags2::BitFlags; use lsp_types::{CompletionItem, CompletionItemKind, CompletionList, InsertTextFormat}; use psl_core::{ @@ -567,6 +569,47 @@ impl Connector for PostgresDatamodelConnector { fn flavour(&self) -> Flavour { Flavour::Postgres } + + fn coerce_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(_) => { + let time_without_tz = str.split('+').next().unwrap(); + + crate::utils::parse_time(time_without_tz) + } + _ => unreachable!(), + }, + None => crate::utils::parse_timestamptz(str), + } + } + + fn coerce_json_decimal( + &self, + str: &str, + nt: Option, + ) -> Result { + let native_type: Option<&PostgresType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(pt) => match pt { + Decimal(_) => crate::utils::parse_decimal(str), + Money => crate::utils::parse_money(str), + _ => unreachable!(), + }, + None => crate::utils::parse_decimal(str), + } + } } 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..188abe850667 --- /dev/null +++ b/psl/builtin-connectors/src/utils.rs @@ -0,0 +1,39 @@ +use bigdecimal::{BigDecimal, ParseBigDecimalError}; +use chrono::*; +use std::str::FromStr; + +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_money(str: &str) -> Result { + // We strip out the currency sign from the string. + BigDecimal::from_str(&str[1..]).map(|bd| bd.normalized()) +} + +pub(crate) fn parse_decimal(str: &str) -> Result { + BigDecimal::from_str(str).map(|bd| bd.normalized()) +} diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index 72671e06688f..504d3f60749a 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -25,6 +25,8 @@ pub use self::{ }; use crate::{configuration::DatasourceConnectorData, Configuration, Datasource, PreviewFeature}; +use bigdecimal::{BigDecimal, ParseBigDecimalError}; +use chrono::{DateTime, FixedOffset}; use diagnostics::{DatamodelError, Diagnostics, NativeTypeErrorFactory, Span}; use enumflags2::BitFlags; use lsp_types::CompletionList; @@ -359,6 +361,22 @@ pub trait Connector: Send + Sync { ) -> DatasourceConnectorData { Default::default() } + + fn coerce_json_datetime( + &self, + _str: &str, + _nt: Option, + ) -> chrono::ParseResult> { + unreachable!("This method is only implemented on connectors with lateral join support.") + } + + fn coerce_json_decimal( + &self, + _str: &str, + _nt: Option, + ) -> Result { + unreachable!("This method is only implemented on connectors with lateral join support.") + } } #[derive(Copy, Clone, Debug, PartialEq)] 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..f996b204e75d --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs @@ -0,0 +1,321 @@ +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, CockroachDb))] +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, CockroachDb))] +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(()) + } +} + +#[test_suite(schema(schema), only(Postgres, CockroachDb))] +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..c73cfaaca6d6 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 @@ -106,10 +106,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 +117,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 +131,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.coerce_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 = sf.coerce_json_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 +177,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,19 +192,25 @@ 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()) diff --git a/query-engine/query-structure/src/field/scalar.rs b/query-engine/query-structure/src/field/scalar.rs index b8ef8ab204e2..aa35420ee38f 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -1,4 +1,6 @@ use crate::{ast, parent_container::ParentContainer, prelude::*, DefaultKind, NativeTypeInstance, ValueGenerator}; +use bigdecimal::{BigDecimal, ParseBigDecimalError}; +use chrono::{DateTime, FixedOffset}; use psl::{ parser_database::{walkers, ScalarFieldType, ScalarType}, schema_ast::ast::FieldArity, @@ -170,6 +172,20 @@ impl ScalarField { }) } + pub fn coerce_json_datetime(&self, value: &str) -> chrono::ParseResult> { + let nt = self.native_type().map(|nt| nt.native_type); + let connector = self.dm.schema.connector; + + connector.coerce_json_datetime(value, nt) + } + + pub fn coerce_json_decimal(&self, value: &str) -> Result { + let nt = self.native_type().map(|nt| nt.native_type); + let connector = self.dm.schema.connector; + + connector.coerce_json_decimal(value, nt) + } + pub fn is_autoincrement(&self) -> bool { match self.id { ScalarFieldId::InModel(id) => self.dm.walk(id).is_autoincrement(), From 0a9b5e9e2d05910c0cbeb925cb9664783470b7e9 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Fri, 8 Dec 2023 12:12:44 +0100 Subject: [PATCH 2/6] fmt + dry --- .../src/cockroach_datamodel_connector.rs | 9 +-------- .../src/postgres_datamodel_connector.rs | 6 +----- psl/builtin-connectors/src/utils.rs | 9 +++++++++ .../src/database/operations/coerce.rs | 18 +++++++++--------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index aa9d9ae27446..7b2dabfe7c27 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -323,14 +323,7 @@ impl Connector for CockroachDatamodelConnector { CockroachType::Timestamp(_) => crate::utils::parse_timestamp(str), CockroachType::Date => crate::utils::parse_date(str), CockroachType::Time(_) => crate::utils::parse_time(str), - CockroachType::Timetz(_) => { - // 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(); - - crate::utils::parse_time(time_without_tz) - } + CockroachType::Timetz(_) => crate::utils::parse_timetz(str), _ => unreachable!(), }, None => crate::utils::parse_timestamptz(str), diff --git a/psl/builtin-connectors/src/postgres_datamodel_connector.rs b/psl/builtin-connectors/src/postgres_datamodel_connector.rs index c16b559a8253..cccd9ad3f865 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -583,11 +583,7 @@ impl Connector for PostgresDatamodelConnector { Timestamp(_) => crate::utils::parse_timestamp(str), Date => crate::utils::parse_date(str), Time(_) => crate::utils::parse_time(str), - Timetz(_) => { - let time_without_tz = str.split('+').next().unwrap(); - - crate::utils::parse_time(time_without_tz) - } + Timetz(_) => crate::utils::parse_timetz(str), _ => unreachable!(), }, None => crate::utils::parse_timestamptz(str), diff --git a/psl/builtin-connectors/src/utils.rs b/psl/builtin-connectors/src/utils.rs index 188abe850667..6e4c1298aeb4 100644 --- a/psl/builtin-connectors/src/utils.rs +++ b/psl/builtin-connectors/src/utils.rs @@ -29,6 +29,15 @@ pub(crate) fn parse_time(str: &str) -> Result, chrono::Par .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) +} + pub(crate) fn parse_money(str: &str) -> Result { // We strip out the currency sign from the string. BigDecimal::from_str(&str[1..]).map(|bd| bd.normalized()) 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 c73cfaaca6d6..2ab888cc92fe 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 @@ -106,10 +106,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(&sf, &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(&sf, &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,13 +117,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(&sf, &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, + sf, &format!("Number({n})"), &format!("{:?}", sf.type_identifier()), )), @@ -134,7 +134,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel TypeIdentifier::DateTime => { let res = sf.coerce_json_datetime(&s).map_err(|err| { build_conversion_error_with_reason( - &sf, + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), @@ -146,7 +146,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel TypeIdentifier::Decimal => { let res = sf.coerce_json_decimal(&s).map_err(|err| { build_conversion_error_with_reason( - &sf, + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), @@ -157,7 +157,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel } TypeIdentifier::UUID => Ok(PrismaValue::Uuid(uuid::Uuid::parse_str(&s).map_err(|err| { build_conversion_error_with_reason( - &sf, + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), @@ -167,7 +167,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel // 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, + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), &err.to_string(), @@ -177,7 +177,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel Ok(PrismaValue::Bytes(bytes)) } _ => Err(build_conversion_error( - &sf, + sf, &format!("String({s})"), &format!("{:?}", sf.type_identifier()), )), From 62e9644e1d74c9e7b0bcf5dbc1a093b8b3d2f880 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Fri, 8 Dec 2023 15:10:55 +0100 Subject: [PATCH 3/6] fix tests + default nt parsing --- .../src/cockroach_datamodel_connector.rs | 14 ++++++++++---- .../src/postgres_datamodel_connector.rs | 14 ++++++++++---- psl/psl-core/src/datamodel_connector.rs | 4 ++-- .../tests/queries/data_types/native/postgres.rs | 6 +++--- .../src/database/operations/coerce.rs | 4 ++-- query-engine/query-structure/src/field/scalar.rs | 8 ++++---- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index 7b2dabfe7c27..d4f3e5fd39e1 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -310,7 +310,7 @@ impl Connector for CockroachDatamodelConnector { Flavour::Cockroach } - fn coerce_json_datetime( + fn parse_json_datetime( &self, str: &str, nt: Option, @@ -326,11 +326,14 @@ impl Connector for CockroachDatamodelConnector { CockroachType::Timetz(_) => crate::utils::parse_timetz(str), _ => unreachable!(), }, - None => crate::utils::parse_timestamptz(str), + None => self.parse_json_datetime( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::DateTime)), + ), } } - fn coerce_json_decimal( + fn parse_json_decimal( &self, str: &str, nt: Option, @@ -342,7 +345,10 @@ impl Connector for CockroachDatamodelConnector { CockroachType::Decimal(_) => crate::utils::parse_decimal(str), _ => unreachable!(), }, - None => crate::utils::parse_decimal(str), + None => self.parse_json_decimal( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::Decimal)), + ), } } } diff --git a/psl/builtin-connectors/src/postgres_datamodel_connector.rs b/psl/builtin-connectors/src/postgres_datamodel_connector.rs index cccd9ad3f865..223b51e74729 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -570,7 +570,7 @@ impl Connector for PostgresDatamodelConnector { Flavour::Postgres } - fn coerce_json_datetime( + fn parse_json_datetime( &self, str: &str, nt: Option, @@ -586,11 +586,14 @@ impl Connector for PostgresDatamodelConnector { Timetz(_) => crate::utils::parse_timetz(str), _ => unreachable!(), }, - None => crate::utils::parse_timestamptz(str), + None => self.parse_json_datetime( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::DateTime)), + ), } } - fn coerce_json_decimal( + fn parse_json_decimal( &self, str: &str, nt: Option, @@ -603,7 +606,10 @@ impl Connector for PostgresDatamodelConnector { Money => crate::utils::parse_money(str), _ => unreachable!(), }, - None => crate::utils::parse_decimal(str), + None => self.parse_json_decimal( + str, + Some(self.default_native_type_for_scalar_type(&ScalarType::Decimal)), + ), } } } diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index 504d3f60749a..252af81b72c1 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -362,7 +362,7 @@ pub trait Connector: Send + Sync { Default::default() } - fn coerce_json_datetime( + fn parse_json_datetime( &self, _str: &str, _nt: Option, @@ -370,7 +370,7 @@ pub trait Connector: Send + Sync { unreachable!("This method is only implemented on connectors with lateral join support.") } - fn coerce_json_decimal( + fn parse_json_decimal( &self, _str: &str, _nt: Option, 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 index f996b204e75d..d09a916e4cd0 100644 --- 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 @@ -72,7 +72,7 @@ mod datetime { } } -#[test_suite(only(Postgres, CockroachDb))] +#[test_suite(only(Postgres))] mod decimal { fn schema_decimal() -> String { let schema = indoc! { @@ -134,7 +134,7 @@ mod decimal { } } -#[test_suite(only(Postgres, CockroachDb))] +#[test_suite(only(Postgres))] mod string { fn schema_string() -> String { let schema = indoc! { @@ -212,7 +212,7 @@ mod string { } } -#[test_suite(schema(schema), only(Postgres, CockroachDb))] +#[test_suite(schema(schema), only(Postgres))] mod others { fn schema_other_types() -> String { let schema = indoc! { 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 2ab888cc92fe..bdf85574039e 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 @@ -132,7 +132,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel TypeIdentifier::String => Ok(PrismaValue::String(s)), TypeIdentifier::Enum(_) => Ok(PrismaValue::Enum(s)), TypeIdentifier::DateTime => { - let res = sf.coerce_json_datetime(&s).map_err(|err| { + let res = sf.parse_json_datetime(&s).map_err(|err| { build_conversion_error_with_reason( sf, &format!("String({s})"), @@ -144,7 +144,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel Ok(PrismaValue::DateTime(res)) } TypeIdentifier::Decimal => { - let res = sf.coerce_json_decimal(&s).map_err(|err| { + let res = sf.parse_json_decimal(&s).map_err(|err| { build_conversion_error_with_reason( sf, &format!("String({s})"), diff --git a/query-engine/query-structure/src/field/scalar.rs b/query-engine/query-structure/src/field/scalar.rs index aa35420ee38f..b7b2f864d9f3 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -172,18 +172,18 @@ impl ScalarField { }) } - pub fn coerce_json_datetime(&self, value: &str) -> chrono::ParseResult> { + 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.coerce_json_datetime(value, nt) + connector.parse_json_datetime(value, nt) } - pub fn coerce_json_decimal(&self, value: &str) -> Result { + pub fn parse_json_decimal(&self, value: &str) -> Result { let nt = self.native_type().map(|nt| nt.native_type); let connector = self.dm.schema.connector; - connector.coerce_json_decimal(value, nt) + connector.parse_json_decimal(value, nt) } pub fn is_autoincrement(&self) -> bool { From 5351afb27c3cd0ad69f38c2737bb564012aa7d73 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Fri, 8 Dec 2023 17:34:58 +0100 Subject: [PATCH 4/6] exclude wasm DAs when necessary --- .../tests/queries/data_types/native/postgres.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index d09a916e4cd0..e25d57e854de 100644 --- 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 @@ -212,7 +212,11 @@ mod string { } } -#[test_suite(schema(schema), only(Postgres))] +// 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! { From a61155a85369f3bca0244e86e38b94770cc5d836 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Thu, 14 Dec 2023 14:50:04 +0100 Subject: [PATCH 5/6] fix money type --- Cargo.lock | 1 - psl/builtin-connectors/Cargo.toml | 2 +- .../src/cockroach_datamodel_connector.rs | 20 ----------- .../src/postgres_datamodel_connector.rs | 21 ----------- psl/builtin-connectors/src/utils.rs | 11 ------ psl/psl-core/src/datamodel_connector.rs | 9 ----- quaint/src/ast/column.rs | 9 ++++- quaint/src/visitor/postgres.rs | 35 ++++++++++++++++++- .../src/database/operations/coerce.rs | 11 +++--- .../src/model_extensions/column.rs | 1 + .../query-structure/src/field/scalar.rs | 8 ----- 11 files changed, 51 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db5912b6377e..13d75bf0cfe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,7 +432,6 @@ dependencies = [ name = "builtin-psl-connectors" version = "0.1.0" dependencies = [ - "bigdecimal", "chrono", "connection-string", "either", diff --git a/psl/builtin-connectors/Cargo.toml b/psl/builtin-connectors/Cargo.toml index 7e8db138bb5c..5ab62a921c63 100644 --- a/psl/builtin-connectors/Cargo.toml +++ b/psl/builtin-connectors/Cargo.toml @@ -14,4 +14,4 @@ lsp-types = "0.91.1" once_cell = "1.3" regex = "1" chrono = { version = "0.4.6", default_features = false } -bigdecimal = "0.3" + diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index d4f3e5fd39e1..99f94c953971 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -1,7 +1,6 @@ mod native_types; mod validations; -use bigdecimal::{BigDecimal, ParseBigDecimalError}; pub use native_types::CockroachType; use chrono::*; @@ -332,25 +331,6 @@ impl Connector for CockroachDatamodelConnector { ), } } - - fn parse_json_decimal( - &self, - str: &str, - nt: Option, - ) -> Result { - let native_type: Option<&CockroachType> = nt.as_ref().map(|nt| nt.downcast_ref()); - - match native_type { - Some(pt) => match pt { - CockroachType::Decimal(_) => crate::utils::parse_decimal(str), - _ => unreachable!(), - }, - None => self.parse_json_decimal( - str, - Some(self.default_native_type_for_scalar_type(&ScalarType::Decimal)), - ), - } - } } /// An `@default(sequence())` function. diff --git a/psl/builtin-connectors/src/postgres_datamodel_connector.rs b/psl/builtin-connectors/src/postgres_datamodel_connector.rs index 223b51e74729..fa3325d1403f 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -4,7 +4,6 @@ mod validations; pub use native_types::PostgresType; -use bigdecimal::{BigDecimal, ParseBigDecimalError}; use chrono::*; use enumflags2::BitFlags; use lsp_types::{CompletionItem, CompletionItemKind, CompletionList, InsertTextFormat}; @@ -592,26 +591,6 @@ impl Connector for PostgresDatamodelConnector { ), } } - - fn parse_json_decimal( - &self, - str: &str, - nt: Option, - ) -> Result { - let native_type: Option<&PostgresType> = nt.as_ref().map(|nt| nt.downcast_ref()); - - match native_type { - Some(pt) => match pt { - Decimal(_) => crate::utils::parse_decimal(str), - Money => crate::utils::parse_money(str), - _ => unreachable!(), - }, - None => self.parse_json_decimal( - str, - Some(self.default_native_type_for_scalar_type(&ScalarType::Decimal)), - ), - } - } } 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 index 6e4c1298aeb4..3ef9f55cd80a 100644 --- a/psl/builtin-connectors/src/utils.rs +++ b/psl/builtin-connectors/src/utils.rs @@ -1,6 +1,4 @@ -use bigdecimal::{BigDecimal, ParseBigDecimalError}; use chrono::*; -use std::str::FromStr; pub(crate) fn parse_date(str: &str) -> Result, chrono::ParseError> { chrono::NaiveDate::parse_from_str(str, "%Y-%m-%d") @@ -37,12 +35,3 @@ pub(crate) fn parse_timetz(str: &str) -> Result, chrono::P parse_time(time_without_tz) } - -pub(crate) fn parse_money(str: &str) -> Result { - // We strip out the currency sign from the string. - BigDecimal::from_str(&str[1..]).map(|bd| bd.normalized()) -} - -pub(crate) fn parse_decimal(str: &str) -> Result { - BigDecimal::from_str(str).map(|bd| bd.normalized()) -} diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index 252af81b72c1..751b03ac9da1 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -25,7 +25,6 @@ pub use self::{ }; use crate::{configuration::DatasourceConnectorData, Configuration, Datasource, PreviewFeature}; -use bigdecimal::{BigDecimal, ParseBigDecimalError}; use chrono::{DateTime, FixedOffset}; use diagnostics::{DatamodelError, Diagnostics, NativeTypeErrorFactory, Span}; use enumflags2::BitFlags; @@ -369,14 +368,6 @@ pub trait Connector: Send + Sync { ) -> chrono::ParseResult> { unreachable!("This method is only implemented on connectors with lateral join support.") } - - fn parse_json_decimal( - &self, - _str: &str, - _nt: Option, - ) -> Result { - 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..64ea0479de4a 100644 --- a/quaint/src/visitor/postgres.rs +++ b/quaint/src/visitor/postgres.rs @@ -17,6 +17,24 @@ pub struct Postgres<'a> { parameters: Vec>, } +impl<'a> Postgres<'a> { + fn visit_json_build_obj_expr(&mut self, expr: Expression<'a>) -> crate::Result<()> { + dbg!(&expr); + 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 +552,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 +1308,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/connectors/sql-query-connector/src/database/operations/coerce.rs b/query-engine/connectors/sql-query-connector/src/database/operations/coerce.rs index bdf85574039e..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}; @@ -144,7 +143,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel Ok(PrismaValue::DateTime(res)) } TypeIdentifier::Decimal => { - let res = sf.parse_json_decimal(&s).map_err(|err| { + let res = parse_decimal(&s).map_err(|err| { build_conversion_error_with_reason( sf, &format!("String({s})"), @@ -215,3 +214,7 @@ fn build_conversion_error_with_reason(sf: &ScalarField, from: &str, to: &str, re 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 b7b2f864d9f3..52d59686fdda 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -1,5 +1,4 @@ use crate::{ast, parent_container::ParentContainer, prelude::*, DefaultKind, NativeTypeInstance, ValueGenerator}; -use bigdecimal::{BigDecimal, ParseBigDecimalError}; use chrono::{DateTime, FixedOffset}; use psl::{ parser_database::{walkers, ScalarFieldType, ScalarType}, @@ -179,13 +178,6 @@ impl ScalarField { connector.parse_json_datetime(value, nt) } - pub fn parse_json_decimal(&self, value: &str) -> Result { - let nt = self.native_type().map(|nt| nt.native_type); - let connector = self.dm.schema.connector; - - connector.parse_json_decimal(value, nt) - } - pub fn is_autoincrement(&self) -> bool { match self.id { ScalarFieldId::InModel(id) => self.dm.walk(id).is_autoincrement(), From 975bc9681ed8a9271d08e55785e8d63da1326682 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Mon, 18 Dec 2023 11:34:19 +0100 Subject: [PATCH 6/6] review fix --- psl/builtin-connectors/Cargo.toml | 2 +- quaint/src/visitor/postgres.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/psl/builtin-connectors/Cargo.toml b/psl/builtin-connectors/Cargo.toml index 5ab62a921c63..ef9e810b8aba 100644 --- a/psl/builtin-connectors/Cargo.toml +++ b/psl/builtin-connectors/Cargo.toml @@ -13,5 +13,5 @@ indoc.workspace = true lsp-types = "0.91.1" once_cell = "1.3" regex = "1" -chrono = { version = "0.4.6", default_features = false } +chrono = { version = "0.4.6", default-features = false } diff --git a/quaint/src/visitor/postgres.rs b/quaint/src/visitor/postgres.rs index 64ea0479de4a..40c80d330c14 100644 --- a/quaint/src/visitor/postgres.rs +++ b/quaint/src/visitor/postgres.rs @@ -19,7 +19,6 @@ pub struct Postgres<'a> { impl<'a> Postgres<'a> { fn visit_json_build_obj_expr(&mut self, expr: Expression<'a>) -> crate::Result<()> { - dbg!(&expr); match expr.kind() { ExpressionKind::Column(col) => match (col.type_family.as_ref(), col.native_type.as_deref()) { (Some(TypeFamily::Decimal(_)), Some("MONEY")) => {