diff --git a/.github/workflows/query-engine-driver-adapters.yml b/.github/workflows/query-engine-driver-adapters.yml index b8434e2fa04c..3de0238aa0e7 100644 --- a/.github/workflows/query-engine-driver-adapters.yml +++ b/.github/workflows/query-engine-driver-adapters.yml @@ -31,8 +31,6 @@ jobs: setup_task: 'dev-neon-ws-postgres13' - name: 'libsql' setup_task: 'dev-libsql-sqlite' - - name: 'planetscale' - setup_task: 'dev-planetscale-vitess8' node_version: ['18'] env: LOG_LEVEL: 'info' # Set to "debug" to trace the query engine and node process running the driver adapter diff --git a/Makefile b/Makefile index a30a32ca1871..e00c122e2713 100644 --- a/Makefile +++ b/Makefile @@ -130,10 +130,10 @@ test-pg-postgres13: dev-pg-postgres13 test-qe-st test-driver-adapter-pg: test-pg-postgres13 -start-neon-postgres13: build-qe-napi build-connector-kit-js +start-neon-postgres13: docker compose -f docker-compose.yml up --wait -d --remove-orphans neon-postgres13 -dev-neon-ws-postgres13: start-neon-postgres13 +dev-neon-ws-postgres13: start-neon-postgres13 build-qe-napi build-connector-kit-js cp $(CONFIG_PATH)/neon-ws-postgres13 $(CONFIG_FILE) test-neon-ws-postgres13: dev-neon-ws-postgres13 test-qe-st @@ -268,10 +268,10 @@ start-vitess_8_0: dev-vitess_8_0: start-vitess_8_0 cp $(CONFIG_PATH)/vitess_8_0 $(CONFIG_FILE) -start-planetscale-vitess8: build-qe-napi build-connector-kit-js +start-planetscale-vitess8: docker compose -f docker-compose.yml up -d --remove-orphans planetscale-vitess8 -dev-planetscale-vitess8: start-planetscale-vitess8 +dev-planetscale-vitess8: start-planetscale-vitess8 build-qe-napi build-connector-kit-js cp $(CONFIG_PATH)/planetscale-vitess8 $(CONFIG_FILE) test-planetscale-vitess8: dev-planetscale-vitess8 test-qe-st diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/assertion_violation_error.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/assertion_violation_error.rs index 62c4e3005f71..a3e45b0a05b5 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/assertion_violation_error.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/assertion_violation_error.rs @@ -1,8 +1,8 @@ use query_engine_tests::*; -#[test_suite(schema(generic), only(Postgres))] +#[test_suite(schema(generic))] mod raw_params { - #[connector_test] + #[connector_test(only(Postgres), exclude(JS))] async fn value_too_many_bind_variables(runner: Runner) -> TestResult<()> { let n = 32768; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/interactive_tx.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/interactive_tx.rs index 9aa34a943560..e45cef8ac306 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/interactive_tx.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/interactive_tx.rs @@ -213,7 +213,7 @@ mod interactive_tx { Ok(()) } - #[connector_test(exclude(JS))] + #[connector_test] async fn batch_queries_failure(mut runner: Runner) -> TestResult<()> { // Tx expires after five second. let tx_id = runner.start_tx(5000, 5000, None).await?; @@ -256,7 +256,7 @@ mod interactive_tx { Ok(()) } - #[connector_test(exclude(JS))] + #[connector_test] async fn tx_expiration_failure_cycle(mut runner: Runner) -> TestResult<()> { // Tx expires after one seconds. let tx_id = runner.start_tx(5000, 1000, None).await?; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs index 8ea08acc85da..393581b8ad91 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -66,7 +66,7 @@ mod one2one_req { } /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL))] async fn delete_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), @@ -167,7 +167,7 @@ mod one2one_opt { } /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL))] async fn delete_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), @@ -270,7 +270,7 @@ mod one2many_req { } /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL))] async fn delete_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), @@ -371,7 +371,7 @@ mod one2many_opt { } /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL))] async fn delete_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs index b0e566ffcb55..974c165ed942 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs @@ -68,7 +68,7 @@ mod one2one_req { } /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL))] async fn update_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), @@ -171,7 +171,7 @@ mod one2one_opt { } /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL))] async fn update_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), @@ -276,7 +276,7 @@ mod one2many_req { } /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(required_with_default), exclude(MongoDb, MySQL))] async fn update_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), @@ -379,7 +379,7 @@ mod one2many_opt { } /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). - #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL, JS))] + #[connector_test(schema(optional_with_default), exclude(MongoDb, MySQL))] async fn update_parent_no_exist_fail(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(&runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), 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 581bc21bebe8..7b25cfff279e 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,8 +187,8 @@ mod max_integer { schema.to_owned() } - #[connector_test(schema(overflow_pg), only(Postgres))] - async fn unfitted_int_should_fail_pg(runner: Runner) -> TestResult<()> { + #[connector_test(schema(overflow_pg), only(Postgres), exclude(JS))] + async fn unfitted_int_should_fail_pg_quaint(runner: Runner) -> TestResult<()> { // int assert_error!( runner, @@ -234,6 +234,55 @@ mod max_integer { Ok(()) } + // The driver adapter for neon provides different error messages on overflow + #[connector_test(schema(overflow_pg), only(JS, Postgres))] + async fn unfitted_int_should_fail_pg_js(runner: Runner) -> TestResult<()> { + // int + assert_error!( + runner, + format!("mutation {{ createOneTest(data: {{ int: {I32_OVERFLOW_MAX} }}) {{ id }} }}"), + None, + "value \\\"2147483648\\\" is out of range for type integer" + ); + assert_error!( + runner, + format!("mutation {{ createOneTest(data: {{ int: {I32_OVERFLOW_MIN} }}) {{ id }} }}"), + None, + "value \\\"-2147483649\\\" is out of range for type integer" + ); + + // smallint + assert_error!( + runner, + format!("mutation {{ createOneTest(data: {{ smallint: {I16_OVERFLOW_MAX} }}) {{ id }} }}"), + None, + "value \\\"32768\\\" is out of range for type smallint" + ); + assert_error!( + runner, + format!("mutation {{ createOneTest(data: {{ smallint: {I16_OVERFLOW_MIN} }}) {{ id }} }}"), + None, + "value \\\"-32769\\\" is out of range for type smallint" + ); + + //oid + assert_error!( + runner, + format!("mutation {{ createOneTest(data: {{ oid: {U32_OVERFLOW_MAX} }}) {{ id }} }}"), + None, + "value \\\"4294967296\\\" is out of range for type oid" + ); + + // The underlying driver swallows a negative id by interpreting it as unsigned. + // {"data":{"createOneTest":{"id":1,"oid":4294967295}}} + run_query!( + runner, + format!("mutation {{ createOneTest(data: {{ oid: {OVERFLOW_MIN} }}) {{ id, oid }} }}") + ); + + Ok(()) + } + #[connector_test(schema(overflow_pg), only(Postgres))] async fn fitted_int_should_work_pg(runner: Runner) -> TestResult<()> { // int diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/prisma_15204.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/prisma_15204.rs index c1df015c577b..ccf04dd2f4af 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/prisma_15204.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/regressions/prisma_15204.rs @@ -24,8 +24,8 @@ mod conversion_error { schema.to_owned() } - #[connector_test(schema(schema_int))] - async fn convert_to_int(runner: Runner) -> TestResult<()> { + #[connector_test(schema(schema_int), only(Sqlite), exclude(JS))] + async fn convert_to_int_sqlite_quaint(runner: Runner) -> TestResult<()> { create_test_data(&runner).await?; assert_error!( @@ -38,8 +38,22 @@ mod conversion_error { Ok(()) } - #[connector_test(schema(schema_bigint))] - async fn convert_to_bigint(runner: Runner) -> TestResult<()> { + #[connector_test(schema(schema_int), only(Sqlite, JS))] + async fn convert_to_int_sqlite_js(runner: Runner) -> TestResult<()> { + create_test_data(&runner).await?; + + assert_error!( + runner, + r#"query { findManyTestModel { field } }"#, + 2023, + "Inconsistent column data: Conversion failed: number must be an integer in column 'field'" + ); + + Ok(()) + } + + #[connector_test(schema(schema_bigint), only(Sqlite), exclude(JS))] + async fn convert_to_bigint_sqlite_quaint(runner: Runner) -> TestResult<()> { create_test_data(&runner).await?; assert_error!( @@ -52,6 +66,20 @@ mod conversion_error { Ok(()) } + #[connector_test(schema(schema_bigint), only(Sqlite, JS))] + async fn convert_to_bigint_sqlite_js(runner: Runner) -> TestResult<()> { + create_test_data(&runner).await?; + + assert_error!( + runner, + r#"query { findManyTestModel { field } }"#, + 2023, + "Inconsistent column data: Conversion failed: number must be an i64 in column 'field'" + ); + + Ok(()) + } + async fn create_test_data(runner: &Runner) -> TestResult<()> { run_query!( runner, diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/filters/json.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/filters/json.rs index 2fe8af850120..5440ff8218f8 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/filters/json.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/filters/json.rs @@ -207,7 +207,9 @@ mod json { Ok(()) } - #[connector_test(schema(json_opt))] + // The external runner for driver adapters, in spite of the protocol being used in the test matrix + // uses the JSON representation of queries, so this test should not apply to driver adapters (exclude(JS)) + #[connector_test(schema(json_opt), exclude(JS))] async fn nested_not_shorthand(runner: Runner) -> TestResult<()> { // Those tests pass with the JSON protocol because the entire object is parsed as JSON. // They remain useful to ensure we don't ever allow a full JSON filter input object type at the schema level. diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/casts.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/casts.rs index 0039b924108c..635726c71380 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/casts.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/casts.rs @@ -5,7 +5,20 @@ use query_engine_tests::*; mod casts { use query_engine_tests::{fmt_query_raw, run_query, RawParam}; - #[connector_test] + // The following tests are excluded for driver adapters. The underlying + // driver rejects queries where the values of the positional arguments do + // not match the expected types. As an example, the following query to the + // driver + // + // ```json + // { + // sql: 'SELECT $1::int4 AS decimal_to_i4; ', + // args: [ 42.51 ] + // } + // + // Bails with: ERROR: invalid input syntax for type integer: "42.51" + // + #[connector_test(only(Postgres), exclude(JS))] async fn query_numeric_casts(runner: Runner) -> TestResult<()> { insta::assert_snapshot!( run_query_pretty!(&runner, fmt_query_raw(r#" diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/errors.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/errors.rs index 88409d8d17f6..43417cb352e9 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/errors.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/raw/sql/errors.rs @@ -34,8 +34,8 @@ mod raw_errors { Ok(()) } - #[connector_test(schema(common_nullable_types))] - async fn list_param_for_scalar_column_should_not_panic(runner: Runner) -> TestResult<()> { + #[connector_test(schema(common_nullable_types), only(Postgres), exclude(JS))] + async fn list_param_for_scalar_column_should_not_panic_quaint(runner: Runner) -> TestResult<()> { assert_error!( runner, fmt_execute_raw( @@ -48,4 +48,19 @@ mod raw_errors { Ok(()) } + + #[connector_test(schema(common_nullable_types), only(JS, Postgres))] + async fn list_param_for_scalar_column_should_not_panic_pg_js(runner: Runner) -> TestResult<()> { + assert_error!( + runner, + fmt_execute_raw( + r#"INSERT INTO "TestModel" ("id") VALUES ($1);"#, + vec![RawParam::array(vec![1])], + ), + 2010, + r#"invalid input syntax for type integer"# + ); + + Ok(()) + } } diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mod.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mod.rs index d92bb5e96314..8c21dd93f903 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mod.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mod.rs @@ -296,19 +296,33 @@ pub(crate) fn should_run( return false; } - if !only.is_empty() { - return only - .iter() - .any(|only| ConnectorVersion::try_from(*only).unwrap().matches_pattern(&version)); - } - + // We skip tests that exclude JS driver adapters when an external test executor is configured. + // A test that you only want to run with rust drivers can be annotated with exclude(JS) if CONFIG.external_test_executor().is_some() && exclude.iter().any(|excl| excl.0.to_uppercase() == "JS") { println!("Excluded test execution for JS driver adapters. Skipping test"); return false; }; + // we consume the JS token to prevent it from being used in the following checks + let exclude: Vec<_> = exclude.iter().filter(|excl| excl.0.to_uppercase() != "JS").collect(); + + // We only run tests that include JS driver adapters when an external test executor is configured. + // A test that you only want to run with js driver adapters can be annotated with only(JS) + if CONFIG.external_test_executor().is_none() && only.iter().any(|incl| incl.0.to_uppercase() == "JS") { + println!("Excluded test execution for rust driver adapters. Skipping test"); + return false; + } + // we consume the JS token to prevent it from being used in the following checks + let only: Vec<_> = only.iter().filter(|incl| incl.0.to_uppercase() != "JS").collect(); + + if !only.is_empty() { + return only + .iter() + .any(|incl| ConnectorVersion::try_from(**incl).unwrap().matches_pattern(&version)); + } if exclude.iter().any(|excl| { - ConnectorVersion::try_from(*excl).map_or(false, |connector_version| connector_version.matches_pattern(&version)) + ConnectorVersion::try_from(**excl) + .map_or(false, |connector_version| connector_version.matches_pattern(&version)) }) { println!("Connector excluded. Skipping test."); return false; diff --git a/query-engine/driver-adapters/src/conversion.rs b/query-engine/driver-adapters/src/conversion.rs index a26afcf07122..00061d72de44 100644 --- a/query-engine/driver-adapters/src/conversion.rs +++ b/query-engine/driver-adapters/src/conversion.rs @@ -49,7 +49,7 @@ impl ToNapiValue for JSArg { for (index, item) in items.into_iter().enumerate() { let js_value = ToNapiValue::to_napi_value(env.raw(), item)?; // TODO: NapiRaw could be implemented for sys::napi_value directly, there should - // be no need for re-wrapping; submit a patch to napi-rs and simplify here. + // be no need for re-wrapping; submit a patch to napi-rs and simplify here. array.set(index as u32, napi::JsUnknown::from_raw_unchecked(env.raw(), js_value))?; } diff --git a/query-engine/driver-adapters/src/error.rs b/query-engine/driver-adapters/src/error.rs index f2fbb7dd9caf..4f4128088f49 100644 --- a/query-engine/driver-adapters/src/error.rs +++ b/query-engine/driver-adapters/src/error.rs @@ -12,7 +12,7 @@ pub(crate) fn into_quaint_error(napi_err: NapiError) -> QuaintError { QuaintError::raw_connector_error(status, reason) } -/// catches a panic thrown during the executuin of an asynchronous closure and transforms it into +/// catches a panic thrown during the execution of an asynchronous closure and transforms it into /// the Error variant of a napi::Result. pub(crate) async fn async_unwinding_panic(fut: F) -> napi::Result where diff --git a/query-engine/driver-adapters/src/result.rs b/query-engine/driver-adapters/src/result.rs index 53133e037b6f..ad4ce7cbb546 100644 --- a/query-engine/driver-adapters/src/result.rs +++ b/query-engine/driver-adapters/src/result.rs @@ -1,5 +1,5 @@ use napi::{bindgen_prelude::FromNapiValue, Env, JsUnknown, NapiValue}; -use quaint::error::{Error as QuaintError, MysqlError, PostgresError, SqliteError}; +use quaint::error::{Error as QuaintError, ErrorKind, MysqlError, PostgresError, SqliteError}; use serde::Deserialize; #[derive(Deserialize)] @@ -36,7 +36,10 @@ pub(crate) enum DriverAdapterError { GenericJs { id: i32, }, - + UnsupportedNativeDataType { + #[serde(rename = "type")] + native_type: String, + }, Postgres(#[serde(with = "PostgresErrorDef")] PostgresError), Mysql(#[serde(with = "MysqlErrorDef")] MysqlError), Sqlite(#[serde(with = "SqliteErrorDef")] SqliteError), @@ -53,6 +56,12 @@ impl FromNapiValue for DriverAdapterError { impl From for QuaintError { fn from(value: DriverAdapterError) -> Self { match value { + DriverAdapterError::UnsupportedNativeDataType { native_type } => { + QuaintError::builder(ErrorKind::UnsupportedColumnType { + column_type: native_type, + }) + .build() + } DriverAdapterError::GenericJs { id } => QuaintError::external_error(id), DriverAdapterError::Postgres(e) => e.into(), DriverAdapterError::Mysql(e) => e.into(),