turbine-orm 0.50.0 → 0.51.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 +66 -66
- package/dist/adapters/cockroachdb.d.ts +5 -5
- package/dist/adapters/cockroachdb.js +10 -10
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/yugabytedb.d.ts +7 -7
- package/dist/adapters/yugabytedb.js +10 -10
- package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
- package/dist/cjs/adapters/cockroachdb.js +10 -10
- package/dist/cjs/adapters/index.d.ts +5 -5
- package/dist/cjs/adapters/index.js +7 -7
- package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
- package/dist/cjs/adapters/yugabytedb.js +10 -10
- package/dist/cjs/cli/config.d.ts +13 -2
- package/dist/cjs/cli/config.js +3 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +1 -1
- package/dist/cjs/cli/index.d.ts +10 -10
- package/dist/cjs/cli/index.js +49 -45
- package/dist/cjs/cli/loader.d.ts +7 -7
- package/dist/cjs/cli/loader.js +9 -9
- package/dist/cjs/cli/mcp.js +4 -4
- package/dist/cjs/cli/migrate.d.ts +5 -5
- package/dist/cjs/cli/migrate.js +11 -11
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/ui.d.ts +2 -2
- package/dist/cjs/cli/ui.js +2 -2
- package/dist/cjs/client.d.ts +49 -38
- package/dist/cjs/client.js +57 -56
- package/dist/cjs/dialect.d.ts +62 -18
- package/dist/cjs/dialect.js +40 -2
- package/dist/cjs/errors.d.ts +5 -5
- package/dist/cjs/errors.js +11 -11
- package/dist/cjs/generate.d.ts +6 -6
- package/dist/cjs/generate.js +31 -29
- package/dist/cjs/index-advisor.d.ts +5 -5
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/introspect.d.ts +35 -9
- package/dist/cjs/introspect.js +83 -32
- package/dist/cjs/mssql.d.ts +11 -11
- package/dist/cjs/mssql.js +64 -29
- package/dist/cjs/mysql.d.ts +8 -8
- package/dist/cjs/mysql.js +61 -23
- package/dist/cjs/nested-write.d.ts +21 -2
- package/dist/cjs/nested-write.js +51 -14
- package/dist/cjs/optional-peer-import.cjs +7 -7
- package/dist/cjs/optional-peer-import.d.cts +7 -7
- package/dist/cjs/pipeline-submittable.d.ts +2 -2
- package/dist/cjs/pipeline-submittable.js +6 -6
- package/dist/cjs/pipeline.d.ts +1 -1
- package/dist/cjs/pipeline.js +4 -4
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +1 -1
- package/dist/cjs/powdb.d.ts +28 -28
- package/dist/cjs/powdb.js +66 -66
- package/dist/cjs/powql.d.ts +27 -27
- package/dist/cjs/powql.js +73 -52
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +5 -5
- package/dist/cjs/query/batched-loader.d.ts +11 -11
- package/dist/cjs/query/batched-loader.js +24 -24
- package/dist/cjs/query/builder.d.ts +39 -21
- package/dist/cjs/query/builder.js +99 -57
- package/dist/cjs/query/compound-unique.d.ts +1 -1
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/deferred.js +1 -1
- package/dist/cjs/query/filters.d.ts +31 -11
- package/dist/cjs/query/filters.js +67 -14
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +1 -1
- package/dist/cjs/query/relations.d.ts +9 -9
- package/dist/cjs/query/relations.js +164 -57
- package/dist/cjs/query/types.d.ts +86 -35
- package/dist/cjs/query/types.js +1 -1
- package/dist/cjs/query/utils.d.ts +27 -10
- package/dist/cjs/query/utils.js +86 -14
- package/dist/cjs/query/where.d.ts +47 -28
- package/dist/cjs/query/where.js +130 -31
- package/dist/cjs/query/writes.d.ts +24 -5
- package/dist/cjs/query/writes.js +102 -13
- package/dist/cjs/realtime.d.ts +7 -7
- package/dist/cjs/realtime.js +9 -9
- package/dist/cjs/schema-builder.d.ts +18 -7
- package/dist/cjs/schema-builder.js +17 -10
- package/dist/cjs/schema-metadata.d.ts +3 -3
- package/dist/cjs/schema-metadata.js +9 -9
- package/dist/cjs/schema-sql.d.ts +9 -9
- package/dist/cjs/schema-sql.js +20 -20
- package/dist/cjs/schema.d.ts +19 -9
- package/dist/cjs/schema.js +6 -6
- package/dist/cjs/serverless.d.ts +15 -15
- package/dist/cjs/serverless.js +16 -16
- package/dist/cjs/sqlite.d.ts +8 -8
- package/dist/cjs/sqlite.js +53 -22
- package/dist/cjs/typed-sql.d.ts +4 -4
- package/dist/cjs/typed-sql.js +5 -5
- package/dist/cli/config.d.ts +13 -2
- package/dist/cli/config.js +3 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +1 -1
- package/dist/cli/index.d.ts +10 -10
- package/dist/cli/index.js +49 -45
- package/dist/cli/loader.d.ts +7 -7
- package/dist/cli/loader.js +9 -9
- package/dist/cli/mcp.js +4 -4
- package/dist/cli/migrate.d.ts +5 -5
- package/dist/cli/migrate.js +11 -11
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +2 -2
- package/dist/client.d.ts +49 -38
- package/dist/client.js +57 -56
- package/dist/dialect.d.ts +62 -18
- package/dist/dialect.js +40 -2
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +11 -11
- package/dist/generate.d.ts +6 -6
- package/dist/generate.js +31 -29
- package/dist/index-advisor.d.ts +5 -5
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/introspect.d.ts +35 -9
- package/dist/introspect.js +82 -32
- package/dist/mssql.d.ts +11 -11
- package/dist/mssql.js +64 -29
- package/dist/mysql.d.ts +8 -8
- package/dist/mysql.js +61 -23
- package/dist/nested-write.d.ts +21 -2
- package/dist/nested-write.js +51 -14
- package/dist/optional-peer-import.cjs +7 -7
- package/dist/optional-peer-import.d.cts +7 -7
- package/dist/pipeline-submittable.d.ts +2 -2
- package/dist/pipeline-submittable.js +6 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +4 -4
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +1 -1
- package/dist/powdb.d.ts +28 -28
- package/dist/powdb.js +66 -66
- package/dist/powql.d.ts +27 -27
- package/dist/powql.js +73 -52
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +5 -5
- package/dist/query/batched-loader.d.ts +11 -11
- package/dist/query/batched-loader.js +24 -24
- package/dist/query/builder.d.ts +39 -21
- package/dist/query/builder.js +100 -58
- package/dist/query/compound-unique.d.ts +1 -1
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/deferred.js +1 -1
- package/dist/query/filters.d.ts +31 -11
- package/dist/query/filters.js +66 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +9 -9
- package/dist/query/relations.js +165 -58
- package/dist/query/types.d.ts +86 -35
- package/dist/query/types.js +1 -1
- package/dist/query/utils.d.ts +27 -10
- package/dist/query/utils.js +84 -14
- package/dist/query/where.d.ts +47 -28
- package/dist/query/where.js +129 -32
- package/dist/query/writes.d.ts +24 -5
- package/dist/query/writes.js +101 -13
- package/dist/realtime.d.ts +7 -7
- package/dist/realtime.js +9 -9
- package/dist/schema-builder.d.ts +18 -7
- package/dist/schema-builder.js +17 -10
- package/dist/schema-metadata.d.ts +3 -3
- package/dist/schema-metadata.js +9 -9
- package/dist/schema-sql.d.ts +9 -9
- package/dist/schema-sql.js +20 -20
- package/dist/schema.d.ts +19 -9
- package/dist/schema.js +6 -6
- package/dist/serverless.d.ts +15 -15
- package/dist/serverless.js +16 -16
- package/dist/sqlite.d.ts +8 -8
- package/dist/sqlite.js +53 -22
- package/dist/typed-sql.d.ts +4 -4
- package/dist/typed-sql.js +5 -5
- package/package.json +2 -2
package/dist/cjs/mssql.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* turbine-orm/mssql
|
|
3
|
+
* turbine-orm/mssql, Microsoft SQL Server engine (driver-injected, optional peer)
|
|
4
4
|
*
|
|
5
5
|
* Binds Turbine to SQL Server 2016+ via the `mssql` driver (which wraps
|
|
6
|
-
* `tedious`). `mssql` is **not** a root dependency
|
|
6
|
+
* `tedious`). `mssql` is **not** a root dependency, it is an **optional peer**:
|
|
7
7
|
* `npm i turbine-orm` pulls nothing extra, and only consumers who
|
|
8
8
|
* `import 'turbine-orm/mssql'` install `mssql` themselves. The factory loads it
|
|
9
9
|
* through a dynamic `import('mssql')` so importing this module never crashes when
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
* real JSON instead of being escaped as a string. `INCLUDE_NULL_VALUES`
|
|
35
35
|
* keeps NULL columns present (matching PostgreSQL `json_build_object`).
|
|
36
36
|
* 3. **No `LIMIT`.** Paging is `ORDER BY … OFFSET n ROWS FETCH NEXT m ROWS ONLY`,
|
|
37
|
-
* which requires an ORDER BY
|
|
37
|
+
* which requires an ORDER BY, a stable `ORDER BY (SELECT NULL)` is injected
|
|
38
38
|
* when the query has none (`Dialect.buildLimitOffset`).
|
|
39
39
|
*
|
|
40
40
|
* ## Named `@pN` placeholders (no positional `?`)
|
|
41
41
|
*
|
|
42
42
|
* `mssqlDialect.paramPlaceholder = (i) => '@p' + i`. The driver shim binds via
|
|
43
43
|
* `request.input('p' + i, value)`, so binding is by NAME and independent of where
|
|
44
|
-
* each placeholder lands in the SQL text
|
|
44
|
+
* each placeholder lands in the SQL text, exactly the guarantee PostgreSQL's
|
|
45
45
|
* numbered `$N` gives. (SQL Server is naturally named-param friendly, sidestepping
|
|
46
46
|
* the positional-`?` mis-bind bug the SQLite/MySQL phases hit.)
|
|
47
47
|
*
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
*
|
|
50
50
|
* - **Single query nested relations preserved** via `FOR JSON PATH` (SQL Server
|
|
51
51
|
* 2016+). Ordered/limited to-many uses `ORDER BY … OFFSET/FETCH` inside the FOR
|
|
52
|
-
* JSON subquery (no inner-subquery rewrite needed
|
|
52
|
+
* JSON subquery (no inner-subquery rewrite needed, FOR JSON aggregates AFTER
|
|
53
53
|
* the row selection).
|
|
54
54
|
* - **Result strategy `'output'`:** create/update/delete/upsert return their rows
|
|
55
55
|
* from the same statement. `createMany` returns the inserted rows via
|
|
56
56
|
* `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows / 2100 params
|
|
57
|
-
* per statement
|
|
57
|
+
* per statement, exceeding either throws a clear `ValidationError`; chunk
|
|
58
58
|
* yourself or use single `create`s).
|
|
59
59
|
* - **MERGE concurrency caveat:** `MERGE` is the upsert primitive; under high
|
|
60
60
|
* concurrency a `MERGE` can still race (it is NOT a substitute for a unique
|
|
@@ -63,20 +63,20 @@
|
|
|
63
63
|
* loser of a race.
|
|
64
64
|
* - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
|
|
65
65
|
* LISTEN/NOTIFY (`$listen`/`$notify`), RLS `sessionContext` (sp_set_session_context
|
|
66
|
-
* exists but is connection-scoped, not transaction-local, so it is not wired
|
|
66
|
+
* exists but is connection-scoped, not transaction-local, so it is not wired -
|
|
67
67
|
* throws rather than silently leaking context across pooled connections).
|
|
68
68
|
* - **Advisory-lock migration locking** is available in principle via
|
|
69
69
|
* `sp_getapplock`/`sp_releaseapplock` (`supportsAdvisoryLock = true`); the
|
|
70
70
|
* migrate CLI is still PostgreSQL-only, so this flag documents intent for a
|
|
71
71
|
* future adapter.
|
|
72
|
-
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)
|
|
72
|
+
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, deterministic
|
|
73
73
|
* regardless of the column's collation (note this can defeat an index unless a
|
|
74
74
|
* computed/persisted `LOWER()` index exists).
|
|
75
75
|
* - **bignum:** the shim applies the same safe-int policy Turbine uses for Postgres
|
|
76
76
|
* `int8` (number when it fits in 2^53, decimal string otherwise) WITHOUT mutating
|
|
77
77
|
* any global driver state. `DECIMAL`/`NUMERIC`/`MONEY` come back as strings;
|
|
78
78
|
* `BIT` binds/returns booleans.
|
|
79
|
-
* - **`DISTINCT ON`** is PostgreSQL-only and is not translated
|
|
79
|
+
* - **`DISTINCT ON`** is PostgreSQL-only and is not translated, avoid `distinct`
|
|
80
80
|
* on SQL Server.
|
|
81
81
|
*
|
|
82
82
|
* ## Example
|
|
@@ -122,7 +122,7 @@ function sortedRelEntries(obj) {
|
|
|
122
122
|
/**
|
|
123
123
|
* Coerce an arbitrary JS value into something `mssql` can bind. `BIT` accepts JS
|
|
124
124
|
* booleans directly; `undefined`/`null` → NULL; `bigint` follows the safe-int
|
|
125
|
-
* policy (number when it fits in 2^53, else a decimal string
|
|
125
|
+
* policy (number when it fits in 2^53, else a decimal string, no precision
|
|
126
126
|
* loss); `Date`/`Uint8Array` pass through; any remaining object/array → JSON text
|
|
127
127
|
* (matches how Turbine already pre-serializes JSON filter / `IN`-list params).
|
|
128
128
|
*/
|
|
@@ -162,7 +162,7 @@ function shapeResult(result) {
|
|
|
162
162
|
* driver returns BIGINT as a string to avoid precision loss, so a BIGINT IDENTITY
|
|
163
163
|
* `id` would otherwise surface as `'1'` instead of `1`. Values outside the safe
|
|
164
164
|
* range are left as strings (same as the Postgres path). Nested relations are
|
|
165
|
-
* unaffected
|
|
165
|
+
* unaffected, `FOR JSON PATH` already renders BIGINT as a JSON number.
|
|
166
166
|
*/
|
|
167
167
|
function coerceBigIntColumns(rows, columns) {
|
|
168
168
|
if (!columns || rows.length === 0)
|
|
@@ -285,12 +285,12 @@ class MssqlTxClient {
|
|
|
285
285
|
const { text: rawSql, params } = normalizeQueryArgs(text, values);
|
|
286
286
|
const sql = rawSql.trim();
|
|
287
287
|
// RELEASE SAVEPOINT has no SQL Server equivalent (savepoints auto-persist until
|
|
288
|
-
// the outer commit/rollback)
|
|
288
|
+
// the outer commit/rollback), releaseSavepointStatement() returns '' → no-op.
|
|
289
289
|
if (sql === '')
|
|
290
290
|
return EMPTY_RESULT;
|
|
291
291
|
// The dialect composes `SET TRANSACTION ISOLATION LEVEL …; BEGIN TRANSACTION`
|
|
292
292
|
// (SQL Server cannot take an inline isolation level on BEGIN). Split and run
|
|
293
|
-
// the parts in order
|
|
293
|
+
// the parts in order, same pattern as the MySQL shim. Gated on the exact
|
|
294
294
|
// transaction-control prefix so no builder/user SQL is ever split.
|
|
295
295
|
if (/^SET TRANSACTION ISOLATION LEVEL /i.test(sql) && sql.includes('; ')) {
|
|
296
296
|
let last = EMPTY_RESULT;
|
|
@@ -351,7 +351,7 @@ class MssqlTxClient {
|
|
|
351
351
|
* physical connection.
|
|
352
352
|
*/
|
|
353
353
|
class MssqlPool {
|
|
354
|
-
/** The underlying `mssql` ConnectionPool
|
|
354
|
+
/** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
355
355
|
pool;
|
|
356
356
|
sqlNS;
|
|
357
357
|
closed = false;
|
|
@@ -457,7 +457,7 @@ function mssqlColumnType(type, maxLength) {
|
|
|
457
457
|
return type;
|
|
458
458
|
}
|
|
459
459
|
// ---------------------------------------------------------------------------
|
|
460
|
-
// mssqlDialect
|
|
460
|
+
// mssqlDialect, the full Dialect contract for SQL Server 2016+
|
|
461
461
|
// ---------------------------------------------------------------------------
|
|
462
462
|
/**
|
|
463
463
|
* Render the SQL Server `OUTPUT` clause for a write's returning selection.
|
|
@@ -530,6 +530,27 @@ exports.mssqlDialect = {
|
|
|
530
530
|
return `[${name.replace(/]/g, ']]')}]`;
|
|
531
531
|
},
|
|
532
532
|
// SQL Server aggregate casts: COUNT → INT, AVG/float → FLOAT.
|
|
533
|
+
jsonWireRule(columnType) {
|
|
534
|
+
const t = columnType.toLowerCase();
|
|
535
|
+
// FOR JSON PATH renders BIGINT as a JSON number, which is an IEEE double:
|
|
536
|
+
// a stored 9007199254740993 came back 9007199254740992 through the join
|
|
537
|
+
// strategy, while a top-level read and the batched loader both returned
|
|
538
|
+
// the exact decimal string. The tedious driver returns BIGINT as a STRING
|
|
539
|
+
// unconditionally (verified for both small and large values), so carrying
|
|
540
|
+
// text and keeping it reproduces the driver exactly.
|
|
541
|
+
if (t === 'bigint') {
|
|
542
|
+
return { sql: (ref) => `CAST(${ref} AS NVARCHAR(50))`, decode: (value) => value };
|
|
543
|
+
}
|
|
544
|
+
// Binary columns come out of FOR JSON PATH as BASE64 text ("AQL/") rather
|
|
545
|
+
// than bytes. Style 2 converts to bare hex, which rebuilds exactly.
|
|
546
|
+
if (t === 'binary' || t === 'varbinary' || t === 'image') {
|
|
547
|
+
return {
|
|
548
|
+
sql: (ref) => `CONVERT(VARCHAR(MAX), ${ref}, 2)`,
|
|
549
|
+
decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
},
|
|
533
554
|
castAggregate(expr, target) {
|
|
534
555
|
return `CAST(${expr} AS ${target === 'int' ? 'INT' : 'FLOAT'})`;
|
|
535
556
|
},
|
|
@@ -544,7 +565,7 @@ exports.mssqlDialect = {
|
|
|
544
565
|
inClauseParam(values) {
|
|
545
566
|
return JSON.stringify(values ?? []);
|
|
546
567
|
},
|
|
547
|
-
// OUTPUT replaces RETURNING
|
|
568
|
+
// OUTPUT replaces RETURNING, injected mid-statement by the statement builders,
|
|
548
569
|
// never as a trailing clause.
|
|
549
570
|
buildReturningClause() {
|
|
550
571
|
return '';
|
|
@@ -554,7 +575,7 @@ exports.mssqlDialect = {
|
|
|
554
575
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
555
576
|
},
|
|
556
577
|
buildBulkInsertStatement(input) {
|
|
557
|
-
// No UNNEST in SQL Server
|
|
578
|
+
// No UNNEST in SQL Server, emit multi-row VALUES with flattened, named `@pN`
|
|
558
579
|
// placeholders. Enforce the engine's 1000-row / 2100-param statement limits.
|
|
559
580
|
const rowCount = input.rowValues.length;
|
|
560
581
|
const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
|
|
@@ -584,7 +605,7 @@ exports.mssqlDialect = {
|
|
|
584
605
|
},
|
|
585
606
|
buildUpsertStatement(input) {
|
|
586
607
|
// MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
|
|
587
|
-
// CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint
|
|
608
|
+
// CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint -
|
|
588
609
|
// keep the conflict columns backed by one and rely on UniqueConstraintError
|
|
589
610
|
// (2627/2601 → E008) for a concurrent loser.
|
|
590
611
|
const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
|
|
@@ -599,7 +620,7 @@ exports.mssqlDialect = {
|
|
|
599
620
|
`${out};`);
|
|
600
621
|
},
|
|
601
622
|
// UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
|
|
602
|
-
// WHERE)
|
|
623
|
+
// WHERE), a trailing clause would be invalid T-SQL.
|
|
603
624
|
buildUpdateStatement(input) {
|
|
604
625
|
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
605
626
|
return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
|
|
@@ -608,7 +629,7 @@ exports.mssqlDialect = {
|
|
|
608
629
|
const out = mssqlOutput(input.returning, 'DELETED');
|
|
609
630
|
return `DELETE FROM ${input.table}${out}${input.whereSql}`;
|
|
610
631
|
},
|
|
611
|
-
// SQL Server has no LIMIT
|
|
632
|
+
// SQL Server has no LIMIT, emit OFFSET/FETCH, injecting a stable ORDER BY when
|
|
612
633
|
// the outer query has none (OFFSET/FETCH requires an ORDER BY).
|
|
613
634
|
buildLimitOffset(input) {
|
|
614
635
|
const { limitPlaceholder, offsetPlaceholder, hasOrderBy } = input;
|
|
@@ -634,7 +655,7 @@ exports.mssqlDialect = {
|
|
|
634
655
|
},
|
|
635
656
|
// SQL Server has no JSON_CONTAINS. Emulate "the JSON array column contains the
|
|
636
657
|
// scalar value" via OPENJSON (documented `limited`: object-containment and deep
|
|
637
|
-
// paths are not supported
|
|
658
|
+
// paths are not supported, use a generated column + index for those).
|
|
638
659
|
buildJsonContains(column, paramRef) {
|
|
639
660
|
return `EXISTS (SELECT 1 FROM OPENJSON(${column}) WHERE [value] = ${paramRef})`;
|
|
640
661
|
},
|
|
@@ -713,7 +734,7 @@ exports.mssqlDialect = {
|
|
|
713
734
|
return `SAVE TRANSACTION ${name}`;
|
|
714
735
|
},
|
|
715
736
|
releaseSavepointStatement() {
|
|
716
|
-
// SQL Server has no RELEASE SAVEPOINT
|
|
737
|
+
// SQL Server has no RELEASE SAVEPOINT, savepoints persist until the outer
|
|
717
738
|
// commit/rollback. The shim treats the empty statement as a no-op.
|
|
718
739
|
return '';
|
|
719
740
|
},
|
|
@@ -771,10 +792,21 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
771
792
|
const orderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
772
793
|
const hasOrder = orderEntries.length > 0;
|
|
773
794
|
const hasLimit = spec !== true && spec.limit !== undefined;
|
|
774
|
-
/**
|
|
795
|
+
/**
|
|
796
|
+
* `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys.
|
|
797
|
+
*
|
|
798
|
+
* A column whose FOR JSON rendering diverges from the driver's own value is
|
|
799
|
+
* wrapped by the dialect's {@link Dialect.jsonWireRule} cast, exactly as the
|
|
800
|
+
* shared `json_build_object` path does, this generator is an override, so it
|
|
801
|
+
* has to opt in explicitly or BIGINT silently loses precision and a binary
|
|
802
|
+
* column arrives as base64 text.
|
|
803
|
+
*/
|
|
775
804
|
const colSelect = (a) => targetColumns.map((col) => {
|
|
776
805
|
const field = targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
777
|
-
|
|
806
|
+
const ref = `${a}.${q(col)}`;
|
|
807
|
+
const type = targetMeta.pgTypes?.[col];
|
|
808
|
+
const expr = (type && dialect.jsonWireRule?.(type)?.sql(ref)) ?? ref;
|
|
809
|
+
return `${expr} AS ${q(field)}`;
|
|
778
810
|
});
|
|
779
811
|
/** Build nested relations as `JSON_QUERY((<subquery>)) AS [<name>]` columns (pushes their params). */
|
|
780
812
|
const buildNested = (parentAlias) => {
|
|
@@ -932,7 +964,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
932
964
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
933
965
|
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
934
966
|
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
935
|
-
* same logical schema
|
|
967
|
+
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
936
968
|
* collision resolution against scalar column fields included.
|
|
937
969
|
*/
|
|
938
970
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
@@ -1120,7 +1152,7 @@ async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1120
1152
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
1121
1153
|
};
|
|
1122
1154
|
}
|
|
1123
|
-
// SQL Server has no first-class enum type
|
|
1155
|
+
// SQL Server has no first-class enum type, enums are CHECK constraints; left empty.
|
|
1124
1156
|
return { tables, enums: {} };
|
|
1125
1157
|
}
|
|
1126
1158
|
/**
|
|
@@ -1136,7 +1168,10 @@ async function introspectMssql(options) {
|
|
|
1136
1168
|
try {
|
|
1137
1169
|
const mp = new MssqlPool(pool, sqlNS);
|
|
1138
1170
|
const exec = async (sql, params) => (await mp.query(sql, params)).rows;
|
|
1139
|
-
return
|
|
1171
|
+
// `await` is LOAD-BEARING: `return somePromise` inside a try/finally runs
|
|
1172
|
+
// the finally BEFORE the promise settles, so `pool.close()` would tear the
|
|
1173
|
+
// connection down underneath the introspection queries.
|
|
1174
|
+
return await introspectMssqlWith(exec, options.schema ?? 'dbo', {
|
|
1140
1175
|
include: options.include,
|
|
1141
1176
|
exclude: options.exclude,
|
|
1142
1177
|
});
|
|
@@ -1177,7 +1212,7 @@ async function loadMssql() {
|
|
|
1177
1212
|
let mod;
|
|
1178
1213
|
try {
|
|
1179
1214
|
// `mssql` ships no bundled type declarations (it needs @types/mssql, which
|
|
1180
|
-
// Turbine deliberately does not depend on)
|
|
1215
|
+
// Turbine deliberately does not depend on), the structural MssqlModule
|
|
1181
1216
|
// above is our typed surface; the helper returns `unknown` so no TS7016.
|
|
1182
1217
|
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
1183
1218
|
// import() even if a future mssql major goes ESM-only (the CommonJS pass
|
|
@@ -1215,7 +1250,7 @@ function isMssqlPool(x) {
|
|
|
1215
1250
|
* Pass one of:
|
|
1216
1251
|
* - a connection string (`'mssql://sa:pass@host:1433/db'`),
|
|
1217
1252
|
* - an `mssql` config object (`{ server, user, password, database, options }`),
|
|
1218
|
-
* - an existing `MssqlPool` (injection
|
|
1253
|
+
* - an existing `MssqlPool` (injection, you own its lifecycle, `disconnect()` is
|
|
1219
1254
|
* a no-op).
|
|
1220
1255
|
*
|
|
1221
1256
|
* When Turbine builds the pool (string/config), it probes
|
package/dist/cjs/mysql.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm/mysql
|
|
2
|
+
* turbine-orm/mysql, MySQL 8 engine (driver-injected, optional peer)
|
|
3
3
|
*
|
|
4
4
|
* Binds Turbine to MySQL 8 via the `mysql2` driver. `mysql2` is **not** a root
|
|
5
|
-
* dependency
|
|
5
|
+
* dependency, it is an **optional peer**: `npm i turbine-orm` pulls nothing
|
|
6
6
|
* extra, and only consumers who `import 'turbine-orm/mysql'` install `mysql2`
|
|
7
7
|
* themselves. The factory loads it through a dynamic `import('mysql2/promise')`
|
|
8
8
|
* so importing this module never crashes when `mysql2` is absent for a consumer
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* (e.g. a `with`-relation `LIMIT` lands in the SELECT list, ahead of the
|
|
22
22
|
* outer `WHERE`). Postgres reconciles this via numbered `$N`; positional `?`
|
|
23
23
|
* silently mis-binds. So `mysqlDialect` uses **mysql2 named placeholders**
|
|
24
|
-
* (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object
|
|
24
|
+
* (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object -
|
|
25
25
|
* exactly mirroring `$N` semantics regardless of text order. (See
|
|
26
26
|
* `turbine-orm/sqlite` for the same fix.)
|
|
27
27
|
*
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
* - **Advisory-lock migration locking** is available in principle via
|
|
43
43
|
* `GET_LOCK`/`RELEASE_LOCK` (`supportsAdvisoryLock = true`); the migrate CLI is
|
|
44
44
|
* still PostgreSQL-only, so this flag documents intent for a future adapter.
|
|
45
|
-
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)
|
|
45
|
+
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, note this
|
|
46
46
|
* can defeat indexes unless a functional/generated index exists.
|
|
47
47
|
* - **bignum:** mysql2 is configured `supportBigNumbers:true, bigNumberStrings:false`
|
|
48
|
-
*
|
|
48
|
+
* - the same safe-int policy Turbine uses for Postgres `int8` (number when it
|
|
49
49
|
* fits, decimal string otherwise). `DECIMAL` comes back as a string; `TINYINT(1)`
|
|
50
50
|
* binds booleans as 1/0. No global parser state is mutated.
|
|
51
51
|
* - **Version:** MySQL **8.0+** required (5.7 lacks `JSON_ARRAYAGG`); MariaDB is
|
|
@@ -92,7 +92,7 @@ type QueryArg = string | {
|
|
|
92
92
|
* `SAVEPOINT` nesting all run on the same physical connection.
|
|
93
93
|
*/
|
|
94
94
|
export declare class MysqlPool implements PgCompatPool {
|
|
95
|
-
/** The underlying mysql2 pool
|
|
95
|
+
/** The underlying mysql2 pool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
96
96
|
readonly pool: Mysql2Pool;
|
|
97
97
|
private closed;
|
|
98
98
|
constructor(pool: Mysql2Pool);
|
|
@@ -109,7 +109,7 @@ export declare class MysqlPool implements PgCompatPool {
|
|
|
109
109
|
export declare function mysqlTypeToTs(dialectType: string, nullable: boolean, columnType?: string): string;
|
|
110
110
|
/**
|
|
111
111
|
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
112
|
-
* quoting, named `:pN` placeholders (NOT positional
|
|
112
|
+
* quoting, named `:pN` placeholders (NOT positional `?`, see the module
|
|
113
113
|
* docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
|
|
114
114
|
* relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
|
|
115
115
|
* (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
|
|
@@ -156,7 +156,7 @@ export interface TurbineMysqlOptions extends Pick<TurbineConfig, 'logging' | 'de
|
|
|
156
156
|
* Pass one of:
|
|
157
157
|
* - a connection string (`'mysql://user:pass@host:3306/db'`),
|
|
158
158
|
* - a mysql2 connection config object (`{ host, user, password, database }`), or
|
|
159
|
-
* - an existing mysql2 pool / {@link MysqlPool} (injection
|
|
159
|
+
* - an existing mysql2 pool / {@link MysqlPool} (injection, you own its lifecycle,
|
|
160
160
|
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
161
161
|
*
|
|
162
162
|
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
package/dist/cjs/mysql.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* turbine-orm/mysql
|
|
3
|
+
* turbine-orm/mysql, MySQL 8 engine (driver-injected, optional peer)
|
|
4
4
|
*
|
|
5
5
|
* Binds Turbine to MySQL 8 via the `mysql2` driver. `mysql2` is **not** a root
|
|
6
|
-
* dependency
|
|
6
|
+
* dependency, it is an **optional peer**: `npm i turbine-orm` pulls nothing
|
|
7
7
|
* extra, and only consumers who `import 'turbine-orm/mysql'` install `mysql2`
|
|
8
8
|
* themselves. The factory loads it through a dynamic `import('mysql2/promise')`
|
|
9
9
|
* so importing this module never crashes when `mysql2` is absent for a consumer
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* (e.g. a `with`-relation `LIMIT` lands in the SELECT list, ahead of the
|
|
23
23
|
* outer `WHERE`). Postgres reconciles this via numbered `$N`; positional `?`
|
|
24
24
|
* silently mis-binds. So `mysqlDialect` uses **mysql2 named placeholders**
|
|
25
|
-
* (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object
|
|
25
|
+
* (`:p1`, `:p2`, …) and the driver shim binds via a `{ p1, p2, … }` object -
|
|
26
26
|
* exactly mirroring `$N` semantics regardless of text order. (See
|
|
27
27
|
* `turbine-orm/sqlite` for the same fix.)
|
|
28
28
|
*
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
* - **Advisory-lock migration locking** is available in principle via
|
|
44
44
|
* `GET_LOCK`/`RELEASE_LOCK` (`supportsAdvisoryLock = true`); the migrate CLI is
|
|
45
45
|
* still PostgreSQL-only, so this flag documents intent for a future adapter.
|
|
46
|
-
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)
|
|
46
|
+
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, note this
|
|
47
47
|
* can defeat indexes unless a functional/generated index exists.
|
|
48
48
|
* - **bignum:** mysql2 is configured `supportBigNumbers:true, bigNumberStrings:false`
|
|
49
|
-
*
|
|
49
|
+
* - the same safe-int policy Turbine uses for Postgres `int8` (number when it
|
|
50
50
|
* fits, decimal string otherwise). `DECIMAL` comes back as a string; `TINYINT(1)`
|
|
51
51
|
* binds booleans as 1/0. No global parser state is mutated.
|
|
52
52
|
* - **Version:** MySQL **8.0+** required (5.7 lacks `JSON_ARRAYAGG`); MariaDB is
|
|
@@ -108,7 +108,7 @@ function toMysqlParam(value) {
|
|
|
108
108
|
/**
|
|
109
109
|
* Bind the positional `params[]` (in 1-indexed generation order) to the named
|
|
110
110
|
* `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
|
|
111
|
-
* independent of where each placeholder lands in the SQL text
|
|
111
|
+
* independent of where each placeholder lands in the SQL text, the same
|
|
112
112
|
* guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
|
|
113
113
|
* statements (BEGIN/COMMIT/DDL) so they bind nothing.
|
|
114
114
|
*/
|
|
@@ -142,7 +142,7 @@ function shapeResult(result) {
|
|
|
142
142
|
* deadlock / lock-wait-timeout become retryable (E012/E013). The original mysql2
|
|
143
143
|
* error (with its real message) is preserved as the wrapped error's `.cause`
|
|
144
144
|
* downstream. Returns the value unchanged when it is not a recognizable mysql2
|
|
145
|
-
* error. We only annotate here
|
|
145
|
+
* error. We only annotate here, `wrapPgError` (invoked downstream in the query
|
|
146
146
|
* executor / transaction proxy) does the actual translation.
|
|
147
147
|
*/
|
|
148
148
|
function augmentMysqlError(err) {
|
|
@@ -225,7 +225,7 @@ async function execOne(runner, sql, params) {
|
|
|
225
225
|
* emits `SET TRANSACTION ISOLATION LEVEL <level>; START TRANSACTION`. mysql2 runs
|
|
226
226
|
* one statement per call (multi-statements stay OFF by design), so split exactly
|
|
227
227
|
* that dialect-generated compound and run the parts in order. Gated on the precise
|
|
228
|
-
* transaction-control prefix
|
|
228
|
+
* transaction-control prefix, the query builder never emits `SET TRANSACTION
|
|
229
229
|
* ISOLATION LEVEL`, so no builder/user SQL is ever split.
|
|
230
230
|
*/
|
|
231
231
|
async function runOnConnection(conn, sql, params) {
|
|
@@ -247,7 +247,7 @@ async function runOnConnection(conn, sql, params) {
|
|
|
247
247
|
* `SAVEPOINT` nesting all run on the same physical connection.
|
|
248
248
|
*/
|
|
249
249
|
class MysqlPool {
|
|
250
|
-
/** The underlying mysql2 pool
|
|
250
|
+
/** The underlying mysql2 pool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
251
251
|
pool;
|
|
252
252
|
closed = false;
|
|
253
253
|
constructor(pool) {
|
|
@@ -359,11 +359,11 @@ function mysqlColumnType(type, maxLength) {
|
|
|
359
359
|
return type;
|
|
360
360
|
}
|
|
361
361
|
// ---------------------------------------------------------------------------
|
|
362
|
-
// mysqlDialect
|
|
362
|
+
// mysqlDialect, the full Dialect contract for MySQL 8
|
|
363
363
|
// ---------------------------------------------------------------------------
|
|
364
364
|
/**
|
|
365
365
|
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
366
|
-
* quoting, named `:pN` placeholders (NOT positional
|
|
366
|
+
* quoting, named `:pN` placeholders (NOT positional `?`, see the module
|
|
367
367
|
* docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
|
|
368
368
|
* relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
|
|
369
369
|
* (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
|
|
@@ -407,7 +407,7 @@ exports.mysqlDialect = {
|
|
|
407
407
|
jsonPathSupport: 'function',
|
|
408
408
|
emptyJsonArrayLiteral: 'JSON_ARRAY()',
|
|
409
409
|
nullJsonLiteral: 'NULL',
|
|
410
|
-
// Named placeholders (`:p1`, `:p2`, …)
|
|
410
|
+
// Named placeholders (`:p1`, `:p2`, …), NOT positional `?`. Turbine pushes
|
|
411
411
|
// params in 1-indexed generation order but may EMIT them in a different SQL
|
|
412
412
|
// text position; positional `?` mis-binds. mysql2 named parameters bind by
|
|
413
413
|
// name, so `:pN` ↔ `params[N-1]` mirrors Postgres' `$N` regardless of text
|
|
@@ -444,7 +444,7 @@ exports.mysqlDialect = {
|
|
|
444
444
|
* Wrap a nested correlated subquery for embedding inside a parent `JSON_OBJECT`.
|
|
445
445
|
* MySQL double-encodes a scalar subquery result as a JSON *string* unless it is
|
|
446
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
|
|
447
|
+
* to be embedded as real JSON (objects/arrays), not a quoted string, the same
|
|
448
448
|
* intent as SQLite's `json(...)` wrap. The fallback (`JSON_ARRAY()` / `NULL`) is
|
|
449
449
|
* already JSON-typed.
|
|
450
450
|
*/
|
|
@@ -453,6 +453,40 @@ exports.mysqlDialect = {
|
|
|
453
453
|
},
|
|
454
454
|
// MySQL aggregate casts: COUNT → SIGNED (BIGINT, comes back as a JS number when
|
|
455
455
|
// safe), AVG/float → DECIMAL (string, the aggregate transform Number()-coerces).
|
|
456
|
+
jsonWireRule(columnType) {
|
|
457
|
+
const t = columnType.toLowerCase();
|
|
458
|
+
// JSON_OBJECT renders these as JSON numbers, which are IEEE doubles, so a
|
|
459
|
+
// relation read through the join strategy disagreed with a top-level read
|
|
460
|
+
// and with the batched loader, LOSSILY. Measured: BIGINT 9007199254740993
|
|
461
|
+
// came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
|
|
462
|
+
// mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
|
|
463
|
+
// DECIMAL is always a string), so carry the text and keep it.
|
|
464
|
+
if (t === 'bigint') {
|
|
465
|
+
return {
|
|
466
|
+
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
467
|
+
decode: (value) => {
|
|
468
|
+
// Same policy as the driver's supportBigNumbers/bigNumberStrings:false.
|
|
469
|
+
if (typeof value !== 'string' || !/^-?\d+$/.test(value))
|
|
470
|
+
return value;
|
|
471
|
+
const asNumber = Number(value);
|
|
472
|
+
return Number.isSafeInteger(asNumber) ? asNumber : value;
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (t === 'decimal' || t === 'numeric') {
|
|
477
|
+
return { sql: (ref) => `CAST(${ref} AS CHAR)`, decode: (value) => value };
|
|
478
|
+
}
|
|
479
|
+
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
480
|
+
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
481
|
+
// bytes. Carry hex and rebuild the buffer.
|
|
482
|
+
if (t === 'binary' || t === 'varbinary' || t.endsWith('blob')) {
|
|
483
|
+
return {
|
|
484
|
+
sql: (ref) => `HEX(${ref})`,
|
|
485
|
+
decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
},
|
|
456
490
|
castAggregate(expr, target) {
|
|
457
491
|
return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
|
|
458
492
|
},
|
|
@@ -468,7 +502,7 @@ exports.mysqlDialect = {
|
|
|
468
502
|
inClauseParam(values) {
|
|
469
503
|
return JSON.stringify(values ?? []);
|
|
470
504
|
},
|
|
471
|
-
// No RETURNING
|
|
505
|
+
// No RETURNING, resultStrategy 'reselect' re-fetches the row.
|
|
472
506
|
buildReturningClause() {
|
|
473
507
|
return '';
|
|
474
508
|
},
|
|
@@ -476,7 +510,7 @@ exports.mysqlDialect = {
|
|
|
476
510
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
477
511
|
},
|
|
478
512
|
buildBulkInsertStatement(input) {
|
|
479
|
-
// No UNNEST in MySQL
|
|
513
|
+
// No UNNEST in MySQL, emit multi-row VALUES with flattened, named
|
|
480
514
|
// placeholders (`:p1`, `:p2`, …) matching the flat param order.
|
|
481
515
|
let n = 0;
|
|
482
516
|
const placeholders = input.rowValues
|
|
@@ -491,7 +525,7 @@ exports.mysqlDialect = {
|
|
|
491
525
|
};
|
|
492
526
|
},
|
|
493
527
|
buildUpsertStatement(input) {
|
|
494
|
-
// MySQL ignores the explicit conflict target
|
|
528
|
+
// MySQL ignores the explicit conflict target, ON DUPLICATE KEY UPDATE keys
|
|
495
529
|
// off the table's PK/unique indexes. The `where`-derived conflictColumns
|
|
496
530
|
// (input.conflictColumns) must therefore correspond to a real unique/PK index
|
|
497
531
|
// for the upsert to target the intended row (plan §4 / R9).
|
|
@@ -600,7 +634,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
600
634
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
601
635
|
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
602
636
|
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
603
|
-
* same logical schema
|
|
637
|
+
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
604
638
|
* collision resolution against scalar column fields included.
|
|
605
639
|
*/
|
|
606
640
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
@@ -787,9 +821,13 @@ async function introspectMysql(options) {
|
|
|
787
821
|
schemaName = rows[0]?.db ?? '';
|
|
788
822
|
}
|
|
789
823
|
if (!schemaName) {
|
|
790
|
-
throw new errors_js_1.ConnectionError('[turbine] Could not determine the MySQL database to introspect
|
|
824
|
+
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.');
|
|
791
825
|
}
|
|
792
|
-
|
|
826
|
+
// `await` is LOAD-BEARING, not stylistic: `return somePromise` inside a
|
|
827
|
+
// try/finally runs the finally BEFORE the promise settles, so `pool.end()`
|
|
828
|
+
// closed the pool out from under every introspection query and the whole
|
|
829
|
+
// call failed with mysql2's "Pool is closed".
|
|
830
|
+
return await introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
|
|
793
831
|
}
|
|
794
832
|
finally {
|
|
795
833
|
await pool.end();
|
|
@@ -800,7 +838,7 @@ async function introspectMysql(options) {
|
|
|
800
838
|
// ---------------------------------------------------------------------------
|
|
801
839
|
/** mysql2 connection flags Turbine pins for correct behavior. */
|
|
802
840
|
const MYSQL_DRIVER_FLAGS = {
|
|
803
|
-
// Named placeholders (`:pN`)
|
|
841
|
+
// Named placeholders (`:pN`), see paramPlaceholder + toNamedBinding.
|
|
804
842
|
namedPlaceholders: true,
|
|
805
843
|
// Safe-int policy matching Postgres int8: number when it fits, decimal string
|
|
806
844
|
// otherwise. DECIMAL always comes back as a string. No global state mutated.
|
|
@@ -886,7 +924,7 @@ function isMysql2Pool(x) {
|
|
|
886
924
|
* Pass one of:
|
|
887
925
|
* - a connection string (`'mysql://user:pass@host:3306/db'`),
|
|
888
926
|
* - a mysql2 connection config object (`{ host, user, password, database }`), or
|
|
889
|
-
* - an existing mysql2 pool / {@link MysqlPool} (injection
|
|
927
|
+
* - an existing mysql2 pool / {@link MysqlPool} (injection, you own its lifecycle,
|
|
890
928
|
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
891
929
|
*
|
|
892
930
|
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
|
@@ -918,12 +956,12 @@ async function turbineMysql(target, schema, options = {}) {
|
|
|
918
956
|
});
|
|
919
957
|
// Every NEW physical connection: disable backslash string escaping so the
|
|
920
958
|
// builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
|
|
921
|
-
// MySQL
|
|
959
|
+
// MySQL, by default MySQL would parse `'\'` as an escaped quote (syntax
|
|
922
960
|
// error). This also makes string-literal semantics match Postgres. Turbine
|
|
923
961
|
// parameterizes every value, so no behavior depends on backslash escaping.
|
|
924
962
|
//
|
|
925
963
|
// NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
|
|
926
|
-
// *raw* (callback-style) connection
|
|
964
|
+
// *raw* (callback-style) connection, its `.query()` returns a non-thenable
|
|
927
965
|
// `Query`, so `.catch()`/`await` on it throws "result of query that is not a
|
|
928
966
|
// promise". Use the callback form here (fire-and-forget): if the SET fails we
|
|
929
967
|
// fall back to MySQL's default escaping, harmless since every value is a param.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, Nested write engine
|
|
3
3
|
*
|
|
4
4
|
* Tree-walking create/update that resolves relation fields in `data` into
|
|
5
5
|
* batched SQL operations within a transaction. Supports create, connect,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* This module is imported by `query/builder.ts` when the `data` argument
|
|
10
10
|
* of `create()` or `update()` contains relation fields. It never imports
|
|
11
|
-
* `client.ts` directly
|
|
11
|
+
* `client.ts` directly, the transaction handle is passed in via
|
|
12
12
|
* `NestedWriteContext`.
|
|
13
13
|
*/
|
|
14
14
|
import type { RelationDef, SchemaMetadata, TableMetadata } from './schema.js';
|
|
@@ -22,6 +22,25 @@ export interface ExtractedFields {
|
|
|
22
22
|
*/
|
|
23
23
|
export interface NestedWriteContext {
|
|
24
24
|
schema: SchemaMetadata;
|
|
25
|
+
/**
|
|
26
|
+
* Refuse a `connect` / `connectOrCreate` that would RE-PARENT a to-many
|
|
27
|
+
* child already owned by a different parent. Off by default.
|
|
28
|
+
*
|
|
29
|
+
* `connect: { id: 42 }` on a to-many relation means "make row 42 mine", and
|
|
30
|
+
* it does that unconditionally: if another parent owns row 42, the row is
|
|
31
|
+
* silently taken from them. A handler that forwards a client-supplied id
|
|
32
|
+
* into a nested connect therefore hands any caller a cross-tenant write
|
|
33
|
+
* primitive, and the only defense is a hand-rolled ownership check at every
|
|
34
|
+
* call site. With this on, connecting a child whose foreign key already
|
|
35
|
+
* points at a DIFFERENT parent raises E003; connecting an unowned child
|
|
36
|
+
* (null FK) or one this parent already owns still succeeds, so the
|
|
37
|
+
* legitimate uses are untouched.
|
|
38
|
+
*
|
|
39
|
+
* Applies to `hasMany` / `hasOne` only. A `belongsTo` connect points the row
|
|
40
|
+
* being written at a parent, which takes nothing from anyone, and a
|
|
41
|
+
* many-to-many connect adds a junction row rather than moving one.
|
|
42
|
+
*/
|
|
43
|
+
scopedConnect?: boolean;
|
|
25
44
|
tx: {
|
|
26
45
|
table<T extends object>(name: string): {
|
|
27
46
|
create(args: {
|