diff --git a/.prettierrc.toml b/.prettierrc.toml new file mode 100644 index 000000000000..addd6d363c2e --- /dev/null +++ b/.prettierrc.toml @@ -0,0 +1,3 @@ +tabWidth = 2 +semi = false +singleQuote = true diff --git a/Makefile b/Makefile index 0f5e60381f9f..2ebe7deefc73 100644 --- a/Makefile +++ b/Makefile @@ -228,6 +228,11 @@ dev-pg-wasm: start-pg-js build-qe-wasm build-driver-adapters-kit test-pg-wasm: dev-pg-wasm test-qe-st +dev-pg-qc: start-pg-js build-qc-wasm build-driver-adapters-kit + cp $(CONFIG_PATH)/pg-qc $(CONFIG_FILE) + +test-pg-qc: dev-pg-qc test-qe-st + test-driver-adapter-pg: test-pg-js test-driver-adapter-pg-wasm: test-pg-wasm diff --git a/libs/driver-adapters/executor/package.json b/libs/driver-adapters/executor/package.json index 334f5dbc1c88..f8f9e2f18300 100644 --- a/libs/driver-adapters/executor/package.json +++ b/libs/driver-adapters/executor/package.json @@ -9,8 +9,9 @@ "description": "", "private": true, "scripts": { - "build": "tsup ./src/testd-qe.ts ./src/demo-se.ts ./src/bench.ts --format esm --dts", + "build": "tsup ./src/testd-qe.ts ./src/testd-qc.ts ./src/demo-se.ts ./src/bench.ts --format esm --dts", "test:qe": "node --import tsx ./src/testd-qe.ts", + "test:qc": "node --import tsx ./src/testd-qc.ts", "demo:se": "node --import tsx ./src/demo-se.ts", "demo:qc": "node --import tsx ./src/demo-qc.ts", "clean:d1": "rm -rf ../../connector-test-kit-rs/query-engine-tests/.wrangler" @@ -27,6 +28,7 @@ "@prisma/adapter-pg": "workspace:*", "@prisma/adapter-planetscale": "workspace:*", "@prisma/bundled-js-drivers": "workspace:*", + "@prisma/client-engine-runtime": "workspace:*", "@prisma/driver-adapter-utils": "workspace:*", "mitata": "0.1.11", "query-engine-wasm-baseline": "npm:@prisma/query-engine-wasm@0.0.19", diff --git a/libs/driver-adapters/executor/script/testd-qc.sh b/libs/driver-adapters/executor/script/testd-qc.sh new file mode 100755 index 000000000000..b6c0fdae9de5 --- /dev/null +++ b/libs/driver-adapters/executor/script/testd-qc.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +node "$(dirname "${BASH_SOURCE[0]}")/../dist/testd-qc.js" diff --git a/libs/driver-adapters/executor/src/demo-qc.ts b/libs/driver-adapters/executor/src/demo-qc.ts index eeffe35a4c25..514d34decd46 100644 --- a/libs/driver-adapters/executor/src/demo-qc.ts +++ b/libs/driver-adapters/executor/src/demo-qc.ts @@ -101,7 +101,7 @@ async function initQC({ const compiler = await qc.initQueryCompiler({ datamodel: schema, - flavour: adapter.provider, + provider: adapter.provider, connectionInfo, }); diff --git a/libs/driver-adapters/executor/src/panic.ts b/libs/driver-adapters/executor/src/panic.ts new file mode 100644 index 000000000000..19281d1d435d --- /dev/null +++ b/libs/driver-adapters/executor/src/panic.ts @@ -0,0 +1,13 @@ +type GlobalWithPanicHandler = typeof globalThis & { + PRISMA_WASM_PANIC_REGISTRY: { + set_message: (message: string) => void + } +} + +export function setupPanicHandler() { + ;(globalThis as GlobalWithPanicHandler).PRISMA_WASM_PANIC_REGISTRY = { + set_message(message: string) { + throw new Error('Panic in WASM module: ' + message) + }, + } +} diff --git a/libs/driver-adapters/executor/src/query-compiler.ts b/libs/driver-adapters/executor/src/query-compiler.ts index ee752b9cbcfa..dc06bdb63715 100644 --- a/libs/driver-adapters/executor/src/query-compiler.ts +++ b/libs/driver-adapters/executor/src/query-compiler.ts @@ -1,24 +1,24 @@ -import { ConnectionInfo } from "@prisma/driver-adapter-utils"; -import { __dirname } from "./utils"; +import { ConnectionInfo } from '@prisma/driver-adapter-utils' +import { __dirname } from './utils' export type QueryCompilerParams = { // TODO: support multiple datamodels - datamodel: string; - flavour: 'postgres' | 'mysql' | 'sqlite'; - connectionInfo: ConnectionInfo; -}; + datamodel: string + provider: 'postgres' | 'mysql' | 'sqlite' + connectionInfo: ConnectionInfo +} export interface QueryCompiler { - new (params: QueryCompilerParams): QueryCompiler; - compile(query: string): Promise; + new (params: QueryCompilerParams): QueryCompiler + compile(query: string): string } export async function initQueryCompiler( params: QueryCompilerParams, ): Promise { - const { getQueryCompilerForProvider } = await import("./query-compiler-wasm"); + const { getQueryCompilerForProvider } = await import('./query-compiler-wasm') const WasmQueryCompiler = (await getQueryCompilerForProvider( - params.flavour, - )) as QueryCompiler; - return new WasmQueryCompiler(params); + params.provider, + )) as QueryCompiler + return new WasmQueryCompiler(params) } diff --git a/libs/driver-adapters/executor/src/testd-qc.ts b/libs/driver-adapters/executor/src/testd-qc.ts new file mode 100644 index 000000000000..ce8340da1371 --- /dev/null +++ b/libs/driver-adapters/executor/src/testd-qc.ts @@ -0,0 +1,384 @@ +import * as readline from 'node:readline' +import * as util from 'node:util' +import * as S from '@effect/schema/Schema' +import { + ConnectionInfo, + DriverAdapter, + Queryable, +} from '@prisma/driver-adapter-utils' +import { + QueryInterpreter, + TransactionManager, + IsolationLevel, +} from '@prisma/client-engine-runtime' + +import type { DriverAdaptersManager } from './driver-adapters-manager' +import { Env, jsonRpc } from './types' +import * as qc from './query-compiler' +import { assertNever, debug, err } from './utils' +import { setupDriverAdaptersManager } from './setup' +import { SchemaId, JsonProtocolQuery } from './types/jsonRpc' +import { setupPanicHandler } from './panic' + +async function main(): Promise { + const env = S.decodeUnknownSync(Env)(process.env) + console.log('[env]', env) + + setupPanicHandler() + + const iface = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }) + + iface.on('line', async (line) => { + try { + const request = S.decodeSync(jsonRpc.RequestFromString)(line) + debug(`Got a request: ${line}`) + + try { + const response = await handleRequest(request, env) + respondOk(request.id, response) + } catch (err) { + debug('[nodejs] Error from request handler: ', err) + respondErr(request.id, { + code: 1, + message: err.stack ?? err.toString(), + }) + } + } catch (err) { + debug('Received non-json line: ', line) + console.error(err) + } + }) +} + +const state: Record< + SchemaId, + { + compiler: qc.QueryCompiler + driverAdapterManager: DriverAdaptersManager + adapter: DriverAdapter + transactionManager: TransactionManager + logs: string[] + } +> = {} + +async function handleRequest( + { method, params }: jsonRpc.Request, + env: Env, +): Promise { + if (method !== 'initializeSchema') { + if (state[params.schemaId] === undefined) { + throw new Error( + `Schema with id ${params.schemaId} is not initialized. Please call 'initializeSchema' first.`, + ) + } + } + + switch (method) { + case 'initializeSchema': { + debug('Got `initializeSchema', params) + + const { url, schema, schemaId, migrationScript } = params + + const driverAdapterManager = await setupDriverAdaptersManager( + env, + migrationScript, + ) + + const { compiler, adapter } = await initQc({ + url, + driverAdapterManager, + schema, + }) + + const transactionManager = new TransactionManager({ + driverAdapter: adapter, + clientVersion: '0.0.0', + }) + + state[schemaId] = { + compiler, + driverAdapterManager, + adapter, + transactionManager, + logs: [], + } + + if (adapter.getConnectionInfo) { + const connectionInfoResult = adapter.getConnectionInfo() + if (connectionInfoResult.ok) { + return connectionInfoResult.value + } + } + + return { maxBindValues: null } + } + + case 'query': { + debug('Got `query`', util.inspect(params, false, null, true)) + + const { query, schemaId, txId } = params + const { compiler, adapter, transactionManager, logs } = state[schemaId] + + const executeQuery = async ( + queryable: Queryable, + query: JsonProtocolQuery, + ) => { + const queryPlanString = compiler.compile(JSON.stringify(query)) + const queryPlan = JSON.parse(queryPlanString) + + debug('🟢 Query plan: ', util.inspect(queryPlan, false, null, true)) + + const interpreter = new QueryInterpreter({ + queryable, + placeholderValues: {}, + onQuery: (event) => { + logs.push(JSON.stringify(event)) + }, + }) + + return interpreter.run(queryPlan) + } + + const executeIndependentBatch = async ( + queries: readonly JsonProtocolQuery[], + ) => Promise.all(queries.map((query) => executeQuery(adapter, query))) + + const executeTransactionalBatch = async ( + queries: readonly JsonProtocolQuery[], + isolationLevel?: IsolationLevel, + ) => { + const txInfo = await transactionManager.startTransaction({ + isolationLevel, + }) + const queryable = transactionManager.getTransaction( + txInfo, + 'batch transaction query', + ) + + try { + const results: unknown[] = [] + + for (const query of queries) { + const result = await executeQuery(queryable, query) + results.push(result) + } + + await transactionManager.commitTransaction(txInfo.id) + + return results + } catch (err) { + await transactionManager + .rollbackTransaction(txInfo.id) + .catch(console.error) + throw err + } + } + + if ('batch' in query) { + const { batch, transaction } = query + + const results = transaction + ? await executeTransactionalBatch( + batch, + parseIsolationLevel(transaction.isolationLevel), + ) + : await executeIndependentBatch(batch) + + debug('🟢 Batch query results: ', results) + + return JSON.stringify({ + batchResult: batch.map((query, index) => + getResponseInQeFormat(query, results[index]), + ), + }) + } else { + const queryable = txId + ? transactionManager.getTransaction( + { id: txId, payload: undefined }, + query.action, + ) + : adapter + + if (!queryable) { + throw new Error( + `No transaction with id ${txId} found. Please call 'startTx' first.`, + ) + } + + const result = await executeQuery(queryable, query) + + debug('🟢 Query result: ', result) + + return JSON.stringify(getResponseInQeFormat(query, result)) + } + } + + case 'startTx': { + debug('Got `startTx`', params) + + const { schemaId, options } = params + const { transactionManager } = state[schemaId] + + return await transactionManager.startTransaction({ + maxWait: options.max_wait, + timeout: options.timeout, + isolationLevel: parseIsolationLevel(options.isolation_level), + }) + } + + case 'commitTx': { + debug('Got `commitTx`', params) + + const { schemaId, txId } = params + const { transactionManager } = state[schemaId] + + return await transactionManager.commitTransaction(txId) + } + + case 'rollbackTx': { + debug('Got `rollbackTx`', params) + + const { schemaId, txId } = params + const { transactionManager } = state[schemaId] + + return await transactionManager.rollbackTransaction(txId) + } + + case 'teardown': { + debug('Got `teardown`', params) + + const { schemaId } = params + const { driverAdapterManager } = state[schemaId] + + await driverAdapterManager.teardown() + delete state[schemaId] + + return {} + } + + case 'getLogs': { + const { schemaId } = params + return state[schemaId].logs + } + + default: { + assertNever(method, `Unknown method: \`${method}\``) + } + } +} + +function respondErr(requestId: number, error: jsonRpc.RpcError) { + const msg: jsonRpc.ErrResponse = { + jsonrpc: '2.0', + id: requestId, + error, + } + console.log(JSON.stringify(msg)) +} + +function respondOk(requestId: number, payload: unknown) { + const msg: jsonRpc.OkResponse = { + jsonrpc: '2.0', + id: requestId, + result: payload, + } + console.log(JSON.stringify(msg)) +} + +type InitQueryCompilerParams = { + driverAdapterManager: DriverAdaptersManager + url: string + schema: string +} + +async function initQc({ + driverAdapterManager, + url, + schema, +}: InitQueryCompilerParams) { + const adapter = await driverAdapterManager.connect({ url }) + + let connectionInfo: ConnectionInfo = {} + if (adapter.getConnectionInfo) { + const result = adapter.getConnectionInfo() + if (!result.ok) { + throw result.error + } + connectionInfo = result.value + } + + const compiler = await qc.initQueryCompiler({ + datamodel: schema, + provider: adapter.provider, + connectionInfo, + }) + + return { + compiler, + adapter, + } +} + +function parseIsolationLevel( + level: string | null | undefined, +): IsolationLevel | undefined { + if (level == null) { + return undefined + } + + switch (level.toLowerCase()) { + case 'readcommitted': + case 'read committed': + return IsolationLevel.ReadCommitted + + case 'readuncommitted': + case 'read uncommitted': + return IsolationLevel.ReadUncommitted + + case 'repeatableread': + case 'repeatable read': + return IsolationLevel.RepeatableRead + + case 'serializable': + return IsolationLevel.Serializable + + case 'snapshot': + return IsolationLevel.Snapshot + + default: + // We don't validate the isolation level on the RPC schema level because some tests + // rely on sending invalid isolation levels to test error handling, and those invalid + // levels must be forwarded to the query engine as-is in `testd-qe.ts`. + throw new Error(`Unknown isolation level: ${level}`) + } +} + +function getResponseInQeFormat(query: JsonProtocolQuery, result: unknown) { + return { + data: { + [getFullOperationName(query)]: getOperationResultInQeFormat(result), + }, + } +} + +function getFullOperationName(query: JsonProtocolQuery): string { + if (query.modelName) { + return query.action + query.modelName + } else { + return query.action + } +} + +function getOperationResultInQeFormat(result: unknown) { + if (typeof result === 'number') { + return { count: result } + } else { + return result + } +} + +main().catch(err) diff --git a/libs/driver-adapters/executor/src/testd-qe.ts b/libs/driver-adapters/executor/src/testd-qe.ts index 3095495b558a..12499cd29f66 100644 --- a/libs/driver-adapters/executor/src/testd-qe.ts +++ b/libs/driver-adapters/executor/src/testd-qe.ts @@ -7,14 +7,17 @@ import { Env, jsonRpc } from './types' import * as qe from './query-engine' import { nextRequestId } from './requestId' import { createRNEngineConnector } from './rn' -import { debug, err } from './utils' +import { assertNever, debug, err } from './utils' import { setupDriverAdaptersManager } from './setup' import { SchemaId } from './types/jsonRpc' +import { setupPanicHandler } from './panic' async function main(): Promise { const env = S.decodeUnknownSync(Env)(process.env) console.log('[env]', env) + setupPanicHandler() + const iface = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -179,7 +182,7 @@ async function handleRequest( return state[schemaId].logs } default: { - throw new Error(`Unknown method: \`${method}\``) + assertNever(method, `Unknown method: \`${method}\``) } } } @@ -224,6 +227,10 @@ async function initQe({ return { engine, adapter: null } } + if (env.EXTERNAL_TEST_EXECUTOR === 'QueryCompiler') { + throw new Error('query compiler tests must be run using testd-qc.ts') + } + const adapter = await driverAdapterManager.connect({ url }) const errorCapturingAdapter = bindAdapter(adapter) const engineInstance = await qe.initQueryEngine( diff --git a/libs/driver-adapters/executor/src/types/env.ts b/libs/driver-adapters/executor/src/types/env.ts index b15bf092b47d..8070058f9c41 100644 --- a/libs/driver-adapters/executor/src/types/env.ts +++ b/libs/driver-adapters/executor/src/types/env.ts @@ -30,7 +30,7 @@ export const MobileAdapterConfig = S.struct({ })), }) -export const ExternalTestExecutor = S.literal('Wasm', 'Napi') +export const ExternalTestExecutor = S.literal('Wasm', 'Napi', 'QueryCompiler') export const Env = S.extend( S.union( diff --git a/libs/driver-adapters/executor/src/types/jsonRpc.ts b/libs/driver-adapters/executor/src/types/jsonRpc.ts index 194150211d84..6c2380772269 100644 --- a/libs/driver-adapters/executor/src/types/jsonRpc.ts +++ b/libs/driver-adapters/executor/src/types/jsonRpc.ts @@ -1,6 +1,8 @@ import * as S from '@effect/schema/Schema' -const SchemaId = S.number.pipe(S.int(), S.nonNegative()).pipe(S.brand('SchemaId')) +const SchemaId = S.number + .pipe(S.int(), S.nonNegative()) + .pipe(S.brand('SchemaId')) export type SchemaId = S.Schema.Type const InitializeSchemaParams = S.struct({ @@ -9,16 +11,58 @@ const InitializeSchemaParams = S.struct({ url: S.string, migrationScript: S.optional(S.string), }) -export type InitializeSchemaParams = S.Schema.Type +export type InitializeSchemaParams = S.Schema.Type< + typeof InitializeSchemaParams +> const InitializeSchema = S.struct({ method: S.literal('initializeSchema'), params: InitializeSchemaParams, }) +const JsonProtocolQuery = S.struct({ + modelName: S.optional(S.nullable(S.string)), + action: S.union( + S.literal('findUnique'), + S.literal('findUniqueOrThrow'), + S.literal('findFirst'), + S.literal('findFirstOrThrow'), + S.literal('findMany'), + S.literal('createOne'), + S.literal('createMany'), + S.literal('createManyAndReturn'), + S.literal('updateOne'), + S.literal('updateMany'), + S.literal('updateManyAndReturn'), + S.literal('deleteOne'), + S.literal('deleteMany'), + S.literal('upsertOne'), + S.literal('aggregate'), + S.literal('groupBy'), + S.literal('executeRaw'), + S.literal('queryRaw'), + S.literal('runCommandRaw'), + S.literal('findRaw'), + S.literal('aggregateRaw'), + ), + query: S.record(S.string, S.unknown), +}) +export type JsonProtocolQuery = S.Schema.Type + +const JsonProtocolBatchQuery = S.struct({ + batch: S.array(JsonProtocolQuery), + transaction: S.optional( + S.nullable( + S.struct({ + isolationLevel: S.optional(S.nullable(S.string)), + }), + ), + ), +}) + const QueryParams = S.struct({ schemaId: SchemaId, - query: S.record(S.string, S.unknown), + query: S.union(JsonProtocolQuery, JsonProtocolBatchQuery), txId: S.nullable(S.string), }) export type QueryParams = S.Schema.Type @@ -28,9 +72,15 @@ const Query = S.struct({ params: QueryParams, }) +const TxOptions = S.struct({ + max_wait: S.number.pipe(S.int(), S.nonNegative()), + timeout: S.number.pipe(S.int(), S.nonNegative()), + isolation_level: S.optional(S.nullable(S.string)), +}) + const StartTxParams = S.struct({ schemaId: SchemaId, - options: S.unknown, + options: TxOptions, }) export type StartTxParams = S.Schema.Type @@ -110,21 +160,21 @@ export type RequestFromString = S.Schema.Type export type Response = OkResponse | ErrResponse export interface OkResponse { - jsonrpc: '2.0' - result: unknown - error?: never - id: number + jsonrpc: '2.0' + result: unknown + error?: never + id: number } export interface ErrResponse { - jsonrpc: '2.0' - error: RpcError - result?: never - id: number + jsonrpc: '2.0' + error: RpcError + result?: never + id: number } export interface RpcError { - code: number - message: string - data?: unknown + code: number + message: string + data?: unknown } diff --git a/libs/driver-adapters/executor/src/utils.ts b/libs/driver-adapters/executor/src/utils.ts index ecd1ca6ac40d..d80a37e705bd 100644 --- a/libs/driver-adapters/executor/src/utils.ts +++ b/libs/driver-adapters/executor/src/utils.ts @@ -63,3 +63,7 @@ export const debug = (() => { // error logger export const err = (...args: any[]) => console.error('[nodejs] ERROR:', ...args) + +export function assertNever(_: never, message: string): never { + throw new Error(message) +} diff --git a/libs/driver-adapters/pnpm-workspace.yaml b/libs/driver-adapters/pnpm-workspace.yaml index c12624bc6a17..1e5c43156d25 100644 --- a/libs/driver-adapters/pnpm-workspace.yaml +++ b/libs/driver-adapters/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - '../../../prisma/packages/adapter-planetscale' - '../../../prisma/packages/adapter-pg' - '../../../prisma/packages/bundled-js-drivers' + - '../../../prisma/packages/client-engine-runtime' - '../../../prisma/packages/debug' - '../../../prisma/packages/driver-adapter-utils' - './executor' diff --git a/query-compiler/query-engine-tests-todo/pg/fail b/query-compiler/query-engine-tests-todo/pg/fail new file mode 100644 index 000000000000..6d4977fa4593 --- /dev/null +++ b/query-compiler/query-engine-tests-todo/pg/fail @@ -0,0 +1,10 @@ +new::disconnect::disconnect_security::must_honor_connect_scope_m2m +new::disconnect::disconnect_security::must_honor_connect_scope_one2m +new::interactive_tx::interactive_tx::basic_commit_workflow +new::interactive_tx::interactive_tx::basic_rollback_workflow +new::interactive_tx::interactive_tx::batch_queries_failure +new::interactive_tx::interactive_tx::no_auto_rollback +new::interactive_tx::interactive_tx::raw_queries +new::interactive_tx::interactive_tx::tx_expiration_cycle +new::interactive_tx::interactive_tx::tx_expiration_failure_cycle +new::interactive_tx::itx_isolation::invalid_isolation diff --git a/query-compiler/query-engine-tests-todo/pg/timeout b/query-compiler/query-engine-tests-todo/pg/timeout new file mode 100644 index 000000000000..f676599a4776 --- /dev/null +++ b/query-compiler/query-engine-tests-todo/pg/timeout @@ -0,0 +1,12 @@ +new::interactive_tx::interactive_tx::batch_queries_rollback +new::interactive_tx::interactive_tx::batch_queries_success +new::interactive_tx::interactive_tx::commit_after_rollback +new::interactive_tx::interactive_tx::double_commit +new::interactive_tx::interactive_tx::double_rollback +new::interactive_tx::interactive_tx::multiple_tx +new::interactive_tx::interactive_tx::rollback_after_commit +new::interactive_tx::interactive_tx::write_conflict +new::interactive_tx::itx_isolation::basic_serializable +new::interactive_tx::itx_isolation::casing_doesnt_matter +new::interactive_tx::itx_isolation::high_concurrency +new::interactive_tx::itx_isolation::spacing_doesnt_matter diff --git a/query-engine/connector-test-kit-rs/query-test-macros/build.rs b/query-engine/connector-test-kit-rs/query-test-macros/build.rs new file mode 100644 index 000000000000..3224d0268350 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-test-macros/build.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-env-changed=WORKSPACE_ROOT"); + println!("cargo:rerun-if-env-changed=SHOULD_PANIC_TESTS"); + println!("cargo:rerun-if-env-changed=IGNORED_TESTS"); + + let workspace_root = env!("WORKSPACE_ROOT"); + + if let Some(should_panic_tests) = option_env!("SHOULD_PANIC_TESTS") { + let path = PathBuf::from(workspace_root).join(should_panic_tests); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } + + if let Some(ignored_tests) = option_env!("IGNORED_TESTS") { + let path = PathBuf::from(workspace_root).join(ignored_tests); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } +} diff --git a/query-engine/connector-test-kit-rs/query-test-macros/src/connector_test.rs b/query-engine/connector-test-kit-rs/query-test-macros/src/connector_test.rs index d0058dcb728b..8d75781da612 100644 --- a/query-engine/connector-test-kit-rs/query-test-macros/src/connector_test.rs +++ b/query-engine/connector-test-kit-rs/query-test-macros/src/connector_test.rs @@ -1,3 +1,9 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; + use super::*; use darling::FromMeta; use proc_macro::TokenStream; @@ -63,10 +69,13 @@ pub fn connector_test_impl(attr: TokenStream, input: TokenStream) -> TokenStream None => quote! { None }, }; + let test_attrs = skip_or_ignore_attr(&test_name, &suite_name); + // The actual test is a shell function that gets the name of the original function, // which is then calling `{orig_name}_run` in the end (see `runner_fn_ident`). let test = quote! { #[test] + #test_attrs fn #test_fn_ident() { query_tests_setup::run_connector_test( #test_database_name, @@ -87,3 +96,45 @@ pub fn connector_test_impl(attr: TokenStream, input: TokenStream) -> TokenStream test.into() } + +fn skip_or_ignore_attr(test_name: &str, suite_name: &str) -> proc_macro2::TokenStream { + option_env!("IGNORED_TESTS") + .and_then(|ignored_tests| conditional_attr(quote! { #[ignore] }, ignored_tests, test_name, suite_name)) + .or_else(|| { + option_env!("SHOULD_PANIC_TESTS").and_then(|should_panic_tests| { + conditional_attr(quote! { #[should_panic] }, should_panic_tests, test_name, suite_name) + }) + }) + .unwrap_or(quote!()) +} + +fn conditional_attr( + attr: proc_macro2::TokenStream, + tests_list_path: &str, + test_name: &str, + suite_name: &str, +) -> Option { + let path = PathBuf::from(env!("WORKSPACE_ROOT")).join(tests_list_path); + let file = File::open(path).expect("could not open file"); + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = line.expect("could not read line"); + + // Support commenting-out tests with `#` in the ignore files. + if line.starts_with('#') { + continue; + } + + // We write the full test names in the ignore file to future-proof it in case we might need + // to find a different way to check the test names if the current approach leads to + // collisions. However, currently we only check the last two parts of the path (test suite + // name and test name itself) because we don't have easy access to the full path including + // parent modules here. + if line.ends_with(&format!("{suite_name}::{test_name}")) { + return Some(attr); + } + } + + None +} diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs index 9b2b1db84892..2112a2adfdae 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs @@ -15,6 +15,7 @@ pub enum TestExecutor { Napi, Wasm, Mobile, + QueryCompiler, } impl Display for TestExecutor { @@ -23,6 +24,7 @@ impl Display for TestExecutor { TestExecutor::Napi => f.write_str("Napi"), TestExecutor::Wasm => f.write_str("Wasm"), TestExecutor::Mobile => f.write_str("Mobile"), + TestExecutor::QueryCompiler => f.write_str("QueryCompiler"), } } } @@ -365,16 +367,27 @@ impl TestConfig { } pub fn external_test_executor_path(&self) -> Option { - const DEFAULT_TEST_EXECUTOR: &str = "libs/driver-adapters/executor/script/testd-qe.sh"; + const TEST_EXECUTOR_ROOT_PATH: &str = "libs/driver-adapters/executor/script"; + const QUERY_ENGINE_TEST_EXECUTOR: &str = "testd-qe.sh"; + const QUERY_COMPILER_TEST_EXECUTOR: &str = "testd-qc.sh"; + self.with_driver_adapter() - .and_then(|_| { - Self::workspace_root().or_else(|| { - exit_with_message( - "WORKSPACE_ROOT needs to be correctly set to the root of the prisma-engines repository", - ) - }) + .and_then(|da| { + Self::workspace_root() + .or_else(|| { + exit_with_message( + "WORKSPACE_ROOT needs to be correctly set to the root of the prisma-engines repository", + ) + }) + .map(|path| path.join(TEST_EXECUTOR_ROOT_PATH)) + .map(|path| { + path.join(if da.test_executor == TestExecutor::QueryCompiler { + QUERY_COMPILER_TEST_EXECUTOR + } else { + QUERY_ENGINE_TEST_EXECUTOR + }) + }) }) - .map(|path| path.join(DEFAULT_TEST_EXECUTOR)) .and_then(|path| path.to_str().map(|s| s.to_owned())) } diff --git a/query-engine/connector-test-kit-rs/test-configs/pg-qc b/query-engine/connector-test-kit-rs/test-configs/pg-qc new file mode 100644 index 000000000000..ac3ed6d23ef6 --- /dev/null +++ b/query-engine/connector-test-kit-rs/test-configs/pg-qc @@ -0,0 +1,6 @@ +{ + "connector": "postgres", + "version": "pg.js", + "driver_adapter": "pg", + "external_test_executor": "QueryCompiler" +} diff --git a/shell.nix b/shell.nix index ac1931e79a3b..2200a7b293c5 100644 --- a/shell.nix +++ b/shell.nix @@ -2,14 +2,6 @@ pkgs ? import { }, }: -let - wasm-bindgen-cli = pkgs.wasm-bindgen-cli.override { - version = "0.2.93"; - hash = "sha256-DDdu5mM3gneraM85pAepBXWn3TMofarVR4NbjMdz3r0="; - cargoHash = "sha256-birrg+XABBHHKJxfTKAMSlmTVYLmnmqMDfRnmG6g/YQ="; - }; - -in pkgs.mkShell { packages = with pkgs; [ binaryen @@ -20,11 +12,12 @@ pkgs.mkShell { graphviz jq llvmPackages_latest.bintools - nodejs_22 + nodejs + nodePackages.prettier pnpm_9 rustup wabt - wasm-bindgen-cli + wasm-bindgen-cli_0_2_93 wasm-pack ];