From 53daea5e19292fbbfc60d3a98b0b811dc848b829 Mon Sep 17 00:00:00 2001 From: Alberto Schiabel Date: Mon, 22 Jan 2024 12:04:26 +0100 Subject: [PATCH] feat(driver-adapters): fix bigint handling on creation and filter (#4648) * feat(driver-adapters): fix bigint handling on creation and filter * chore(driver-adapters): comment out native_other_types, like it was before * chore(driver-adapters): excluded PlanetScale from test * tmp/fix: avoid adding quotes to bigint numbers in PlanetScale, waiting for upstream PR to be merged * chore: update comments, remove PlanetScale exclusion for "using_bigint_field" test --- .../tests/new/regressions/max_integer.rs | 2 + .../tests/writes/data_types/bigint.rs | 6 +- .../data_types/native_types/postgres.rs | 2 +- .../executor/src/planetscale/sanitize.ts | 90 +++++++++++++++++++ .../driver-adapters/executor/src/testd.ts | 5 +- .../driver-adapters/src/wasm/to_js.rs | 11 ++- 6 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 query-engine/driver-adapters/executor/src/planetscale/sanitize.ts diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/max_integer.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/max_integer.rs index e00b2d22e198..3eee8d0d4aee 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/max_integer.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/max_integer.rs @@ -187,6 +187,8 @@ mod max_integer { schema.to_owned() } + // Info: `driver-adapters` are currently excluded because they yield a different error message, + // coming straight from the database. #[connector_test( schema(overflow_pg), only(Postgres), diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/bigint.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/bigint.rs index c78b522f4994..469ebd227d49 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/bigint.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/bigint.rs @@ -17,11 +17,7 @@ mod bigint { } // "Using a BigInt field" should "work" - #[connector_test(exclude( - Postgres("pg.js.wasm", "neon.js.wasm"), - Sqlite("libsql.js.wasm"), - Vitess("planetscale.js.wasm") - ))] + #[connector_test()] async fn using_bigint_field(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/native_types/postgres.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/native_types/postgres.rs index cb3cf9b366d6..f47f3e528b17 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/native_types/postgres.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/data_types/native_types/postgres.rs @@ -23,7 +23,7 @@ mod postgres { } //"Postgres native int types" should "work" - #[connector_test(schema(schema_int), only(Postgres), exclude(Postgres("pg.js.wasm", "neon.js.wasm")))] + #[connector_test(schema(schema_int), only(Postgres))] async fn native_int_types(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { diff --git a/query-engine/driver-adapters/executor/src/planetscale/sanitize.ts b/query-engine/driver-adapters/executor/src/planetscale/sanitize.ts new file mode 100644 index 000000000000..5c7d24fcde4c --- /dev/null +++ b/query-engine/driver-adapters/executor/src/planetscale/sanitize.ts @@ -0,0 +1,90 @@ +// This is a temporary workaround for `bigint` serialization issues in the Planetscale driver, +// which will be fixed upstream once https://github.com/planetscale/database-js/pull/159 is published. +// This only impacts Rust tests concerning `driver-adapters`. + +type Stringable = { toString: () => string } +type Value = null | undefined | number | boolean | string | Array | Date | Stringable + +export function format(query: string, values: Value[] | Record): string { + return Array.isArray(values) ? replacePosition(query, values) : replaceNamed(query, values) +} + +function replacePosition(query: string, values: Value[]): string { + let index = 0 + return query.replace(/\?/g, (match) => { + return index < values.length ? sanitize(values[index++]) : match + }) +} + +function replaceNamed(query: string, values: Record): string { + return query.replace(/:(\w+)/g, (match, name) => { + return hasOwn(values, name) ? sanitize(values[name]) : match + }) +} + +function hasOwn(obj: unknown, name: string): boolean { + return Object.prototype.hasOwnProperty.call(obj, name) +} + +function sanitize(value: Value): string { + if (value == null) { + return 'null' + } + + if (['number', 'bigint'].includes(typeof value)) { + return String(value) + } + + if (typeof value === 'boolean') { + return value ? 'true' : 'false' + } + + if (typeof value === 'string') { + return quote(value) + } + + if (Array.isArray(value)) { + return value.map(sanitize).join(', ') + } + + if (value instanceof Date) { + return quote(value.toISOString().slice(0, -1)) + } + + return quote(value.toString()) +} + +function quote(text: string): string { + return `'${escape(text)}'` +} + +const re = /[\0\b\n\r\t\x1a\\"']/g + +function escape(text: string): string { + return text.replace(re, replacement) +} + +function replacement(text: string): string { + switch (text) { + case '"': + return '\\"' + case "'": + return "\\'" + case '\n': + return '\\n' + case '\r': + return '\\r' + case '\t': + return '\\t' + case '\\': + return '\\\\' + case '\0': + return '\\0' + case '\b': + return '\\b' + case '\x1a': + return '\\Z' + default: + return '' + } +} diff --git a/query-engine/driver-adapters/executor/src/testd.ts b/query-engine/driver-adapters/executor/src/testd.ts index 8ee0d8b74d92..2575e6ab404b 100644 --- a/query-engine/driver-adapters/executor/src/testd.ts +++ b/query-engine/driver-adapters/executor/src/testd.ts @@ -19,6 +19,7 @@ import { PrismaLibSQL } from '@prisma/adapter-libsql' // planetscale dependencies import { Client as PlanetscaleClient } from '@planetscale/database' import { PrismaPlanetScale } from '@prisma/adapter-planetscale' +import { format as formatPlanetScaleQuery } from './planetscale/sanitize' @@ -292,11 +293,13 @@ async function planetscaleAdapter(url: string): Promise { throw new Error("DRIVER_ADAPTER_CONFIG is not defined or empty, but its required for planetscale adapter."); } - const client = new PlanetscaleClient({ // preserving path name so proxy url would look like real DB url url: copyPathName(url, proxyUrl), fetch, + + // TODO: remove once https://github.com/planetscale/database-js/pull/159 is published upstream. + format: formatPlanetScaleQuery, }) return new PrismaPlanetScale(client) diff --git a/query-engine/driver-adapters/src/wasm/to_js.rs b/query-engine/driver-adapters/src/wasm/to_js.rs index 4a76dee6a513..a97b7a0f70c2 100644 --- a/query-engine/driver-adapters/src/wasm/to_js.rs +++ b/query-engine/driver-adapters/src/wasm/to_js.rs @@ -2,10 +2,13 @@ use serde::Serialize; use serde_wasm_bindgen::Serializer; use wasm_bindgen::{JsError, JsValue}; -// `serialize_missing_as_null` is required to make sure that "empty" values (e.g., `None` and `()`) -// are serialized as `null` and not `undefined`. -// This is due to certain drivers (e.g., LibSQL) not supporting `undefined` values. -static DEFAULT_SERIALIZER: Serializer = Serializer::new().serialize_missing_as_null(true); +// - `serialize_missing_as_null` is required to make sure that "empty" values (e.g., `None` and `()`) +// are serialized as `null` and not `undefined`. +// This is due to certain drivers (e.g., LibSQL) not supporting `undefined` values. +// - `serialize_large_number_types_as_bigints` is required to allow reading bigints from Prisma Client. +static DEFAULT_SERIALIZER: Serializer = Serializer::new() + .serialize_large_number_types_as_bigints(true) + .serialize_missing_as_null(true); pub(crate) trait ToJsValue: Sized { fn to_js_value(&self) -> Result;