turbine-orm 0.19.2 → 0.22.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/README.md +99 -18
- package/dist/adapters/cockroachdb.js +4 -9
- package/dist/cjs/adapters/cockroachdb.js +4 -9
- package/dist/cjs/cli/index.js +6 -3
- package/dist/cjs/cli/observe-ui.js +12 -2
- package/dist/cjs/cli/observe.js +6 -7
- package/dist/cjs/cli/studio.js +6 -7
- package/dist/cjs/client.js +44 -22
- package/dist/cjs/dialect.js +116 -0
- package/dist/cjs/errors.js +19 -1
- package/dist/cjs/generate.js +4 -0
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/introspect.js +20 -0
- package/dist/cjs/mssql.js +1338 -0
- package/dist/cjs/mysql.js +1052 -0
- package/dist/cjs/powdb.js +851 -0
- package/dist/cjs/powql.js +903 -0
- package/dist/cjs/query/builder.js +293 -73
- package/dist/cjs/sqlite.js +849 -0
- package/dist/cjs/typed-sql.js +9 -7
- package/dist/cli/index.js +6 -3
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +12 -2
- package/dist/cli/observe.js +7 -8
- package/dist/cli/studio.js +7 -8
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +23 -1
- package/dist/client.js +45 -23
- package/dist/dialect.d.ts +258 -1
- package/dist/dialect.js +83 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +17 -0
- package/dist/generate.js +4 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +18 -0
- package/dist/introspect.js +19 -0
- package/dist/mssql.d.ts +233 -0
- package/dist/mssql.js +1298 -0
- package/dist/mysql.d.ts +174 -0
- package/dist/mysql.js +1012 -0
- package/dist/powdb.d.ts +338 -0
- package/dist/powdb.js +802 -0
- package/dist/powql.d.ts +153 -0
- package/dist/powql.js +900 -0
- package/dist/query/builder.d.ts +105 -0
- package/dist/query/builder.js +294 -74
- package/dist/query/index.d.ts +1 -1
- package/dist/sqlite.d.ts +144 -0
- package/dist/sqlite.js +842 -0
- package/dist/typed-sql.d.ts +7 -5
- package/dist/typed-sql.js +9 -7
- package/package.json +45 -1
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm/mysql — MySQL 8 engine (driver-injected, optional peer)
|
|
4
|
+
*
|
|
5
|
+
* Binds Turbine to MySQL 8 via the `mysql2` driver. `mysql2` is **not** a root
|
|
6
|
+
* dependency — it is an **optional peer**: `npm i turbine-orm` pulls nothing
|
|
7
|
+
* extra, and only consumers who `import 'turbine-orm/mysql'` install `mysql2`
|
|
8
|
+
* themselves. The factory loads it through a dynamic `import('mysql2/promise')`
|
|
9
|
+
* so importing this module never crashes when `mysql2` is absent for a consumer
|
|
10
|
+
* who does not use it. Turbine's root runtime dependency stays exactly `pg`.
|
|
11
|
+
*
|
|
12
|
+
* ## The two hard MySQL realities this engine solves
|
|
13
|
+
*
|
|
14
|
+
* 1. **No `RETURNING`.** `INSERT`/`UPDATE`/`DELETE` cannot return the affected
|
|
15
|
+
* rows. `mysqlDialect.resultStrategy = 'reselect'`: the executor runs the
|
|
16
|
+
* write, then issues a follow-up `SELECT` by primary key (using mysql2's
|
|
17
|
+
* `insertId` for auto-increment PKs) / by the `where` predicate, so
|
|
18
|
+
* `create`/`update`/`upsert`/`delete` still return real rows. This is the
|
|
19
|
+
* first shipped engine to exercise the Phase-0 `reselect` path.
|
|
20
|
+
* 2. **Positional `?` is broken for this builder.** Turbine pushes params in
|
|
21
|
+
* 1-indexed generation order but EMITS them in a different SQL-text position
|
|
22
|
+
* (e.g. a `with`-relation `LIMIT` lands in the SELECT list, ahead of the
|
|
23
|
+
* outer `WHERE`). Postgres reconciles this via numbered `$N`; positional `?`
|
|
24
|
+
* silently mis-binds. So `mysqlDialect` uses **mysql2 named placeholders**
|
|
25
|
+
* (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object —
|
|
26
|
+
* exactly mirroring `$N` semantics regardless of text order. (See
|
|
27
|
+
* `turbine-orm/sqlite` for the same fix.)
|
|
28
|
+
*
|
|
29
|
+
* ## Capabilities & limits (vs PostgreSQL)
|
|
30
|
+
*
|
|
31
|
+
* - **Single query nested relations preserved** via MySQL 8 `JSON_OBJECT` /
|
|
32
|
+
* `JSON_ARRAYAGG` with `COALESCE(…, JSON_ARRAY())`. `JSON_ARRAYAGG` has no
|
|
33
|
+
* inline `ORDER BY` argument, so every ordered to-many relation is forced
|
|
34
|
+
* through the inner-subquery rewrite (`aggSupportsInlineOrderBy = false`).
|
|
35
|
+
* Nested subresults are `CAST(… AS JSON)`-wrapped so MySQL embeds them as real
|
|
36
|
+
* nested JSON instead of double-encoding a scalar subquery result as a string.
|
|
37
|
+
* - **`createMany` returns an empty array** (count-not-rows). MySQL has no
|
|
38
|
+
* `RETURNING` and the bulk-insert `insertId` is only the first generated id, so
|
|
39
|
+
* re-selecting N rows reliably is unsafe. The rows ARE inserted; re-query if you
|
|
40
|
+
* need them back. (Plan §4 documented divergence.)
|
|
41
|
+
* - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
|
|
42
|
+
* LISTEN/NOTIFY (`$listen`/`$notify`), RLS `sessionContext` (no GUCs).
|
|
43
|
+
* - **Advisory-lock migration locking** is available in principle via
|
|
44
|
+
* `GET_LOCK`/`RELEASE_LOCK` (`supportsAdvisoryLock = true`); the migrate CLI is
|
|
45
|
+
* still PostgreSQL-only, so this flag documents intent for a future adapter.
|
|
46
|
+
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)` — note this
|
|
47
|
+
* can defeat indexes unless a functional/generated index exists.
|
|
48
|
+
* - **bignum:** mysql2 is configured `supportBigNumbers:true, bigNumberStrings:false`
|
|
49
|
+
* — the same safe-int policy Turbine uses for Postgres `int8` (number when it
|
|
50
|
+
* fits, decimal string otherwise). `DECIMAL` comes back as a string; `TINYINT(1)`
|
|
51
|
+
* binds booleans as 1/0. No global parser state is mutated.
|
|
52
|
+
* - **Version:** MySQL **8.0+** required (5.7 lacks `JSON_ARRAYAGG`); MariaDB is
|
|
53
|
+
* unsupported. The factory probes `SELECT VERSION()` and fails fast otherwise.
|
|
54
|
+
*
|
|
55
|
+
* ## Example
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { turbineMysql } from 'turbine-orm/mysql';
|
|
59
|
+
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
60
|
+
*
|
|
61
|
+
* const db = await turbineMysql('mysql://user:pass@localhost:3306/app', SCHEMA);
|
|
62
|
+
* const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
|
|
63
|
+
* await db.disconnect();
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
67
|
+
if (k2 === undefined) k2 = k;
|
|
68
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
69
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
70
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(o, k2, desc);
|
|
73
|
+
}) : (function(o, m, k, k2) {
|
|
74
|
+
if (k2 === undefined) k2 = k;
|
|
75
|
+
o[k2] = m[k];
|
|
76
|
+
}));
|
|
77
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
78
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
79
|
+
}) : function(o, v) {
|
|
80
|
+
o["default"] = v;
|
|
81
|
+
});
|
|
82
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
83
|
+
var ownKeys = function(o) {
|
|
84
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
85
|
+
var ar = [];
|
|
86
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
87
|
+
return ar;
|
|
88
|
+
};
|
|
89
|
+
return ownKeys(o);
|
|
90
|
+
};
|
|
91
|
+
return function (mod) {
|
|
92
|
+
if (mod && mod.__esModule) return mod;
|
|
93
|
+
var result = {};
|
|
94
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
95
|
+
__setModuleDefault(result, mod);
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
})();
|
|
99
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
100
|
+
exports.mysqlDialect = exports.MysqlPool = void 0;
|
|
101
|
+
exports.mysqlTypeToTs = mysqlTypeToTs;
|
|
102
|
+
exports.introspectMysqlWith = introspectMysqlWith;
|
|
103
|
+
exports.introspectMysql = introspectMysql;
|
|
104
|
+
exports.turbineMysql = turbineMysql;
|
|
105
|
+
const client_js_1 = require("./client.js");
|
|
106
|
+
const dialect_js_1 = require("./dialect.js");
|
|
107
|
+
const errors_js_1 = require("./errors.js");
|
|
108
|
+
const schema_js_1 = require("./schema.js");
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Value coercion (params in)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* Coerce an arbitrary JS value into something mysql2 can bind. Booleans →
|
|
114
|
+
* 1/0 (MySQL has no boolean type; `TINYINT(1)` stores 1/0), `undefined`/`null`
|
|
115
|
+
* → NULL, `Date`/`Uint8Array` pass through (mysql2 formats them), and any
|
|
116
|
+
* remaining object/array → JSON text (matches how Turbine already pre-serializes
|
|
117
|
+
* JSON filter / `IN`-list params).
|
|
118
|
+
*/
|
|
119
|
+
function toMysqlParam(value) {
|
|
120
|
+
if (value === undefined || value === null)
|
|
121
|
+
return null;
|
|
122
|
+
switch (typeof value) {
|
|
123
|
+
case 'boolean':
|
|
124
|
+
return value ? 1 : 0;
|
|
125
|
+
case 'number':
|
|
126
|
+
case 'bigint':
|
|
127
|
+
case 'string':
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
if (value instanceof Date)
|
|
131
|
+
return value;
|
|
132
|
+
if (value instanceof Uint8Array)
|
|
133
|
+
return value; // also covers Buffer
|
|
134
|
+
return JSON.stringify(value);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Bind the positional `params[]` (in 1-indexed generation order) to the named
|
|
138
|
+
* `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
|
|
139
|
+
* independent of where each placeholder lands in the SQL text — the same
|
|
140
|
+
* guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
|
|
141
|
+
* statements (BEGIN/COMMIT/DDL) so they bind nothing.
|
|
142
|
+
*/
|
|
143
|
+
function toNamedBinding(values) {
|
|
144
|
+
if (values.length === 0)
|
|
145
|
+
return undefined;
|
|
146
|
+
const named = {};
|
|
147
|
+
for (let i = 0; i < values.length; i++) {
|
|
148
|
+
named[`p${i + 1}`] = toMysqlParam(values[i]);
|
|
149
|
+
}
|
|
150
|
+
return named;
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Result shaping + error translation
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
/** Shape a mysql2 `[result]` into a `pg.QueryResult`-like object. */
|
|
156
|
+
function shapeResult(result) {
|
|
157
|
+
if (Array.isArray(result)) {
|
|
158
|
+
return { rows: result, rowCount: result.length };
|
|
159
|
+
}
|
|
160
|
+
// Write statement → ResultSetHeader (no rows). Expose insertId/lastID for the
|
|
161
|
+
// `reselect` strategy to re-fetch the auto-increment row.
|
|
162
|
+
const header = (result ?? {});
|
|
163
|
+
const insertId = header.insertId;
|
|
164
|
+
return { rows: [], rowCount: header.affectedRows ?? 0, insertId, lastID: insertId };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Augment a mysql2 driver error with the Postgres-shaped `.code` (SQLSTATE) and
|
|
168
|
+
* detail fields that `wrapPgError` understands, so MySQL constraint failures
|
|
169
|
+
* surface as the same typed Turbine errors as Postgres (E008/E009/E010/E011) and
|
|
170
|
+
* deadlock / lock-wait-timeout become retryable (E012/E013). The original mysql2
|
|
171
|
+
* error (with its real message) is preserved as the wrapped error's `.cause`
|
|
172
|
+
* downstream. Returns the value unchanged when it is not a recognizable mysql2
|
|
173
|
+
* error. We only annotate here — `wrapPgError` (invoked downstream in the query
|
|
174
|
+
* executor / transaction proxy) does the actual translation.
|
|
175
|
+
*/
|
|
176
|
+
function augmentMysqlError(err) {
|
|
177
|
+
if (!err || typeof err !== 'object')
|
|
178
|
+
return err;
|
|
179
|
+
const e = err;
|
|
180
|
+
if (typeof e.errno !== 'number')
|
|
181
|
+
return err;
|
|
182
|
+
const target = e;
|
|
183
|
+
const msg = e.sqlMessage ?? e.message ?? '';
|
|
184
|
+
switch (e.errno) {
|
|
185
|
+
// ER_DUP_ENTRY → unique violation
|
|
186
|
+
case 1062: {
|
|
187
|
+
// "Duplicate entry 'x' for key 'users.email'"
|
|
188
|
+
const m = /for key '([^']+)'/i.exec(msg);
|
|
189
|
+
target.code = '23505';
|
|
190
|
+
if (m?.[1]) {
|
|
191
|
+
const key = m[1];
|
|
192
|
+
const parts = key.split('.');
|
|
193
|
+
target.constraint = parts.length > 1 ? parts[parts.length - 1] : key;
|
|
194
|
+
}
|
|
195
|
+
return err;
|
|
196
|
+
}
|
|
197
|
+
// ER_ROW_IS_REFERENCED_2 / ER_NO_REFERENCED_ROW_2 → foreign key violation
|
|
198
|
+
case 1451:
|
|
199
|
+
case 1452:
|
|
200
|
+
target.code = '23503';
|
|
201
|
+
return err;
|
|
202
|
+
// ER_BAD_NULL_ERROR / ER_NO_DEFAULT_FOR_FIELD → not-null violation
|
|
203
|
+
case 1048:
|
|
204
|
+
case 1364: {
|
|
205
|
+
const m = /Column '([^']+)'/i.exec(msg);
|
|
206
|
+
target.code = '23502';
|
|
207
|
+
if (m?.[1])
|
|
208
|
+
target.column = m[1];
|
|
209
|
+
return err;
|
|
210
|
+
}
|
|
211
|
+
// ER_CHECK_CONSTRAINT_VIOLATED (8.0.16+) / ER_CONSTRAINT_FAILED → check violation
|
|
212
|
+
case 3819:
|
|
213
|
+
case 3813:
|
|
214
|
+
target.code = '23514';
|
|
215
|
+
return err;
|
|
216
|
+
// ER_LOCK_DEADLOCK → deadlock (retryable, maps to pg 40P01)
|
|
217
|
+
case 1213:
|
|
218
|
+
target.code = '40P01';
|
|
219
|
+
return err;
|
|
220
|
+
// ER_LOCK_WAIT_TIMEOUT → treat as a serialization failure (retryable, pg 40001)
|
|
221
|
+
case 1205:
|
|
222
|
+
target.code = '40001';
|
|
223
|
+
return err;
|
|
224
|
+
default:
|
|
225
|
+
return err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function normalizeQueryArgs(arg, values) {
|
|
229
|
+
if (typeof arg === 'string')
|
|
230
|
+
return { text: arg, params: values ?? [] };
|
|
231
|
+
return { text: arg.text, params: arg.values ?? values ?? [] };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Run one statement against a mysql2 queryable (pool or connection). Parameterized
|
|
235
|
+
* statements use `.execute()` (mysql2 server-side prepared statements, cached per
|
|
236
|
+
* connection); parameter-less statements (transaction control, DDL) use `.query()`,
|
|
237
|
+
* since not every such statement can be prepared. Named placeholders are bound via
|
|
238
|
+
* the `{ p1, p2, … }` object built from the positional params.
|
|
239
|
+
*/
|
|
240
|
+
async function execOne(runner, sql, params) {
|
|
241
|
+
const binding = toNamedBinding(params);
|
|
242
|
+
try {
|
|
243
|
+
const [result] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
244
|
+
return shapeResult(result);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
throw augmentMysqlError(err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Run a statement on a dedicated transaction connection. MySQL cannot take an
|
|
252
|
+
* inline isolation level on `START TRANSACTION`, so {@link mysqlDialect.beginStatement}
|
|
253
|
+
* emits `SET TRANSACTION ISOLATION LEVEL <level>; START TRANSACTION`. mysql2 runs
|
|
254
|
+
* one statement per call (multi-statements stay OFF by design), so split exactly
|
|
255
|
+
* that dialect-generated compound and run the parts in order. Gated on the precise
|
|
256
|
+
* transaction-control prefix — the query builder never emits `SET TRANSACTION
|
|
257
|
+
* ISOLATION LEVEL`, so no builder/user SQL is ever split.
|
|
258
|
+
*/
|
|
259
|
+
async function runOnConnection(conn, sql, params) {
|
|
260
|
+
if (params.length === 0 && /^SET TRANSACTION ISOLATION LEVEL /i.test(sql) && sql.includes('; ')) {
|
|
261
|
+
let last = shapeResult([]);
|
|
262
|
+
for (const part of sql.split('; ')) {
|
|
263
|
+
const trimmed = part.trim();
|
|
264
|
+
if (trimmed)
|
|
265
|
+
last = await execOne(conn, trimmed, []);
|
|
266
|
+
}
|
|
267
|
+
return last;
|
|
268
|
+
}
|
|
269
|
+
return execOne(conn, sql, params);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* A {@link PgCompatPool} backed by a `mysql2` pool. Non-transaction queries run
|
|
273
|
+
* on the pool directly (any connection); `connect()` checks out a dedicated
|
|
274
|
+
* connection so a transaction's `START TRANSACTION` / `COMMIT` / `ROLLBACK` /
|
|
275
|
+
* `SAVEPOINT` nesting all run on the same physical connection.
|
|
276
|
+
*/
|
|
277
|
+
class MysqlPool {
|
|
278
|
+
/** The underlying mysql2 pool — exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
279
|
+
pool;
|
|
280
|
+
closed = false;
|
|
281
|
+
constructor(pool) {
|
|
282
|
+
this.pool = pool;
|
|
283
|
+
}
|
|
284
|
+
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; execOne returns plain objects.
|
|
285
|
+
async query(text, values) {
|
|
286
|
+
const { text: sql, params } = normalizeQueryArgs(text, values);
|
|
287
|
+
return execOne(this.pool, sql, params);
|
|
288
|
+
}
|
|
289
|
+
async connect() {
|
|
290
|
+
const conn = await this.pool.getConnection();
|
|
291
|
+
return {
|
|
292
|
+
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
293
|
+
query: async (text, values) => {
|
|
294
|
+
const { text: sql, params } = normalizeQueryArgs(text, values);
|
|
295
|
+
return runOnConnection(conn, sql, params);
|
|
296
|
+
},
|
|
297
|
+
release: () => conn.release(),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async end() {
|
|
301
|
+
if (this.closed)
|
|
302
|
+
return;
|
|
303
|
+
this.closed = true;
|
|
304
|
+
await this.pool.end();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.MysqlPool = MysqlPool;
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Type mapping (MySQL data type → TypeScript)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
/**
|
|
312
|
+
* Map a MySQL column type to a TypeScript type. `dialectType` is the
|
|
313
|
+
* `information_schema.COLUMNS.DATA_TYPE` (e.g. `bigint`, `varchar`, `datetime`);
|
|
314
|
+
* `columnType` is the full `COLUMN_TYPE` (e.g. `tinyint(1)`, `enum('a','b')`)
|
|
315
|
+
* used to detect `TINYINT(1)` booleans.
|
|
316
|
+
*/
|
|
317
|
+
function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
318
|
+
const t = dialectType.toLowerCase();
|
|
319
|
+
const full = (columnType ?? '').toLowerCase();
|
|
320
|
+
let base;
|
|
321
|
+
if (t === 'tinyint' && full === 'tinyint(1)')
|
|
322
|
+
base = 'boolean';
|
|
323
|
+
else if (/^(tinyint|smallint|mediumint|int|integer|bigint)$/.test(t))
|
|
324
|
+
base = 'number';
|
|
325
|
+
else if (/^(decimal|numeric)$/.test(t))
|
|
326
|
+
base = 'string';
|
|
327
|
+
else if (/^(float|double|real)$/.test(t))
|
|
328
|
+
base = 'number';
|
|
329
|
+
else if (/^(datetime|timestamp|date)$/.test(t))
|
|
330
|
+
base = 'Date';
|
|
331
|
+
else if (t === 'json')
|
|
332
|
+
base = 'unknown';
|
|
333
|
+
else if (/(blob|binary|varbinary)/.test(t))
|
|
334
|
+
base = 'Uint8Array';
|
|
335
|
+
else if (/(char|text|enum|set|time|year|bit)/.test(t))
|
|
336
|
+
base = 'string';
|
|
337
|
+
else
|
|
338
|
+
base = 'unknown';
|
|
339
|
+
return nullable ? `${base} | null` : base;
|
|
340
|
+
}
|
|
341
|
+
/** Is a MySQL declared type a date/time type (so values coerce back to `Date`)? */
|
|
342
|
+
function isMysqlDateType(dialectType) {
|
|
343
|
+
const t = dialectType.toLowerCase();
|
|
344
|
+
return (0, schema_js_1.isDateType)(t) || /^(datetime|timestamp|date)$/.test(t);
|
|
345
|
+
}
|
|
346
|
+
/** Map a schema-builder (Postgres-flavored) column type to MySQL DDL. */
|
|
347
|
+
function mysqlColumnType(type, maxLength) {
|
|
348
|
+
const t = type.toUpperCase();
|
|
349
|
+
if (/BIGSERIAL/.test(t))
|
|
350
|
+
return 'BIGINT AUTO_INCREMENT';
|
|
351
|
+
if (/SERIAL/.test(t))
|
|
352
|
+
return 'INT AUTO_INCREMENT';
|
|
353
|
+
if (/BIGINT|INT8/.test(t))
|
|
354
|
+
return 'BIGINT';
|
|
355
|
+
if (/SMALLINT|INT2/.test(t))
|
|
356
|
+
return 'SMALLINT';
|
|
357
|
+
if (/INTEGER|INT4|\bINT\b/.test(t))
|
|
358
|
+
return 'INT';
|
|
359
|
+
if (/BOOL/.test(t))
|
|
360
|
+
return 'TINYINT(1)';
|
|
361
|
+
if (/DOUBLE|FLOAT8/.test(t))
|
|
362
|
+
return 'DOUBLE';
|
|
363
|
+
if (/REAL|FLOAT4|FLOAT/.test(t))
|
|
364
|
+
return 'FLOAT';
|
|
365
|
+
if (/NUMERIC|DECIMAL|MONEY/.test(t))
|
|
366
|
+
return 'DECIMAL(65,30)';
|
|
367
|
+
if (/JSONB|JSON/.test(t))
|
|
368
|
+
return 'JSON';
|
|
369
|
+
if (/UUID/.test(t))
|
|
370
|
+
return 'CHAR(36)';
|
|
371
|
+
if (/TIMESTAMPTZ|TIMESTAMP|DATETIME/.test(t))
|
|
372
|
+
return 'DATETIME';
|
|
373
|
+
if (/\bDATE\b/.test(t))
|
|
374
|
+
return 'DATE';
|
|
375
|
+
if (/\bTIME\b/.test(t))
|
|
376
|
+
return 'TIME';
|
|
377
|
+
if (/BYTEA|BLOB/.test(t))
|
|
378
|
+
return 'BLOB';
|
|
379
|
+
if (/VARCHAR/.test(t))
|
|
380
|
+
return maxLength != null ? `VARCHAR(${maxLength})` : 'VARCHAR(255)';
|
|
381
|
+
if (/CHAR/.test(t))
|
|
382
|
+
return maxLength != null ? `CHAR(${maxLength})` : 'CHAR(255)';
|
|
383
|
+
if (/TEXT|CLOB/.test(t))
|
|
384
|
+
return 'TEXT';
|
|
385
|
+
if (/ENUM/.test(t))
|
|
386
|
+
return 'TEXT';
|
|
387
|
+
return type;
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// mysqlDialect — the full Dialect contract for MySQL 8
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
/**
|
|
393
|
+
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
394
|
+
* quoting, named `:pN` placeholders (NOT positional `?` — see the module
|
|
395
|
+
* docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
|
|
396
|
+
* relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
|
|
397
|
+
* (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
|
|
398
|
+
* and the Postgres-only capabilities disabled (vector / LISTEN-NOTIFY / RLS).
|
|
399
|
+
*/
|
|
400
|
+
exports.mysqlDialect = {
|
|
401
|
+
...dialect_js_1.postgresDialect,
|
|
402
|
+
name: 'mysql',
|
|
403
|
+
// No RETURNING → run the write, then re-SELECT by PK/where (Phase-0 reselect).
|
|
404
|
+
resultStrategy: 'reselect',
|
|
405
|
+
supportsReturning: false,
|
|
406
|
+
supportsILike: false,
|
|
407
|
+
supportsVector: false,
|
|
408
|
+
supportsListenNotify: false,
|
|
409
|
+
supportsRLS: false,
|
|
410
|
+
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
411
|
+
supportsAdvisoryLock: true,
|
|
412
|
+
// JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
|
|
413
|
+
// rewrite for every ordered to-many relation.
|
|
414
|
+
aggSupportsInlineOrderBy: false,
|
|
415
|
+
// mysql2's binary (prepared) protocol sends JS numbers as DOUBLE, which MySQL's
|
|
416
|
+
// LIMIT/OFFSET reject ("Incorrect arguments to mysqld_stmt_execute"). Render
|
|
417
|
+
// pagination as inline integer literals (Turbine-validated, injection-safe).
|
|
418
|
+
inlineLimitOffset: true,
|
|
419
|
+
jsonPathSupport: 'function',
|
|
420
|
+
emptyJsonArrayLiteral: 'JSON_ARRAY()',
|
|
421
|
+
nullJsonLiteral: 'NULL',
|
|
422
|
+
// Named placeholders (`:p1`, `:p2`, …) — NOT positional `?`. Turbine pushes
|
|
423
|
+
// params in 1-indexed generation order but may EMIT them in a different SQL
|
|
424
|
+
// text position; positional `?` mis-binds. mysql2 named parameters bind by
|
|
425
|
+
// name, so `:pN` ↔ `params[N-1]` mirrors Postgres' `$N` regardless of text
|
|
426
|
+
// order. The driver shim binds via a `{ p1, p2, … }` object.
|
|
427
|
+
paramPlaceholder(index) {
|
|
428
|
+
return `:p${index}`;
|
|
429
|
+
},
|
|
430
|
+
quoteIdentifier(name) {
|
|
431
|
+
return `\`${name.replace(/`/g, '``')}\``;
|
|
432
|
+
},
|
|
433
|
+
buildJsonObject(pairs) {
|
|
434
|
+
const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
|
|
435
|
+
return `JSON_OBJECT(${args.join(', ')})`;
|
|
436
|
+
},
|
|
437
|
+
// JSON_ARRAYAGG over an empty group returns NULL → COALESCE to JSON_ARRAY().
|
|
438
|
+
// The orderBy argument is ignored (MySQL's JSON_ARRAYAGG has no ORDER BY arg);
|
|
439
|
+
// aggSupportsInlineOrderBy=false guarantees the builder never passes one.
|
|
440
|
+
buildJsonArrayAgg(jsonObjectExpr) {
|
|
441
|
+
return `COALESCE(JSON_ARRAYAGG(${jsonObjectExpr}), JSON_ARRAY())`;
|
|
442
|
+
},
|
|
443
|
+
/**
|
|
444
|
+
* Wrap a nested correlated subquery for embedding inside a parent `JSON_OBJECT`.
|
|
445
|
+
* MySQL double-encodes a scalar subquery result as a JSON *string* unless it is
|
|
446
|
+
* explicitly typed JSON, so `CAST((subquery) AS JSON)` forces the nested value
|
|
447
|
+
* to be embedded as real JSON (objects/arrays), not a quoted string — the same
|
|
448
|
+
* intent as SQLite's `json(...)` wrap. The fallback (`JSON_ARRAY()` / `NULL`) is
|
|
449
|
+
* already JSON-typed.
|
|
450
|
+
*/
|
|
451
|
+
wrapJsonSubresult(subquery, fallback) {
|
|
452
|
+
return `COALESCE(CAST((${subquery}) AS JSON), ${fallback})`;
|
|
453
|
+
},
|
|
454
|
+
// MySQL aggregate casts: COUNT → SIGNED (BIGINT, comes back as a JS number when
|
|
455
|
+
// safe), AVG/float → DECIMAL (string, the aggregate transform Number()-coerces).
|
|
456
|
+
castAggregate(expr, target) {
|
|
457
|
+
return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
|
|
458
|
+
},
|
|
459
|
+
// No array params in MySQL. JSON_TABLE expands a single JSON-array param into a
|
|
460
|
+
// row set, keeping ONE placeholder (so the SQL cache stays valid regardless of
|
|
461
|
+
// list length) and handling the empty-list case (zero rows). MySQL coerces the
|
|
462
|
+
// non-JSON `expr` to JSON for the comparison, so this works for numbers AND
|
|
463
|
+
// strings. JSON_TABLE requires MySQL 8.0+.
|
|
464
|
+
buildInClause(expr, paramRef, negated) {
|
|
465
|
+
const inner = `SELECT \`v\` FROM JSON_TABLE(${paramRef}, '$[*]' COLUMNS (\`v\` JSON PATH '$')) AS \`jt\``;
|
|
466
|
+
return `${expr} ${negated ? 'NOT IN' : 'IN'} (${inner})`;
|
|
467
|
+
},
|
|
468
|
+
inClauseParam(values) {
|
|
469
|
+
return JSON.stringify(values ?? []);
|
|
470
|
+
},
|
|
471
|
+
// No RETURNING — resultStrategy 'reselect' re-fetches the row.
|
|
472
|
+
buildReturningClause() {
|
|
473
|
+
return '';
|
|
474
|
+
},
|
|
475
|
+
buildInsertStatement(input) {
|
|
476
|
+
return `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
477
|
+
},
|
|
478
|
+
buildBulkInsertStatement(input) {
|
|
479
|
+
// No UNNEST in MySQL — emit multi-row VALUES with flattened, named
|
|
480
|
+
// placeholders (`:p1`, `:p2`, …) matching the flat param order.
|
|
481
|
+
let n = 0;
|
|
482
|
+
const placeholders = input.rowValues
|
|
483
|
+
.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
|
|
484
|
+
.join(', ');
|
|
485
|
+
// skipDuplicates → no-op ON DUPLICATE KEY UPDATE (keeps the existing row).
|
|
486
|
+
const firstCol = input.columns[0] ?? '`id`';
|
|
487
|
+
const conflict = input.skipDuplicates ? ` ON DUPLICATE KEY UPDATE ${firstCol} = ${firstCol}` : '';
|
|
488
|
+
return {
|
|
489
|
+
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${placeholders}${conflict}`,
|
|
490
|
+
params: input.rowValues.flat(),
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
buildUpsertStatement(input) {
|
|
494
|
+
// MySQL ignores the explicit conflict target — ON DUPLICATE KEY UPDATE keys
|
|
495
|
+
// off the table's PK/unique indexes. The `where`-derived conflictColumns
|
|
496
|
+
// (input.conflictColumns) must therefore correspond to a real unique/PK index
|
|
497
|
+
// for the upsert to target the intended row (plan §4 / R9).
|
|
498
|
+
return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
|
|
499
|
+
` ON DUPLICATE KEY UPDATE ${input.updateSetClauses.join(', ')}`);
|
|
500
|
+
},
|
|
501
|
+
buildInsensitiveLike(column, paramRef) {
|
|
502
|
+
// Deterministic case-insensitive match. NOTE: LOWER(col) can defeat an index
|
|
503
|
+
// unless a functional/generated index exists.
|
|
504
|
+
return `LOWER(${column}) LIKE LOWER(${paramRef})`;
|
|
505
|
+
},
|
|
506
|
+
buildJsonContains(column, paramRef) {
|
|
507
|
+
return `JSON_CONTAINS(${column}, ${paramRef})`;
|
|
508
|
+
},
|
|
509
|
+
buildJsonPathExtract(column, pathParamRef) {
|
|
510
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${pathParamRef}))`;
|
|
511
|
+
},
|
|
512
|
+
// ---- Type mapping -------------------------------------------------------
|
|
513
|
+
typeToTypeScript(dialectType, nullable) {
|
|
514
|
+
return mysqlTypeToTs(dialectType, nullable);
|
|
515
|
+
},
|
|
516
|
+
// MySQL has no true array columns; bulk insert uses multi-row VALUES, so no
|
|
517
|
+
// array cast is ever needed. Disables the UNNEST path.
|
|
518
|
+
arrayType: undefined,
|
|
519
|
+
// ---- DDL ----------------------------------------------------------------
|
|
520
|
+
buildColumnType(input) {
|
|
521
|
+
return mysqlColumnType(input.type, input.maxLength);
|
|
522
|
+
},
|
|
523
|
+
buildColumnDefinition(input) {
|
|
524
|
+
const isSerial = /serial/i.test(input.type);
|
|
525
|
+
const parts = [input.name, this.buildColumnType(input)];
|
|
526
|
+
// AUTO_INCREMENT columns must be a key; emit PRIMARY KEY inline for a serial PK.
|
|
527
|
+
if (input.primaryKey)
|
|
528
|
+
parts.push('PRIMARY KEY');
|
|
529
|
+
else if (input.unique)
|
|
530
|
+
parts.push('UNIQUE');
|
|
531
|
+
if (input.notNull && !isSerial && !input.primaryKey)
|
|
532
|
+
parts.push('NOT NULL');
|
|
533
|
+
if (input.defaultValue != null)
|
|
534
|
+
parts.push(`DEFAULT ${input.defaultValue}`);
|
|
535
|
+
if (input.references)
|
|
536
|
+
parts.push(`REFERENCES ${input.references.table}(${input.references.column})`);
|
|
537
|
+
return parts.join(' ');
|
|
538
|
+
},
|
|
539
|
+
buildCreateIndexStatement(input) {
|
|
540
|
+
return `CREATE INDEX ${input.name} ON ${input.table}(${input.columns.join(', ')});`;
|
|
541
|
+
},
|
|
542
|
+
buildCreateTableStatement(input) {
|
|
543
|
+
const body = input.definitions.map((d) => ` ${d}`).join(',\n');
|
|
544
|
+
return `CREATE TABLE ${input.table} (\n${body}\n);`;
|
|
545
|
+
},
|
|
546
|
+
// ---- Migration tracking -------------------------------------------------
|
|
547
|
+
// SelectApplied / UpdateChecksum / DeleteApplied inherit from postgresDialect:
|
|
548
|
+
// they call `this.paramPlaceholder(n)` (→ `:pN`) and standard SQL, all valid
|
|
549
|
+
// MySQL. The tracking-table DDL and the upsert INSERT need MySQL forms.
|
|
550
|
+
buildMigrationTrackingTable(table) {
|
|
551
|
+
return `
|
|
552
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
553
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
554
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
555
|
+
checksum VARCHAR(255) NOT NULL,
|
|
556
|
+
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
557
|
+
);
|
|
558
|
+
`;
|
|
559
|
+
},
|
|
560
|
+
buildMigrationInsertApplied(table) {
|
|
561
|
+
return `INSERT INTO ${table} (name, checksum) VALUES (${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}) ON DUPLICATE KEY UPDATE name = name`;
|
|
562
|
+
},
|
|
563
|
+
// ---- Transaction control ------------------------------------------------
|
|
564
|
+
beginStatement(isolationLevel) {
|
|
565
|
+
// MySQL cannot take an inline isolation level on START TRANSACTION. The
|
|
566
|
+
// driver shim (runOnConnection) splits this exact compound and runs the
|
|
567
|
+
// parts in order on the same connection.
|
|
568
|
+
return isolationLevel
|
|
569
|
+
? `SET TRANSACTION ISOLATION LEVEL ${isolationLevel}; START TRANSACTION`
|
|
570
|
+
: 'START TRANSACTION';
|
|
571
|
+
},
|
|
572
|
+
buildSetSessionConfig() {
|
|
573
|
+
// Never reached: supportsRLS=false makes $transaction throw before calling
|
|
574
|
+
// this. Guarded here too so a misuse fails loudly rather than emitting GUC SQL.
|
|
575
|
+
throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', 'mysql', 'MySQL has no transaction-local GUCs / set_config equivalent.');
|
|
576
|
+
},
|
|
577
|
+
// ---- Streaming ----------------------------------------------------------
|
|
578
|
+
/**
|
|
579
|
+
* The {@link StreamableConnection} seam only exposes `query()`, not mysql2's
|
|
580
|
+
* native `.stream()`, so the "stream" fetches once and yields the rows in
|
|
581
|
+
* `batchSize` chunks. True server-side streaming would require direct mysql2
|
|
582
|
+
* access (documented limitation).
|
|
583
|
+
*/
|
|
584
|
+
async *openStream(connection, sql, params, batchSize) {
|
|
585
|
+
const result = await connection.query(sql, params);
|
|
586
|
+
for (let i = 0; i < result.rows.length; i += batchSize) {
|
|
587
|
+
yield result.rows.slice(i, i + batchSize);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
// ---- Introspection ------------------------------------------------------
|
|
591
|
+
introspector: {
|
|
592
|
+
async introspect(options) {
|
|
593
|
+
return introspectMysql(options);
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
598
|
+
/**
|
|
599
|
+
* Derive belongsTo + hasMany relations (and conservatively-detected manyToMany
|
|
600
|
+
* junctions) from a flat foreign-key list. Mirrors the PostgreSQL / SQLite
|
|
601
|
+
* introspectors so the produced {@link SchemaMetadata} has an identical relation
|
|
602
|
+
* shape across engines.
|
|
603
|
+
*/
|
|
604
|
+
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
605
|
+
const tableSet = new Set(tableNames);
|
|
606
|
+
const fkCounts = new Map();
|
|
607
|
+
for (const fk of foreignKeys) {
|
|
608
|
+
const key = `${fk.sourceTable}->${fk.targetTable}`;
|
|
609
|
+
fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
|
|
610
|
+
}
|
|
611
|
+
const relationsByTable = new Map();
|
|
612
|
+
for (const fk of foreignKeys) {
|
|
613
|
+
if (!tableSet.has(fk.targetTable))
|
|
614
|
+
continue;
|
|
615
|
+
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
616
|
+
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
617
|
+
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
618
|
+
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
619
|
+
? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
620
|
+
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
621
|
+
if (!relationsByTable.has(fk.sourceTable))
|
|
622
|
+
relationsByTable.set(fk.sourceTable, {});
|
|
623
|
+
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
624
|
+
type: 'belongsTo',
|
|
625
|
+
name: belongsToName,
|
|
626
|
+
from: fk.sourceTable,
|
|
627
|
+
to: fk.targetTable,
|
|
628
|
+
foreignKey,
|
|
629
|
+
referenceKey,
|
|
630
|
+
};
|
|
631
|
+
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
632
|
+
? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
633
|
+
: (0, schema_js_1.snakeToCamel)(fk.sourceTable);
|
|
634
|
+
if (!relationsByTable.has(fk.targetTable))
|
|
635
|
+
relationsByTable.set(fk.targetTable, {});
|
|
636
|
+
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
637
|
+
type: 'hasMany',
|
|
638
|
+
name: hasManyName,
|
|
639
|
+
from: fk.targetTable,
|
|
640
|
+
to: fk.sourceTable,
|
|
641
|
+
foreignKey,
|
|
642
|
+
referenceKey,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// Conservative many-to-many auto-detection (additive): a table J is a pure
|
|
646
|
+
// junction iff PK is exactly two columns, exactly two single-column FKs whose
|
|
647
|
+
// source columns ARE the PK, two distinct target tables, and no payload columns.
|
|
648
|
+
for (const tableName of tableNames) {
|
|
649
|
+
const pk = pkByTable.get(tableName) ?? [];
|
|
650
|
+
if (pk.length !== 2)
|
|
651
|
+
continue;
|
|
652
|
+
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
653
|
+
if (tableFks.length !== 2)
|
|
654
|
+
continue;
|
|
655
|
+
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
656
|
+
continue;
|
|
657
|
+
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
658
|
+
const pkSet = new Set(pk);
|
|
659
|
+
if (!fkCols.every((c) => pkSet.has(c)))
|
|
660
|
+
continue;
|
|
661
|
+
if (new Set(fkCols).size !== 2)
|
|
662
|
+
continue;
|
|
663
|
+
const [fkA, fkB] = tableFks;
|
|
664
|
+
if (fkA.targetTable === fkB.targetTable)
|
|
665
|
+
continue;
|
|
666
|
+
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
667
|
+
if (jCols.length !== 2)
|
|
668
|
+
continue;
|
|
669
|
+
const addM2M = (self, other) => {
|
|
670
|
+
const sourceTbl = self.targetTable;
|
|
671
|
+
const targetTbl = other.targetTable;
|
|
672
|
+
const relName = (0, schema_js_1.snakeToCamel)(targetTbl);
|
|
673
|
+
if (!relationsByTable.has(sourceTbl))
|
|
674
|
+
relationsByTable.set(sourceTbl, {});
|
|
675
|
+
const existing = relationsByTable.get(sourceTbl);
|
|
676
|
+
if (existing[relName])
|
|
677
|
+
return;
|
|
678
|
+
existing[relName] = {
|
|
679
|
+
type: 'manyToMany',
|
|
680
|
+
name: relName,
|
|
681
|
+
from: sourceTbl,
|
|
682
|
+
to: targetTbl,
|
|
683
|
+
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
684
|
+
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
685
|
+
through: {
|
|
686
|
+
table: tableName,
|
|
687
|
+
sourceKey: self.sourceColumns[0],
|
|
688
|
+
targetKey: other.sourceColumns[0],
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
addM2M(fkA, fkB);
|
|
693
|
+
addM2M(fkB, fkA);
|
|
694
|
+
}
|
|
695
|
+
return relationsByTable;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Introspect a MySQL database into the same {@link SchemaMetadata} shape the
|
|
699
|
+
* Postgres catalog introspector produces, using a caller-supplied query
|
|
700
|
+
* executor (so tests can dogfood an already-open mysql2 pool/connection).
|
|
701
|
+
*
|
|
702
|
+
* @param exec Runs a parameterized (`:p1`, `:p2`, …) query and returns rows.
|
|
703
|
+
* @param schema The MySQL database (schema) name to introspect.
|
|
704
|
+
* @param options Optional include / exclude table-name filters.
|
|
705
|
+
*/
|
|
706
|
+
async function introspectMysqlWith(exec, schema, options = {}) {
|
|
707
|
+
// ----- Tables -----
|
|
708
|
+
let tableNames = (await exec("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = :p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
|
|
709
|
+
if (options.include?.length) {
|
|
710
|
+
const inc = new Set(options.include);
|
|
711
|
+
tableNames = tableNames.filter((t) => inc.has(t));
|
|
712
|
+
}
|
|
713
|
+
if (options.exclude?.length) {
|
|
714
|
+
const exc = new Set(options.exclude);
|
|
715
|
+
tableNames = tableNames.filter((t) => !exc.has(t));
|
|
716
|
+
}
|
|
717
|
+
const tableSet = new Set(tableNames);
|
|
718
|
+
// ----- Columns -----
|
|
719
|
+
const columnRows = (await exec(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY,
|
|
720
|
+
COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH
|
|
721
|
+
FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
|
|
722
|
+
const columnsByTable = new Map();
|
|
723
|
+
const enums = {};
|
|
724
|
+
for (const c of columnRows) {
|
|
725
|
+
if (!tableSet.has(c.TABLE_NAME))
|
|
726
|
+
continue;
|
|
727
|
+
const nullable = c.IS_NULLABLE === 'YES' && c.COLUMN_KEY !== 'PRI';
|
|
728
|
+
const dataType = c.DATA_TYPE.toLowerCase();
|
|
729
|
+
// Inline ENUM parsing from COLUMN_TYPE: enum('a','b','c')
|
|
730
|
+
if (dataType === 'enum') {
|
|
731
|
+
const m = /^enum\((.*)\)$/i.exec(c.COLUMN_TYPE);
|
|
732
|
+
if (m?.[1]) {
|
|
733
|
+
enums[`${c.TABLE_NAME}.${c.COLUMN_NAME}`] = m[1].split(',').map((s) => s
|
|
734
|
+
.trim()
|
|
735
|
+
.replace(/^'(.*)'$/, '$1')
|
|
736
|
+
.replace(/''/g, "'"));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const maxLen = c.CHARACTER_MAXIMUM_LENGTH != null ? num(c.CHARACTER_MAXIMUM_LENGTH) : undefined;
|
|
740
|
+
const col = {
|
|
741
|
+
name: c.COLUMN_NAME,
|
|
742
|
+
field: (0, schema_js_1.snakeToCamel)(c.COLUMN_NAME),
|
|
743
|
+
dialectType: dataType,
|
|
744
|
+
pgType: dataType,
|
|
745
|
+
tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
|
|
746
|
+
nullable,
|
|
747
|
+
hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
|
|
748
|
+
isArray: false,
|
|
749
|
+
arrayType: undefined,
|
|
750
|
+
pgArrayType: 'text[]',
|
|
751
|
+
};
|
|
752
|
+
if (maxLen !== undefined)
|
|
753
|
+
col.maxLength = maxLen;
|
|
754
|
+
if (!columnsByTable.has(c.TABLE_NAME))
|
|
755
|
+
columnsByTable.set(c.TABLE_NAME, []);
|
|
756
|
+
columnsByTable.get(c.TABLE_NAME).push(col);
|
|
757
|
+
}
|
|
758
|
+
// ----- Primary keys (ordered) -----
|
|
759
|
+
const pkRows = (await exec(`SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION FROM information_schema.KEY_COLUMN_USAGE
|
|
760
|
+
WHERE TABLE_SCHEMA = :p1 AND CONSTRAINT_NAME = 'PRIMARY' ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
|
|
761
|
+
const pkByTable = new Map();
|
|
762
|
+
for (const r of pkRows) {
|
|
763
|
+
if (!tableSet.has(r.TABLE_NAME))
|
|
764
|
+
continue;
|
|
765
|
+
if (!pkByTable.has(r.TABLE_NAME))
|
|
766
|
+
pkByTable.set(r.TABLE_NAME, []);
|
|
767
|
+
pkByTable.get(r.TABLE_NAME).push(r.COLUMN_NAME);
|
|
768
|
+
}
|
|
769
|
+
// ----- Foreign keys (grouped by constraint for composite FKs) -----
|
|
770
|
+
const fkRows = (await exec(`SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME, CONSTRAINT_NAME, ORDINAL_POSITION
|
|
771
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
772
|
+
WHERE TABLE_SCHEMA = :p1 AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
773
|
+
ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION`, [schema]));
|
|
774
|
+
const fkByConstraint = new Map();
|
|
775
|
+
for (const r of fkRows) {
|
|
776
|
+
if (!tableSet.has(r.TABLE_NAME))
|
|
777
|
+
continue;
|
|
778
|
+
const key = `${r.TABLE_NAME}.${r.CONSTRAINT_NAME}`;
|
|
779
|
+
let entry = fkByConstraint.get(key);
|
|
780
|
+
if (!entry) {
|
|
781
|
+
entry = {
|
|
782
|
+
sourceTable: r.TABLE_NAME,
|
|
783
|
+
sourceColumns: [],
|
|
784
|
+
targetTable: r.REFERENCED_TABLE_NAME,
|
|
785
|
+
targetColumns: [],
|
|
786
|
+
constraintName: r.CONSTRAINT_NAME,
|
|
787
|
+
};
|
|
788
|
+
fkByConstraint.set(key, entry);
|
|
789
|
+
}
|
|
790
|
+
entry.sourceColumns.push(r.COLUMN_NAME);
|
|
791
|
+
entry.targetColumns.push(r.REFERENCED_COLUMN_NAME);
|
|
792
|
+
}
|
|
793
|
+
const foreignKeys = [...fkByConstraint.values()].filter((fk) => tableSet.has(fk.targetTable));
|
|
794
|
+
// ----- Indexes + unique constraints -----
|
|
795
|
+
const idxRows = (await exec(`SELECT TABLE_NAME, INDEX_NAME, NON_UNIQUE, COLUMN_NAME, SEQ_IN_INDEX FROM information_schema.STATISTICS
|
|
796
|
+
WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`, [schema]));
|
|
797
|
+
const indexGroups = new Map();
|
|
798
|
+
for (const r of idxRows) {
|
|
799
|
+
if (!tableSet.has(r.TABLE_NAME))
|
|
800
|
+
continue;
|
|
801
|
+
const key = `${r.TABLE_NAME}.${r.INDEX_NAME}`;
|
|
802
|
+
let g = indexGroups.get(key);
|
|
803
|
+
if (!g) {
|
|
804
|
+
g = { table: r.TABLE_NAME, name: r.INDEX_NAME, unique: num(r.NON_UNIQUE) === 0, columns: [] };
|
|
805
|
+
indexGroups.set(key, g);
|
|
806
|
+
}
|
|
807
|
+
g.columns.push(r.COLUMN_NAME);
|
|
808
|
+
}
|
|
809
|
+
const indexesByTable = new Map();
|
|
810
|
+
const uniqueByTable = new Map();
|
|
811
|
+
for (const g of indexGroups.values()) {
|
|
812
|
+
if (g.name === 'PRIMARY')
|
|
813
|
+
continue; // PK is tracked separately
|
|
814
|
+
if (!indexesByTable.has(g.table))
|
|
815
|
+
indexesByTable.set(g.table, []);
|
|
816
|
+
indexesByTable.get(g.table).push({
|
|
817
|
+
name: g.name,
|
|
818
|
+
columns: g.columns,
|
|
819
|
+
unique: g.unique,
|
|
820
|
+
definition: `${g.unique ? 'UNIQUE ' : ''}INDEX ${g.name} ON ${g.table}(${g.columns.join(', ')})`,
|
|
821
|
+
});
|
|
822
|
+
if (g.unique && g.columns.length > 0) {
|
|
823
|
+
if (!uniqueByTable.has(g.table))
|
|
824
|
+
uniqueByTable.set(g.table, []);
|
|
825
|
+
uniqueByTable.get(g.table).push(g.columns);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// ----- Relations -----
|
|
829
|
+
const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
830
|
+
// ----- Assemble TableMetadata -----
|
|
831
|
+
const tables = {};
|
|
832
|
+
for (const tableName of tableNames) {
|
|
833
|
+
const columns = columnsByTable.get(tableName) ?? [];
|
|
834
|
+
const columnMap = {};
|
|
835
|
+
const reverseColumnMap = {};
|
|
836
|
+
const dateColumns = new Set();
|
|
837
|
+
const dialectTypes = {};
|
|
838
|
+
const pgTypes = {};
|
|
839
|
+
const allColumns = [];
|
|
840
|
+
for (const col of columns) {
|
|
841
|
+
columnMap[col.field] = col.name;
|
|
842
|
+
reverseColumnMap[col.name] = col.field;
|
|
843
|
+
allColumns.push(col.name);
|
|
844
|
+
dialectTypes[col.name] = col.dialectType ?? col.pgType;
|
|
845
|
+
pgTypes[col.name] = col.pgType;
|
|
846
|
+
if (isMysqlDateType(col.dialectType ?? col.pgType))
|
|
847
|
+
dateColumns.add(col.name);
|
|
848
|
+
}
|
|
849
|
+
tables[tableName] = {
|
|
850
|
+
name: tableName,
|
|
851
|
+
columns,
|
|
852
|
+
columnMap,
|
|
853
|
+
reverseColumnMap,
|
|
854
|
+
dateColumns,
|
|
855
|
+
dialectTypes,
|
|
856
|
+
pgTypes,
|
|
857
|
+
allColumns,
|
|
858
|
+
primaryKey: pkByTable.get(tableName) ?? [],
|
|
859
|
+
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
860
|
+
relations: relationsByTable.get(tableName) ?? {},
|
|
861
|
+
indexes: indexesByTable.get(tableName) ?? [],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return { tables, enums };
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Open a short-lived mysql2 connection from `options.connectionString`,
|
|
868
|
+
* introspect the database (schema = `options.schema` or the connection's current
|
|
869
|
+
* database), and close it. Wraps {@link introspectMysqlWith} for the
|
|
870
|
+
* {@link DialectIntrospector} seam used by `introspect()` / `npx turbine generate`.
|
|
871
|
+
*/
|
|
872
|
+
async function introspectMysql(options) {
|
|
873
|
+
const createPool = await loadCreatePool();
|
|
874
|
+
const config = parseMysqlConfig(options.connectionString);
|
|
875
|
+
const pool = createPool({ ...config, ...MYSQL_DRIVER_FLAGS });
|
|
876
|
+
try {
|
|
877
|
+
const exec = async (sql, params) => (await execOne(pool, sql, params)).rows;
|
|
878
|
+
let schemaName = options.schema ?? config.database;
|
|
879
|
+
if (!schemaName) {
|
|
880
|
+
const rows = await exec('SELECT DATABASE() AS db', []);
|
|
881
|
+
schemaName = rows[0]?.db ?? '';
|
|
882
|
+
}
|
|
883
|
+
if (!schemaName) {
|
|
884
|
+
throw new errors_js_1.ConnectionError('[turbine] Could not determine the MySQL database to introspect — pass a database in the connection string or a `schema` option.');
|
|
885
|
+
}
|
|
886
|
+
return introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
await pool.end();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// mysql2 loading + config
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
/** mysql2 connection flags Turbine pins for correct behavior. */
|
|
896
|
+
const MYSQL_DRIVER_FLAGS = {
|
|
897
|
+
// Named placeholders (`:pN`) — see paramPlaceholder + toNamedBinding.
|
|
898
|
+
namedPlaceholders: true,
|
|
899
|
+
// Safe-int policy matching Postgres int8: number when it fits, decimal string
|
|
900
|
+
// otherwise. DECIMAL always comes back as a string. No global state mutated.
|
|
901
|
+
supportBigNumbers: true,
|
|
902
|
+
bigNumberStrings: false,
|
|
903
|
+
// DATETIME/TIMESTAMP → JS Date interpreted as UTC (matches Postgres).
|
|
904
|
+
dateStrings: false,
|
|
905
|
+
timezone: 'Z',
|
|
906
|
+
// Force JSON columns to raw strings so the nested-relation parser
|
|
907
|
+
// (parseNestedRow) always takes its well-tested JSON.parse path, instead of
|
|
908
|
+
// mysql2's auto-parsed objects or a Buffer. Top-level JSON columns therefore
|
|
909
|
+
// come back as strings (consistent with the SQLite engine; parse them yourself).
|
|
910
|
+
// biome-ignore lint/suspicious/noExplicitAny: mysql2 typeCast field shape (has .type and .string()).
|
|
911
|
+
typeCast: (field, next) => (field.type === 'JSON' ? field.string() : next()),
|
|
912
|
+
};
|
|
913
|
+
/** Parse a `mysql://user:pass@host:port/db` connection string into mysql2 config. */
|
|
914
|
+
function parseMysqlConfig(connectionString) {
|
|
915
|
+
try {
|
|
916
|
+
const u = new URL(connectionString);
|
|
917
|
+
const config = {
|
|
918
|
+
host: u.hostname || 'localhost',
|
|
919
|
+
port: u.port ? Number(u.port) : 3306,
|
|
920
|
+
};
|
|
921
|
+
if (u.username)
|
|
922
|
+
config.user = decodeURIComponent(u.username);
|
|
923
|
+
if (u.password)
|
|
924
|
+
config.password = decodeURIComponent(u.password);
|
|
925
|
+
const db = u.pathname.replace(/^\//, '');
|
|
926
|
+
if (db)
|
|
927
|
+
config.database = decodeURIComponent(db);
|
|
928
|
+
return config;
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
throw new errors_js_1.ConnectionError(`[turbine] Invalid MySQL connection string: "${connectionString}"`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Dynamically load `mysql2/promise`'s `createPool`. Kept out of the module's
|
|
936
|
+
* static import graph so `import 'turbine-orm/mysql'` never throws when `mysql2`
|
|
937
|
+
* is absent for a consumer that does not use the factory.
|
|
938
|
+
*/
|
|
939
|
+
async function loadCreatePool() {
|
|
940
|
+
let mod;
|
|
941
|
+
try {
|
|
942
|
+
mod = (await Promise.resolve().then(() => __importStar(require('mysql2/promise'))));
|
|
943
|
+
}
|
|
944
|
+
catch (err) {
|
|
945
|
+
throw new errors_js_1.ConnectionError("[turbine] turbine-orm/mysql requires the optional peer dependency 'mysql2'. Install it: npm i mysql2. " +
|
|
946
|
+
`(${err.message})`);
|
|
947
|
+
}
|
|
948
|
+
const createPool = mod.createPool ?? mod.default?.createPool;
|
|
949
|
+
if (typeof createPool !== 'function') {
|
|
950
|
+
throw new errors_js_1.ConnectionError("[turbine] Loaded 'mysql2/promise' but it has no createPool export.");
|
|
951
|
+
}
|
|
952
|
+
return createPool;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Fail fast on unsupported servers: MySQL 8.0+ is required (5.7 lacks
|
|
956
|
+
* JSON_ARRAYAGG) and MariaDB is unsupported (its JSON_ARRAYAGG semantics differ).
|
|
957
|
+
*/
|
|
958
|
+
function assertSupportedVersion(version) {
|
|
959
|
+
if (/mariadb/i.test(version)) {
|
|
960
|
+
throw new errors_js_1.ConnectionError(`[turbine] MariaDB is not supported by turbine-orm/mysql (got "${version}"). Use MySQL 8.0+.`);
|
|
961
|
+
}
|
|
962
|
+
const m = /^(\d+)\.(\d+)/.exec(version);
|
|
963
|
+
const major = m ? Number(m[1]) : 0;
|
|
964
|
+
if (major < 8) {
|
|
965
|
+
throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/mysql requires MySQL 8.0+ (5.7 lacks JSON_ARRAYAGG); got "${version}".`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function isMysql2Pool(x) {
|
|
969
|
+
return (!!x &&
|
|
970
|
+
typeof x.getConnection === 'function' &&
|
|
971
|
+
typeof x.query === 'function' &&
|
|
972
|
+
typeof x.end === 'function');
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Create a {@link TurbineClient} bound to MySQL 8 via `mysql2`.
|
|
976
|
+
*
|
|
977
|
+
* Pass one of:
|
|
978
|
+
* - a connection string (`'mysql://user:pass@host:3306/db'`),
|
|
979
|
+
* - a mysql2 connection config object (`{ host, user, password, database }`), or
|
|
980
|
+
* - an existing mysql2 pool / {@link MysqlPool} (injection — you own its lifecycle,
|
|
981
|
+
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
982
|
+
*
|
|
983
|
+
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
|
984
|
+
* (named placeholders, bignum, UTC dates, JSON-as-string), probes `SELECT VERSION()`
|
|
985
|
+
* to reject MySQL < 8.0 / MariaDB, and `disconnect()` closes the pool it created.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* ```ts
|
|
989
|
+
* import { turbineMysql } from 'turbine-orm/mysql';
|
|
990
|
+
* const db = await turbineMysql('mysql://root:root@localhost:3306/app', SCHEMA);
|
|
991
|
+
* ```
|
|
992
|
+
*/
|
|
993
|
+
async function turbineMysql(target, schema, options = {}) {
|
|
994
|
+
let pool;
|
|
995
|
+
let owns = false;
|
|
996
|
+
if (target instanceof MysqlPool) {
|
|
997
|
+
pool = target;
|
|
998
|
+
}
|
|
999
|
+
else if (isMysql2Pool(target)) {
|
|
1000
|
+
pool = new MysqlPool(target);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
const createPool = await loadCreatePool();
|
|
1004
|
+
const baseConfig = typeof target === 'string' ? parseMysqlConfig(target) : target;
|
|
1005
|
+
const mysql2Pool = createPool({
|
|
1006
|
+
...baseConfig,
|
|
1007
|
+
connectionLimit: options.connectionLimit ?? 10,
|
|
1008
|
+
...MYSQL_DRIVER_FLAGS,
|
|
1009
|
+
});
|
|
1010
|
+
// Every NEW physical connection: disable backslash string escaping so the
|
|
1011
|
+
// builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
|
|
1012
|
+
// MySQL — by default MySQL would parse `'\'` as an escaped quote (syntax
|
|
1013
|
+
// error). This also makes string-literal semantics match Postgres. Turbine
|
|
1014
|
+
// parameterizes every value, so no behavior depends on backslash escaping.
|
|
1015
|
+
//
|
|
1016
|
+
// NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
|
|
1017
|
+
// *raw* (callback-style) connection — its `.query()` returns a non-thenable
|
|
1018
|
+
// `Query`, so `.catch()`/`await` on it throws "result of query that is not a
|
|
1019
|
+
// promise". Use the callback form here (fire-and-forget): if the SET fails we
|
|
1020
|
+
// fall back to MySQL's default escaping, harmless since every value is a param.
|
|
1021
|
+
mysql2Pool.on?.('connection', (rawConn) => {
|
|
1022
|
+
rawConn.query("SET SESSION sql_mode = CONCAT(@@sql_mode, ',NO_BACKSLASH_ESCAPES')", () => { });
|
|
1023
|
+
});
|
|
1024
|
+
pool = new MysqlPool(mysql2Pool);
|
|
1025
|
+
owns = true;
|
|
1026
|
+
}
|
|
1027
|
+
// Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
|
|
1028
|
+
const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
|
|
1029
|
+
assertSupportedVersion(String(versionRows[0]?.v ?? ''));
|
|
1030
|
+
const client = new client_js_1.TurbineClient({
|
|
1031
|
+
pool,
|
|
1032
|
+
dialect: exports.mysqlDialect,
|
|
1033
|
+
preparedStatements: false,
|
|
1034
|
+
logging: options.logging,
|
|
1035
|
+
defaultLimit: options.defaultLimit,
|
|
1036
|
+
warnOnUnlimited: options.warnOnUnlimited,
|
|
1037
|
+
}, schema);
|
|
1038
|
+
if (owns) {
|
|
1039
|
+
// Turbine built this pool, so disconnect()/end() must close it. External
|
|
1040
|
+
// pools (injection) stay the caller's responsibility (disconnect() no-op),
|
|
1041
|
+
// consistent with turbineHttp / turbineSqlite.
|
|
1042
|
+
const baseDisconnect = client.disconnect.bind(client);
|
|
1043
|
+
const close = async () => {
|
|
1044
|
+
await baseDisconnect();
|
|
1045
|
+
await pool.end();
|
|
1046
|
+
};
|
|
1047
|
+
const patch = client;
|
|
1048
|
+
patch.disconnect = close;
|
|
1049
|
+
patch.end = close;
|
|
1050
|
+
}
|
|
1051
|
+
return client;
|
|
1052
|
+
}
|