diff --git a/.buildkite/engineer b/.buildkite/engineer index 5de99cea539..880db196723 100755 --- a/.buildkite/engineer +++ b/.buildkite/engineer @@ -54,7 +54,7 @@ fi # Check if the system has engineer installed, if not, use a local copy. if ! type "engineer" &> /dev/null; then # Setup Prisma engine build & test tool (engineer). - curl --fail -sSL "https://prisma-engineer.s3-eu-west-1.amazonaws.com/1.60/latest/$OS/engineer.gz" --output engineer.gz + curl --fail -sSL "https://prisma-engineer.s3-eu-west-1.amazonaws.com/1.62/latest/$OS/engineer.gz" --output engineer.gz gzip -d engineer.gz chmod +x engineer diff --git a/.github/workflows/query-engine-driver-adapters.yml b/.github/workflows/query-engine-driver-adapters.yml index b8434e2fa04..3de0238aa0e 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/CODEOWNERS b/CODEOWNERS index c1a996de1f2..cb8fc144133 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @prisma/team-orm-rust +* @prisma/ORM-Rust diff --git a/Cargo.lock b/Cargo.lock index 35eff530999..573e31eabab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,7 @@ dependencies = [ "query-engine-metrics", "query-engine-tests", "query-tests-setup", + "regex", "reqwest", "serde_json", "tokio", @@ -2396,9 +2397,9 @@ dependencies = [ [[package]] name = "mobc" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bdeff49b387edef305eccfe166af3e1483bb57902dbf369dddc42dc824df23b" +checksum = "90eb49dc5d193287ff80e72a86f34cfb27aae562299d22fea215e06ea1059dd3" dependencies = [ "async-trait", "futures-channel", diff --git a/Makefile b/Makefile index a30a32ca187..e00c122e271 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/README.md b/README.md index 49c7c1a8ab3..c28a53a6d65 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,29 @@ GitHub actions will then pick up the branch name and use it to clone that branch When it's time to merge the sibling PRs, you'll need to merge the prisma/prisma PR first, so when merging the engines PR you have the code of the adapters ready in prisma/prisma `main` branch. +### Testing engines in `prisma/prisma` + +You can trigger releases from this repository to npm that can be used for testing the engines in `prisma/prisma` either automatically or manually: + +#### Automated integration releases from this repository to npm + +(Since July 2022). Any branch name starting with `integration/` will, first, run the full test suite in Buildkite `[Test] Prisma Engines` and, second, if passing, run the publish pipeline (build and upload engines to S3 & R2) + +The journey through the pipeline is the same as a commit on the `main` branch. +- It will trigger [`prisma/engines-wrapper`](https://github.com/prisma/engines-wrapper) and publish a new [`@prisma/engines-version`](https://www.npmjs.com/package/@prisma/engines-version) npm package but on the `integration` tag. +- Which triggers [`prisma/prisma`](https://github.com/prisma/prisma) to create a `chore(Automated Integration PR): [...]` PR with a branch name also starting with `integration/` +- Since in `prisma/prisma` we also trigger the publish pipeline when a branch name starts with `integration/`, this will publish all `prisma/prisma` monorepo packages to npm on the `integration` tag. +- Our [ecosystem-tests](https://github.com/prisma/ecosystem-tests/) tests will automatically pick up this new version and run tests, results will show in [GitHub Actions](https://github.com/prisma/ecosystem-tests/actions?query=branch%3Aintegration) + +This end to end will take minimum ~1h20 to complete, but is completely automated :robot: + +Notes: +- in `prisma/prisma` repository, we do not run tests for `integration/` branches, it is much faster and also means that there is no risk of tests failing (e.g. flaky tests, snapshots) that would stop the publishing process. +- in `prisma/prisma-engines` the Buildkite test pipeline must first pass, then the engines will be built and uploaded to our storage via the Buildkite release pipeline. These 2 pipelines can fail for different reasons, it's recommended to keep an eye on them (check notifications in Slack) and restart jobs as needed. Finally, it will trigger [`prisma/engines-wrapper`](https://github.com/prisma/engines-wrapper). + +#### Manual integration releases from this repository to npm + +Additionally to the automated integration release for `integration/` branches, you can also trigger a publish **manually** in the Buildkite `[Test] Prisma Engines` job if that succeeds for _any_ branch name. Click "🚀 Publish binaries" at the bottom of the test list to unlock the publishing step. When all the jobs in `[Release] Prisma Engines` succeed, you also have to unlock the next step by clicking "🚀 Publish client". This will then trigger the same journey as described above. ## Parallel rust-analyzer builds @@ -269,22 +292,25 @@ rust-analyzer. To avoid this. Open VSCode settings and search for `Check on Save --target-dir:/tmp/rust-analyzer-check ``` -### Automated integration releases from this repository to npm -(Since July 2022). Any branch name starting with `integration/` will, first, run the full test suite and, second, if passing, run the publish pipeline (build and upload engines to S3) +## Community PRs: create a local branch for a branch coming from a fork -The journey through the pipeline is the same as a commit on the `main` branch. -- It will trigger [prisma/engines-wrapper](https://github.com/prisma/engines-wrapper) and publish a new [`@prisma/engines-version`](https://www.npmjs.com/package/@prisma/engines-version) npm package but on the `integration` tag. -- Which triggers [prisma/prisma](https://github.com/prisma/prisma) to create a `chore(Automated Integration PR): [...]` PR with a branch name also starting with `integration/` -- Since in prisma/prisma we also trigger the publish pipeline when a branch name starts with `integration/`, this will publish all prisma/prisma monorepo packages to npm on the `integration` tag. -- Our [ecosystem-tests](https://github.com/prisma/ecosystem-tests/) tests will automatically pick up this new version and run tests, results will show in [GitHub Actions](https://github.com/prisma/ecosystem-tests/actions?query=branch%3Aintegration) +To trigger an [Automated integration releases from this repository to npm](#automated-integration-releases-from-this-repository-to-npm) or [Manual integration releases from this repository to npm](#manual-integration-releases-from-this-repository-to-npm) branches of forks need to be pulled into this repository so the Buildkite job is triggered. You can use these GitHub and git CLI commands to achieve that easily: -This end to end will take minimum ~1h20 to complete, but is completely automated :robot: +``` +gh pr checkout 4375 +git checkout -b integration/sql-nested-transactions +git push --set-upstream origin integration/sql-nested-transactions +``` -Notes: -- in prisma/prisma repository, we do not run tests for `integration/` branches, it is much faster and also means that there is no risk of test failing (e.g. flaky tests, snapshots) that would stop the publishing process. -- in prisma/prisma-engines tests must first pass, before publishing starts. So better keep an eye on them and restart them as needed. +If there is a need to re-create this branch because it has been updated, deleting it and re-creating will make sure the content is identical and avoid any conflicts. +``` +git branch --delete integration/sql-nested-transactions +gh pr checkout 4375 +git checkout -b integration/sql-nested-transactions +git push --set-upstream origin integration/sql-nested-transactions --force +``` ## Security diff --git a/docker-compose.yml b/docker-compose.yml index c0d4f179e0a..a8b48748abc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,7 +187,6 @@ services: restart: unless-stopped platform: linux/x86_64 environment: - MYSQL_USER: root MYSQL_ROOT_PASSWORD: prisma MYSQL_DATABASE: prisma ports: diff --git a/psl/psl-core/src/validate/validation_pipeline/validations.rs b/psl/psl-core/src/validate/validation_pipeline/validations.rs index 4040844bb76..90f8ec9fe79 100644 --- a/psl/psl-core/src/validate/validation_pipeline/validations.rs +++ b/psl/psl-core/src/validate/validation_pipeline/validations.rs @@ -123,7 +123,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) { indexes::supports_clustering_setting(index, ctx); indexes::clustering_can_be_defined_only_once(index, ctx); indexes::opclasses_are_not_allowed_with_other_than_normal_indices(index, ctx); - indexes::composite_types_are_not_allowed_in_index(index, ctx); + indexes::composite_type_in_compound_unique_index(index, ctx); for field_attribute in index.scalar_field_attributes() { let span = index.ast_attribute().span; diff --git a/psl/psl-core/src/validate/validation_pipeline/validations/indexes.rs b/psl/psl-core/src/validate/validation_pipeline/validations/indexes.rs index 5f328826401..7a7d0e1d105 100644 --- a/psl/psl-core/src/validate/validation_pipeline/validations/indexes.rs +++ b/psl/psl-core/src/validate/validation_pipeline/validations/indexes.rs @@ -386,20 +386,25 @@ pub(crate) fn opclasses_are_not_allowed_with_other_than_normal_indices(index: In } } -pub(crate) fn composite_types_are_not_allowed_in_index(index: IndexWalker<'_>, ctx: &mut Context<'_>) { - for field in index.fields() { - if field.scalar_field_type().as_composite_type().is_some() { - let message = format!( - "Indexes can only contain scalar attributes. Please remove {:?} from the argument list of the indexes.", - field.name() - ); - ctx.push_error(DatamodelError::new_attribute_validation_error( - &message, - index.attribute_name(), - index.ast_attribute().span, - )); - return; - } +pub(crate) fn composite_type_in_compound_unique_index(index: IndexWalker<'_>, ctx: &mut Context<'_>) { + if !index.is_unique() { + return; + } + + let composite_type = index + .fields() + .find(|f| f.scalar_field_type().as_composite_type().is_some()); + + if index.fields().len() > 1 && composite_type.is_some() { + let message = format!( + "Prisma does not currently support composite types in compound unique indices, please remove {:?} from the index. See https://pris.ly/d/mongodb-composite-compound-indices for more details", + composite_type.unwrap().name() + ); + ctx.push_error(DatamodelError::new_attribute_validation_error( + &message, + index.attribute_name(), + index.ast_attribute().span, + )); } } diff --git a/query-engine/black-box-tests/Cargo.toml b/query-engine/black-box-tests/Cargo.toml index 056ee2bcdb4..cc9e99b8ca3 100644 --- a/query-engine/black-box-tests/Cargo.toml +++ b/query-engine/black-box-tests/Cargo.toml @@ -15,3 +15,4 @@ user-facing-errors.workspace = true insta = "1.7.1" enumflags2 = "0.7" query-engine-metrics = {path = "../metrics"} +regex = "1.9.3" diff --git a/query-engine/black-box-tests/tests/metrics/smoke_tests.rs b/query-engine/black-box-tests/tests/metrics/smoke_tests.rs index 3397de75af9..5ff7ec8ad9b 100644 --- a/query-engine/black-box-tests/tests/metrics/smoke_tests.rs +++ b/query-engine/black-box-tests/tests/metrics/smoke_tests.rs @@ -4,6 +4,7 @@ use query_engine_tests::*; /// Asserts common basics for composite type writes. #[test_suite(schema(schema))] mod smoke_tests { + use regex::Regex; fn schema() -> String { let schema = indoc! { r#"model Person { @@ -14,6 +15,24 @@ mod smoke_tests { schema.to_owned() } + fn assert_value_in_range(metrics: &str, metric: &str, low: f64, high: f64) { + let regex = Regex::new(format!(r"{metric}\s+([+-]?\d+(\.\d+)?)").as_str()).unwrap(); + match regex.captures(&metrics) { + Some(capture) => { + let value = capture.get(1).unwrap().as_str().parse::().unwrap(); + assert!( + value >= low && value <= high, + "expected {} value of {} to be between {} and {}", + metric, + value, + low, + high + ); + } + None => panic!("Metric {} not found in metrics text", metric), + } + } + #[connector_test] #[rustfmt::skip] async fn expected_metrics_rendered(r: Runner) -> TestResult<()> { @@ -62,6 +81,8 @@ mod smoke_tests { // counters assert_eq!(metrics.matches("# HELP prisma_client_queries_total The total number of Prisma Client queries executed").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_client_queries_total counter").count(), 1); + assert_eq!(metrics.matches("prisma_client_queries_total 1").count(), 1); + assert_eq!(metrics.matches("# HELP prisma_datasource_queries_total The total number of datasource queries executed").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_datasource_queries_total counter").count(), 1); @@ -81,13 +102,15 @@ mod smoke_tests { assert_eq!(metrics.matches("# HELP prisma_pool_connections_busy The number of pool connections currently executing datasource queries").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_pool_connections_busy gauge").count(), 1); + assert_value_in_range(&metrics, "prisma_pool_connections_busy", 0f64, 1f64); assert_eq!(metrics.matches("# HELP prisma_pool_connections_idle The number of pool connections that are not busy running a query").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_pool_connections_idle gauge").count(), 1); assert_eq!(metrics.matches("# HELP prisma_pool_connections_open The number of pool connections currently open").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_pool_connections_open gauge").count(), 1); - + assert_value_in_range(&metrics, "prisma_pool_connections_open", 0f64, 1f64); + // histograms assert_eq!(metrics.matches("# HELP prisma_client_queries_duration_histogram_ms The distribution of the time Prisma Client queries took to run end to end").count(), 1); assert_eq!(metrics.matches("# TYPE prisma_client_queries_duration_histogram_ms histogram").count(), 1); 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 62c4e3005f7..a3e45b0a05b 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 9aa34a94356..e45cef8ac30 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 8ea08acc85d..393581b8ad9 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 b0e566ffcb5..974c165ed94 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 581bc21bebe..7b25cfff279 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 c1df015c577..ccf04dd2f4a 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 2fe8af85012..2b4b880b497 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, MySQL(5.6)))] 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 0039b924108..635726c7138 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 88409d8d17f..43417cb352e 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 d92bb5e9631..8c21dd93f90 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/connector-test-kit-executor/src/index.ts b/query-engine/driver-adapters/connector-test-kit-executor/src/index.ts index b89348fb3e7..2318c052576 100644 --- a/query-engine/driver-adapters/connector-test-kit-executor/src/index.ts +++ b/query-engine/driver-adapters/connector-test-kit-executor/src/index.ts @@ -18,7 +18,7 @@ import { createClient } from '@libsql/client' import { PrismaLibSQL } from '@prisma/adapter-libsql' // planetscale dependencies -import { connect as planetscaleConnect } from '@planetscale/database' +import { Client as PlanetscaleClient } from '@planetscale/database' import { PrismaPlanetScale } from '@prisma/adapter-planetscale' @@ -276,12 +276,12 @@ async function planetscaleAdapter(url: string): Promise { throw new Error("DRIVER_ADAPTER_CONFIG is not defined or empty, but its required for planetscale adapter."); } - const connection = planetscaleConnect({ + const client = new PlanetscaleClient({ url: proxyURL, fetch, }) - return new PrismaPlanetScale(connection) + return new PrismaPlanetScale(client) } main().catch(err) diff --git a/query-engine/driver-adapters/src/conversion.rs b/query-engine/driver-adapters/src/conversion.rs index a26afcf0712..00061d72de4 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 f2fbb7dd9ca..4f4128088f4 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/proxy.rs b/query-engine/driver-adapters/src/proxy.rs index 62086a24519..a708d75c0e3 100644 --- a/query-engine/driver-adapters/src/proxy.rs +++ b/query-engine/driver-adapters/src/proxy.rs @@ -249,6 +249,12 @@ fn js_value_to_quaint( column_type: ColumnType, column_name: &str, ) -> quaint::Result> { + let parse_number_as_i64 = |n: &serde_json::Number| { + n.as_i64().ok_or(conversion_error!( + "number must be an integer in column '{column_name}', got '{n}'" + )) + }; + // Note for the future: it may be worth revisiting how much bloat so many panics with different static // strings add to the compiled artefact, and in case we should come up with a restricted set of panic // messages, or even find a way of removing them altogether. @@ -256,8 +262,7 @@ fn js_value_to_quaint( ColumnType::Int32 => match json_value { serde_json::Value::Number(n) => { // n.as_i32() is not implemented, so we need to downcast from i64 instead - n.as_i64() - .ok_or(conversion_error!("number must be an integer in column '{column_name}'")) + parse_number_as_i64(&n) .and_then(|n| -> quaint::Result { n.try_into() .map_err(|e| conversion_error!("cannot convert {n} to i32 in column '{column_name}': {e}")) @@ -273,9 +278,7 @@ fn js_value_to_quaint( )), }, ColumnType::Int64 => match json_value { - serde_json::Value::Number(n) => n.as_i64().map(QuaintValue::int64).ok_or(conversion_error!( - "number must be an i64 in column '{column_name}', got {n}" - )), + serde_json::Value::Number(n) => parse_number_as_i64(&n).map(QuaintValue::int64), serde_json::Value::String(s) => s.parse::().map(QuaintValue::int64).map_err(|e| { conversion_error!("string-encoded number must be an i64 in column '{column_name}', got {s}: {e}") }), diff --git a/query-engine/driver-adapters/src/result.rs b/query-engine/driver-adapters/src/result.rs index 53133e037b6..ad4ce7cbb54 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(), diff --git a/query-engine/prisma-models/src/field/scalar.rs b/query-engine/prisma-models/src/field/scalar.rs index 92039da5366..b8ef8ab204e 100644 --- a/query-engine/prisma-models/src/field/scalar.rs +++ b/query-engine/prisma-models/src/field/scalar.rs @@ -91,7 +91,7 @@ impl ScalarField { match scalar_field_type { ScalarFieldType::CompositeType(_) => { - unreachable!("Cannot convert a composite type to a type identifier. This error is typically caused by mistakenly using a composite type within a composite index.",) + unreachable!("This shouldn't be reached; composite types are not supported in compound unique indices.",) } ScalarFieldType::Enum(x) => TypeIdentifier::Enum(x), ScalarFieldType::BuiltInScalar(scalar) => scalar.into(), diff --git a/query-engine/prisma-models/tests/datamodel_converter_tests.rs b/query-engine/prisma-models/tests/datamodel_converter_tests.rs index 0a45c80ed16..a2ee28ca6c0 100644 --- a/query-engine/prisma-models/tests/datamodel_converter_tests.rs +++ b/query-engine/prisma-models/tests/datamodel_converter_tests.rs @@ -38,31 +38,159 @@ fn converting_enums() { } } +// region: composite #[test] -fn converting_composite_types() { +fn converting_composite_types_compound() { let res = psl::parse_schema( r#" - datasource db { - provider = "mongodb" - url = "mongodb://localhost:27017/hello" - } + datasource db { + provider = "mongodb" + url = "mongodb://localhost:27017/hello" + } - model MyModel { - id String @id @default(auto()) @map("_id") @db.ObjectId - attribute Attribute + model Post { + id String @id @default(auto()) @map("_id") @db.ObjectId + author User @relation(fields: [authorId], references: [id]) + authorId String @db.ObjectId + attributes Attribute[] + + @@index([authorId, attributes]) + } + + type Attribute { + name String + value String + group String + } + + model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + Post Post[] + } + "#, + ); - @@unique([attribute], name: "composite_index") - } + assert!(res.is_ok()); +} - type Attribute { - name String - value String - group String - } +#[test] +fn converting_composite_types_compound_unique() { + let res = psl::parse_schema( + r#" + datasource db { + provider = "mongodb" + url = "mongodb://localhost:27017/hello" + } + + model Post { + id String @id @default(auto()) @map("_id") @db.ObjectId + author User @relation(fields: [authorId], references: [id]) + authorId String @db.ObjectId + attributes Attribute[] + + @@unique([authorId, attributes]) + // ^^^^^^^^^^^^^^^^^^^^^^ + // Prisma does not currently support composite types in compound unique indices... + } + + type Attribute { + name String + value String + group String + } + + model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + Post Post[] + } "#, ); - assert!(res.unwrap_err().contains("Indexes can only contain scalar attributes. Please remove \"attribute\" from the argument list of the indexes.")); + + assert!(res + .unwrap_err() + .contains(r#"Prisma does not currently support composite types in compound unique indices, please remove "attributes" from the index. See https://pris.ly/d/mongodb-composite-compound-indices for more details"#)); +} + +#[test] +fn converting_composite_types_nested() { + let res = psl::parse_schema( + r#" + datasource db { + provider = "mongodb" + url = "mongodb://localhost:27017/hello" + } + + type TheatersLocation { + address TheatersLocationAddress + geo TheatersLocationGeo + } + + type TheatersLocationAddress { + city String + state String + street1 String + street2 String? + zipcode String + } + + type TheatersLocationGeo { + coordinates Float[] + type String + } + + model theaters { + id String @id @default(auto()) @map("_id") @db.ObjectId + location TheatersLocation + theaterId Int + + @@index([location.geo], map: "geo index") + } + "#, + ); + + assert!(res.is_ok()); +} + +#[test] +fn converting_composite_types_nested_scalar() { + let res = psl::parse_schema( + r#" + datasource db { + provider = "mongodb" + url = "mongodb://localhost:27017/hello" + } + + type TheatersLocation { + address TheatersLocationAddress + geo TheatersLocationGeo + } + + type TheatersLocationAddress { + city String + state String + street1 String + street2 String? + zipcode String + } + + type TheatersLocationGeo { + coordinates Float[] + type String + } + + model theaters { + id String @id @default(auto()) @map("_id") @db.ObjectId + location TheatersLocation + theaterId Int + + @@index([location.geo.type], map: "geo index") + } + "#, + ); + + assert!(res.is_ok()); } +// endregion #[test] fn models_with_only_scalar_fields() {