diff --git a/Cargo.lock b/Cargo.lock index 6fb116734862..9c98ef5687ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3498,6 +3498,7 @@ dependencies = [ "diagnostics", "either", "enumflags2", + "hex", "indoc 2.0.3", "itertools 0.12.0", "lsp-types", @@ -5007,7 +5008,6 @@ dependencies = [ "chrono", "cuid", "futures", - "hex", "itertools 0.12.0", "once_cell", "opentelemetry", diff --git a/psl/psl-core/Cargo.toml b/psl/psl-core/Cargo.toml index 97f4dd56d470..64343301c2c7 100644 --- a/psl/psl-core/Cargo.toml +++ b/psl/psl-core/Cargo.toml @@ -20,6 +20,7 @@ serde_json.workspace = true enumflags2 = "0.7" indoc.workspace = true either = "1.8.1" +hex = "0.4" # For the connector API. lsp-types = "0.91.1" diff --git a/psl/psl-core/src/builtin_connectors/cockroach_datamodel_connector.rs b/psl/psl-core/src/builtin_connectors/cockroach_datamodel_connector.rs index 64d945e33556..84968b15a6f0 100644 --- a/psl/psl-core/src/builtin_connectors/cockroach_datamodel_connector.rs +++ b/psl/psl-core/src/builtin_connectors/cockroach_datamodel_connector.rs @@ -331,6 +331,23 @@ impl Connector for CockroachDatamodelConnector { None => self.parse_json_datetime(str, self.default_native_type_for_scalar_type(&ScalarType::DateTime)), } } + + fn parse_json_bytes(&self, str: &str, nt: Option) -> prisma_value::PrismaValueResult> { + let native_type: Option<&CockroachType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(ct) => match ct { + CockroachType::Bytes => { + super::utils::postgres::parse_bytes(str).map_err(|_| prisma_value::ConversionFailure { + from: "hex".into(), + to: "bytes".into(), + }) + } + _ => unreachable!(), + }, + None => self.parse_json_bytes(str, self.default_native_type_for_scalar_type(&ScalarType::Bytes)), + } + } } /// An `@default(sequence())` function. diff --git a/psl/psl-core/src/builtin_connectors/mysql_datamodel_connector.rs b/psl/psl-core/src/builtin_connectors/mysql_datamodel_connector.rs index 1fc889c45a2e..2489a144f09c 100644 --- a/psl/psl-core/src/builtin_connectors/mysql_datamodel_connector.rs +++ b/psl/psl-core/src/builtin_connectors/mysql_datamodel_connector.rs @@ -3,6 +3,7 @@ mod validations; use chrono::FixedOffset; pub use native_types::MySqlType; +use prisma_value::{decode_bytes, PrismaValueResult}; use super::completions; use crate::{ @@ -306,4 +307,9 @@ impl Connector for MySqlDatamodelConnector { None => self.parse_json_datetime(str, self.default_native_type_for_scalar_type(&ScalarType::DateTime)), } } + + // On MySQL, bytes are encoded as base64 in the database directly. + fn parse_json_bytes(&self, str: &str, _nt: Option) -> PrismaValueResult> { + decode_bytes(str) + } } diff --git a/psl/psl-core/src/builtin_connectors/postgres_datamodel_connector.rs b/psl/psl-core/src/builtin_connectors/postgres_datamodel_connector.rs index 725e4247c4fd..7fd56b2075fc 100644 --- a/psl/psl-core/src/builtin_connectors/postgres_datamodel_connector.rs +++ b/psl/psl-core/src/builtin_connectors/postgres_datamodel_connector.rs @@ -591,6 +591,23 @@ impl Connector for PostgresDatamodelConnector { None => self.parse_json_datetime(str, self.default_native_type_for_scalar_type(&ScalarType::DateTime)), } } + + fn parse_json_bytes(&self, str: &str, nt: Option) -> prisma_value::PrismaValueResult> { + let native_type: Option<&PostgresType> = nt.as_ref().map(|nt| nt.downcast_ref()); + + match native_type { + Some(ct) => match ct { + PostgresType::ByteA => { + super::utils::postgres::parse_bytes(str).map_err(|_| prisma_value::ConversionFailure { + from: "hex".into(), + to: "bytes".into(), + }) + } + _ => unreachable!(), + }, + None => self.parse_json_bytes(str, self.default_native_type_for_scalar_type(&ScalarType::Bytes)), + } + } } fn allowed_index_operator_classes(algo: IndexAlgorithm, field: walkers::ScalarFieldWalker<'_>) -> Vec { diff --git a/psl/psl-core/src/builtin_connectors/utils.rs b/psl/psl-core/src/builtin_connectors/utils.rs index fa9a71ddde62..a8d5618f5d23 100644 --- a/psl/psl-core/src/builtin_connectors/utils.rs +++ b/psl/psl-core/src/builtin_connectors/utils.rs @@ -44,6 +44,10 @@ pub(crate) mod postgres { super::common::parse_time(time_without_tz) } + + pub(crate) fn parse_bytes(str: &str) -> Result, hex::FromHexError> { + hex::decode(&str[2..]) + } } pub(crate) mod mysql { diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index 4874ad4a198c..107abd24710f 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -321,6 +321,14 @@ pub trait Connector: Send + Sync { ) -> chrono::ParseResult> { unreachable!("This method is only implemented on connectors with lateral join support.") } + + fn parse_json_bytes( + &self, + _str: &str, + _nt: Option, + ) -> prisma_value::PrismaValueResult> { + unreachable!("This method is only implemented on connectors with lateral join support.") + } } #[derive(Copy, Clone, Debug, PartialEq)] diff --git a/quaint/src/visitor/postgres.rs b/quaint/src/visitor/postgres.rs index cccce66ca662..8ab679f42701 100644 --- a/quaint/src/visitor/postgres.rs +++ b/quaint/src/visitor/postgres.rs @@ -27,13 +27,6 @@ impl<'a> Postgres<'a> { Ok(()) } - (_, Some("BYTES") | Some("BYTEA")) => { - self.write("ENCODE(")?; - self.visit_expression(expr)?; - self.write(", 'base64')")?; - - Ok(()) - } _ => self.visit_expression(expr), }, _ => self.visit_expression(expr), diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mysql.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mysql.rs index 6510d5ac02c4..895543a198e0 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mysql.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/mysql.rs @@ -282,115 +282,3 @@ mod bytes { 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/Cargo.toml b/query-engine/connectors/sql-query-connector/Cargo.toml index b467fcd277bf..354ec5bc0887 100644 --- a/query-engine/connectors/sql-query-connector/Cargo.toml +++ b/query-engine/connectors/sql-query-connector/Cargo.toml @@ -27,7 +27,6 @@ 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 4a12e4d7d183..11f3b990f6b7 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 @@ -174,7 +174,7 @@ pub(crate) fn coerce_json_scalar_to_pv(value: serde_json::Value, sf: &ScalarFiel ) })?)), TypeIdentifier::Bytes => { - let bytes = decode_bytes(&s).map_err(|err| { + let bytes = sf.parse_json_bytes(&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 9b1314639d1e..becd438db276 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -156,27 +156,21 @@ impl ScalarField { pub fn native_type(&self) -> Option { let connector = self.dm.schema.connector; + let raw_nt = match self.id { ScalarFieldId::InModel(id) => self.dm.walk(id).raw_native_type(), ScalarFieldId::InCompositeType(id) => self.dm.walk(id).raw_native_type(), }; + let psl_nt = raw_nt .and_then(|(_, name, args, span)| connector.parse_native_type(name, args, span, &mut Default::default())); - let nt = match self.id { - ScalarFieldId::InModel(id) => psl_nt.or_else(|| { - self.dm - .walk(id) - .scalar_type() - .and_then(|st| connector.default_native_type_for_scalar_type(&st)) - }), - ScalarFieldId::InCompositeType(id) => psl_nt.or_else(|| { - self.dm - .walk(id) - .scalar_type() - .and_then(|st| connector.default_native_type_for_scalar_type(&st)) - }), - }?; + let scalar_type = match self.id { + ScalarFieldId::InModel(id) => self.dm.walk(id).scalar_type(), + ScalarFieldId::InCompositeType(id) => self.dm.walk(id).scalar_type(), + }; + + let nt = psl_nt.or_else(|| scalar_type.and_then(|st| connector.default_native_type_for_scalar_type(&st)))?; Some(NativeTypeInstance { native_type: nt, @@ -191,6 +185,13 @@ impl ScalarField { connector.parse_json_datetime(value, nt) } + pub fn parse_json_bytes(&self, value: &str) -> PrismaValueResult> { + let nt = self.native_type().map(|nt| nt.native_type); + let connector = self.dm.schema.connector; + + connector.parse_json_bytes(value, nt) + } + pub fn is_autoincrement(&self) -> bool { match self.id { ScalarFieldId::InModel(id) => self.dm.walk(id).is_autoincrement(),