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/mssql.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm/mssql
|
|
2
|
+
* turbine-orm/mssql, Microsoft SQL Server engine (driver-injected, optional peer)
|
|
3
3
|
*
|
|
4
4
|
* Binds Turbine to SQL Server 2016+ via the `mssql` driver (which wraps
|
|
5
|
-
* `tedious`). `mssql` is **not** a root dependency
|
|
5
|
+
* `tedious`). `mssql` is **not** a root dependency, it is an **optional peer**:
|
|
6
6
|
* `npm i turbine-orm` pulls nothing extra, and only consumers who
|
|
7
7
|
* `import 'turbine-orm/mssql'` install `mssql` themselves. The factory loads it
|
|
8
8
|
* through a dynamic `import('mssql')` so importing this module never crashes when
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
* real JSON instead of being escaped as a string. `INCLUDE_NULL_VALUES`
|
|
34
34
|
* keeps NULL columns present (matching PostgreSQL `json_build_object`).
|
|
35
35
|
* 3. **No `LIMIT`.** Paging is `ORDER BY … OFFSET n ROWS FETCH NEXT m ROWS ONLY`,
|
|
36
|
-
* which requires an ORDER BY
|
|
36
|
+
* which requires an ORDER BY, a stable `ORDER BY (SELECT NULL)` is injected
|
|
37
37
|
* when the query has none (`Dialect.buildLimitOffset`).
|
|
38
38
|
*
|
|
39
39
|
* ## Named `@pN` placeholders (no positional `?`)
|
|
40
40
|
*
|
|
41
41
|
* `mssqlDialect.paramPlaceholder = (i) => '@p' + i`. The driver shim binds via
|
|
42
42
|
* `request.input('p' + i, value)`, so binding is by NAME and independent of where
|
|
43
|
-
* each placeholder lands in the SQL text
|
|
43
|
+
* each placeholder lands in the SQL text, exactly the guarantee PostgreSQL's
|
|
44
44
|
* numbered `$N` gives. (SQL Server is naturally named-param friendly, sidestepping
|
|
45
45
|
* the positional-`?` mis-bind bug the SQLite/MySQL phases hit.)
|
|
46
46
|
*
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
*
|
|
49
49
|
* - **Single query nested relations preserved** via `FOR JSON PATH` (SQL Server
|
|
50
50
|
* 2016+). Ordered/limited to-many uses `ORDER BY … OFFSET/FETCH` inside the FOR
|
|
51
|
-
* JSON subquery (no inner-subquery rewrite needed
|
|
51
|
+
* JSON subquery (no inner-subquery rewrite needed, FOR JSON aggregates AFTER
|
|
52
52
|
* the row selection).
|
|
53
53
|
* - **Result strategy `'output'`:** create/update/delete/upsert return their rows
|
|
54
54
|
* from the same statement. `createMany` returns the inserted rows via
|
|
55
55
|
* `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows / 2100 params
|
|
56
|
-
* per statement
|
|
56
|
+
* per statement, exceeding either throws a clear `ValidationError`; chunk
|
|
57
57
|
* yourself or use single `create`s).
|
|
58
58
|
* - **MERGE concurrency caveat:** `MERGE` is the upsert primitive; under high
|
|
59
59
|
* concurrency a `MERGE` can still race (it is NOT a substitute for a unique
|
|
@@ -62,20 +62,20 @@
|
|
|
62
62
|
* loser of a race.
|
|
63
63
|
* - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
|
|
64
64
|
* LISTEN/NOTIFY (`$listen`/`$notify`), RLS `sessionContext` (sp_set_session_context
|
|
65
|
-
* exists but is connection-scoped, not transaction-local, so it is not wired
|
|
65
|
+
* exists but is connection-scoped, not transaction-local, so it is not wired -
|
|
66
66
|
* throws rather than silently leaking context across pooled connections).
|
|
67
67
|
* - **Advisory-lock migration locking** is available in principle via
|
|
68
68
|
* `sp_getapplock`/`sp_releaseapplock` (`supportsAdvisoryLock = true`); the
|
|
69
69
|
* migrate CLI is still PostgreSQL-only, so this flag documents intent for a
|
|
70
70
|
* future adapter.
|
|
71
|
-
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)
|
|
71
|
+
* - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)`, deterministic
|
|
72
72
|
* regardless of the column's collation (note this can defeat an index unless a
|
|
73
73
|
* computed/persisted `LOWER()` index exists).
|
|
74
74
|
* - **bignum:** the shim applies the same safe-int policy Turbine uses for Postgres
|
|
75
75
|
* `int8` (number when it fits in 2^53, decimal string otherwise) WITHOUT mutating
|
|
76
76
|
* any global driver state. `DECIMAL`/`NUMERIC`/`MONEY` come back as strings;
|
|
77
77
|
* `BIT` binds/returns booleans.
|
|
78
|
-
* - **`DISTINCT ON`** is PostgreSQL-only and is not translated
|
|
78
|
+
* - **`DISTINCT ON`** is PostgreSQL-only and is not translated, avoid `distinct`
|
|
79
79
|
* on SQL Server.
|
|
80
80
|
*
|
|
81
81
|
* ## Example
|
|
@@ -112,7 +112,7 @@ function sortedRelEntries(obj) {
|
|
|
112
112
|
/**
|
|
113
113
|
* Coerce an arbitrary JS value into something `mssql` can bind. `BIT` accepts JS
|
|
114
114
|
* booleans directly; `undefined`/`null` → NULL; `bigint` follows the safe-int
|
|
115
|
-
* policy (number when it fits in 2^53, else a decimal string
|
|
115
|
+
* policy (number when it fits in 2^53, else a decimal string, no precision
|
|
116
116
|
* loss); `Date`/`Uint8Array` pass through; any remaining object/array → JSON text
|
|
117
117
|
* (matches how Turbine already pre-serializes JSON filter / `IN`-list params).
|
|
118
118
|
*/
|
|
@@ -152,7 +152,7 @@ function shapeResult(result) {
|
|
|
152
152
|
* driver returns BIGINT as a string to avoid precision loss, so a BIGINT IDENTITY
|
|
153
153
|
* `id` would otherwise surface as `'1'` instead of `1`. Values outside the safe
|
|
154
154
|
* range are left as strings (same as the Postgres path). Nested relations are
|
|
155
|
-
* unaffected
|
|
155
|
+
* unaffected, `FOR JSON PATH` already renders BIGINT as a JSON number.
|
|
156
156
|
*/
|
|
157
157
|
function coerceBigIntColumns(rows, columns) {
|
|
158
158
|
if (!columns || rows.length === 0)
|
|
@@ -275,12 +275,12 @@ class MssqlTxClient {
|
|
|
275
275
|
const { text: rawSql, params } = normalizeQueryArgs(text, values);
|
|
276
276
|
const sql = rawSql.trim();
|
|
277
277
|
// RELEASE SAVEPOINT has no SQL Server equivalent (savepoints auto-persist until
|
|
278
|
-
// the outer commit/rollback)
|
|
278
|
+
// the outer commit/rollback), releaseSavepointStatement() returns '' → no-op.
|
|
279
279
|
if (sql === '')
|
|
280
280
|
return EMPTY_RESULT;
|
|
281
281
|
// The dialect composes `SET TRANSACTION ISOLATION LEVEL …; BEGIN TRANSACTION`
|
|
282
282
|
// (SQL Server cannot take an inline isolation level on BEGIN). Split and run
|
|
283
|
-
// the parts in order
|
|
283
|
+
// the parts in order, same pattern as the MySQL shim. Gated on the exact
|
|
284
284
|
// transaction-control prefix so no builder/user SQL is ever split.
|
|
285
285
|
if (/^SET TRANSACTION ISOLATION LEVEL /i.test(sql) && sql.includes('; ')) {
|
|
286
286
|
let last = EMPTY_RESULT;
|
|
@@ -341,7 +341,7 @@ class MssqlTxClient {
|
|
|
341
341
|
* physical connection.
|
|
342
342
|
*/
|
|
343
343
|
export class MssqlPool {
|
|
344
|
-
/** The underlying `mssql` ConnectionPool
|
|
344
|
+
/** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
345
345
|
pool;
|
|
346
346
|
sqlNS;
|
|
347
347
|
closed = false;
|
|
@@ -446,7 +446,7 @@ function mssqlColumnType(type, maxLength) {
|
|
|
446
446
|
return type;
|
|
447
447
|
}
|
|
448
448
|
// ---------------------------------------------------------------------------
|
|
449
|
-
// mssqlDialect
|
|
449
|
+
// mssqlDialect, the full Dialect contract for SQL Server 2016+
|
|
450
450
|
// ---------------------------------------------------------------------------
|
|
451
451
|
/**
|
|
452
452
|
* Render the SQL Server `OUTPUT` clause for a write's returning selection.
|
|
@@ -519,6 +519,27 @@ export const mssqlDialect = {
|
|
|
519
519
|
return `[${name.replace(/]/g, ']]')}]`;
|
|
520
520
|
},
|
|
521
521
|
// SQL Server aggregate casts: COUNT → INT, AVG/float → FLOAT.
|
|
522
|
+
jsonWireRule(columnType) {
|
|
523
|
+
const t = columnType.toLowerCase();
|
|
524
|
+
// FOR JSON PATH renders BIGINT as a JSON number, which is an IEEE double:
|
|
525
|
+
// a stored 9007199254740993 came back 9007199254740992 through the join
|
|
526
|
+
// strategy, while a top-level read and the batched loader both returned
|
|
527
|
+
// the exact decimal string. The tedious driver returns BIGINT as a STRING
|
|
528
|
+
// unconditionally (verified for both small and large values), so carrying
|
|
529
|
+
// text and keeping it reproduces the driver exactly.
|
|
530
|
+
if (t === 'bigint') {
|
|
531
|
+
return { sql: (ref) => `CAST(${ref} AS NVARCHAR(50))`, decode: (value) => value };
|
|
532
|
+
}
|
|
533
|
+
// Binary columns come out of FOR JSON PATH as BASE64 text ("AQL/") rather
|
|
534
|
+
// than bytes. Style 2 converts to bare hex, which rebuilds exactly.
|
|
535
|
+
if (t === 'binary' || t === 'varbinary' || t === 'image') {
|
|
536
|
+
return {
|
|
537
|
+
sql: (ref) => `CONVERT(VARCHAR(MAX), ${ref}, 2)`,
|
|
538
|
+
decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return undefined;
|
|
542
|
+
},
|
|
522
543
|
castAggregate(expr, target) {
|
|
523
544
|
return `CAST(${expr} AS ${target === 'int' ? 'INT' : 'FLOAT'})`;
|
|
524
545
|
},
|
|
@@ -533,7 +554,7 @@ export const mssqlDialect = {
|
|
|
533
554
|
inClauseParam(values) {
|
|
534
555
|
return JSON.stringify(values ?? []);
|
|
535
556
|
},
|
|
536
|
-
// OUTPUT replaces RETURNING
|
|
557
|
+
// OUTPUT replaces RETURNING, injected mid-statement by the statement builders,
|
|
537
558
|
// never as a trailing clause.
|
|
538
559
|
buildReturningClause() {
|
|
539
560
|
return '';
|
|
@@ -543,7 +564,7 @@ export const mssqlDialect = {
|
|
|
543
564
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
544
565
|
},
|
|
545
566
|
buildBulkInsertStatement(input) {
|
|
546
|
-
// No UNNEST in SQL Server
|
|
567
|
+
// No UNNEST in SQL Server, emit multi-row VALUES with flattened, named `@pN`
|
|
547
568
|
// placeholders. Enforce the engine's 1000-row / 2100-param statement limits.
|
|
548
569
|
const rowCount = input.rowValues.length;
|
|
549
570
|
const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
|
|
@@ -573,7 +594,7 @@ export const mssqlDialect = {
|
|
|
573
594
|
},
|
|
574
595
|
buildUpsertStatement(input) {
|
|
575
596
|
// MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
|
|
576
|
-
// CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint
|
|
597
|
+
// CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint -
|
|
577
598
|
// keep the conflict columns backed by one and rely on UniqueConstraintError
|
|
578
599
|
// (2627/2601 → E008) for a concurrent loser.
|
|
579
600
|
const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
|
|
@@ -588,7 +609,7 @@ export const mssqlDialect = {
|
|
|
588
609
|
`${out};`);
|
|
589
610
|
},
|
|
590
611
|
// UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
|
|
591
|
-
// WHERE)
|
|
612
|
+
// WHERE), a trailing clause would be invalid T-SQL.
|
|
592
613
|
buildUpdateStatement(input) {
|
|
593
614
|
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
594
615
|
return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
|
|
@@ -597,7 +618,7 @@ export const mssqlDialect = {
|
|
|
597
618
|
const out = mssqlOutput(input.returning, 'DELETED');
|
|
598
619
|
return `DELETE FROM ${input.table}${out}${input.whereSql}`;
|
|
599
620
|
},
|
|
600
|
-
// SQL Server has no LIMIT
|
|
621
|
+
// SQL Server has no LIMIT, emit OFFSET/FETCH, injecting a stable ORDER BY when
|
|
601
622
|
// the outer query has none (OFFSET/FETCH requires an ORDER BY).
|
|
602
623
|
buildLimitOffset(input) {
|
|
603
624
|
const { limitPlaceholder, offsetPlaceholder, hasOrderBy } = input;
|
|
@@ -623,7 +644,7 @@ export const mssqlDialect = {
|
|
|
623
644
|
},
|
|
624
645
|
// SQL Server has no JSON_CONTAINS. Emulate "the JSON array column contains the
|
|
625
646
|
// scalar value" via OPENJSON (documented `limited`: object-containment and deep
|
|
626
|
-
// paths are not supported
|
|
647
|
+
// paths are not supported, use a generated column + index for those).
|
|
627
648
|
buildJsonContains(column, paramRef) {
|
|
628
649
|
return `EXISTS (SELECT 1 FROM OPENJSON(${column}) WHERE [value] = ${paramRef})`;
|
|
629
650
|
},
|
|
@@ -702,7 +723,7 @@ export const mssqlDialect = {
|
|
|
702
723
|
return `SAVE TRANSACTION ${name}`;
|
|
703
724
|
},
|
|
704
725
|
releaseSavepointStatement() {
|
|
705
|
-
// SQL Server has no RELEASE SAVEPOINT
|
|
726
|
+
// SQL Server has no RELEASE SAVEPOINT, savepoints persist until the outer
|
|
706
727
|
// commit/rollback. The shim treats the empty statement as a no-op.
|
|
707
728
|
return '';
|
|
708
729
|
},
|
|
@@ -760,10 +781,21 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
760
781
|
const orderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
761
782
|
const hasOrder = orderEntries.length > 0;
|
|
762
783
|
const hasLimit = spec !== true && spec.limit !== undefined;
|
|
763
|
-
/**
|
|
784
|
+
/**
|
|
785
|
+
* `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys.
|
|
786
|
+
*
|
|
787
|
+
* A column whose FOR JSON rendering diverges from the driver's own value is
|
|
788
|
+
* wrapped by the dialect's {@link Dialect.jsonWireRule} cast, exactly as the
|
|
789
|
+
* shared `json_build_object` path does, this generator is an override, so it
|
|
790
|
+
* has to opt in explicitly or BIGINT silently loses precision and a binary
|
|
791
|
+
* column arrives as base64 text.
|
|
792
|
+
*/
|
|
764
793
|
const colSelect = (a) => targetColumns.map((col) => {
|
|
765
794
|
const field = targetMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
766
|
-
|
|
795
|
+
const ref = `${a}.${q(col)}`;
|
|
796
|
+
const type = targetMeta.pgTypes?.[col];
|
|
797
|
+
const expr = (type && dialect.jsonWireRule?.(type)?.sql(ref)) ?? ref;
|
|
798
|
+
return `${expr} AS ${q(field)}`;
|
|
767
799
|
});
|
|
768
800
|
/** Build nested relations as `JSON_QUERY((<subquery>)) AS [<name>]` columns (pushes their params). */
|
|
769
801
|
const buildNested = (parentAlias) => {
|
|
@@ -921,7 +953,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
921
953
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
922
954
|
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
923
955
|
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
924
|
-
* same logical schema
|
|
956
|
+
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
925
957
|
* collision resolution against scalar column fields included.
|
|
926
958
|
*/
|
|
927
959
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
@@ -1109,7 +1141,7 @@ export async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1109
1141
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
1110
1142
|
};
|
|
1111
1143
|
}
|
|
1112
|
-
// SQL Server has no first-class enum type
|
|
1144
|
+
// SQL Server has no first-class enum type, enums are CHECK constraints; left empty.
|
|
1113
1145
|
return { tables, enums: {} };
|
|
1114
1146
|
}
|
|
1115
1147
|
/**
|
|
@@ -1125,7 +1157,10 @@ export async function introspectMssql(options) {
|
|
|
1125
1157
|
try {
|
|
1126
1158
|
const mp = new MssqlPool(pool, sqlNS);
|
|
1127
1159
|
const exec = async (sql, params) => (await mp.query(sql, params)).rows;
|
|
1128
|
-
return
|
|
1160
|
+
// `await` is LOAD-BEARING: `return somePromise` inside a try/finally runs
|
|
1161
|
+
// the finally BEFORE the promise settles, so `pool.close()` would tear the
|
|
1162
|
+
// connection down underneath the introspection queries.
|
|
1163
|
+
return await introspectMssqlWith(exec, options.schema ?? 'dbo', {
|
|
1129
1164
|
include: options.include,
|
|
1130
1165
|
exclude: options.exclude,
|
|
1131
1166
|
});
|
|
@@ -1166,7 +1201,7 @@ async function loadMssql() {
|
|
|
1166
1201
|
let mod;
|
|
1167
1202
|
try {
|
|
1168
1203
|
// `mssql` ships no bundled type declarations (it needs @types/mssql, which
|
|
1169
|
-
// Turbine deliberately does not depend on)
|
|
1204
|
+
// Turbine deliberately does not depend on), the structural MssqlModule
|
|
1170
1205
|
// above is our typed surface; the helper returns `unknown` so no TS7016.
|
|
1171
1206
|
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
1172
1207
|
// import() even if a future mssql major goes ESM-only (the CommonJS pass
|
|
@@ -1204,7 +1239,7 @@ function isMssqlPool(x) {
|
|
|
1204
1239
|
* Pass one of:
|
|
1205
1240
|
* - a connection string (`'mssql://sa:pass@host:1433/db'`),
|
|
1206
1241
|
* - an `mssql` config object (`{ server, user, password, database, options }`),
|
|
1207
|
-
* - an existing `MssqlPool` (injection
|
|
1242
|
+
* - an existing `MssqlPool` (injection, you own its lifecycle, `disconnect()` is
|
|
1208
1243
|
* a no-op).
|
|
1209
1244
|
*
|
|
1210
1245
|
* When Turbine builds the pool (string/config), it probes
|
package/dist/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/mysql.js
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
|
|
@@ -98,7 +98,7 @@ function toMysqlParam(value) {
|
|
|
98
98
|
/**
|
|
99
99
|
* Bind the positional `params[]` (in 1-indexed generation order) to the named
|
|
100
100
|
* `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
|
|
101
|
-
* independent of where each placeholder lands in the SQL text
|
|
101
|
+
* independent of where each placeholder lands in the SQL text, the same
|
|
102
102
|
* guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
|
|
103
103
|
* statements (BEGIN/COMMIT/DDL) so they bind nothing.
|
|
104
104
|
*/
|
|
@@ -132,7 +132,7 @@ function shapeResult(result) {
|
|
|
132
132
|
* deadlock / lock-wait-timeout become retryable (E012/E013). The original mysql2
|
|
133
133
|
* error (with its real message) is preserved as the wrapped error's `.cause`
|
|
134
134
|
* downstream. Returns the value unchanged when it is not a recognizable mysql2
|
|
135
|
-
* error. We only annotate here
|
|
135
|
+
* error. We only annotate here, `wrapPgError` (invoked downstream in the query
|
|
136
136
|
* executor / transaction proxy) does the actual translation.
|
|
137
137
|
*/
|
|
138
138
|
function augmentMysqlError(err) {
|
|
@@ -215,7 +215,7 @@ async function execOne(runner, sql, params) {
|
|
|
215
215
|
* emits `SET TRANSACTION ISOLATION LEVEL <level>; START TRANSACTION`. mysql2 runs
|
|
216
216
|
* one statement per call (multi-statements stay OFF by design), so split exactly
|
|
217
217
|
* that dialect-generated compound and run the parts in order. Gated on the precise
|
|
218
|
-
* transaction-control prefix
|
|
218
|
+
* transaction-control prefix, the query builder never emits `SET TRANSACTION
|
|
219
219
|
* ISOLATION LEVEL`, so no builder/user SQL is ever split.
|
|
220
220
|
*/
|
|
221
221
|
async function runOnConnection(conn, sql, params) {
|
|
@@ -237,7 +237,7 @@ async function runOnConnection(conn, sql, params) {
|
|
|
237
237
|
* `SAVEPOINT` nesting all run on the same physical connection.
|
|
238
238
|
*/
|
|
239
239
|
export class MysqlPool {
|
|
240
|
-
/** The underlying mysql2 pool
|
|
240
|
+
/** The underlying mysql2 pool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
241
241
|
pool;
|
|
242
242
|
closed = false;
|
|
243
243
|
constructor(pool) {
|
|
@@ -348,11 +348,11 @@ function mysqlColumnType(type, maxLength) {
|
|
|
348
348
|
return type;
|
|
349
349
|
}
|
|
350
350
|
// ---------------------------------------------------------------------------
|
|
351
|
-
// mysqlDialect
|
|
351
|
+
// mysqlDialect, the full Dialect contract for MySQL 8
|
|
352
352
|
// ---------------------------------------------------------------------------
|
|
353
353
|
/**
|
|
354
354
|
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
355
|
-
* quoting, named `:pN` placeholders (NOT positional
|
|
355
|
+
* quoting, named `:pN` placeholders (NOT positional `?`, see the module
|
|
356
356
|
* docstring), `JSON_OBJECT` / `JSON_ARRAYAGG` for the single-query nested
|
|
357
357
|
* relation engine (`CAST(… AS JSON)`-wrapped nested subresults), no `RETURNING`
|
|
358
358
|
* (`resultStrategy = 'reselect'`), `INSERT … ON DUPLICATE KEY UPDATE` upserts,
|
|
@@ -396,7 +396,7 @@ export const mysqlDialect = {
|
|
|
396
396
|
jsonPathSupport: 'function',
|
|
397
397
|
emptyJsonArrayLiteral: 'JSON_ARRAY()',
|
|
398
398
|
nullJsonLiteral: 'NULL',
|
|
399
|
-
// Named placeholders (`:p1`, `:p2`, …)
|
|
399
|
+
// Named placeholders (`:p1`, `:p2`, …), NOT positional `?`. Turbine pushes
|
|
400
400
|
// params in 1-indexed generation order but may EMIT them in a different SQL
|
|
401
401
|
// text position; positional `?` mis-binds. mysql2 named parameters bind by
|
|
402
402
|
// name, so `:pN` ↔ `params[N-1]` mirrors Postgres' `$N` regardless of text
|
|
@@ -433,7 +433,7 @@ export const mysqlDialect = {
|
|
|
433
433
|
* Wrap a nested correlated subquery for embedding inside a parent `JSON_OBJECT`.
|
|
434
434
|
* MySQL double-encodes a scalar subquery result as a JSON *string* unless it is
|
|
435
435
|
* explicitly typed JSON, so `CAST((subquery) AS JSON)` forces the nested value
|
|
436
|
-
* to be embedded as real JSON (objects/arrays), not a quoted string
|
|
436
|
+
* to be embedded as real JSON (objects/arrays), not a quoted string, the same
|
|
437
437
|
* intent as SQLite's `json(...)` wrap. The fallback (`JSON_ARRAY()` / `NULL`) is
|
|
438
438
|
* already JSON-typed.
|
|
439
439
|
*/
|
|
@@ -442,6 +442,40 @@ export const mysqlDialect = {
|
|
|
442
442
|
},
|
|
443
443
|
// MySQL aggregate casts: COUNT → SIGNED (BIGINT, comes back as a JS number when
|
|
444
444
|
// safe), AVG/float → DECIMAL (string, the aggregate transform Number()-coerces).
|
|
445
|
+
jsonWireRule(columnType) {
|
|
446
|
+
const t = columnType.toLowerCase();
|
|
447
|
+
// JSON_OBJECT renders these as JSON numbers, which are IEEE doubles, so a
|
|
448
|
+
// relation read through the join strategy disagreed with a top-level read
|
|
449
|
+
// and with the batched loader, LOSSILY. Measured: BIGINT 9007199254740993
|
|
450
|
+
// came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
|
|
451
|
+
// mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
|
|
452
|
+
// DECIMAL is always a string), so carry the text and keep it.
|
|
453
|
+
if (t === 'bigint') {
|
|
454
|
+
return {
|
|
455
|
+
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
456
|
+
decode: (value) => {
|
|
457
|
+
// Same policy as the driver's supportBigNumbers/bigNumberStrings:false.
|
|
458
|
+
if (typeof value !== 'string' || !/^-?\d+$/.test(value))
|
|
459
|
+
return value;
|
|
460
|
+
const asNumber = Number(value);
|
|
461
|
+
return Number.isSafeInteger(asNumber) ? asNumber : value;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (t === 'decimal' || t === 'numeric') {
|
|
466
|
+
return { sql: (ref) => `CAST(${ref} AS CHAR)`, decode: (value) => value };
|
|
467
|
+
}
|
|
468
|
+
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
469
|
+
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
470
|
+
// bytes. Carry hex and rebuild the buffer.
|
|
471
|
+
if (t === 'binary' || t === 'varbinary' || t.endsWith('blob')) {
|
|
472
|
+
return {
|
|
473
|
+
sql: (ref) => `HEX(${ref})`,
|
|
474
|
+
decode: (value) => (typeof value === 'string' ? Uint8Array.from(Buffer.from(value, 'hex')) : value),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
},
|
|
445
479
|
castAggregate(expr, target) {
|
|
446
480
|
return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
|
|
447
481
|
},
|
|
@@ -457,7 +491,7 @@ export const mysqlDialect = {
|
|
|
457
491
|
inClauseParam(values) {
|
|
458
492
|
return JSON.stringify(values ?? []);
|
|
459
493
|
},
|
|
460
|
-
// No RETURNING
|
|
494
|
+
// No RETURNING, resultStrategy 'reselect' re-fetches the row.
|
|
461
495
|
buildReturningClause() {
|
|
462
496
|
return '';
|
|
463
497
|
},
|
|
@@ -465,7 +499,7 @@ export const mysqlDialect = {
|
|
|
465
499
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
466
500
|
},
|
|
467
501
|
buildBulkInsertStatement(input) {
|
|
468
|
-
// No UNNEST in MySQL
|
|
502
|
+
// No UNNEST in MySQL, emit multi-row VALUES with flattened, named
|
|
469
503
|
// placeholders (`:p1`, `:p2`, …) matching the flat param order.
|
|
470
504
|
let n = 0;
|
|
471
505
|
const placeholders = input.rowValues
|
|
@@ -480,7 +514,7 @@ export const mysqlDialect = {
|
|
|
480
514
|
};
|
|
481
515
|
},
|
|
482
516
|
buildUpsertStatement(input) {
|
|
483
|
-
// MySQL ignores the explicit conflict target
|
|
517
|
+
// MySQL ignores the explicit conflict target, ON DUPLICATE KEY UPDATE keys
|
|
484
518
|
// off the table's PK/unique indexes. The `where`-derived conflictColumns
|
|
485
519
|
// (input.conflictColumns) must therefore correspond to a real unique/PK index
|
|
486
520
|
// for the upsert to target the intended row (plan §4 / R9).
|
|
@@ -589,7 +623,7 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
589
623
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
590
624
|
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
591
625
|
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
592
|
-
* same logical schema
|
|
626
|
+
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
593
627
|
* collision resolution against scalar column fields included.
|
|
594
628
|
*/
|
|
595
629
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
@@ -776,9 +810,13 @@ export async function introspectMysql(options) {
|
|
|
776
810
|
schemaName = rows[0]?.db ?? '';
|
|
777
811
|
}
|
|
778
812
|
if (!schemaName) {
|
|
779
|
-
throw new ConnectionError('[turbine] Could not determine the MySQL database to introspect
|
|
813
|
+
throw new ConnectionError('[turbine] Could not determine the MySQL database to introspect, pass a database in the connection string or a `schema` option.');
|
|
780
814
|
}
|
|
781
|
-
|
|
815
|
+
// `await` is LOAD-BEARING, not stylistic: `return somePromise` inside a
|
|
816
|
+
// try/finally runs the finally BEFORE the promise settles, so `pool.end()`
|
|
817
|
+
// closed the pool out from under every introspection query and the whole
|
|
818
|
+
// call failed with mysql2's "Pool is closed".
|
|
819
|
+
return await introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
|
|
782
820
|
}
|
|
783
821
|
finally {
|
|
784
822
|
await pool.end();
|
|
@@ -789,7 +827,7 @@ export async function introspectMysql(options) {
|
|
|
789
827
|
// ---------------------------------------------------------------------------
|
|
790
828
|
/** mysql2 connection flags Turbine pins for correct behavior. */
|
|
791
829
|
const MYSQL_DRIVER_FLAGS = {
|
|
792
|
-
// Named placeholders (`:pN`)
|
|
830
|
+
// Named placeholders (`:pN`), see paramPlaceholder + toNamedBinding.
|
|
793
831
|
namedPlaceholders: true,
|
|
794
832
|
// Safe-int policy matching Postgres int8: number when it fits, decimal string
|
|
795
833
|
// otherwise. DECIMAL always comes back as a string. No global state mutated.
|
|
@@ -875,7 +913,7 @@ function isMysql2Pool(x) {
|
|
|
875
913
|
* Pass one of:
|
|
876
914
|
* - a connection string (`'mysql://user:pass@host:3306/db'`),
|
|
877
915
|
* - a mysql2 connection config object (`{ host, user, password, database }`), or
|
|
878
|
-
* - an existing mysql2 pool / {@link MysqlPool} (injection
|
|
916
|
+
* - an existing mysql2 pool / {@link MysqlPool} (injection, you own its lifecycle,
|
|
879
917
|
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
880
918
|
*
|
|
881
919
|
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
|
@@ -907,12 +945,12 @@ export async function turbineMysql(target, schema, options = {}) {
|
|
|
907
945
|
});
|
|
908
946
|
// Every NEW physical connection: disable backslash string escaping so the
|
|
909
947
|
// builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
|
|
910
|
-
// MySQL
|
|
948
|
+
// MySQL, by default MySQL would parse `'\'` as an escaped quote (syntax
|
|
911
949
|
// error). This also makes string-literal semantics match Postgres. Turbine
|
|
912
950
|
// parameterizes every value, so no behavior depends on backslash escaping.
|
|
913
951
|
//
|
|
914
952
|
// NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
|
|
915
|
-
// *raw* (callback-style) connection
|
|
953
|
+
// *raw* (callback-style) connection, its `.query()` returns a non-thenable
|
|
916
954
|
// `Query`, so `.catch()`/`await` on it throws "result of query that is not a
|
|
917
955
|
// promise". Use the callback form here (fire-and-forget): if the SET fails we
|
|
918
956
|
// fall back to MySQL's default escaping, harmless since every value is a param.
|
package/dist/nested-write.d.ts
CHANGED
|
@@ -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: {
|