From 8e3bb7e610f46d1a7da3ab04c7c8ab2074bf77ab Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 11 Oct 2023 08:16:59 -0700 Subject: [PATCH 01/10] Add experimental support for prisma Signed-off-by: Alexis Rico --- packages/plugin-client-prisma/.npmignore | 5 + packages/plugin-client-prisma/README.md | 1 + packages/plugin-client-prisma/package.json | 30 +++++ .../plugin-client-prisma/rollup.config.mjs | 29 +++++ .../plugin-client-prisma/src/conversion.ts | 110 ++++++++++++++++++ packages/plugin-client-prisma/src/driver.ts | 62 ++++++++++ packages/plugin-client-prisma/src/index.ts | 12 ++ packages/plugin-client-prisma/tsconfig.json | 22 ++++ pnpm-lock.yaml | 18 +++ 9 files changed, 289 insertions(+) create mode 100644 packages/plugin-client-prisma/.npmignore create mode 100644 packages/plugin-client-prisma/README.md create mode 100644 packages/plugin-client-prisma/package.json create mode 100644 packages/plugin-client-prisma/rollup.config.mjs create mode 100644 packages/plugin-client-prisma/src/conversion.ts create mode 100644 packages/plugin-client-prisma/src/driver.ts create mode 100644 packages/plugin-client-prisma/src/index.ts create mode 100644 packages/plugin-client-prisma/tsconfig.json diff --git a/packages/plugin-client-prisma/.npmignore b/packages/plugin-client-prisma/.npmignore new file mode 100644 index 000000000..c97b8ad26 --- /dev/null +++ b/packages/plugin-client-prisma/.npmignore @@ -0,0 +1,5 @@ +src +tsconfig.json +rollup.config.mjs +.eslintrc.cjs +.gitignore diff --git a/packages/plugin-client-prisma/README.md b/packages/plugin-client-prisma/README.md new file mode 100644 index 000000000..065b2b2d0 --- /dev/null +++ b/packages/plugin-client-prisma/README.md @@ -0,0 +1 @@ +# @xata.io/prisma diff --git a/packages/plugin-client-prisma/package.json b/packages/plugin-client-prisma/package.json new file mode 100644 index 000000000..fda2f2ad1 --- /dev/null +++ b/packages/plugin-client-prisma/package.json @@ -0,0 +1,30 @@ +{ + "name": "@xata.io/prisma", + "version": "0.0.0", + "description": "", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "rimraf dist && rollup -c", + "tsc": "tsc --noEmit" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/xataio/client-ts/issues" + }, + "dependencies": { + "@xata.io/client": "workspace:*", + "@prisma/driver-adapter-utils": "^5.4.2" + }, + "devDependencies": {}, + "peerDependencies": {} +} diff --git a/packages/plugin-client-prisma/rollup.config.mjs b/packages/plugin-client-prisma/rollup.config.mjs new file mode 100644 index 000000000..9c57e2aa4 --- /dev/null +++ b/packages/plugin-client-prisma/rollup.config.mjs @@ -0,0 +1,29 @@ +import dts from 'rollup-plugin-dts'; +import esbuild from 'rollup-plugin-esbuild'; + +export default [ + { + input: 'src/index.ts', + plugins: [esbuild()], + output: [ + { + file: `dist/index.cjs`, + format: 'cjs', + sourcemap: true + }, + { + file: `dist/index.mjs`, + format: 'es', + sourcemap: true + } + ] + }, + { + input: 'src/index.ts', + plugins: [dts()], + output: { + file: `dist/index.d.ts`, + format: 'es' + } + } +]; diff --git a/packages/plugin-client-prisma/src/conversion.ts b/packages/plugin-client-prisma/src/conversion.ts new file mode 100644 index 000000000..786f99429 --- /dev/null +++ b/packages/plugin-client-prisma/src/conversion.ts @@ -0,0 +1,110 @@ +import { ColumnTypeEnum, type ColumnType, JsonNullMarker } from '@prisma/driver-adapter-utils'; + +/** + * PostgreSQL array column types. + */ +const ArrayColumnType = { + BOOL_ARRAY: 1000, + BYTEA_ARRAY: 1001, + BPCHAR_ARRAY: 1014, + CHAR_ARRAY: 1002, + DATE_ARRAY: 1182, + FLOAT4_ARRAY: 1021, + FLOAT8_ARRAY: 1022, + INT2_ARRAY: 1005, + INT4_ARRAY: 1007, + JSONB_ARRAY: 3807, + JSON_ARRAY: 199, + MONEY_ARRAY: 791, + NUMERIC_ARRAY: 1231, + TEXT_ARRAY: 1009, + TIMESTAMP_ARRAY: 1115, + TIME_ARRAY: 1183, + UUID_ARRAY: 2951, + VARCHAR_ARRAY: 1015, + XML_ARRAY: 143 +}; + +/** + * This is a simplification of quaint's value inference logic. Take a look at quaint's conversion.rs + * module to see how other attributes of the field packet such as the field length are used to infer + * the correct quaint::Value variant. + */ +export function fieldToColumnType(fieldTypeId: number): ColumnType { + switch (fieldTypeId) { + case ArrayColumnType.INT2_ARRAY: + case ArrayColumnType.INT4_ARRAY: + return ColumnTypeEnum.Int32Array; + case ArrayColumnType.FLOAT4_ARRAY: + return ColumnTypeEnum.FloatArray; + case ArrayColumnType.FLOAT8_ARRAY: + return ColumnTypeEnum.DoubleArray; + case ArrayColumnType.NUMERIC_ARRAY: + case ArrayColumnType.MONEY_ARRAY: + return ColumnTypeEnum.NumericArray; + case ArrayColumnType.BOOL_ARRAY: + return ColumnTypeEnum.BooleanArray; + case ArrayColumnType.CHAR_ARRAY: + return ColumnTypeEnum.CharArray; + case ArrayColumnType.TEXT_ARRAY: + case ArrayColumnType.VARCHAR_ARRAY: + case ArrayColumnType.BPCHAR_ARRAY: + case ArrayColumnType.XML_ARRAY: + return ColumnTypeEnum.TextArray; + case ArrayColumnType.DATE_ARRAY: + return ColumnTypeEnum.DateArray; + case ArrayColumnType.TIME_ARRAY: + return ColumnTypeEnum.TimeArray; + case ArrayColumnType.TIMESTAMP_ARRAY: + return ColumnTypeEnum.DateTimeArray; + case ArrayColumnType.JSON_ARRAY: + case ArrayColumnType.JSONB_ARRAY: + return ColumnTypeEnum.JsonArray; + case ArrayColumnType.BYTEA_ARRAY: + return ColumnTypeEnum.BytesArray; + case ArrayColumnType.UUID_ARRAY: + return ColumnTypeEnum.UuidArray; + + default: + if (fieldTypeId >= 10000) { + // Postgres Custom Types + return ColumnTypeEnum.Enum; + } + throw new Error(`Unsupported column type: ${fieldTypeId}`); + } +} + +/** + * JsonNull are stored in JSON strings as the string "null", distinguishable from + * the `null` value which is used by the driver to represent the database NULL. + * By default, JSON and JSONB columns use JSON.parse to parse a JSON column value + * and this will lead to serde_json::Value::Null in Rust, which will be interpreted + * as DbNull. + * + * By converting "null" to JsonNullMarker, we can signal JsonNull in Rust side and + * convert it to QuaintValue::Json(Some(Null)). + */ +function convertJson(json: string): unknown { + return json === 'null' ? JsonNullMarker : JSON.parse(json); +} + +/** + * Convert bytes to a JSON-encodable representation since we can't + * currently send a parsed Buffer or ArrayBuffer across JS to Rust + * boundary. + */ +function convertBytes(serializedBytes: string): number[] { + const buffer = Buffer.from(serializedBytes, 'hex'); + return encodeBuffer(buffer); +} + +/** + * TODO: + * 1. Check if using base64 would be more efficient than this encoding. + * 2. Consider the possibility of eliminating re-encoding altogether + * and passing bytea hex format to the engine if that can be aligned + * with other adapter flavours. + */ +function encodeBuffer(buffer: Buffer) { + return Array.from(new Uint8Array(buffer)); +} diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts new file mode 100644 index 000000000..bc91340bd --- /dev/null +++ b/packages/plugin-client-prisma/src/driver.ts @@ -0,0 +1,62 @@ +import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction } from '@prisma/driver-adapter-utils'; +import { Debug, ok } from '@prisma/driver-adapter-utils'; +import { Responses, SQLPluginResult } from '@xata.io/client'; +import { fieldToColumnType } from './conversion'; + +const debug = Debug('prisma:driver-adapter:xata'); + +type PerformIOResult = Responses.SQLResponse; + +abstract class XataQueryable implements Queryable { + readonly flavour = 'postgres'; + + async queryRaw(query: Query): Promise> { + const tag = '[js::query_raw]'; + debug(`${tag} %O`, query); + + const response = await this.performIO(query); + + return response.map(({ records = [], warning }) => { + if (warning) { + debug(`${tag} warning: %O`, warning); + } + + return { + columnNames: Object.keys(records[0] ?? {}), + columnTypes: Object.values(records[0] ?? {}).map((v) => fieldToColumnType(v.type)), + rows: records.map((r) => Object.values(r)) + }; + }); + } + + async executeRaw(query: Query): Promise> { + const tag = '[js::execute_raw]'; + debug(`${tag} %O`, query); + + // Note: `rowsAffected` can sometimes be null (e.g., when executing `"BEGIN"`) + return (await this.performIO(query)).map((r) => r.records?.length ?? 0); + } + + abstract performIO(query: Query): Promise>; +} + +type XataClient = { sql: SQLPluginResult }; + +export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { + constructor(private client: XataClient) { + super(); + } + + override async performIO(query: Query): Promise> { + const result = await this.client.sql({ statement: query.sql, params: query.args }); + return ok(result as Responses.SQLResponse); + } + + startTransaction(): Promise> { + return Promise.reject(new Error('Transactions are not supported in HTTP mode')); + } + + async close() { + return ok(undefined); + } +} diff --git a/packages/plugin-client-prisma/src/index.ts b/packages/plugin-client-prisma/src/index.ts new file mode 100644 index 000000000..c7e76f847 --- /dev/null +++ b/packages/plugin-client-prisma/src/index.ts @@ -0,0 +1,12 @@ +import { SQLPlugin, XataPlugin, XataPluginOptions } from '@xata.io/client'; +import { PrismaXataHTTP } from './driver'; + +export class PrismaPlugin extends XataPlugin { + build(pluginOptions: XataPluginOptions) { + const xata = { sql: new SQLPlugin().build(pluginOptions) }; + + return new PrismaXataHTTP(xata); + } +} + +export * from './driver'; diff --git a/packages/plugin-client-prisma/tsconfig.json b/packages/plugin-client-prisma/tsconfig.json new file mode 100644 index 000000000..654c56dd1 --- /dev/null +++ b/packages/plugin-client-prisma/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "lib": ["esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "es2020", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": false, + "outDir": "dist", + "declaration": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d4a4d58c..b2b61dd9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -474,6 +474,15 @@ importers: specifier: workspace:* version: link:../client + packages/plugin-client-prisma: + dependencies: + '@prisma/driver-adapter-utils': + specifier: ^5.4.2 + version: 5.4.2 + '@xata.io/client': + specifier: workspace:* + version: link:../client + packages: /@aashutoshrathi/word-wrap@1.2.6: resolution: @@ -4906,6 +4915,15 @@ packages: dev: true optional: true + /@prisma/driver-adapter-utils@5.4.2: + resolution: + { integrity: sha512-V+mtlBXBxQuiOufaSH98S3x2j9G1G0+tgnx3lAlfd6YEeceTCcFFhg85rESW9UIshHvULeGFJsG7+qScgGX0Ng== } + dependencies: + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + dev: false + /@protobufjs/aspromise@1.1.2: resolution: { integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== } From 6c818dcad802ab07fd430e837e65f1580e3dd398 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Oct 2023 08:44:43 -0700 Subject: [PATCH 02/10] Forward xata tables Signed-off-by: Alexis Rico --- packages/client/src/client.ts | 9 +++++---- packages/client/src/plugins.ts | 3 ++- packages/client/src/schema/index.ts | 6 +++--- packages/client/src/search/index.ts | 5 +++-- packages/plugin-client-prisma/src/driver.ts | 4 ++-- packages/plugin-client-prisma/src/index.ts | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index fb85e5805..46b1553f7 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -43,18 +43,19 @@ export const buildClient = = {}>(plu sql: SQLPluginResult; files: FilesPluginResult; - constructor(options: BaseClientOptions = {}, schemaTables?: Schemas.Table[]) { + constructor(options: BaseClientOptions = {}, schemaTables: Schemas.Table[]) { const safeOptions = this.#parseOptions(options); this.#options = safeOptions; const pluginOptions: XataPluginOptions = { ...this.#getFetchProps(safeOptions), cache: safeOptions.cache, - host: safeOptions.host + host: safeOptions.host, + tables: schemaTables ?? [] }; - const db = new SchemaPlugin(schemaTables).build(pluginOptions); - const search = new SearchPlugin(db, schemaTables).build(pluginOptions); + const db = new SchemaPlugin().build(pluginOptions); + const search = new SearchPlugin(db).build(pluginOptions); const transactions = new TransactionPlugin().build(pluginOptions); const sql = new SQLPlugin().build(pluginOptions); const files = new FilesPlugin().build(pluginOptions); diff --git a/packages/client/src/plugins.ts b/packages/client/src/plugins.ts index ff56041ff..b16338dd2 100644 --- a/packages/client/src/plugins.ts +++ b/packages/client/src/plugins.ts @@ -1,4 +1,4 @@ -import { ApiExtraProps, HostProvider } from './api'; +import { ApiExtraProps, HostProvider, Schemas } from './api'; import { CacheImpl } from './schema/cache'; export abstract class XataPlugin { @@ -8,4 +8,5 @@ export abstract class XataPlugin { export type XataPluginOptions = ApiExtraProps & { cache: CacheImpl; host: HostProvider; + tables: Schemas.Table[]; }; diff --git a/packages/client/src/schema/index.ts b/packages/client/src/schema/index.ts index c3ba586c2..16179a489 100644 --- a/packages/client/src/schema/index.ts +++ b/packages/client/src/schema/index.ts @@ -29,13 +29,13 @@ export class SchemaPlugin> extends Xa #tables: Record> = {}; #schemaTables?: Table[]; - constructor(schemaTables?: Table[]) { + constructor() { super(); - - this.#schemaTables = schemaTables; } build(pluginOptions: XataPluginOptions): SchemaPluginResult { + this.#schemaTables = pluginOptions.tables; + const db: any = new Proxy( {}, { diff --git a/packages/client/src/search/index.ts b/packages/client/src/search/index.ts index 4f05c030c..749e79ae1 100644 --- a/packages/client/src/search/index.ts +++ b/packages/client/src/search/index.ts @@ -59,12 +59,13 @@ export type SearchPluginResult> = { export class SearchPlugin> extends XataPlugin { #schemaTables?: Table[]; - constructor(private db: SchemaPluginResult, schemaTables?: Table[]) { + constructor(private db: SchemaPluginResult) { super(); - this.#schemaTables = schemaTables; } build(pluginOptions: XataPluginOptions): SearchPluginResult { + this.#schemaTables = pluginOptions.tables; + return { all: async >(query: string, options: SearchOptions = {}) => { const records = await this.#search(query, options, pluginOptions); diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index bc91340bd..db38d22bc 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -1,6 +1,6 @@ import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction } from '@prisma/driver-adapter-utils'; import { Debug, ok } from '@prisma/driver-adapter-utils'; -import { Responses, SQLPluginResult } from '@xata.io/client'; +import { Schemas, Responses, SQLPluginResult } from '@xata.io/client'; import { fieldToColumnType } from './conversion'; const debug = Debug('prisma:driver-adapter:xata'); @@ -43,7 +43,7 @@ abstract class XataQueryable implements Queryable { type XataClient = { sql: SQLPluginResult }; export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { - constructor(private client: XataClient) { + constructor(private client: XataClient, _tables?: Schemas.Table[]) { super(); } diff --git a/packages/plugin-client-prisma/src/index.ts b/packages/plugin-client-prisma/src/index.ts index c7e76f847..41e7c1266 100644 --- a/packages/plugin-client-prisma/src/index.ts +++ b/packages/plugin-client-prisma/src/index.ts @@ -5,7 +5,7 @@ export class PrismaPlugin extends XataPlugin { build(pluginOptions: XataPluginOptions) { const xata = { sql: new SQLPlugin().build(pluginOptions) }; - return new PrismaXataHTTP(xata); + return new PrismaXataHTTP(xata, pluginOptions.tables); } } From a87dd43e68a553396efd96fa6c2d2377e5650ca6 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Oct 2023 08:56:06 -0700 Subject: [PATCH 03/10] Expose schema in client Signed-off-by: Alexis Rico --- packages/client/src/client.ts | 6 ++++-- packages/plugin-client-prisma/src/driver.ts | 4 ++-- packages/plugin-client-prisma/src/index.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 46b1553f7..dfbb51943 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -37,13 +37,14 @@ export const buildClient = = {}>(plu class { #options: SafeOptions; + schema: Schemas.Schema; db: SchemaPluginResult; search: SearchPluginResult; transactions: TransactionPluginResult; sql: SQLPluginResult; files: FilesPluginResult; - constructor(options: BaseClientOptions = {}, schemaTables: Schemas.Table[]) { + constructor(options: BaseClientOptions = {}, tables: Schemas.Table[]) { const safeOptions = this.#parseOptions(options); this.#options = safeOptions; @@ -51,7 +52,7 @@ export const buildClient = = {}>(plu ...this.#getFetchProps(safeOptions), cache: safeOptions.cache, host: safeOptions.host, - tables: schemaTables ?? [] + tables }; const db = new SchemaPlugin().build(pluginOptions); @@ -61,6 +62,7 @@ export const buildClient = = {}>(plu const files = new FilesPlugin().build(pluginOptions); // We assign the namespaces after creating in case the user overrides the db plugin + this.schema = { tables }; this.db = db; this.search = search; this.transactions = transactions; diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index db38d22bc..bc6b59734 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -1,6 +1,6 @@ import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction } from '@prisma/driver-adapter-utils'; import { Debug, ok } from '@prisma/driver-adapter-utils'; -import { Schemas, Responses, SQLPluginResult } from '@xata.io/client'; +import { Responses, SQLPluginResult, Schemas } from '@xata.io/client'; import { fieldToColumnType } from './conversion'; const debug = Debug('prisma:driver-adapter:xata'); @@ -40,7 +40,7 @@ abstract class XataQueryable implements Queryable { abstract performIO(query: Query): Promise>; } -type XataClient = { sql: SQLPluginResult }; +type XataClient = { schema: Schemas.Schema; sql: SQLPluginResult }; export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { constructor(private client: XataClient, _tables?: Schemas.Table[]) { diff --git a/packages/plugin-client-prisma/src/index.ts b/packages/plugin-client-prisma/src/index.ts index 41e7c1266..de85f9761 100644 --- a/packages/plugin-client-prisma/src/index.ts +++ b/packages/plugin-client-prisma/src/index.ts @@ -3,7 +3,7 @@ import { PrismaXataHTTP } from './driver'; export class PrismaPlugin extends XataPlugin { build(pluginOptions: XataPluginOptions) { - const xata = { sql: new SQLPlugin().build(pluginOptions) }; + const xata = { schema: { tables: pluginOptions.tables }, sql: new SQLPlugin().build(pluginOptions) }; return new PrismaXataHTTP(xata, pluginOptions.tables); } From 0392c891a8c7eed53e52c432882b150784e7dbc2 Mon Sep 17 00:00:00 2001 From: Emily Date: Fri, 13 Oct 2023 12:58:13 +0200 Subject: [PATCH 04/10] get basic queries working --- .../plugin-client-prisma/src/conversion.ts | 45 +++++++++++- packages/plugin-client-prisma/src/driver.ts | 66 +++++++++++++++--- .../plugin-client-prisma/test/plugin.test.ts | 69 +++++++++++++++++++ .../plugin-client-prisma/test/schema.prisma | 25 +++++++ 4 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 packages/plugin-client-prisma/test/plugin.test.ts create mode 100644 packages/plugin-client-prisma/test/schema.prisma diff --git a/packages/plugin-client-prisma/src/conversion.ts b/packages/plugin-client-prisma/src/conversion.ts index 786f99429..79f9cbdaf 100644 --- a/packages/plugin-client-prisma/src/conversion.ts +++ b/packages/plugin-client-prisma/src/conversion.ts @@ -1,4 +1,38 @@ import { ColumnTypeEnum, type ColumnType, JsonNullMarker } from '@prisma/driver-adapter-utils'; +import { Schemas } from '@xata.io/client'; + +export function xataToColumnTypeEnum(column: Schemas.Column) { + switch (column.type) { + case 'string': + case 'text': + case 'email': + case 'link': + return ScalarColumnType.TEXT; + case 'bool': + return ScalarColumnType.BOOL; + case 'int': + return ScalarColumnType.INT2; + case 'float': + return ScalarColumnType.FLOAT8; + case 'datetime': + return ScalarColumnType.DATE; + case 'multiple': + case 'object': + case 'vector': + case 'file[]': + case 'file': + case 'json': + throw new Error(`Unsupported column type: ${column.type}`); + } +} + +const ScalarColumnType = { + TEXT: ColumnTypeEnum.Text, + BOOL: ColumnTypeEnum.Boolean, + INT2: ColumnTypeEnum.Int32, + FLOAT8: ColumnTypeEnum.Double, + DATE: ColumnTypeEnum.Date +}; /** * PostgreSQL array column types. @@ -64,7 +98,16 @@ export function fieldToColumnType(fieldTypeId: number): ColumnType { return ColumnTypeEnum.BytesArray; case ArrayColumnType.UUID_ARRAY: return ColumnTypeEnum.UuidArray; - + case ScalarColumnType.TEXT: + return ColumnTypeEnum.Text; + case ScalarColumnType.BOOL: + return ColumnTypeEnum.Boolean; + case ScalarColumnType.INT2: + return ColumnTypeEnum.Int32; + case ScalarColumnType.FLOAT8: + return ColumnTypeEnum.Double; + case ScalarColumnType.DATE: + return ColumnTypeEnum.Date; default: if (fieldTypeId >= 10000) { // Postgres Custom Types diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index bc6b59734..fb0ccecf9 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -1,11 +1,11 @@ import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction } from '@prisma/driver-adapter-utils'; import { Debug, ok } from '@prisma/driver-adapter-utils'; import { Responses, SQLPluginResult, Schemas } from '@xata.io/client'; -import { fieldToColumnType } from './conversion'; +import { fieldToColumnType, xataToColumnTypeEnum } from './conversion'; const debug = Debug('prisma:driver-adapter:xata'); -type PerformIOResult = Responses.SQLResponse; +type PerformIOResult = Responses.SQLResponse & { table: Schemas.Table }; abstract class XataQueryable implements Queryable { readonly flavour = 'postgres'; @@ -16,22 +16,41 @@ abstract class XataQueryable implements Queryable { const response = await this.performIO(query); - return response.map(({ records = [], warning }) => { + return response.map(({ records = [], warning, table }) => { if (warning) { debug(`${tag} warning: %O`, warning); } + if (!response.ok) { + // TODO handle this more gracefully + throw new Error(); + } + + const types: { [k: string]: number } = {}; + table.columns.forEach((column) => { + types[column.name] = xataToColumnTypeEnum(column)!; + }); + // Column names and types need to appear in the order they were in the query + const sortedRecords = records.flatMap((record) => { + const copy: { [key: string]: any } = {}; + orderRecordKeys({ appearanceIn: query.sql, record }).map((key) => { + copy[key] = record[key]; + }); + return copy; + }); return { - columnNames: Object.keys(records[0] ?? {}), - columnTypes: Object.values(records[0] ?? {}).map((v) => fieldToColumnType(v.type)), - rows: records.map((r) => Object.values(r)) + columnNames: sortedRecords.flatMap((record) => Object.keys(record)), + columnTypes: sortedRecords.flatMap((record) => + Object.entries(record).map(([k, _v]) => fieldToColumnType(types[k])) + ), + rows: sortedRecords.map((record) => Object.values(record)) }; }); } async executeRaw(query: Query): Promise> { const tag = '[js::execute_raw]'; - debug(`${tag} %O`, query); + debug(`${tag} % O`, query); // Note: `rowsAffected` can sometimes be null (e.g., when executing `"BEGIN"`) return (await this.performIO(query)).map((r) => r.records?.length ?? 0); @@ -40,16 +59,21 @@ abstract class XataQueryable implements Queryable { abstract performIO(query: Query): Promise>; } -type XataClient = { schema: Schemas.Schema; sql: SQLPluginResult }; +export type XataClient = { schema: Schemas.Schema; sql: SQLPluginResult }; export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { + #_tables: Schemas.Table[]; constructor(private client: XataClient, _tables?: Schemas.Table[]) { super(); + this.#_tables = _tables ?? []; } override async performIO(query: Query): Promise> { - const result = await this.client.sql({ statement: query.sql, params: query.args }); - return ok(result as Responses.SQLResponse); + const { formatted, table } = prepareSql(query, this.#_tables); + const result = await this.client.sql({ statement: formatted, params: query.args }); + const resultFormatted = { ...result, table }; + // @ts-expect-error + return ok(resultFormatted); } startTransaction(): Promise> { @@ -60,3 +84,25 @@ export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { return ok(undefined); } } + +const orderRecordKeys = (params: { appearanceIn: string; record: Record }) => { + // Will break if column names are inside a string literal in the query + const ordered = Object.keys(params.record).sort((a, b) => { + return params.appearanceIn.indexOf(a) > params.appearanceIn.indexOf(b) ? 1 : -1; + }); + return ordered; +}; + +// TODO any queries involving relations will not work. Xata Links are one sided so if you have +// User and post, with the link to post on User table +// You cannot get the post information without manipulation of the query. +const prepareSql = (query: Query, tables: Schemas.Table[]) => { + // Xata client will throw error with a schema prefixed table name + const formatted = query.sql.replaceAll('"public".', ''); + const table = tables.find((table) => formatted.includes(table.name)); + // Xata does not keep the ID as part of the table schema + // so we need to add it manually + if (!table) throw new Error('Table not found'); + table.columns.push({ type: 'string', name: 'id' }); + return { formatted, table }; +}; diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts new file mode 100644 index 000000000..5122f2bac --- /dev/null +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -0,0 +1,69 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { getXataClient } from './src/xata'; // Generated client +import { PrismaXataHTTP, XataClient } from '../src/driver'; +import { PrismaClient } from '@prisma/client'; + +const getXataClientWithPlugin = async () => { + const xata = getXataClient(); + return xata as unknown as XataClient; +}; + +const xata = await getXataClientWithPlugin(); +const adapter = new PrismaXataHTTP(xata, xata.schema.tables); +const prisma = new PrismaClient({ adapter }); + +const data = { + email: 'test', + id: 'test', + name: 'test' +}; + +// Skipping for now because generate functions would need to be called +describe.skip('@xata.io/prisma plugin', () => { + beforeAll(async () => { + await prisma.post.create({ + data + }); + await prisma.user.create({ + data + }); + }); + afterAll(async () => { + await prisma.post.deleteMany({}); + await prisma.user.deleteMany({}); + }); + test('can query one', async () => { + const res = await prisma.post.findFirst({}); + expect(res).toHaveProperty('id'); + }); + test('can query many', async () => { + const res = await prisma.post.findMany(); + expect(res[0]).toHaveProperty('id'); + }); + test('can order', async () => { + const res = await prisma.user.findMany({ + orderBy: { + email: 'asc' + } + }); + expect(res[0]).toHaveProperty('id'); + }); + test('can filter', async () => { + const res = await prisma.user.findMany({ + where: { + email: { + startsWith: 't' + } + } + }); + expect(res[0]).toHaveProperty('id'); + }); + test('can query relations', async () => { + const res = await prisma.user.findFirst({ + include: { + posts: true + } + }); + expect(res).toHaveProperty('posts'); + }); +}); diff --git a/packages/plugin-client-prisma/test/schema.prisma b/packages/plugin-client-prisma/test/schema.prisma new file mode 100644 index 000000000..0065f131a --- /dev/null +++ b/packages/plugin-client-prisma/test/schema.prisma @@ -0,0 +1,25 @@ +// schema.prisma +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = "postgres://notexistent" +} + +model User { + id String @id + email String @unique + name String? + posts Post[] +} + +model Post { + id String @id + email String @unique + name String? + author User? @relation(fields: [authorId], references: [id]) + authorId String? +} \ No newline at end of file From 935d819a87ae5e9b39e7c70dfa307c5e51db7d90 Mon Sep 17 00:00:00 2001 From: Emily Date: Fri, 13 Oct 2023 15:35:38 +0200 Subject: [PATCH 05/10] int,float,bool --- .../plugin-client-prisma/src/conversion.ts | 33 +++++++++++-------- packages/plugin-client-prisma/src/driver.ts | 13 ++++++-- .../plugin-client-prisma/test/plugin.test.ts | 19 +++++++---- .../plugin-client-prisma/test/schema.prisma | 3 ++ 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/packages/plugin-client-prisma/src/conversion.ts b/packages/plugin-client-prisma/src/conversion.ts index 79f9cbdaf..a1b861daa 100644 --- a/packages/plugin-client-prisma/src/conversion.ts +++ b/packages/plugin-client-prisma/src/conversion.ts @@ -1,6 +1,7 @@ import { ColumnTypeEnum, type ColumnType, JsonNullMarker } from '@prisma/driver-adapter-utils'; import { Schemas } from '@xata.io/client'; +// According to https://xata.io/docs/sdk/sql/overview export function xataToColumnTypeEnum(column: Schemas.Column) { switch (column.type) { case 'string': @@ -11,16 +12,18 @@ export function xataToColumnTypeEnum(column: Schemas.Column) { case 'bool': return ScalarColumnType.BOOL; case 'int': - return ScalarColumnType.INT2; + return ScalarColumnType.INT; case 'float': - return ScalarColumnType.FLOAT8; + return ScalarColumnType.FLOAT; case 'datetime': return ScalarColumnType.DATE; case 'multiple': - case 'object': - case 'vector': + return ScalarColumnType.TEXT_ARRAY; case 'file[]': case 'file': + return ScalarColumnType.JSONB; + case 'object': + case 'vector': case 'json': throw new Error(`Unsupported column type: ${column.type}`); } @@ -29,9 +32,11 @@ export function xataToColumnTypeEnum(column: Schemas.Column) { const ScalarColumnType = { TEXT: ColumnTypeEnum.Text, BOOL: ColumnTypeEnum.Boolean, - INT2: ColumnTypeEnum.Int32, - FLOAT8: ColumnTypeEnum.Double, - DATE: ColumnTypeEnum.Date + INT: ColumnTypeEnum.Int64, + FLOAT: ColumnTypeEnum.Double, + DATE: ColumnTypeEnum.Time, + TEXT_ARRAY: ColumnTypeEnum.TextArray, + JSONB: ColumnTypeEnum.Json }; /** @@ -99,15 +104,15 @@ export function fieldToColumnType(fieldTypeId: number): ColumnType { case ArrayColumnType.UUID_ARRAY: return ColumnTypeEnum.UuidArray; case ScalarColumnType.TEXT: - return ColumnTypeEnum.Text; + return ScalarColumnType.TEXT; case ScalarColumnType.BOOL: - return ColumnTypeEnum.Boolean; - case ScalarColumnType.INT2: - return ColumnTypeEnum.Int32; - case ScalarColumnType.FLOAT8: - return ColumnTypeEnum.Double; + return ScalarColumnType.BOOL; + case ScalarColumnType.INT: + return ScalarColumnType.INT; + case ScalarColumnType.FLOAT: + return ScalarColumnType.FLOAT; case ScalarColumnType.DATE: - return ColumnTypeEnum.Date; + return ScalarColumnType.DATE; default: if (fieldTypeId >= 10000) { // Postgres Custom Types diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index fb0ccecf9..e76d7dcf6 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -87,8 +87,15 @@ export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { const orderRecordKeys = (params: { appearanceIn: string; record: Record }) => { // Will break if column names are inside a string literal in the query + let toCheck = params.appearanceIn; + const divider = 'RETURNING'; + // If there is a returning clause we need to check the columns in the order they appear + // instead of the order they were inserted/updated in + if (params.appearanceIn.includes(divider)) { + toCheck = params.appearanceIn.split(divider)[1]; + } const ordered = Object.keys(params.record).sort((a, b) => { - return params.appearanceIn.indexOf(a) > params.appearanceIn.indexOf(b) ? 1 : -1; + return toCheck.indexOf(a) > toCheck.indexOf(b) ? 1 : -1; }); return ordered; }; @@ -103,6 +110,8 @@ const prepareSql = (query: Query, tables: Schemas.Table[]) => { // Xata does not keep the ID as part of the table schema // so we need to add it manually if (!table) throw new Error('Table not found'); - table.columns.push({ type: 'string', name: 'id' }); + if (!table.id) { + table.columns.push({ type: 'string', name: 'id' }); + } return { formatted, table }; }; diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts index 5122f2bac..86410ef66 100644 --- a/packages/plugin-client-prisma/test/plugin.test.ts +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -13,16 +13,23 @@ const adapter = new PrismaXataHTTP(xata, xata.schema.tables); const prisma = new PrismaClient({ adapter }); const data = { - email: 'test', - id: 'test', - name: 'test' + email: 'email', + id: 'id', + name: 'name' }; // Skipping for now because generate functions would need to be called +// Should pass locally with matching schemas describe.skip('@xata.io/prisma plugin', () => { beforeAll(async () => { await prisma.post.create({ - data + data: { + ...data, + bool: true, + float: 2.2324, + int: 44, + authorId: '' + } }); await prisma.user.create({ data @@ -52,13 +59,13 @@ describe.skip('@xata.io/prisma plugin', () => { const res = await prisma.user.findMany({ where: { email: { - startsWith: 't' + startsWith: 'e' } } }); expect(res[0]).toHaveProperty('id'); }); - test('can query relations', async () => { + test.skip('can query relations', async () => { const res = await prisma.user.findFirst({ include: { posts: true diff --git a/packages/plugin-client-prisma/test/schema.prisma b/packages/plugin-client-prisma/test/schema.prisma index 0065f131a..c6e44167c 100644 --- a/packages/plugin-client-prisma/test/schema.prisma +++ b/packages/plugin-client-prisma/test/schema.prisma @@ -22,4 +22,7 @@ model Post { name String? author User? @relation(fields: [authorId], references: [id]) authorId String? + int Int? + bool Boolean? + float Float? } \ No newline at end of file From 7f07a41018d22e0863f1cbe05e49073e27a80fcc Mon Sep 17 00:00:00 2001 From: Emily Date: Fri, 13 Oct 2023 15:49:02 +0200 Subject: [PATCH 06/10] json,datetime --- packages/plugin-client-prisma/src/conversion.ts | 8 +++++--- packages/plugin-client-prisma/test/plugin.test.ts | 6 +++++- packages/plugin-client-prisma/test/schema.prisma | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/plugin-client-prisma/src/conversion.ts b/packages/plugin-client-prisma/src/conversion.ts index a1b861daa..e304ff1a7 100644 --- a/packages/plugin-client-prisma/src/conversion.ts +++ b/packages/plugin-client-prisma/src/conversion.ts @@ -19,12 +19,12 @@ export function xataToColumnTypeEnum(column: Schemas.Column) { return ScalarColumnType.DATE; case 'multiple': return ScalarColumnType.TEXT_ARRAY; + case 'json': + return ScalarColumnType.JSONB; case 'file[]': case 'file': - return ScalarColumnType.JSONB; case 'object': case 'vector': - case 'json': throw new Error(`Unsupported column type: ${column.type}`); } } @@ -34,7 +34,7 @@ const ScalarColumnType = { BOOL: ColumnTypeEnum.Boolean, INT: ColumnTypeEnum.Int64, FLOAT: ColumnTypeEnum.Double, - DATE: ColumnTypeEnum.Time, + DATE: ColumnTypeEnum.DateTime, TEXT_ARRAY: ColumnTypeEnum.TextArray, JSONB: ColumnTypeEnum.Json }; @@ -113,6 +113,8 @@ export function fieldToColumnType(fieldTypeId: number): ColumnType { return ScalarColumnType.FLOAT; case ScalarColumnType.DATE: return ScalarColumnType.DATE; + case ScalarColumnType.JSONB: + return ScalarColumnType.JSONB; default: if (fieldTypeId >= 10000) { // Postgres Custom Types diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts index 86410ef66..bfb17680d 100644 --- a/packages/plugin-client-prisma/test/plugin.test.ts +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -28,7 +28,11 @@ describe.skip('@xata.io/prisma plugin', () => { bool: true, float: 2.2324, int: 44, - authorId: '' + authorId: '', + json: { + hello: 'world' + }, + datetime: new Date() } }); await prisma.user.create({ diff --git a/packages/plugin-client-prisma/test/schema.prisma b/packages/plugin-client-prisma/test/schema.prisma index c6e44167c..61a70bae2 100644 --- a/packages/plugin-client-prisma/test/schema.prisma +++ b/packages/plugin-client-prisma/test/schema.prisma @@ -25,4 +25,6 @@ model Post { int Int? bool Boolean? float Float? + json Json? + datetime DateTime @default(now()) } \ No newline at end of file From ea5e8785648f882bcfac1560ffbca0582d38f7b1 Mon Sep 17 00:00:00 2001 From: Emily Date: Fri, 13 Oct 2023 17:27:43 +0200 Subject: [PATCH 07/10] queryable relation data --- .../plugin-client-prisma/test/plugin.test.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts index bfb17680d..88fef6e1c 100644 --- a/packages/plugin-client-prisma/test/plugin.test.ts +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -22,22 +22,26 @@ const data = { // Should pass locally with matching schemas describe.skip('@xata.io/prisma plugin', () => { beforeAll(async () => { + await prisma.user.create({ + data + }); await prisma.post.create({ data: { ...data, bool: true, float: 2.2324, int: 44, - authorId: '', json: { hello: 'world' }, + // With this, the link object in Xata is not populated + // So if using this plugin we could suggest just not using links? + // connect or create is not supported anyways because it involves transactions. + //https://github.com/prisma/prisma-engines/blob/main/query-engine/driver-adapters/js/adapter-neon/src/neon.ts#L159 + authorId: 'id', datetime: new Date() } }); - await prisma.user.create({ - data - }); }); afterAll(async () => { await prisma.post.deleteMany({}); @@ -69,12 +73,23 @@ describe.skip('@xata.io/prisma plugin', () => { }); expect(res[0]).toHaveProperty('id'); }); - test.skip('can query relations', async () => { + test('can query relations on user', async () => { const res = await prisma.user.findFirst({ include: { posts: true } }); expect(res).toHaveProperty('posts'); + expect(res?.posts).toHaveLength(1); + }); + test('can query relations on post', async () => { + const res = await prisma.post.findFirst({ + include: { + author: true + } + }); + expect(res).toHaveProperty('author'); + expect(res?.author).toHaveProperty('id'); }); + // TODO try many to many }); From 99a18d1249163c2242f603e297028b413b7a31ce Mon Sep 17 00:00:00 2001 From: Emily Date: Mon, 16 Oct 2023 09:20:51 +0200 Subject: [PATCH 08/10] table name --- packages/plugin-client-prisma/src/driver.ts | 6 ++--- .../plugin-client-prisma/test/plugin.test.ts | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index e76d7dcf6..f631ce8d2 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -72,7 +72,6 @@ export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { const { formatted, table } = prepareSql(query, this.#_tables); const result = await this.client.sql({ statement: formatted, params: query.args }); const resultFormatted = { ...result, table }; - // @ts-expect-error return ok(resultFormatted); } @@ -106,11 +105,12 @@ const orderRecordKeys = (params: { appearanceIn: string; record: Record { // Xata client will throw error with a schema prefixed table name const formatted = query.sql.replaceAll('"public".', ''); - const table = tables.find((table) => formatted.includes(table.name)); - // Xata does not keep the ID as part of the table schema + const tname = new RegExp(`${tables.map((t) => t.name).join('|')}`).exec(formatted)?.[0]; // First occurrence of table name + const table = tables.find((t) => t.name === tname); // so we need to add it manually if (!table) throw new Error('Table not found'); if (!table.id) { + // Xata does not keep the ID as part of the table schema table.columns.push({ type: 'string', name: 'id' }); } return { formatted, table }; diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts index 88fef6e1c..c4711232b 100644 --- a/packages/plugin-client-prisma/test/plugin.test.ts +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -92,4 +92,31 @@ describe.skip('@xata.io/prisma plugin', () => { expect(res?.author).toHaveProperty('id'); }); // TODO try many to many + test('can filter on relations', async () => { + const res = await prisma.post.findFirst({ + where: { + author: { + id: 'id' + } + }, + include: { + author: true + } + }); + + expect(res).toHaveProperty('author'); + expect(res?.author).toHaveProperty('id'); + expect(res?.author?.id).toBe('id'); + }); + test.skip('can filter on relations', async () => { + const res = await prisma.user.findFirst({ + select: { + // aggregations don't work + _count: true + } + }); + + expect(res).toHaveProperty('author'); + expect(res?._count).toHaveProperty('id'); + }); }); From 17e00525aa80c3d5704def6c5815dde755be47c7 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 27 Nov 2023 07:58:40 +0100 Subject: [PATCH 09/10] Use metadata from sql endpoint Signed-off-by: Alexis Rico --- packages/plugin-client-prisma/package.json | 13 +- .../plugin-client-prisma/src/conversion.ts | 276 ++++++++---------- packages/plugin-client-prisma/src/driver.ts | 119 +++----- .../plugin-client-prisma/test/plugin.test.ts | 125 +------- .../plugin-client-prisma/test/schema.prisma | 122 ++++++-- packages/plugin-client-prisma/test/smoke.ts | 265 +++++++++++++++++ pnpm-lock.yaml | 68 +++++ 7 files changed, 614 insertions(+), 374 deletions(-) create mode 100644 packages/plugin-client-prisma/test/smoke.ts diff --git a/packages/plugin-client-prisma/package.json b/packages/plugin-client-prisma/package.json index fda2f2ad1..24f112948 100644 --- a/packages/plugin-client-prisma/package.json +++ b/packages/plugin-client-prisma/package.json @@ -1,6 +1,6 @@ { "name": "@xata.io/prisma", - "version": "0.0.0", + "version": "0.0.1", "description": "", "main": "./dist/index.cjs", "module": "./dist/index.mjs", @@ -22,9 +22,12 @@ "url": "https://github.com/xataio/client-ts/issues" }, "dependencies": { - "@xata.io/client": "workspace:*", - "@prisma/driver-adapter-utils": "^5.4.2" + "@prisma/driver-adapter-utils": "^5.4.2", + "@xata.io/client": "workspace:*" }, - "devDependencies": {}, - "peerDependencies": {} + "devDependencies": { + "@prisma/client": "^5.6.0", + "prisma": "^5.6.0", + "superjson": "^2.2.1" + } } diff --git a/packages/plugin-client-prisma/src/conversion.ts b/packages/plugin-client-prisma/src/conversion.ts index e304ff1a7..f5749bd97 100644 --- a/packages/plugin-client-prisma/src/conversion.ts +++ b/packages/plugin-client-prisma/src/conversion.ts @@ -1,160 +1,128 @@ -import { ColumnTypeEnum, type ColumnType, JsonNullMarker } from '@prisma/driver-adapter-utils'; -import { Schemas } from '@xata.io/client'; +import { type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils'; -// According to https://xata.io/docs/sdk/sql/overview -export function xataToColumnTypeEnum(column: Schemas.Column) { - switch (column.type) { - case 'string': - case 'text': - case 'email': - case 'link': - return ScalarColumnType.TEXT; +export function fieldToColumnType(fieldType: string): ColumnType { + switch (fieldType) { case 'bool': - return ScalarColumnType.BOOL; - case 'int': - return ScalarColumnType.INT; - case 'float': - return ScalarColumnType.FLOAT; - case 'datetime': - return ScalarColumnType.DATE; - case 'multiple': - return ScalarColumnType.TEXT_ARRAY; + return ColumnTypeEnum.Boolean; + case 'bytea': + return ColumnTypeEnum.Bytes; + case 'char': + return ColumnTypeEnum.Text; + case 'int8': + return ColumnTypeEnum.Int64; + case 'int2': + return ColumnTypeEnum.Int32; + case 'int4': + return ColumnTypeEnum.Int32; + case 'regproc': + return ColumnTypeEnum.Text; + case 'text': + return ColumnTypeEnum.Text; + case 'oid': + return ColumnTypeEnum.Int64; + case 'tid': + return ColumnTypeEnum.Text; + case 'xid': + return ColumnTypeEnum.Text; + case 'cid': + return ColumnTypeEnum.Text; case 'json': - return ScalarColumnType.JSONB; - case 'file[]': - case 'file': - case 'object': - case 'vector': - throw new Error(`Unsupported column type: ${column.type}`); - } -} - -const ScalarColumnType = { - TEXT: ColumnTypeEnum.Text, - BOOL: ColumnTypeEnum.Boolean, - INT: ColumnTypeEnum.Int64, - FLOAT: ColumnTypeEnum.Double, - DATE: ColumnTypeEnum.DateTime, - TEXT_ARRAY: ColumnTypeEnum.TextArray, - JSONB: ColumnTypeEnum.Json -}; - -/** - * PostgreSQL array column types. - */ -const ArrayColumnType = { - BOOL_ARRAY: 1000, - BYTEA_ARRAY: 1001, - BPCHAR_ARRAY: 1014, - CHAR_ARRAY: 1002, - DATE_ARRAY: 1182, - FLOAT4_ARRAY: 1021, - FLOAT8_ARRAY: 1022, - INT2_ARRAY: 1005, - INT4_ARRAY: 1007, - JSONB_ARRAY: 3807, - JSON_ARRAY: 199, - MONEY_ARRAY: 791, - NUMERIC_ARRAY: 1231, - TEXT_ARRAY: 1009, - TIMESTAMP_ARRAY: 1115, - TIME_ARRAY: 1183, - UUID_ARRAY: 2951, - VARCHAR_ARRAY: 1015, - XML_ARRAY: 143 -}; - -/** - * This is a simplification of quaint's value inference logic. Take a look at quaint's conversion.rs - * module to see how other attributes of the field packet such as the field length are used to infer - * the correct quaint::Value variant. - */ -export function fieldToColumnType(fieldTypeId: number): ColumnType { - switch (fieldTypeId) { - case ArrayColumnType.INT2_ARRAY: - case ArrayColumnType.INT4_ARRAY: - return ColumnTypeEnum.Int32Array; - case ArrayColumnType.FLOAT4_ARRAY: - return ColumnTypeEnum.FloatArray; - case ArrayColumnType.FLOAT8_ARRAY: - return ColumnTypeEnum.DoubleArray; - case ArrayColumnType.NUMERIC_ARRAY: - case ArrayColumnType.MONEY_ARRAY: - return ColumnTypeEnum.NumericArray; - case ArrayColumnType.BOOL_ARRAY: - return ColumnTypeEnum.BooleanArray; - case ArrayColumnType.CHAR_ARRAY: - return ColumnTypeEnum.CharArray; - case ArrayColumnType.TEXT_ARRAY: - case ArrayColumnType.VARCHAR_ARRAY: - case ArrayColumnType.BPCHAR_ARRAY: - case ArrayColumnType.XML_ARRAY: - return ColumnTypeEnum.TextArray; - case ArrayColumnType.DATE_ARRAY: - return ColumnTypeEnum.DateArray; - case ArrayColumnType.TIME_ARRAY: - return ColumnTypeEnum.TimeArray; - case ArrayColumnType.TIMESTAMP_ARRAY: - return ColumnTypeEnum.DateTimeArray; - case ArrayColumnType.JSON_ARRAY: - case ArrayColumnType.JSONB_ARRAY: - return ColumnTypeEnum.JsonArray; - case ArrayColumnType.BYTEA_ARRAY: - return ColumnTypeEnum.BytesArray; - case ArrayColumnType.UUID_ARRAY: - return ColumnTypeEnum.UuidArray; - case ScalarColumnType.TEXT: - return ScalarColumnType.TEXT; - case ScalarColumnType.BOOL: - return ScalarColumnType.BOOL; - case ScalarColumnType.INT: - return ScalarColumnType.INT; - case ScalarColumnType.FLOAT: - return ScalarColumnType.FLOAT; - case ScalarColumnType.DATE: - return ScalarColumnType.DATE; - case ScalarColumnType.JSONB: - return ScalarColumnType.JSONB; + return ColumnTypeEnum.Json; + case 'xml': + return ColumnTypeEnum.Text; + case 'pg_node_tree': + return ColumnTypeEnum.Text; + case 'smgr': + return ColumnTypeEnum.Text; + case 'path': + return ColumnTypeEnum.Text; + case 'polygon': + return ColumnTypeEnum.Text; + case 'cidr': + return ColumnTypeEnum.Text; + case 'float4': + return ColumnTypeEnum.Float; + case 'float8': + return ColumnTypeEnum.Double; + case 'abstime': + return ColumnTypeEnum.Text; + case 'reltime': + return ColumnTypeEnum.Text; + case 'tinterval': + return ColumnTypeEnum.Text; + case 'circle': + return ColumnTypeEnum.Text; + case 'macaddr8': + return ColumnTypeEnum.Text; + case 'money': + return ColumnTypeEnum.Numeric; + case 'macaddr': + return ColumnTypeEnum.Text; + case 'inet': + return ColumnTypeEnum.Text; + case 'aclitem': + return ColumnTypeEnum.Text; + case 'bpchar': + return ColumnTypeEnum.Text; + case 'varchar': + return ColumnTypeEnum.Text; + case 'date': + return ColumnTypeEnum.Date; + case 'time': + return ColumnTypeEnum.Time; + case 'timestamp': + return ColumnTypeEnum.DateTime; + case 'timestamptz': + return ColumnTypeEnum.DateTime; + case 'interval': + return ColumnTypeEnum.Text; + case 'timetz': + return ColumnTypeEnum.Time; + case 'bit': + return ColumnTypeEnum.Text; + case 'varbit': + return ColumnTypeEnum.Text; + case 'numeric': + return ColumnTypeEnum.Numeric; + case 'refcursor': + return ColumnTypeEnum.Text; + case 'regprocedure': + return ColumnTypeEnum.Text; + case 'regoper': + return ColumnTypeEnum.Text; + case 'regoperator': + return ColumnTypeEnum.Text; + case 'regclass': + return ColumnTypeEnum.Text; + case 'regtype': + return ColumnTypeEnum.Text; + case 'uuid': + return ColumnTypeEnum.Uuid; + case 'txid_snapshot': + return ColumnTypeEnum.Text; + case 'pg_lsn': + return ColumnTypeEnum.Text; + case 'pg_ndistinct': + return ColumnTypeEnum.Text; + case 'pg_dependencies': + return ColumnTypeEnum.Text; + case 'tsvector': + return ColumnTypeEnum.Text; + case 'tsquery': + return ColumnTypeEnum.Text; + case 'gtsvector': + return ColumnTypeEnum.Text; + case 'regconfig': + return ColumnTypeEnum.Text; + case 'regdictionary': + return ColumnTypeEnum.Text; + case 'jsonb': + return ColumnTypeEnum.Json; + case 'regnamespace': + return ColumnTypeEnum.Text; + case 'regrole': + return ColumnTypeEnum.Text; default: - if (fieldTypeId >= 10000) { - // Postgres Custom Types - return ColumnTypeEnum.Enum; - } - throw new Error(`Unsupported column type: ${fieldTypeId}`); + return ColumnTypeEnum.Text; } } - -/** - * JsonNull are stored in JSON strings as the string "null", distinguishable from - * the `null` value which is used by the driver to represent the database NULL. - * By default, JSON and JSONB columns use JSON.parse to parse a JSON column value - * and this will lead to serde_json::Value::Null in Rust, which will be interpreted - * as DbNull. - * - * By converting "null" to JsonNullMarker, we can signal JsonNull in Rust side and - * convert it to QuaintValue::Json(Some(Null)). - */ -function convertJson(json: string): unknown { - return json === 'null' ? JsonNullMarker : JSON.parse(json); -} - -/** - * Convert bytes to a JSON-encodable representation since we can't - * currently send a parsed Buffer or ArrayBuffer across JS to Rust - * boundary. - */ -function convertBytes(serializedBytes: string): number[] { - const buffer = Buffer.from(serializedBytes, 'hex'); - return encodeBuffer(buffer); -} - -/** - * TODO: - * 1. Check if using base64 would be more efficient than this encoding. - * 2. Consider the possibility of eliminating re-encoding altogether - * and passing bytea hex format to the engine if that can be aligned - * with other adapter flavours. - */ -function encodeBuffer(buffer: Buffer) { - return Array.from(new Uint8Array(buffer)); -} diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index f631ce8d2..ccb4f4bff 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -1,11 +1,20 @@ -import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction } from '@prisma/driver-adapter-utils'; -import { Debug, ok } from '@prisma/driver-adapter-utils'; -import { Responses, SQLPluginResult, Schemas } from '@xata.io/client'; -import { fieldToColumnType, xataToColumnTypeEnum } from './conversion'; +/* eslint-disable @typescript-eslint/require-await */ +import type { + ColumnType, + DriverAdapter, + Query, + Queryable, + Result, + ResultSet, + Transaction +} from '@prisma/driver-adapter-utils'; +import { Debug, err, ok } from '@prisma/driver-adapter-utils'; +import { Responses, SQLPluginResult } from '@xata.io/client'; +import { fieldToColumnType } from './conversion'; const debug = Debug('prisma:driver-adapter:xata'); -type PerformIOResult = Responses.SQLResponse & { table: Schemas.Table }; +type PerformIOResult = Responses.SQLResponse; abstract class XataQueryable implements Queryable { readonly flavour = 'postgres'; @@ -14,65 +23,47 @@ abstract class XataQueryable implements Queryable { const tag = '[js::query_raw]'; debug(`${tag} %O`, query); - const response = await this.performIO(query); - - return response.map(({ records = [], warning, table }) => { - if (warning) { - debug(`${tag} warning: %O`, warning); - } - if (!response.ok) { - // TODO handle this more gracefully - throw new Error(); - } - - const types: { [k: string]: number } = {}; - - table.columns.forEach((column) => { - types[column.name] = xataToColumnTypeEnum(column)!; - }); - // Column names and types need to appear in the order they were in the query - const sortedRecords = records.flatMap((record) => { - const copy: { [key: string]: any } = {}; - orderRecordKeys({ appearanceIn: query.sql, record }).map((key) => { - copy[key] = record[key]; - }); - return copy; - }); - return { - columnNames: sortedRecords.flatMap((record) => Object.keys(record)), - columnTypes: sortedRecords.flatMap((record) => - Object.entries(record).map(([k, _v]) => fieldToColumnType(types[k])) - ), - rows: sortedRecords.map((record) => Object.values(record)) - }; - }); + const res = await this.performIO(query); + + if (!res.ok) { + return err(res.error); + } + + const { records, columns = {}, warning } = res.value; + if (warning) debug(`${tag} %O`, warning); + + const [columnNames, columnTypes] = Object.fromEntries(columns as any).reduce( + ([names, types]: [string[], ColumnType[]], [name, { type_name }]: [string, { type_name: string }]) => { + names.push(name); + types.push(fieldToColumnType(type_name)); + return [names, types]; + }, + [[], []] as [string[], ColumnType[]] + ); + + return ok({ columnNames, columnTypes, rows: records as any[] }); } async executeRaw(query: Query): Promise> { const tag = '[js::execute_raw]'; - debug(`${tag} % O`, query); + debug(`${tag} %O`, query); - // Note: `rowsAffected` can sometimes be null (e.g., when executing `"BEGIN"`) - return (await this.performIO(query)).map((r) => r.records?.length ?? 0); + return (await this.performIO(query)).map((r) => r.total ?? 0); } abstract performIO(query: Query): Promise>; } -export type XataClient = { schema: Schemas.Schema; sql: SQLPluginResult }; +type XataClient = { sql: SQLPluginResult }; export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { - #_tables: Schemas.Table[]; - constructor(private client: XataClient, _tables?: Schemas.Table[]) { + constructor(private xata: XataClient) { super(); - this.#_tables = _tables ?? []; } override async performIO(query: Query): Promise> { - const { formatted, table } = prepareSql(query, this.#_tables); - const result = await this.client.sql({ statement: formatted, params: query.args }); - const resultFormatted = { ...result, table }; - return ok(resultFormatted); + const { sql, args: values } = query; + return ok(await this.xata.sql(sql, values, { arrayMode: true, fullResults: true })); } startTransaction(): Promise> { @@ -83,35 +74,3 @@ export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { return ok(undefined); } } - -const orderRecordKeys = (params: { appearanceIn: string; record: Record }) => { - // Will break if column names are inside a string literal in the query - let toCheck = params.appearanceIn; - const divider = 'RETURNING'; - // If there is a returning clause we need to check the columns in the order they appear - // instead of the order they were inserted/updated in - if (params.appearanceIn.includes(divider)) { - toCheck = params.appearanceIn.split(divider)[1]; - } - const ordered = Object.keys(params.record).sort((a, b) => { - return toCheck.indexOf(a) > toCheck.indexOf(b) ? 1 : -1; - }); - return ordered; -}; - -// TODO any queries involving relations will not work. Xata Links are one sided so if you have -// User and post, with the link to post on User table -// You cannot get the post information without manipulation of the query. -const prepareSql = (query: Query, tables: Schemas.Table[]) => { - // Xata client will throw error with a schema prefixed table name - const formatted = query.sql.replaceAll('"public".', ''); - const tname = new RegExp(`${tables.map((t) => t.name).join('|')}`).exec(formatted)?.[0]; // First occurrence of table name - const table = tables.find((t) => t.name === tname); - // so we need to add it manually - if (!table) throw new Error('Table not found'); - if (!table.id) { - // Xata does not keep the ID as part of the table schema - table.columns.push({ type: 'string', name: 'id' }); - } - return { formatted, table }; -}; diff --git a/packages/plugin-client-prisma/test/plugin.test.ts b/packages/plugin-client-prisma/test/plugin.test.ts index c4711232b..ebabfdc9b 100644 --- a/packages/plugin-client-prisma/test/plugin.test.ts +++ b/packages/plugin-client-prisma/test/plugin.test.ts @@ -1,122 +1,13 @@ -import { afterAll, beforeAll, describe, expect, test } from 'vitest'; -import { getXataClient } from './src/xata'; // Generated client -import { PrismaXataHTTP, XataClient } from '../src/driver'; -import { PrismaClient } from '@prisma/client'; +import { describe, test } from 'vitest'; +import { PrismaXataHTTP } from '../src/driver'; +import { smokeTest } from './smoke'; +import { BaseClient } from '../../client/src'; -const getXataClientWithPlugin = async () => { - const xata = getXataClient(); - return xata as unknown as XataClient; -}; - -const xata = await getXataClientWithPlugin(); -const adapter = new PrismaXataHTTP(xata, xata.schema.tables); -const prisma = new PrismaClient({ adapter }); - -const data = { - email: 'email', - id: 'id', - name: 'name' -}; - -// Skipping for now because generate functions would need to be called -// Should pass locally with matching schemas describe.skip('@xata.io/prisma plugin', () => { - beforeAll(async () => { - await prisma.user.create({ - data - }); - await prisma.post.create({ - data: { - ...data, - bool: true, - float: 2.2324, - int: 44, - json: { - hello: 'world' - }, - // With this, the link object in Xata is not populated - // So if using this plugin we could suggest just not using links? - // connect or create is not supported anyways because it involves transactions. - //https://github.com/prisma/prisma-engines/blob/main/query-engine/driver-adapters/js/adapter-neon/src/neon.ts#L159 - authorId: 'id', - datetime: new Date() - } - }); - }); - afterAll(async () => { - await prisma.post.deleteMany({}); - await prisma.user.deleteMany({}); - }); - test('can query one', async () => { - const res = await prisma.post.findFirst({}); - expect(res).toHaveProperty('id'); - }); - test('can query many', async () => { - const res = await prisma.post.findMany(); - expect(res[0]).toHaveProperty('id'); - }); - test('can order', async () => { - const res = await prisma.user.findMany({ - orderBy: { - email: 'asc' - } - }); - expect(res[0]).toHaveProperty('id'); - }); - test('can filter', async () => { - const res = await prisma.user.findMany({ - where: { - email: { - startsWith: 'e' - } - } - }); - expect(res[0]).toHaveProperty('id'); - }); - test('can query relations on user', async () => { - const res = await prisma.user.findFirst({ - include: { - posts: true - } - }); - expect(res).toHaveProperty('posts'); - expect(res?.posts).toHaveLength(1); - }); - test('can query relations on post', async () => { - const res = await prisma.post.findFirst({ - include: { - author: true - } - }); - expect(res).toHaveProperty('author'); - expect(res?.author).toHaveProperty('id'); - }); - // TODO try many to many - test('can filter on relations', async () => { - const res = await prisma.post.findFirst({ - where: { - author: { - id: 'id' - } - }, - include: { - author: true - } - }); - - expect(res).toHaveProperty('author'); - expect(res?.author).toHaveProperty('id'); - expect(res?.author?.id).toBe('id'); - }); - test.skip('can filter on relations', async () => { - const res = await prisma.user.findFirst({ - select: { - // aggregations don't work - _count: true - } - }); + test('run smoke tests', async () => { + const xata = new BaseClient(); + const adapter = new PrismaXataHTTP(xata); - expect(res).toHaveProperty('author'); - expect(res?._count).toHaveProperty('id'); + await smokeTest(adapter); }); }); diff --git a/packages/plugin-client-prisma/test/schema.prisma b/packages/plugin-client-prisma/test/schema.prisma index 61a70bae2..9248a1ac6 100644 --- a/packages/plugin-client-prisma/test/schema.prisma +++ b/packages/plugin-client-prisma/test/schema.prisma @@ -1,30 +1,116 @@ -// schema.prisma generator client { provider = "prisma-client-js" + output = "../../node_modules/.prisma/client" previewFeatures = ["driverAdapters"] } datasource db { - provider = "postgresql" - url = "postgres://notexistent" + provider = "postgres" + url = env("DATABASE_URL") } -model User { - id String @id - email String @unique - name String? +model type_test { + id Int @id @default(autoincrement()) + smallint_column Int @db.SmallInt + smallint_column_null Int? @db.SmallInt + int_column Int + int_column_null Int? + bigint_column BigInt + bigint_column_null BigInt? + float_column Float @db.Real + float_column_null Float? @db.Real + double_column Float + double_column_null Float? + decimal_column Decimal @db.Decimal(10, 2) + decimal_column_null Decimal? @db.Decimal(10, 2) + boolean_column Boolean + boolean_column_null Boolean? + char_column String @db.Char(10) + char_column_null String? @db.Char(10) + varchar_column String @db.VarChar(255) + varchar_column_null String? @db.VarChar(255) + text_column String + text_column_null String? + date_column DateTime @db.Date + date_column_null DateTime? @db.Date + time_column DateTime @db.Time(0) + time_column_null DateTime? @db.Time(0) + datetime_column DateTime @db.Timestamp(3) + datetime_column_null DateTime? @db.Timestamp(3) + timestamp_column DateTime @db.Timestamp(0) + timestamp_column_null DateTime? @db.Timestamp(0) + json_column Json + json_column_null Json? + enum_column type_test_enum_column + enum_column_null type_test_enum_column_null? +} + +// This will eventually supersede type_test +model type_test_2 { + id String @id @default(cuid()) + datetime_column DateTime @default(now()) @db.Timestamp(3) + datetime_column_null DateTime? @db.Timestamp(3) +} + +model Child { + c String @unique + c_1 String + c_2 String + parentId String? @unique + non_unique String? + id String @id + + @@unique([c_1, c_2]) +} + +model Parent { + p String @unique + p_1 String + p_2 String + non_unique String? + id String @id + + @@unique([p_1, p_2]) +} + +enum type_test_enum_column { + value1 + value2 + value3 +} + +enum type_test_enum_column_null { + value1 + value2 + value3 +} + +model Author { + id Int @id @default(autoincrement()) + firstName String + lastName String + age Int posts Post[] + + @@map("authors") } model Post { - id String @id - email String @unique - name String? - author User? @relation(fields: [authorId], references: [id]) - authorId String? - int Int? - bool Boolean? - float Float? - json Json? - datetime DateTime @default(now()) -} \ No newline at end of file + id Int @id @default(autoincrement()) + title String? + published Boolean @default(false) + authorId Int? + author Author? @relation(fields: [authorId], references: [id]) + + @@index([authorId], name: "author_id") +} + +model Product { + id String @id @default(cuid()) + properties Json + properties_null Json? +} + +model leak_test { + id String @id @default(cuid()) +} diff --git a/packages/plugin-client-prisma/test/smoke.ts b/packages/plugin-client-prisma/test/smoke.ts new file mode 100644 index 000000000..507d74ec0 --- /dev/null +++ b/packages/plugin-client-prisma/test/smoke.ts @@ -0,0 +1,265 @@ +import superjson from 'superjson'; +import { PrismaClient } from '.prisma/client'; +import { setImmediate, setTimeout } from 'node:timers/promises'; +import type { DriverAdapter } from '@prisma/driver-adapter-utils'; + +export async function smokeTest(adapter: DriverAdapter) { + // wait for the database pool to be initialized + await setImmediate(0); + + // @ts-ignore Adapter types do not match + const prisma = new PrismaClient({ adapter }); + + console.log('[nodejs] connecting...'); + await prisma.$connect(); + console.log('[nodejs] connected'); + + const test = new SmokeTest(prisma, adapter.flavour); + + await test.testJSON(); + await test.testTypeTest2(); + await test.$raw(); + await test.testFindManyTypeTest(); + await test.transactionsWithConflits(); + await test.testCreateAndDeleteChildParent(); + await test.interactiveTransactions(); + await test.explicitTransaction(); + + console.log('[nodejs] disconnecting...'); + await prisma.$disconnect(); + console.log('[nodejs] disconnected'); + + console.log('[nodejs] re-connecting...'); + await prisma.$connect(); + console.log('[nodejs] re-connecting'); + + await setTimeout(0); + + console.log('[nodejs] re-disconnecting...'); + await prisma.$disconnect(); + console.log('[nodejs] re-disconnected'); +} + +class SmokeTest { + constructor(private readonly prisma: PrismaClient, readonly flavour: DriverAdapter['flavour']) {} + + async testJSON() { + const json = JSON.stringify({ + foo: 'bar', + baz: 1 + }); + + const created = await this.prisma.product.create({ + data: { + properties: json + }, + select: { + properties: true + } + }); + + console.log('[nodejs] created', superjson.serialize(created).json); + + const resultSet = await this.prisma.product.findMany({}); + console.log('[nodejs] resultSet', superjson.serialize(resultSet).json); + + await this.prisma.product.deleteMany({}); + } + + async transactionsWithConflits() { + await this.prisma.leak_test.deleteMany(); + + const one = async () => { + await this.prisma.$transaction(async (tx) => { + await tx.leak_test.create({ data: {} }); + await setTimeout(1000); + throw new Error('Abort the mission'); + }); + }; + + const two = async () => { + await setTimeout(500); + await this.prisma.leak_test.create({ data: {} }); + }; + + await this.prisma.leak_test.deleteMany(); + await Promise.allSettled([one(), two()]); + } + + async explicitTransaction() { + const [children, totalChildren] = await this.prisma.$transaction( + [this.prisma.child.findMany(), this.prisma.child.count()], + { + isolationLevel: 'Serializable' + } + ); + + console.log('[nodejs] children', superjson.serialize(children).json); + console.log('[nodejs] totalChildren', totalChildren); + } + + async $raw() { + const cleanUp = async () => { + await this.prisma.$executeRaw`DELETE FROM leak_test`; + }; + + await cleanUp(); + + await this.prisma.$executeRaw`INSERT INTO leak_test (id) VALUES (1)`; + const result = await this.prisma.$queryRaw`SELECT * FROM leak_test`; + console.log('[nodejs] result', superjson.serialize(result).json); + + await cleanUp(); + } + + async interactiveTransactions() { + const author = await this.prisma.author.create({ + data: { + firstName: 'Firstname 1 from autoincrement', + lastName: 'Lastname 1 from autoincrement', + age: 99 + } + }); + console.log('[nodejs] author', superjson.serialize(author).json); + + const result = await this.prisma.$transaction(async (tx) => { + await tx.author.deleteMany(); + await tx.post.deleteMany(); + + const author = await tx.author.create({ + data: { + firstName: 'Firstname 2 from autoincrement', + lastName: 'Lastname 2 from autoincrement', + age: 100 + } + }); + const post = await tx.post.create({ + data: { + title: 'Title from transaction', + published: false, + author: { + connect: { + id: author.id + } + } + } + }); + return { author, post }; + }); + + console.log('[nodejs] result', superjson.serialize(result).json); + } + + async testTypeTest2() { + const created = await this.prisma.type_test_2.create({ + data: {} + }); + console.log('[nodejs] created', superjson.serialize(created).json); + + const resultSet = await this.prisma.type_test_2.findMany({}); + console.log('[nodejs] resultSet', superjson.serialize(resultSet).json); + + await this.prisma.type_test_2.deleteMany({}); + } + + async testFindManyTypeTest() { + await this.testFindManyTypeTestMySQL(); + await this.testFindManyTypeTestPostgres(); + } + + private async testFindManyTypeTestMySQL() { + if (this.flavour !== 'mysql') { + return; + } + + const resultSet = await this.prisma.type_test.findMany({ + select: { + smallint_column: true, + int_column: true, + bigint_column: true, + float_column: true, + double_column: true, + decimal_column: true, + boolean_column: true, + char_column: true, + varchar_column: true, + text_column: true, + date_column: true, + time_column: true, + datetime_column: true, + timestamp_column: true, + json_column: true, + enum_column: true + } + }); + console.log('[nodejs] findMany resultSet', superjson.serialize(resultSet).json); + + return resultSet; + } + + private async testFindManyTypeTestPostgres() { + if (this.flavour !== 'postgres') { + return; + } + + const resultSet = await this.prisma.type_test.findMany({ + select: { + smallint_column: true, + int_column: true, + bigint_column: true, + float_column: true, + double_column: true, + decimal_column: true, + boolean_column: true, + char_column: true, + varchar_column: true, + text_column: true, + date_column: true, + time_column: true, + datetime_column: true, + timestamp_column: true, + json_column: true, + enum_column: true + } + }); + console.log('[nodejs] findMany resultSet', superjson.serialize(resultSet).json); + + return resultSet; + } + + async testCreateAndDeleteChildParent() { + /* Delete all child and parent records */ + + await this.prisma.child.deleteMany(); + await this.prisma.parent.deleteMany(); + + /* Create a parent with some new children */ + + await this.prisma.child.create({ + data: { + c: 'c1', + c_1: 'foo', + c_2: 'bar', + id: '0001' + } + }); + + await this.prisma.parent.create({ + data: { + p: 'p1', + p_1: '1', + p_2: '2', + id: '0001' + } + }); + + /* Delete the parent */ + + const resultDeleteMany = await this.prisma.parent.deleteMany({ + where: { + p: 'p1' + } + }); + console.log('[nodejs] resultDeleteMany', superjson.serialize(resultDeleteMany).json); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d2e95a9..7e7572c23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -440,6 +440,16 @@ importers: '@xata.io/client': specifier: workspace:* version: link:../client + devDependencies: + '@prisma/client': + specifier: ^5.6.0 + version: 5.6.0(prisma@5.6.0) + prisma: + specifier: ^5.6.0 + version: 5.6.0 + superjson: + specifier: ^2.2.1 + version: 2.2.1 packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -4731,6 +4741,21 @@ packages: dev: true optional: true + /@prisma/client@5.6.0(prisma@5.6.0): + resolution: + { integrity: sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug== } + engines: { node: '>=16.13' } + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee + prisma: 5.6.0 + dev: true + /@prisma/driver-adapter-utils@5.4.2: resolution: { integrity: sha512-V+mtlBXBxQuiOufaSH98S3x2j9G1G0+tgnx3lAlfd6YEeceTCcFFhg85rESW9UIshHvULeGFJsG7+qScgGX0Ng== } @@ -4740,6 +4765,17 @@ packages: - supports-color dev: false + /@prisma/engines-version@5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee: + resolution: + { integrity: sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw== } + dev: true + + /@prisma/engines@5.6.0: + resolution: + { integrity: sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw== } + requiresBuild: true + dev: true + /@protobufjs/aspromise@1.1.2: resolution: { integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== } @@ -7424,6 +7460,14 @@ packages: engines: { node: '>= 0.6' } dev: true + /copy-anything@3.0.5: + resolution: + { integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== } + engines: { node: '>=12.13' } + dependencies: + is-what: 4.1.16 + dev: true + /core-js-compat@3.32.2: resolution: { integrity: sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ== } @@ -10595,6 +10639,12 @@ packages: call-bind: 1.0.2 dev: true + /is-what@4.1.16: + resolution: + { integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== } + engines: { node: '>=12.13' } + dev: true + /is-windows@1.0.2: resolution: { integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== } @@ -13587,6 +13637,16 @@ packages: parse-ms: 3.0.0 dev: false + /prisma@5.6.0: + resolution: + { integrity: sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A== } + engines: { node: '>=16.13' } + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.6.0 + dev: true + /proc-log@1.0.0: resolution: { integrity: sha512-aCk8AO51s+4JyuYGg3Q/a6gnrlDO09NpVWePtjp7xwphcoQ04x5WAfCyugcsbLooWcMJ87CLkD4+604IckEdhg== } @@ -15185,6 +15245,14 @@ packages: acorn: 8.10.0 dev: true + /superjson@2.2.1: + resolution: + { integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA== } + engines: { node: '>=16' } + dependencies: + copy-anything: 3.0.5 + dev: true + /supports-color@5.5.0: resolution: { integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== } From 93d6c3159611414963055f2caadf42090aae7170 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 28 Nov 2023 14:47:19 +0100 Subject: [PATCH 10/10] Update packages/plugin-client-prisma/src/driver.ts --- packages/plugin-client-prisma/src/driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-client-prisma/src/driver.ts b/packages/plugin-client-prisma/src/driver.ts index ccb4f4bff..a9dbcf983 100644 --- a/packages/plugin-client-prisma/src/driver.ts +++ b/packages/plugin-client-prisma/src/driver.ts @@ -63,7 +63,7 @@ export class PrismaXataHTTP extends XataQueryable implements DriverAdapter { override async performIO(query: Query): Promise> { const { sql, args: values } = query; - return ok(await this.xata.sql(sql, values, { arrayMode: true, fullResults: true })); + return ok(await this.xata.sql(sql, values)); } startTransaction(): Promise> {