@onreza/prisma-adapter-bun 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ONREZA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # @onreza/prisma-adapter-bun
2
+
3
+ Prisma 7+ driver adapter for [Bun.sql](https://bun.com/docs/api/sql) — use Bun's built-in PostgreSQL client instead of `pg`.
4
+
5
+ - Zero external database dependencies — uses Bun's native PostgreSQL bindings
6
+ - Full Prisma compatibility — CRUD, relations, transactions, migrations, raw queries, TypedSQL
7
+ - Type-safe — ships as TypeScript source, ready to use with Bun
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun add @onreza/prisma-adapter-bun @prisma/driver-adapter-utils
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Prisma Schema
18
+
19
+ ```prisma
20
+ // prisma/schema.prisma
21
+ datasource db {
22
+ provider = "postgresql"
23
+ }
24
+
25
+ generator client {
26
+ provider = "prisma-client"
27
+ output = "./generated"
28
+ }
29
+
30
+ model User {
31
+ id Int @id @default(autoincrement())
32
+ email String @unique
33
+ name String?
34
+ }
35
+ ```
36
+
37
+ ### 2. Prisma Config (for migrations)
38
+
39
+ ```ts
40
+ // prisma.config.ts
41
+ import { defineConfig } from "prisma/config";
42
+
43
+ export default defineConfig({
44
+ schema: "prisma/schema.prisma",
45
+ datasource: {
46
+ url: process.env.DATABASE_URL!,
47
+ },
48
+ });
49
+ ```
50
+
51
+ ### 3. Generate & Migrate
52
+
53
+ ```bash
54
+ bunx prisma generate
55
+ DATABASE_URL="postgres://user:pass@localhost:5432/mydb" bunx prisma db push
56
+ ```
57
+
58
+ ### 4. Use
59
+
60
+ ```ts
61
+ import { PrismaClient } from "./prisma/generated/client";
62
+ import { PrismaBun } from "@onreza/prisma-adapter-bun";
63
+
64
+ const adapter = new PrismaBun("postgres://user:pass@localhost:5432/mydb");
65
+ const prisma = new PrismaClient({ adapter });
66
+
67
+ const users = await prisma.user.findMany();
68
+ console.log(users);
69
+ ```
70
+
71
+ ## Configuration
72
+
73
+ ### Connection String
74
+
75
+ ```ts
76
+ const adapter = new PrismaBun("postgres://user:pass@localhost:5432/mydb");
77
+ ```
78
+
79
+ ### Connection Options
80
+
81
+ ```ts
82
+ import { SQL } from "bun";
83
+
84
+ const adapter = new PrismaBun({
85
+ hostname: "localhost",
86
+ port: 5432,
87
+ database: "mydb",
88
+ username: "user",
89
+ password: "pass",
90
+ tls: true,
91
+ } satisfies SQL.PostgresOrMySQLOptions);
92
+ ```
93
+
94
+ ### Existing Bun.sql Client
95
+
96
+ ```ts
97
+ import { SQL } from "bun";
98
+
99
+ const client = new SQL("postgres://user:pass@localhost:5432/mydb");
100
+ const adapter = new PrismaBun(client);
101
+ ```
102
+
103
+ ### Schema Option
104
+
105
+ ```ts
106
+ const adapter = new PrismaBun("postgres://...", { schema: "my_schema" });
107
+ ```
108
+
109
+ ## API
110
+
111
+ ### `PrismaBun` (factory)
112
+
113
+ The main export. Implements Prisma's `SqlMigrationAwareDriverAdapterFactory`.
114
+
115
+ ```ts
116
+ new PrismaBun(config: string | URL | SQL.PostgresOrMySQLOptions, options?: PrismaBunOptions)
117
+ new PrismaBun(client: SQL, options?: PrismaBunOptions)
118
+ ```
119
+
120
+ **Options:**
121
+
122
+ | Option | Type | Default | Description |
123
+ |----------|----------|------------|-------------------------|
124
+ | `schema` | `string` | `"public"` | PostgreSQL search path |
125
+
126
+ **Methods:**
127
+
128
+ | Method | Description |
129
+ |---------------------|-----------------------------------------------------|
130
+ | `connect()` | Creates a new connection and returns an adapter |
131
+ | `connectToShadowDb()` | Creates a temporary database for Prisma Migrate |
132
+
133
+ ### `PrismaBunAdapter`
134
+
135
+ Low-level adapter for direct use (without factory). Implements `SqlDriverAdapter`.
136
+
137
+ ```ts
138
+ import { SQL } from "bun";
139
+ import { PrismaBunAdapter } from "@onreza/prisma-adapter-bun";
140
+
141
+ const client = new SQL("postgres://...");
142
+ const adapter = new PrismaBunAdapter(client);
143
+ ```
144
+
145
+ ## Supported Features
146
+
147
+ | Feature | Status |
148
+ |-----------------------------|--------|
149
+ | CRUD (create, read, update, delete) | Supported |
150
+ | Relations (1:1, 1:N, M:N) | Supported |
151
+ | Interactive transactions | Supported |
152
+ | Batch transactions | Supported |
153
+ | Isolation levels | Supported |
154
+ | `$queryRaw` / `$executeRaw`| Supported |
155
+ | TypedSQL | Supported |
156
+ | Prisma Migrate | Supported |
157
+ | Shadow database | Supported |
158
+ | All PostgreSQL scalar types | Supported |
159
+ | Array types (`String[]`, etc.) | Supported |
160
+ | Json / Bytes / Decimal / BigInt | Supported |
161
+ | Enums | Supported |
162
+ | Connection pooling | Built-in (Bun.sql) |
163
+
164
+ ## Requirements
165
+
166
+ - **Bun** >= 1.2.0 (for `Bun.sql` support)
167
+ - **PostgreSQL** >= 12
168
+ - **Prisma** >= 7.0.0
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ # Install dependencies
174
+ bun install
175
+
176
+ # Run unit tests (no database needed)
177
+ bun test tests/conversion.test.ts tests/errors.test.ts
178
+
179
+ # Run integration tests (needs PostgreSQL)
180
+ DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres" bun test tests/adapter.test.ts
181
+
182
+ # Run e2e tests (needs Docker)
183
+ bun test --timeout 120000 tests/factory.test.ts tests/prisma.test.ts
184
+
185
+ # Run all tests
186
+ DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres" bun test --timeout 120000
187
+
188
+ # Type check
189
+ bunx tsc --noEmit
190
+ ```
191
+
192
+ ## How It Works
193
+
194
+ This adapter bridges Prisma's driver adapter protocol with Bun's built-in `Bun.sql` PostgreSQL client:
195
+
196
+ 1. **Queries** — Prisma generates parameterized SQL (`$1`, `$2`, ...). The adapter passes them to `Bun.sql.unsafe(sql, values)`, which binds parameters at the PostgreSQL wire protocol level.
197
+
198
+ 2. **Type Mapping** — PostgreSQL column types are mapped to Prisma's `ColumnType` enum. Since Bun.sql doesn't yet expose column OID metadata, types are inferred from JavaScript values. When Bun adds `.columns` support, the adapter will automatically use native OIDs.
199
+
200
+ 3. **Transactions** — Uses `Bun.sql.reserve()` to get a dedicated connection, then sends `BEGIN`/`COMMIT`/`ROLLBACK` as Prisma expects.
201
+
202
+ 4. **Migrations** — The factory's `connectToShadowDb()` creates a temporary database for Prisma Migrate, and cleans it up on dispose.
203
+
204
+ ## License
205
+
206
+ MIT
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@onreza/prisma-adapter-bun",
3
+ "version": "0.3.0",
4
+ "description": "Prisma 7+ driver adapter for Bun.sql — use Bun's built-in PostgreSQL client instead of pg",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "module": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ONREZA/prisma-adapter-bun.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/ONREZA/prisma-adapter-bun/issues"
22
+ },
23
+ "homepage": "https://github.com/ONREZA/prisma-adapter-bun#readme",
24
+ "keywords": [
25
+ "prisma",
26
+ "bun",
27
+ "bun-sql",
28
+ "postgresql",
29
+ "postgres",
30
+ "driver-adapter",
31
+ "database",
32
+ "orm"
33
+ ],
34
+ "engines": {
35
+ "bun": ">=1.2.0"
36
+ },
37
+ "dependencies": {
38
+ "@prisma/driver-adapter-utils": "^7.3.0"
39
+ },
40
+ "devDependencies": {
41
+ "@biomejs/biome": "^2.3.14",
42
+ "@commitlint/types": "^20.4.0",
43
+ "@prisma/client": "^7.3.0",
44
+ "@types/bun": "latest",
45
+ "lefthook": "^2.1.0",
46
+ "onreza-release": "^2.3.0",
47
+ "prisma": "^7.3.0"
48
+ },
49
+ "peerDependencies": {
50
+ "typescript": "^5"
51
+ },
52
+ "scripts": {
53
+ "lint": "biome check",
54
+ "lint:fix": "biome check --write",
55
+ "format": "biome format --write",
56
+ "typecheck": "tsc --noEmit",
57
+ "check": "biome check && tsc --noEmit",
58
+ "test": "bun test tests/conversion.test.ts tests/errors.test.ts",
59
+ "test:integration": "bun test tests/adapter.test.ts",
60
+ "test:e2e": "bun test --timeout 120000 tests/factory.test.ts tests/prisma.test.ts",
61
+ "test:all": "bun test --timeout 120000",
62
+ "prepublishOnly": "bunx tsc --noEmit",
63
+ "release": "onreza-release",
64
+ "release:dry": "onreza-release --dry-run",
65
+ "prepare": "lefthook install || true"
66
+ }
67
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,256 @@
1
+ import {
2
+ type ColumnType,
3
+ type ConnectionInfo,
4
+ DriverAdapterError,
5
+ type IsolationLevel,
6
+ type SqlDriverAdapter,
7
+ type SqlQuery,
8
+ type SqlQueryable,
9
+ type SqlResultSet,
10
+ type Transaction,
11
+ type TransactionOptions,
12
+ } from "@prisma/driver-adapter-utils";
13
+ import type { SQL as BunSQL, ReservedSQL } from "bun";
14
+ import {
15
+ fieldToColumnType,
16
+ findFirstNonNullInColumn,
17
+ inferOidFromValue,
18
+ mapArg,
19
+ resultNormalizers,
20
+ UnsupportedNativeDataType,
21
+ } from "./conversion.ts";
22
+ import { convertDriverError } from "./errors.ts";
23
+ import type { ColumnMetadata, PrismaBunOptions } from "./types.ts";
24
+
25
+ const ADAPTER_NAME = "@onreza/prisma-adapter-bun";
26
+
27
+ const VALID_ISOLATION_LEVELS = new Set(["READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE"]);
28
+
29
+ type QueryResult = {
30
+ columns: ColumnMetadata[];
31
+ rows: unknown[][];
32
+ rowCount: number;
33
+ };
34
+
35
+ class BunQueryable implements SqlQueryable {
36
+ readonly provider = "postgres" as const;
37
+ readonly adapterName = ADAPTER_NAME;
38
+
39
+ constructor(
40
+ protected readonly client: BunSQL | ReservedSQL,
41
+ protected readonly bunOptions?: PrismaBunOptions,
42
+ ) {}
43
+
44
+ protected async performIO(query: SqlQuery): Promise<QueryResult> {
45
+ const { sql, args, argTypes } = query;
46
+ const values = args.map((arg, i) => {
47
+ const argType = argTypes[i];
48
+ if (!argType) return arg;
49
+ return mapArg(arg, argType);
50
+ });
51
+
52
+ try {
53
+ // Execute query in regular (object) mode
54
+ const result = await this.client.unsafe(sql, values);
55
+ const resultRecord = result as unknown as Record<string, unknown>;
56
+ const rowCount: number = (resultRecord.count as number) ?? result.length;
57
+
58
+ // Primary path: use .columns metadata if Bun.sql exposes it
59
+ const rawColumns = resultRecord.columns;
60
+ if (Array.isArray(rawColumns) && rawColumns.length > 0) {
61
+ const columns: ColumnMetadata[] = rawColumns.map((c: Record<string, unknown>) => ({
62
+ name: String(c.name),
63
+ type: Number(c.type),
64
+ }));
65
+ // Convert object rows to array rows using column order from metadata
66
+ const objectRows = result as unknown as Record<string, unknown>[];
67
+ const rows: unknown[][] = objectRows.map((row) => columns.map((col) => row[col.name]));
68
+ return { columns, rowCount, rows };
69
+ }
70
+
71
+ // Fallback: extract column names from object keys, infer types from values
72
+ if (result.length === 0) {
73
+ return { columns: [], rowCount, rows: [] };
74
+ }
75
+
76
+ const firstRow = result[0] as Record<string, unknown>;
77
+ const columnNames = Object.keys(firstRow);
78
+
79
+ // Convert object rows to array rows
80
+ const objectRows = result as unknown as Record<string, unknown>[];
81
+ const rows: unknown[][] = objectRows.map((row) => columnNames.map((name) => row[name]));
82
+
83
+ // Infer OID for each column from first non-null value
84
+ const columns: ColumnMetadata[] = columnNames.map((name, i) => ({
85
+ name,
86
+ type: inferOidFromValue(findFirstNonNullInColumn(rows, i)),
87
+ }));
88
+
89
+ return { columns, rowCount, rows };
90
+ } catch (e) {
91
+ throw new DriverAdapterError(convertDriverError(e));
92
+ }
93
+ }
94
+
95
+ async queryRaw(query: SqlQuery): Promise<SqlResultSet> {
96
+ const { columns, rows } = await this.performIO(query);
97
+
98
+ const columnNames = columns.map((col) => col.name);
99
+ const columnTypes = this.mapColumnTypes(columns);
100
+ this.normalizeRows(columns, rows);
101
+
102
+ return { columnNames, columnTypes, rows };
103
+ }
104
+
105
+ private mapColumnTypes(columns: ColumnMetadata[]): ColumnType[] {
106
+ try {
107
+ return columns.map((col) => fieldToColumnType(col.type));
108
+ } catch (e) {
109
+ if (e instanceof UnsupportedNativeDataType) {
110
+ throw new DriverAdapterError({
111
+ kind: "UnsupportedNativeDataType",
112
+ type: e.type,
113
+ });
114
+ }
115
+ throw e;
116
+ }
117
+ }
118
+
119
+ private normalizeRows(columns: ColumnMetadata[], rows: unknown[][]): void {
120
+ for (let i = 0; i < columns.length; i++) {
121
+ const col = columns[i];
122
+ if (!col) continue;
123
+ const normalizer = resultNormalizers[col.type];
124
+ if (normalizer) {
125
+ for (const row of rows) {
126
+ if (row[i] !== null && row[i] !== undefined) {
127
+ row[i] = normalizer(row[i]);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ async executeRaw(query: SqlQuery): Promise<number> {
135
+ const { rowCount } = await this.performIO(query);
136
+ return rowCount;
137
+ }
138
+ }
139
+
140
+ export class BunTransaction extends BunQueryable implements Transaction {
141
+ readonly txOptions: TransactionOptions = { usePhantomQuery: false };
142
+ private released = false;
143
+
144
+ constructor(
145
+ protected override readonly client: ReservedSQL,
146
+ bunOptions?: PrismaBunOptions,
147
+ ) {
148
+ super(client, bunOptions);
149
+ }
150
+
151
+ get options(): TransactionOptions {
152
+ return this.txOptions;
153
+ }
154
+
155
+ private release(): void {
156
+ if (!this.released) {
157
+ this.released = true;
158
+ this.client.release();
159
+ }
160
+ }
161
+
162
+ commit(): Promise<void> {
163
+ // Prisma Engine sends COMMIT via executeRaw before calling commit().
164
+ // We only release the reserved connection back to the pool.
165
+ this.release();
166
+ return Promise.resolve();
167
+ }
168
+
169
+ rollback(): Promise<void> {
170
+ // Prisma Engine sends ROLLBACK via executeRaw before calling rollback().
171
+ // We only release the reserved connection.
172
+ this.release();
173
+ return Promise.resolve();
174
+ }
175
+ }
176
+
177
+ export class PrismaBunAdapter extends BunQueryable implements SqlDriverAdapter {
178
+ private readonly disposeCallback?: () => Promise<void>;
179
+
180
+ constructor(
181
+ protected override readonly client: BunSQL,
182
+ bunOptions?: PrismaBunOptions,
183
+ disposeCallback?: () => Promise<void>,
184
+ ) {
185
+ super(client, bunOptions);
186
+ this.disposeCallback = disposeCallback;
187
+ }
188
+
189
+ async startTransaction(isolationLevel?: IsolationLevel): Promise<Transaction> {
190
+ if (isolationLevel && !VALID_ISOLATION_LEVELS.has(isolationLevel.toUpperCase())) {
191
+ throw new DriverAdapterError({
192
+ code: "INVALID_ISOLATION_LEVEL",
193
+ column: undefined,
194
+ detail: undefined,
195
+ hint: undefined,
196
+ kind: "postgres",
197
+ message: `Invalid isolation level: ${isolationLevel}`,
198
+ severity: "ERROR",
199
+ });
200
+ }
201
+
202
+ const reserved = await this.client.reserve();
203
+
204
+ try {
205
+ const tx = new BunTransaction(reserved, this.bunOptions);
206
+
207
+ await tx.executeRaw({ args: [], argTypes: [], sql: "BEGIN" });
208
+
209
+ if (isolationLevel) {
210
+ await tx.executeRaw({
211
+ args: [],
212
+ argTypes: [],
213
+ sql: `SET TRANSACTION ISOLATION LEVEL ${isolationLevel.toUpperCase()}`,
214
+ });
215
+ }
216
+
217
+ return tx;
218
+ } catch (error) {
219
+ try {
220
+ await reserved.unsafe("ROLLBACK");
221
+ } catch {
222
+ // Ignore rollback errors during cleanup
223
+ }
224
+ reserved.release();
225
+ throw error;
226
+ }
227
+ }
228
+
229
+ async executeScript(script: string): Promise<void> {
230
+ try {
231
+ await this.client.unsafe(script).simple();
232
+ } catch (e) {
233
+ throw new DriverAdapterError(convertDriverError(e));
234
+ }
235
+ }
236
+
237
+ getConnectionInfo(): ConnectionInfo {
238
+ return {
239
+ maxBindValues: 65535,
240
+ schemaName: this.bunOptions?.schema ?? "public",
241
+ supportsRelationJoins: true,
242
+ };
243
+ }
244
+
245
+ async dispose(): Promise<void> {
246
+ try {
247
+ if (this.disposeCallback) {
248
+ await this.disposeCallback();
249
+ } else {
250
+ await this.client.close();
251
+ }
252
+ } catch (e) {
253
+ throw new DriverAdapterError(convertDriverError(e));
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,388 @@
1
+ import { type ArgType, type ColumnType, ColumnTypeEnum } from "@prisma/driver-adapter-utils";
2
+
3
+ // Top-level regex constants (biome: useTopLevelRegex)
4
+ const RE_TIMESTAMPTZ_OFFSET = /([+-]\d{2})$/;
5
+ const RE_TIMETZ_STRIP = /[+-]\d{2}(:\d{2})?$/;
6
+ const RE_MONEY_SYMBOL = /^\$/;
7
+ const RE_PG_ESCAPE_BACKSLASH = /\\/g;
8
+ const RE_PG_ESCAPE_QUOTE = /"/g;
9
+
10
+ // PostgreSQL OIDs (from pg_type system catalog)
11
+ export const PgOid = {
12
+ BIT: 1560,
13
+ BIT_ARRAY: 1561,
14
+ BOOL: 16,
15
+
16
+ // Array types
17
+ BOOL_ARRAY: 1000,
18
+ BPCHAR: 1042,
19
+ BPCHAR_ARRAY: 1014,
20
+ BYTEA: 17,
21
+ BYTEA_ARRAY: 1001,
22
+ CHAR: 18,
23
+ CHAR_ARRAY: 1002,
24
+ CIDR: 650,
25
+ CIDR_ARRAY: 651,
26
+ DATE: 1082,
27
+ DATE_ARRAY: 1182,
28
+ FLOAT4: 700,
29
+ FLOAT4_ARRAY: 1021,
30
+ FLOAT8: 701,
31
+ FLOAT8_ARRAY: 1022,
32
+ INET: 869,
33
+ INET_ARRAY: 1041,
34
+ INT2: 21,
35
+ INT2_ARRAY: 1005,
36
+ INT4: 23,
37
+ INT4_ARRAY: 1007,
38
+ INT8: 20,
39
+ INT8_ARRAY: 1016,
40
+ JSON: 114,
41
+ JSON_ARRAY: 199,
42
+ JSONB: 3802,
43
+ JSONB_ARRAY: 3807,
44
+ MONEY: 790,
45
+ MONEY_ARRAY: 791,
46
+ NAME: 19,
47
+ NAME_ARRAY: 1003,
48
+ NUMERIC: 1700,
49
+ NUMERIC_ARRAY: 1231,
50
+ OID: 26,
51
+ OID_ARRAY: 1028,
52
+ TEXT: 25,
53
+ TEXT_ARRAY: 1009,
54
+ TIME: 1083,
55
+ TIME_ARRAY: 1183,
56
+ TIMESTAMP: 1114,
57
+ TIMESTAMP_ARRAY: 1115,
58
+ TIMESTAMPTZ: 1184,
59
+ TIMESTAMPTZ_ARRAY: 1185,
60
+ TIMETZ: 1266,
61
+ TIMETZ_ARRAY: 1270,
62
+ UUID: 2950,
63
+ UUID_ARRAY: 2951,
64
+ VARBIT: 1562,
65
+ VARBIT_ARRAY: 1563,
66
+ VARCHAR: 1043,
67
+ VARCHAR_ARRAY: 1015,
68
+ XML: 142,
69
+ XML_ARRAY: 143,
70
+ } as const;
71
+
72
+ // OID threshold: types >= this are user-defined (enums, composites, etc.)
73
+ const FIRST_NORMAL_OBJECT_ID = 16384;
74
+
75
+ export class UnsupportedNativeDataType extends Error {
76
+ type: string;
77
+ constructor(oid: number) {
78
+ const message = `Unsupported column type with OID ${oid}`;
79
+ super(message);
80
+ this.type = `${oid}`;
81
+ }
82
+ }
83
+
84
+ const scalarMapping: Record<number, ColumnType> = {
85
+ [PgOid.INT2]: ColumnTypeEnum.Int32,
86
+ [PgOid.INT4]: ColumnTypeEnum.Int32,
87
+ [PgOid.INT8]: ColumnTypeEnum.Int64,
88
+ [PgOid.FLOAT4]: ColumnTypeEnum.Float,
89
+ [PgOid.FLOAT8]: ColumnTypeEnum.Double,
90
+ [PgOid.BOOL]: ColumnTypeEnum.Boolean,
91
+ [PgOid.DATE]: ColumnTypeEnum.Date,
92
+ [PgOid.TIME]: ColumnTypeEnum.Time,
93
+ [PgOid.TIMETZ]: ColumnTypeEnum.Time,
94
+ [PgOid.TIMESTAMP]: ColumnTypeEnum.DateTime,
95
+ [PgOid.TIMESTAMPTZ]: ColumnTypeEnum.DateTime,
96
+ [PgOid.NUMERIC]: ColumnTypeEnum.Numeric,
97
+ [PgOid.MONEY]: ColumnTypeEnum.Numeric,
98
+ [PgOid.JSON]: ColumnTypeEnum.Json,
99
+ [PgOid.JSONB]: ColumnTypeEnum.Json,
100
+ [PgOid.UUID]: ColumnTypeEnum.Uuid,
101
+ [PgOid.OID]: ColumnTypeEnum.Int64,
102
+ [PgOid.BPCHAR]: ColumnTypeEnum.Text,
103
+ [PgOid.TEXT]: ColumnTypeEnum.Text,
104
+ [PgOid.VARCHAR]: ColumnTypeEnum.Text,
105
+ [PgOid.BIT]: ColumnTypeEnum.Text,
106
+ [PgOid.VARBIT]: ColumnTypeEnum.Text,
107
+ [PgOid.INET]: ColumnTypeEnum.Text,
108
+ [PgOid.CIDR]: ColumnTypeEnum.Text,
109
+ [PgOid.XML]: ColumnTypeEnum.Text,
110
+ [PgOid.NAME]: ColumnTypeEnum.Text,
111
+ [PgOid.CHAR]: ColumnTypeEnum.Character,
112
+ [PgOid.BYTEA]: ColumnTypeEnum.Bytes,
113
+ };
114
+
115
+ const arrayMapping: Record<number, ColumnType> = {
116
+ [PgOid.INT2_ARRAY]: ColumnTypeEnum.Int32Array,
117
+ [PgOid.INT4_ARRAY]: ColumnTypeEnum.Int32Array,
118
+ [PgOid.INT8_ARRAY]: ColumnTypeEnum.Int64Array,
119
+ [PgOid.FLOAT4_ARRAY]: ColumnTypeEnum.FloatArray,
120
+ [PgOid.FLOAT8_ARRAY]: ColumnTypeEnum.DoubleArray,
121
+ [PgOid.BOOL_ARRAY]: ColumnTypeEnum.BooleanArray,
122
+ [PgOid.DATE_ARRAY]: ColumnTypeEnum.DateArray,
123
+ [PgOid.TIME_ARRAY]: ColumnTypeEnum.TimeArray,
124
+ [PgOid.TIMETZ_ARRAY]: ColumnTypeEnum.TimeArray,
125
+ [PgOid.TIMESTAMP_ARRAY]: ColumnTypeEnum.DateTimeArray,
126
+ [PgOid.TIMESTAMPTZ_ARRAY]: ColumnTypeEnum.DateTimeArray,
127
+ [PgOid.NUMERIC_ARRAY]: ColumnTypeEnum.NumericArray,
128
+ [PgOid.MONEY_ARRAY]: ColumnTypeEnum.NumericArray,
129
+ [PgOid.JSON_ARRAY]: ColumnTypeEnum.JsonArray,
130
+ [PgOid.JSONB_ARRAY]: ColumnTypeEnum.JsonArray,
131
+ [PgOid.UUID_ARRAY]: ColumnTypeEnum.UuidArray,
132
+ [PgOid.OID_ARRAY]: ColumnTypeEnum.Int64Array,
133
+ [PgOid.BPCHAR_ARRAY]: ColumnTypeEnum.TextArray,
134
+ [PgOid.TEXT_ARRAY]: ColumnTypeEnum.TextArray,
135
+ [PgOid.VARCHAR_ARRAY]: ColumnTypeEnum.TextArray,
136
+ [PgOid.BIT_ARRAY]: ColumnTypeEnum.TextArray,
137
+ [PgOid.VARBIT_ARRAY]: ColumnTypeEnum.TextArray,
138
+ [PgOid.INET_ARRAY]: ColumnTypeEnum.TextArray,
139
+ [PgOid.CIDR_ARRAY]: ColumnTypeEnum.TextArray,
140
+ [PgOid.XML_ARRAY]: ColumnTypeEnum.TextArray,
141
+ [PgOid.NAME_ARRAY]: ColumnTypeEnum.TextArray,
142
+ [PgOid.CHAR_ARRAY]: ColumnTypeEnum.CharacterArray,
143
+ [PgOid.BYTEA_ARRAY]: ColumnTypeEnum.BytesArray,
144
+ };
145
+
146
+ export function fieldToColumnType(oid: number): ColumnType {
147
+ const scalar = scalarMapping[oid];
148
+ if (scalar !== undefined) return scalar;
149
+
150
+ const array = arrayMapping[oid];
151
+ if (array !== undefined) return array;
152
+
153
+ // User-defined types (enums, composites) are treated as Text
154
+ if (oid >= FIRST_NORMAL_OBJECT_ID) return ColumnTypeEnum.Text;
155
+
156
+ throw new UnsupportedNativeDataType(oid);
157
+ }
158
+
159
+ // --- Value normalizers (post-processing Bun.sql results) ---
160
+
161
+ function pad2(n: number): string {
162
+ return n < 10 ? `0${n}` : `${n}`;
163
+ }
164
+
165
+ function pad3(n: number): string {
166
+ if (n < 10) return `00${n}`;
167
+ if (n < 100) return `0${n}`;
168
+ return `${n}`;
169
+ }
170
+
171
+ function formatDateTime(date: Date): string {
172
+ return (
173
+ `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +
174
+ `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.${pad3(date.getUTCMilliseconds())}`
175
+ );
176
+ }
177
+
178
+ function formatDate(date: Date): string {
179
+ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;
180
+ }
181
+
182
+ function formatTime(date: Date): string {
183
+ return `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.${pad3(date.getUTCMilliseconds())}`;
184
+ }
185
+
186
+ // Normalize a timestamp string to ISO-like format: "2024-01-01 12:00:00" -> "2024-01-01T12:00:00.000+00:00"
187
+ function normalizeTimestamp(value: unknown): unknown {
188
+ if (value instanceof Date) {
189
+ return `${formatDateTime(value)}+00:00`;
190
+ }
191
+ if (typeof value === "string") {
192
+ return `${value.replace(" ", "T")}+00:00`;
193
+ }
194
+ return value;
195
+ }
196
+
197
+ function normalizeTimestamptz(value: unknown): unknown {
198
+ if (value instanceof Date) {
199
+ return `${formatDateTime(value)}+00:00`;
200
+ }
201
+ if (typeof value === "string") {
202
+ // Normalize various timezone formats to +00:00
203
+ return value.replace(" ", "T").replace(RE_TIMESTAMPTZ_OFFSET, "$1:00");
204
+ }
205
+ return value;
206
+ }
207
+
208
+ function normalizeTimetz(value: unknown): unknown {
209
+ if (typeof value === "string") {
210
+ // Remove timezone offset from time value
211
+ return value.replace(RE_TIMETZ_STRIP, "");
212
+ }
213
+ return value;
214
+ }
215
+
216
+ function normalizeDate(value: unknown): unknown {
217
+ if (value instanceof Date) {
218
+ return formatDate(value);
219
+ }
220
+ return value;
221
+ }
222
+
223
+ function normalizeTime(value: unknown): unknown {
224
+ if (value instanceof Date) {
225
+ return formatTime(value);
226
+ }
227
+ return value;
228
+ }
229
+
230
+ function normalizeNumeric(value: unknown): unknown {
231
+ return String(value);
232
+ }
233
+
234
+ function normalizeMoney(value: unknown): unknown {
235
+ const s = String(value);
236
+ return s.replace(RE_MONEY_SYMBOL, "");
237
+ }
238
+
239
+ function normalizeJson(value: unknown): unknown {
240
+ if (typeof value === "object" && value !== null) {
241
+ return JSON.stringify(value);
242
+ }
243
+ return value;
244
+ }
245
+
246
+ function normalizeBytes(value: unknown): unknown {
247
+ if (value instanceof Uint8Array || value instanceof Buffer) {
248
+ return Buffer.from(value);
249
+ }
250
+ return value;
251
+ }
252
+
253
+ function normalizeArray(value: unknown, elementNormalizer: (v: unknown) => unknown): unknown {
254
+ if (Array.isArray(value)) {
255
+ return value.map((el) => (el === null ? null : elementNormalizer(el)));
256
+ }
257
+ return value;
258
+ }
259
+
260
+ type Normalizer = (value: unknown) => unknown;
261
+
262
+ export const resultNormalizers: Record<number, Normalizer> = {
263
+ [PgOid.NUMERIC]: normalizeNumeric,
264
+ [PgOid.MONEY]: normalizeMoney,
265
+ [PgOid.TIME]: normalizeTime,
266
+ [PgOid.TIMETZ]: normalizeTimetz,
267
+ [PgOid.DATE]: normalizeDate,
268
+ [PgOid.TIMESTAMP]: normalizeTimestamp,
269
+ [PgOid.TIMESTAMPTZ]: normalizeTimestamptz,
270
+ [PgOid.JSON]: normalizeJson,
271
+ [PgOid.JSONB]: normalizeJson,
272
+ [PgOid.BYTEA]: normalizeBytes,
273
+
274
+ // Array normalizers
275
+ [PgOid.NUMERIC_ARRAY]: (v) => normalizeArray(v, normalizeNumeric),
276
+ [PgOid.MONEY_ARRAY]: (v) => normalizeArray(v, normalizeMoney),
277
+ [PgOid.TIME_ARRAY]: (v) => normalizeArray(v, normalizeTime),
278
+ [PgOid.TIMETZ_ARRAY]: (v) => normalizeArray(v, normalizeTimetz),
279
+ [PgOid.DATE_ARRAY]: (v) => normalizeArray(v, normalizeDate),
280
+ [PgOid.TIMESTAMP_ARRAY]: (v) => normalizeArray(v, normalizeTimestamp),
281
+ [PgOid.TIMESTAMPTZ_ARRAY]: (v) => normalizeArray(v, normalizeTimestamptz),
282
+ [PgOid.JSON_ARRAY]: (v) => normalizeArray(v, normalizeJson),
283
+ [PgOid.JSONB_ARRAY]: (v) => normalizeArray(v, normalizeJson),
284
+ [PgOid.BYTEA_ARRAY]: (v) => normalizeArray(v, normalizeBytes),
285
+ [PgOid.BIT_ARRAY]: (v) => normalizeArray(v, String),
286
+ [PgOid.VARBIT_ARRAY]: (v) => normalizeArray(v, String),
287
+ [PgOid.XML_ARRAY]: (v) => normalizeArray(v, String),
288
+ };
289
+
290
+ // --- Type inference (fallback when .columns metadata is unavailable) ---
291
+
292
+ function inferArrayOid(arr: unknown[]): number {
293
+ if (arr.length === 0) return PgOid.TEXT_ARRAY;
294
+ const first = arr.find((v) => v !== null && v !== undefined);
295
+ if (first === undefined) return PgOid.TEXT_ARRAY;
296
+ if (typeof first === "number") {
297
+ return Number.isInteger(first) ? PgOid.INT8_ARRAY : PgOid.FLOAT8_ARRAY;
298
+ }
299
+ if (typeof first === "boolean") return PgOid.BOOL_ARRAY;
300
+ if (first instanceof Date) return PgOid.TIMESTAMPTZ_ARRAY;
301
+ if (typeof first === "object") return PgOid.JSONB_ARRAY;
302
+ return PgOid.TEXT_ARRAY;
303
+ }
304
+
305
+ /**
306
+ * Infer a PostgreSQL OID from a JavaScript value.
307
+ * Used as a fallback when Bun.sql doesn't expose column metadata.
308
+ * The inferred OID is then mapped to ColumnType via fieldToColumnType().
309
+ */
310
+ export function inferOidFromValue(value: unknown): number {
311
+ if (value === null || value === undefined) return PgOid.TEXT;
312
+ if (typeof value === "boolean") return PgOid.BOOL;
313
+ if (typeof value === "bigint") return PgOid.INT8;
314
+ if (typeof value === "number") {
315
+ return Number.isInteger(value) ? PgOid.INT8 : PgOid.FLOAT8;
316
+ }
317
+ if (value instanceof Date) return PgOid.TIMESTAMPTZ;
318
+ if (value instanceof Uint8Array || value instanceof Buffer) return PgOid.BYTEA;
319
+ if (Array.isArray(value)) return inferArrayOid(value);
320
+ if (typeof value === "object") return PgOid.JSONB;
321
+ return PgOid.TEXT;
322
+ }
323
+
324
+ /**
325
+ * Find the first non-null value for a given column across all rows.
326
+ */
327
+ export function findFirstNonNullInColumn(rows: unknown[][], colIndex: number): unknown {
328
+ for (const row of rows) {
329
+ const val = row[colIndex];
330
+ if (val !== null && val !== undefined) return val;
331
+ }
332
+ return null;
333
+ }
334
+
335
+ // --- Array literal formatting (Bun.sql doesn't handle JS arrays in unsafe()) ---
336
+
337
+ function escapePgArrayElement(value: unknown): string {
338
+ if (value === null || value === undefined) return "NULL";
339
+ if (typeof value === "boolean") return value ? "t" : "f";
340
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
341
+ // Strings: escape backslashes and double-quotes, then wrap in quotes
342
+ const s = String(value);
343
+ return `"${s.replace(RE_PG_ESCAPE_BACKSLASH, "\\\\").replace(RE_PG_ESCAPE_QUOTE, '\\"')}"`;
344
+ }
345
+
346
+ /**
347
+ * Convert a JS array to a PostgreSQL array literal string.
348
+ * Bun.sql's `unsafe()` does not serialize JS arrays as PostgreSQL arrays,
349
+ * so we must pass them in `{elem1,elem2,...}` format.
350
+ */
351
+ export function toPgArrayLiteral(arr: unknown[]): string {
352
+ return `{${arr.map(escapePgArrayElement).join(",")}}`;
353
+ }
354
+
355
+ // --- Argument mapping (input parameters) ---
356
+
357
+ export function mapArg(arg: unknown, argType: ArgType): unknown {
358
+ if (arg === null || arg === undefined) return null;
359
+
360
+ if (Array.isArray(arg) && argType.arity === "list") {
361
+ const mapped = arg.map((v) => mapArg(v, { ...argType, arity: "scalar" }));
362
+ return toPgArrayLiteral(mapped);
363
+ }
364
+
365
+ const value = typeof arg === "string" && argType.scalarType === "datetime" ? new Date(arg) : arg;
366
+
367
+ if (value instanceof Date) {
368
+ switch (argType.dbType) {
369
+ case "TIME":
370
+ case "TIMETZ":
371
+ return formatTime(value);
372
+ case "DATE":
373
+ return formatDate(value);
374
+ default:
375
+ return formatDateTime(value);
376
+ }
377
+ }
378
+
379
+ if (typeof value === "string" && argType.scalarType === "bytes") {
380
+ return Buffer.from(value, "base64");
381
+ }
382
+
383
+ if (ArrayBuffer.isView(value)) {
384
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
385
+ }
386
+
387
+ return value;
388
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,175 @@
1
+ import type { Error as AdapterError } from "@prisma/driver-adapter-utils";
2
+ import { SQL } from "bun";
3
+
4
+ const RE_CONSTRAINT_KEY = /Key \((.+?)\)=/;
5
+
6
+ function extractConstraintFields(detail?: string): { fields: string[] } | undefined {
7
+ if (!detail) return;
8
+ // PostgreSQL detail format: "Key (field1, field2)=(value1, value2) already exists."
9
+ const match = detail.match(RE_CONSTRAINT_KEY);
10
+ if (match?.[1]) {
11
+ return { fields: match[1].split(",").map((s) => s.trim()) };
12
+ }
13
+ return;
14
+ }
15
+
16
+ /**
17
+ * Get the PostgreSQL error code from a Bun SQL.PostgresError.
18
+ * Bun.sql stores the PG error code in `errno` (e.g., "42P01"),
19
+ * while `code` contains Bun's internal error code (e.g., "ERR_POSTGRES_SERVER_ERROR").
20
+ */
21
+ function getPgErrorCode(error: SQL.PostgresError): string {
22
+ return error.errno ?? error.code;
23
+ }
24
+
25
+ function mapPostgresError(error: SQL.PostgresError): AdapterError {
26
+ const pgCode = getPgErrorCode(error);
27
+ const base = {
28
+ originalCode: pgCode,
29
+ originalMessage: error.message,
30
+ };
31
+
32
+ switch (pgCode) {
33
+ // Value too long for type
34
+ case "22001":
35
+ return { ...base, column: error.column, kind: "LengthMismatch" };
36
+
37
+ // Numeric value out of range
38
+ case "22003":
39
+ return { ...base, cause: error.message, kind: "ValueOutOfRange" };
40
+
41
+ // Invalid text representation / invalid input syntax
42
+ case "22P02":
43
+ return { ...base, kind: "InvalidInputValue", message: error.message };
44
+
45
+ // Unique violation
46
+ case "23505": {
47
+ const constraint =
48
+ extractConstraintFields(error.detail) ?? (error.constraint ? { index: error.constraint } : undefined);
49
+ return { ...base, constraint, kind: "UniqueConstraintViolation" };
50
+ }
51
+
52
+ // Not null violation
53
+ case "23502": {
54
+ const constraint = error.column ? { fields: [error.column] } : undefined;
55
+ return { ...base, constraint, kind: "NullConstraintViolation" };
56
+ }
57
+
58
+ // Foreign key violation
59
+ case "23503": {
60
+ const constraint =
61
+ extractConstraintFields(error.detail) ?? (error.constraint ? { index: error.constraint } : undefined);
62
+ return { ...base, constraint, kind: "ForeignKeyConstraintViolation" };
63
+ }
64
+
65
+ // Database does not exist
66
+ case "3D000":
67
+ return { ...base, db: error.message, kind: "DatabaseDoesNotExist" };
68
+
69
+ // Database already exists
70
+ case "42P04":
71
+ return { ...base, db: error.message, kind: "DatabaseAlreadyExists" };
72
+
73
+ // Insufficient privilege / authorization
74
+ case "28000":
75
+ return { ...base, kind: "DatabaseAccessDenied" };
76
+
77
+ // Password authentication failed
78
+ case "28P01":
79
+ return { ...base, kind: "AuthenticationFailed" };
80
+
81
+ // Serialization failure (transaction conflict)
82
+ case "40001":
83
+ return { ...base, kind: "TransactionWriteConflict" };
84
+
85
+ // Undefined table
86
+ case "42P01":
87
+ return { ...base, kind: "TableDoesNotExist", table: error.table };
88
+
89
+ // Undefined column
90
+ case "42703":
91
+ return { ...base, column: error.column, kind: "ColumnNotFound" };
92
+
93
+ // Too many connections
94
+ case "53300":
95
+ return { ...base, cause: error.message, kind: "TooManyConnections" };
96
+
97
+ // Default: pass through as raw PostgreSQL error
98
+ default:
99
+ return {
100
+ ...base,
101
+ code: pgCode,
102
+ column: error.column,
103
+ detail: error.detail,
104
+ hint: error.hint,
105
+ kind: "postgres",
106
+ message: error.message,
107
+ severity: error.severity ?? "ERROR",
108
+ };
109
+ }
110
+ }
111
+
112
+ function mapConnectionError(error: Error & { code?: string }): AdapterError {
113
+ const code = error.code ?? "";
114
+ const base = { originalCode: code, originalMessage: error.message };
115
+
116
+ if (code.includes("CONNECTION_CLOSED") || code === "ECONNRESET") {
117
+ return { ...base, kind: "ConnectionClosed" };
118
+ }
119
+
120
+ if (
121
+ code.includes("CONNECTION_TIMEOUT") ||
122
+ code === "ETIMEDOUT" ||
123
+ code.includes("IDLE_TIMEOUT") ||
124
+ code.includes("LIFETIME_TIMEOUT")
125
+ ) {
126
+ return { ...base, kind: "SocketTimeout" };
127
+ }
128
+
129
+ if (code.includes("TLS") || code.includes("SSL")) {
130
+ return { ...base, kind: "TlsConnectionError", reason: error.message };
131
+ }
132
+
133
+ if (code.includes("AUTHENTICATION_FAILED")) {
134
+ return { ...base, kind: "AuthenticationFailed" };
135
+ }
136
+
137
+ if (code === "ENOTFOUND" || code === "ECONNREFUSED") {
138
+ return { ...base, kind: "DatabaseNotReachable" };
139
+ }
140
+
141
+ return {
142
+ ...base,
143
+ code,
144
+ column: undefined,
145
+ detail: undefined,
146
+ hint: undefined,
147
+ kind: "postgres",
148
+ message: error.message,
149
+ severity: "ERROR",
150
+ };
151
+ }
152
+
153
+ export function convertDriverError(error: unknown): AdapterError {
154
+ if (error instanceof SQL.PostgresError) {
155
+ return mapPostgresError(error);
156
+ }
157
+
158
+ if (error instanceof SQL.SQLError) {
159
+ return mapConnectionError(error as Error & { code?: string });
160
+ }
161
+
162
+ if (error instanceof Error) {
163
+ return mapConnectionError(error as Error & { code?: string });
164
+ }
165
+
166
+ return {
167
+ code: "UNKNOWN",
168
+ column: undefined,
169
+ detail: undefined,
170
+ hint: undefined,
171
+ kind: "postgres",
172
+ message: String(error),
173
+ severity: "ERROR",
174
+ };
175
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { SqlDriverAdapter, SqlMigrationAwareDriverAdapterFactory } from "@prisma/driver-adapter-utils";
2
+ import { SQL } from "bun";
3
+ import { PrismaBunAdapter } from "./adapter.ts";
4
+ import type { BunSqlConfig, PrismaBunOptions } from "./types.ts";
5
+
6
+ const ADAPTER_NAME = "@onreza/prisma-adapter-bun";
7
+
8
+ /**
9
+ * Detect a Bun SQL client instance via duck-typing.
10
+ * `instanceof SQL` doesn't work because SQL.prototype is undefined in Bun.
11
+ */
12
+ function isSqlClient(value: unknown): value is SQL {
13
+ const v = value as Record<string, unknown>;
14
+ return (
15
+ typeof value === "function" &&
16
+ typeof v.unsafe === "function" &&
17
+ typeof v.reserve === "function" &&
18
+ typeof v.close === "function"
19
+ );
20
+ }
21
+
22
+ export class PrismaBunFactory implements SqlMigrationAwareDriverAdapterFactory {
23
+ readonly provider = "postgres" as const;
24
+ readonly adapterName = ADAPTER_NAME;
25
+
26
+ private readonly config: BunSqlConfig;
27
+ private readonly bunOptions?: PrismaBunOptions;
28
+ private externalClient: SQL | null = null;
29
+
30
+ constructor(config: BunSqlConfig, options?: PrismaBunOptions);
31
+ constructor(client: SQL, options?: PrismaBunOptions);
32
+ constructor(configOrClient: BunSqlConfig | SQL, options?: PrismaBunOptions) {
33
+ this.bunOptions = options;
34
+ if (isSqlClient(configOrClient)) {
35
+ this.externalClient = configOrClient;
36
+ this.config = {};
37
+ } else {
38
+ this.config = configOrClient;
39
+ }
40
+ }
41
+
42
+ connect(): Promise<SqlDriverAdapter> {
43
+ if (this.externalClient) {
44
+ return Promise.resolve(new PrismaBunAdapter(this.externalClient, this.bunOptions, () => Promise.resolve()));
45
+ }
46
+
47
+ const client = new SQL(this.config as SQL.Options);
48
+
49
+ return Promise.resolve(
50
+ new PrismaBunAdapter(client, this.bunOptions, async () => {
51
+ await client.close();
52
+ }),
53
+ );
54
+ }
55
+
56
+ async connectToShadowDb(): Promise<SqlDriverAdapter> {
57
+ const mainAdapter = await this.connect();
58
+ const database = `prisma_migrate_shadow_db_${crypto.randomUUID()}`;
59
+
60
+ await mainAdapter.executeScript(`CREATE DATABASE "${database}"`);
61
+
62
+ let shadowClient: SQL;
63
+ try {
64
+ const shadowConfig = this.buildShadowConfig(database);
65
+ shadowClient = new SQL(shadowConfig as SQL.Options);
66
+ } catch (error) {
67
+ try {
68
+ await mainAdapter.executeScript(`DROP DATABASE IF EXISTS "${database}"`);
69
+ } finally {
70
+ await mainAdapter.dispose();
71
+ }
72
+ throw error;
73
+ }
74
+
75
+ return new PrismaBunAdapter(shadowClient, this.bunOptions, async () => {
76
+ await shadowClient.close();
77
+ try {
78
+ await mainAdapter.executeScript(`DROP DATABASE IF EXISTS "${database}"`);
79
+ } finally {
80
+ await mainAdapter.dispose();
81
+ }
82
+ });
83
+ }
84
+
85
+ private buildShadowConfig(database: string): SQL.PostgresOrMySQLOptions {
86
+ if (typeof this.config === "string" || this.config instanceof URL) {
87
+ const url = new URL(this.config.toString());
88
+ url.pathname = `/${database}`;
89
+ return { url: url.toString() };
90
+ }
91
+
92
+ if (typeof this.config === "object" && this.config !== null) {
93
+ const opts = this.config as SQL.PostgresOrMySQLOptions;
94
+ if (opts.url) {
95
+ const url = new URL(opts.url.toString());
96
+ url.pathname = `/${database}`;
97
+ return { ...opts, url: url.toString() };
98
+ }
99
+ return { ...opts, database };
100
+ }
101
+
102
+ return { database };
103
+ }
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { PrismaBunAdapter } from "./adapter.ts";
2
+ export { PrismaBunFactory as PrismaBun } from "./factory.ts";
3
+ export type { BunSqlConfig, PrismaBunOptions } from "./types.ts";
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { SQL } from "bun";
2
+
3
+ export type PrismaBunOptions = {
4
+ schema?: string;
5
+ };
6
+
7
+ export type BunSqlConfig = string | URL | SQL.PostgresOrMySQLOptions;
8
+
9
+ export type ColumnMetadata = {
10
+ name: string;
11
+ type: number;
12
+ };