turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/mysql.js
CHANGED
|
@@ -62,12 +62,13 @@
|
|
|
62
62
|
* await db.disconnect();
|
|
63
63
|
* ```
|
|
64
64
|
*/
|
|
65
|
-
import { TurbineClient } from './client.js';
|
|
65
|
+
import { TurbineClient, } from './client.js';
|
|
66
66
|
import { postgresDialect, } from './dialect.js';
|
|
67
|
-
import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, UnsupportedFeatureError, } from './errors.js';
|
|
68
|
-
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
67
|
+
import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
68
|
+
import { applyTableFilters, assertDistinctColumnFields, deriveEngineRelations, nullRecord } from './introspect.js';
|
|
69
69
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
70
70
|
import { isDateType, snakeToCamel, } from './schema.js';
|
|
71
|
+
import { rewritePositionalPlaceholders, scanPlaceholders } from './sql-placeholders.js';
|
|
71
72
|
// ---------------------------------------------------------------------------
|
|
72
73
|
// Value coercion (params in)
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
@@ -76,7 +77,7 @@ import { isDateType, snakeToCamel, } from './schema.js';
|
|
|
76
77
|
* 1/0 (MySQL has no boolean type; `TINYINT(1)` stores 1/0), `undefined`/`null`
|
|
77
78
|
* → NULL, `Date`/`Uint8Array` pass through (mysql2 formats them), and any
|
|
78
79
|
* remaining object/array → JSON text (matches how Turbine already pre-serializes
|
|
79
|
-
* JSON filter
|
|
80
|
+
* JSON filter params). An `in` list element binds through here too, one by one.
|
|
80
81
|
*/
|
|
81
82
|
function toMysqlParam(value) {
|
|
82
83
|
if (value === undefined || value === null)
|
|
@@ -85,31 +86,136 @@ function toMysqlParam(value) {
|
|
|
85
86
|
case 'boolean':
|
|
86
87
|
return value ? 1 : 0;
|
|
87
88
|
case 'number':
|
|
89
|
+
// MySQL has no NaN or Infinity: the server stored NaN as 0 without a
|
|
90
|
+
// word, and refused Infinity with a raw driver error.
|
|
91
|
+
if (!Number.isFinite(value)) {
|
|
92
|
+
throw new ValidationError(`MySQL cannot store or compare ${value}: a bound number must be finite. Pass a finite number, or null.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
88
95
|
case 'bigint':
|
|
89
96
|
case 'string':
|
|
90
97
|
return value;
|
|
91
98
|
}
|
|
92
|
-
if (value instanceof Date)
|
|
99
|
+
if (value instanceof Date) {
|
|
100
|
+
// mysql2 renders an Invalid Date as '0000-00-00 00:00:00.000', which the
|
|
101
|
+
// server then reports as that value instead of the caller's mistake.
|
|
102
|
+
if (Number.isNaN(value.getTime())) {
|
|
103
|
+
throw new ValidationError('MySQL cannot bind an Invalid Date. Pass a valid Date, or null.');
|
|
104
|
+
}
|
|
93
105
|
return value;
|
|
106
|
+
}
|
|
94
107
|
if (value instanceof Uint8Array)
|
|
95
108
|
return value; // also covers Buffer
|
|
96
109
|
return JSON.stringify(value);
|
|
97
110
|
}
|
|
98
111
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
112
|
+
* An `in` / `notIn` list, as the builder hands it to the shim: ONE param, so
|
|
113
|
+
* the SQL text and its cache entry stay independent of the list's length (see
|
|
114
|
+
* {@link mysqlDialect.buildInClause}). The shim expands it into one placeholder
|
|
115
|
+
* per element, each bound exactly as an equality operand is, which is what
|
|
116
|
+
* makes `{ in: [v] }` match the rows `{ equals: v }` matches on every column
|
|
117
|
+
* type: a `JSON_TABLE` list compared JSON values, so a boolean, a date or time,
|
|
118
|
+
* a binary value, a decimal or bigint written as a string, and every
|
|
119
|
+
* case-folded string missed, and `notIn` then matched every row.
|
|
120
|
+
*/
|
|
121
|
+
class MysqlInList {
|
|
122
|
+
values;
|
|
123
|
+
constructor(values) {
|
|
124
|
+
this.values = values;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** MySQL's limit on placeholders in one prepared statement. */
|
|
128
|
+
const MYSQL_MAX_PLACEHOLDERS = 65_535;
|
|
129
|
+
/** A list of `n` elements is sent padded to the next power of two (by repeating its last element). */
|
|
130
|
+
function paddedListLength(n) {
|
|
131
|
+
return n <= 1 ? n : 2 ** Math.ceil(Math.log2(n));
|
|
132
|
+
}
|
|
133
|
+
/** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
|
|
134
|
+
function refusePlaceholder(message) {
|
|
135
|
+
throw new ValidationError(message);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The statement text and named binding for one execution: `$n` placeholders
|
|
139
|
+
* (the spelling `rawUnsafe` documents) rewritten to `:pN`, every
|
|
140
|
+
* {@link MysqlInList} expanded, and every other value bound by name. Mapping by
|
|
141
|
+
* NAME makes binding independent of where each placeholder lands in the SQL
|
|
142
|
+
* text, the same guarantee Postgres' numbered `$N` gives. `binding` is
|
|
143
|
+
* `undefined` when nothing is bound, so parameter-less statements (BEGIN,
|
|
144
|
+
* COMMIT, DDL) run through `.query()`.
|
|
104
145
|
*/
|
|
105
|
-
function
|
|
146
|
+
function bindStatement(sql, values) {
|
|
106
147
|
if (values.length === 0)
|
|
107
|
-
return undefined;
|
|
148
|
+
return { sql, binding: undefined };
|
|
149
|
+
const text = rewritePositionalPlaceholders(sql, 'mysql', values.length, (i) => `:p${i}`, refusePlaceholder);
|
|
108
150
|
const named = {};
|
|
151
|
+
let lists;
|
|
109
152
|
for (let i = 0; i < values.length; i++) {
|
|
110
|
-
|
|
153
|
+
const value = values[i];
|
|
154
|
+
if (value instanceof MysqlInList) {
|
|
155
|
+
lists ??= new Map();
|
|
156
|
+
lists.set(`p${i + 1}`, value);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
named[`p${i + 1}`] = toMysqlParam(value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (lists === undefined)
|
|
163
|
+
return { sql: text, binding: named };
|
|
164
|
+
const expanded = expandInLists(text, lists, named);
|
|
165
|
+
return { sql: expanded, binding: Object.keys(named).length > 0 ? named : undefined };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Replace each list placeholder (`:p3`) with one placeholder per element
|
|
169
|
+
* (`:p3_0, :p3_1, ...`), binding each element into `named`. An empty list
|
|
170
|
+
* becomes a subquery with no rows, so `IN` is false and `NOT IN` true, as
|
|
171
|
+
* Postgres answers `= ANY('{}')` and `!= ALL('{}')`.
|
|
172
|
+
*
|
|
173
|
+
* Padding to a power of two bounds the statement texts a caller can produce
|
|
174
|
+
* (mysql2 prepares and caches one server-side statement per distinct text and
|
|
175
|
+
* connection, and the server caps the total across every client at
|
|
176
|
+
* `max_prepared_stmt_count`): about seventeen lengths per list instead of one
|
|
177
|
+
* per length. A repeated element changes neither `IN` nor `NOT IN`.
|
|
178
|
+
*/
|
|
179
|
+
function expandInLists(sql, lists, named) {
|
|
180
|
+
const tokens = scanPlaceholders(sql, 'mysql');
|
|
181
|
+
const listTokens = tokens.filter((t) => t.kind === 'named' && lists.has(t.name));
|
|
182
|
+
const others = tokens.length - listTokens.length;
|
|
183
|
+
let padded = 0;
|
|
184
|
+
let exact = 0;
|
|
185
|
+
for (const t of listTokens) {
|
|
186
|
+
const n = lists.get(t.name).values.length;
|
|
187
|
+
padded += paddedListLength(n);
|
|
188
|
+
exact += n;
|
|
189
|
+
}
|
|
190
|
+
if (others + exact > MYSQL_MAX_PLACEHOLDERS) {
|
|
191
|
+
throw new ValidationError(`An \`in\` / \`notIn\` list on MySQL binds one placeholder per element, and this statement needs ` +
|
|
192
|
+
`${others + exact} where MySQL allows ${MYSQL_MAX_PLACEHOLDERS}. Split the list across several queries.`);
|
|
193
|
+
}
|
|
194
|
+
const pad = others + padded <= MYSQL_MAX_PLACEHOLDERS;
|
|
195
|
+
for (const [name, list] of lists) {
|
|
196
|
+
list.values.forEach((v, j) => {
|
|
197
|
+
named[`${name}_${j}`] = toMysqlParam(v);
|
|
198
|
+
});
|
|
111
199
|
}
|
|
112
|
-
|
|
200
|
+
let out = '';
|
|
201
|
+
let last = 0;
|
|
202
|
+
for (const t of listTokens) {
|
|
203
|
+
const n = lists.get(t.name).values.length;
|
|
204
|
+
let replacement;
|
|
205
|
+
if (n === 0) {
|
|
206
|
+
replacement = 'SELECT NULL FROM DUAL WHERE FALSE';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const parts = [];
|
|
210
|
+
const length = pad ? paddedListLength(n) : n;
|
|
211
|
+
for (let j = 0; j < length; j++)
|
|
212
|
+
parts.push(`:${t.name}_${Math.min(j, n - 1)}`);
|
|
213
|
+
replacement = parts.join(', ');
|
|
214
|
+
}
|
|
215
|
+
out += sql.slice(last, t.start) + replacement;
|
|
216
|
+
last = t.end;
|
|
217
|
+
}
|
|
218
|
+
return out + sql.slice(last);
|
|
113
219
|
}
|
|
114
220
|
// ---------------------------------------------------------------------------
|
|
115
221
|
// Result shaping + error translation
|
|
@@ -244,10 +350,50 @@ function augmentMysqlError(err) {
|
|
|
244
350
|
case 1205:
|
|
245
351
|
target.code = '40001';
|
|
246
352
|
return err;
|
|
247
|
-
default:
|
|
353
|
+
default: {
|
|
354
|
+
// Data exceptions (a bound value that does not fit the column) take the
|
|
355
|
+
// SQLSTATE class 22 path in `wrapPgError`, which builds a ValidationError
|
|
356
|
+
// naming the column and redacts the value-bearing message in 'safe' mode.
|
|
357
|
+
const sqlstate = mysqlDataExceptionState(e.errno, e.sqlState, msg);
|
|
358
|
+
if (sqlstate === undefined)
|
|
359
|
+
return err;
|
|
360
|
+
target.code = sqlstate;
|
|
361
|
+
// "... for column 'name' at row 1", or "... in value for column 't.meta'."
|
|
362
|
+
const column = /for column '([^']+)'/i.exec(msg)?.[1];
|
|
363
|
+
if (column)
|
|
364
|
+
target.column = column.slice(column.lastIndexOf('.') + 1);
|
|
248
365
|
return err;
|
|
366
|
+
}
|
|
249
367
|
}
|
|
250
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* The SQLSTATE a MySQL data exception maps to, or `undefined` when `errno` is
|
|
371
|
+
* not one. MySQL reports several of these under the generic `HY000`, so the
|
|
372
|
+
* common ones are mapped by errno to the code PostgreSQL uses for the same
|
|
373
|
+
* failure; anything else MySQL itself files under class 22 keeps its own code.
|
|
374
|
+
*/
|
|
375
|
+
function mysqlDataExceptionState(errno, sqlState, message) {
|
|
376
|
+
switch (errno) {
|
|
377
|
+
case 1406: // ER_DATA_TOO_LONG
|
|
378
|
+
return '22001';
|
|
379
|
+
case 1264: // ER_WARN_DATA_OUT_OF_RANGE
|
|
380
|
+
case 1690: // ER_DATA_OUT_OF_RANGE
|
|
381
|
+
return '22003';
|
|
382
|
+
case 1366: // ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: "Incorrect integer value: 'abc' for column ..."
|
|
383
|
+
case 1265: // WARN_DATA_TRUNCATED: a value outside an ENUM, a malformed DECIMAL
|
|
384
|
+
return '22P02';
|
|
385
|
+
case 1292: // ER_TRUNCATED_WRONG_VALUE: an invalid datetime, or a non-numeric string compared as a number
|
|
386
|
+
case 1411: // ER_WRONG_VALUE_FOR_TYPE
|
|
387
|
+
return /\b(?:date|time|datetime|timestamp)\b/i.test(message) ? '22007' : '22P02';
|
|
388
|
+
case 1365: // ER_DIVISION_BY_ZERO
|
|
389
|
+
return '22012';
|
|
390
|
+
case 3140: // ER_INVALID_JSON_TEXT
|
|
391
|
+
case 3141: // ER_INVALID_JSON_TEXT_IN_PARAM
|
|
392
|
+
case 3144: // ER_INVALID_JSON_CHARSET
|
|
393
|
+
return '22032';
|
|
394
|
+
}
|
|
395
|
+
return typeof sqlState === 'string' && /^22[0-9A-Z]{3}$/.test(sqlState) ? sqlState : undefined;
|
|
396
|
+
}
|
|
251
397
|
function normalizeQueryArgs(arg, values) {
|
|
252
398
|
if (typeof arg === 'string')
|
|
253
399
|
return { text: arg, params: values ?? [] };
|
|
@@ -261,9 +407,10 @@ function normalizeQueryArgs(arg, values) {
|
|
|
261
407
|
* the `{ p1, p2, … }` object built from the positional params.
|
|
262
408
|
*/
|
|
263
409
|
async function execOne(runner, sql, params) {
|
|
264
|
-
const
|
|
410
|
+
const bound = bindStatement(sql, params);
|
|
411
|
+
const binding = bound.binding;
|
|
265
412
|
try {
|
|
266
|
-
const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
413
|
+
const [result, fields] = binding ? await runner.execute(bound.sql, binding) : await runner.query(bound.sql);
|
|
267
414
|
return shapeResult(result, fields);
|
|
268
415
|
}
|
|
269
416
|
catch (err) {
|
|
@@ -376,6 +523,18 @@ export function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
376
523
|
* matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
|
|
377
524
|
*/
|
|
378
525
|
const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
|
|
526
|
+
/** Integer column types narrower than 64 bits, in MySQL and code-first (PostgreSQL) spellings. */
|
|
527
|
+
const SMALL_INTEGER_TYPES = new Set([
|
|
528
|
+
'tinyint',
|
|
529
|
+
'smallint',
|
|
530
|
+
'mediumint',
|
|
531
|
+
'int',
|
|
532
|
+
'integer',
|
|
533
|
+
'int2',
|
|
534
|
+
'int4',
|
|
535
|
+
'serial',
|
|
536
|
+
'smallserial',
|
|
537
|
+
]);
|
|
379
538
|
/**
|
|
380
539
|
* Rewrite every boolean column's recorded dialect type to
|
|
381
540
|
* {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
|
|
@@ -428,6 +587,11 @@ function isMysqlDateType(dialectType) {
|
|
|
428
587
|
}
|
|
429
588
|
/** Map a schema-builder (Postgres-flavored) column type to MySQL DDL. */
|
|
430
589
|
function mysqlColumnType(type, maxLength) {
|
|
590
|
+
// An enum column's type from `buildEnumColumn`, already spelled for MySQL. It
|
|
591
|
+
// returns first: its labels are free text, and one named `int` or `date`
|
|
592
|
+
// would otherwise match a pattern below.
|
|
593
|
+
if (/^ENUM\(/.test(type))
|
|
594
|
+
return type;
|
|
431
595
|
const t = type.toUpperCase();
|
|
432
596
|
if (/BIGSERIAL/.test(t))
|
|
433
597
|
return 'BIGINT AUTO_INCREMENT';
|
|
@@ -451,14 +615,19 @@ function mysqlColumnType(type, maxLength) {
|
|
|
451
615
|
return 'JSON';
|
|
452
616
|
if (/UUID/.test(t))
|
|
453
617
|
return 'CHAR(36)';
|
|
618
|
+
// DATETIME(3), not bare DATETIME: the bare type has zero fractional digits and
|
|
619
|
+
// rounds a JS Date's milliseconds away on write.
|
|
454
620
|
if (/TIMESTAMPTZ|TIMESTAMP|DATETIME/.test(t))
|
|
455
|
-
return 'DATETIME';
|
|
621
|
+
return 'DATETIME(3)';
|
|
456
622
|
if (/\bDATE\b/.test(t))
|
|
457
623
|
return 'DATE';
|
|
458
624
|
if (/\bTIME\b/.test(t))
|
|
459
625
|
return 'TIME';
|
|
626
|
+
// A bounded binary column is VARBINARY(n): a BLOB cannot be a key.
|
|
627
|
+
if (/VARBINARY/.test(t))
|
|
628
|
+
return `VARBINARY(${maxLength ?? 255})`;
|
|
460
629
|
if (/BYTEA|BLOB/.test(t))
|
|
461
|
-
return 'BLOB';
|
|
630
|
+
return maxLength != null ? `VARBINARY(${maxLength})` : 'BLOB';
|
|
462
631
|
if (/VARCHAR/.test(t))
|
|
463
632
|
return maxLength != null ? `VARCHAR(${maxLength})` : 'VARCHAR(255)';
|
|
464
633
|
if (/CHAR/.test(t))
|
|
@@ -472,6 +641,32 @@ function mysqlColumnType(type, maxLength) {
|
|
|
472
641
|
// ---------------------------------------------------------------------------
|
|
473
642
|
// mysqlDialect, the full Dialect contract for MySQL 8
|
|
474
643
|
// ---------------------------------------------------------------------------
|
|
644
|
+
/** The row alias a multi-row upsert gives its incoming rows (`AS ... ON DUPLICATE KEY UPDATE`). */
|
|
645
|
+
const MYSQL_INCOMING_ALIAS = '`turbine_excluded`';
|
|
646
|
+
/**
|
|
647
|
+
* The LIKE escape character on MySQL (see `mysqlDialect.likeEscapeCharacter`).
|
|
648
|
+
* One character, nothing special inside a single-quoted literal in either
|
|
649
|
+
* `sql_mode`, and not a LIKE metacharacter.
|
|
650
|
+
*/
|
|
651
|
+
const MYSQL_LIKE_ESCAPE = '!';
|
|
652
|
+
/** The LIKE metacharacters under {@link MYSQL_LIKE_ESCAPE}: itself, `%` and `_`. */
|
|
653
|
+
const MYSQL_LIKE_META_G = /[!%_]/g;
|
|
654
|
+
/**
|
|
655
|
+
* A `JSON_OBJECT` key as SQL text that reads the same in every `sql_mode`.
|
|
656
|
+
* A key without a backslash is a plain quoted literal. A backslash has no
|
|
657
|
+
* literal spelling that both modes agree on (`'a\\b'` is one backslash by
|
|
658
|
+
* default and two under `NO_BACKSLASH_ESCAPES`, which Turbine's own
|
|
659
|
+
* connections set and a caller-supplied pool may not), so such a key is
|
|
660
|
+
* written as its UTF-8 bytes in hex, which no mode re-reads.
|
|
661
|
+
*/
|
|
662
|
+
function mysqlKeyLiteral(key, dialect) {
|
|
663
|
+
if (!key.includes('\\'))
|
|
664
|
+
return `'${dialect.escapeStringLiteral(key)}'`;
|
|
665
|
+
let hex = '';
|
|
666
|
+
for (const byte of new TextEncoder().encode(key))
|
|
667
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
668
|
+
return `CONVERT(X'${hex}' USING utf8mb4)`;
|
|
669
|
+
}
|
|
475
670
|
/**
|
|
476
671
|
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
477
672
|
* quoting, named `:pN` placeholders (NOT positional `?`, see the module
|
|
@@ -483,6 +678,47 @@ function mysqlColumnType(type, maxLength) {
|
|
|
483
678
|
export const mysqlDialect = {
|
|
484
679
|
...postgresDialect,
|
|
485
680
|
name: 'mysql',
|
|
681
|
+
capabilities: Object.freeze({
|
|
682
|
+
distinctOn: false,
|
|
683
|
+
positionalJson: false,
|
|
684
|
+
nullsOrderingClause: false,
|
|
685
|
+
nullsSortHigh: false,
|
|
686
|
+
insensitiveInList: false,
|
|
687
|
+
jsonDocumentEquality: 'exact',
|
|
688
|
+
jsonExtractTyped: false,
|
|
689
|
+
jsonExactNumericCast: false,
|
|
690
|
+
numericTextComparison: true,
|
|
691
|
+
enforcesForeignKeys: true,
|
|
692
|
+
partitionLimitPushdown: false,
|
|
693
|
+
// A zone-less literal reads in the session time zone; only `date` columns rewrite.
|
|
694
|
+
utcTimestampRewrite: false,
|
|
695
|
+
paramTypeCasts: false,
|
|
696
|
+
namedEnumTypes: false,
|
|
697
|
+
structuredValueColumns: 'scalars-and-text',
|
|
698
|
+
// Counted in a 16-bit field.
|
|
699
|
+
maxBindParameters: 65535,
|
|
700
|
+
maxInsertRows: Number.POSITIVE_INFINITY,
|
|
701
|
+
upsertConflictTarget: 'any-unique-key',
|
|
702
|
+
setClauseOrder: 'left-to-right',
|
|
703
|
+
reselectLocksRows: true,
|
|
704
|
+
integerDivision: 'DIV',
|
|
705
|
+
updateExpressions: true,
|
|
706
|
+
// Caught by the onConflict re-read, which runs in the reselect transaction.
|
|
707
|
+
bulkUpsertAppliesRepeatedKeys: true,
|
|
708
|
+
readOnlyTransactions: true,
|
|
709
|
+
connectionParameters: false,
|
|
710
|
+
backendCancel: false,
|
|
711
|
+
idleTransactionTimeout: false,
|
|
712
|
+
abortsTransactionOnError: false,
|
|
713
|
+
connectionDriver: 'engine-driver',
|
|
714
|
+
// mysql2 execute() prepares each statement text; max_prepared_stmt_count caps them server-wide.
|
|
715
|
+
preparesEveryStatement: true,
|
|
716
|
+
statementSyntax: 'mysql',
|
|
717
|
+
// An inline REFERENCES is parsed and ignored.
|
|
718
|
+
inlineForeignKeys: false,
|
|
719
|
+
// TEXT / BLOB need a prefix length in a key (ER_BLOB_KEY_WITHOUT_LENGTH).
|
|
720
|
+
unboundedTextKeys: false,
|
|
721
|
+
}),
|
|
486
722
|
// No RETURNING → run the write, then re-SELECT by PK/where (Phase-0 reselect).
|
|
487
723
|
resultStrategy: 'reselect',
|
|
488
724
|
supportsReturning: false,
|
|
@@ -503,6 +739,13 @@ export const mysqlDialect = {
|
|
|
503
739
|
// Postgres-only in this release (flipping it on is a one-line change + tests).
|
|
504
740
|
supportsLateralJoin: false,
|
|
505
741
|
supportsPlanCacheMode: false,
|
|
742
|
+
// FOR UPDATE / FOR SHARE (8.0); there is no NO KEY UPDATE or KEY SHARE.
|
|
743
|
+
rowLockModes: ['update', 'share'],
|
|
744
|
+
// No percentile_cont / percentile_disc.
|
|
745
|
+
supportsPercentileAggregates: false,
|
|
746
|
+
// INSERT ... AS alias ON DUPLICATE KEY UPDATE, with the refusals that
|
|
747
|
+
// keyless conflict resolution needs.
|
|
748
|
+
supportsBulkUpsert: true,
|
|
506
749
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
507
750
|
supportsAdvisoryLock: true,
|
|
508
751
|
// Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
|
|
@@ -535,15 +778,18 @@ export const mysqlDialect = {
|
|
|
535
778
|
* `NO_BACKSLASH_ESCAPES`, `\` is an escape character inside a string literal,
|
|
536
779
|
* so doubling only `'` (the inherited Postgres rule) leaves a value ending in
|
|
537
780
|
* a backslash able to escape its own closing quote. Escape the backslash
|
|
538
|
-
* first, then the quote; every other byte is safe raw.
|
|
539
|
-
*
|
|
540
|
-
* is
|
|
781
|
+
* first, then the quote; every other byte is safe raw.
|
|
782
|
+
*
|
|
783
|
+
* That is right for the server's default `sql_mode` only: under
|
|
784
|
+
* `NO_BACKSLASH_ESCAPES` (which Turbine sets on the connections it opens) the
|
|
785
|
+
* doubled backslash reads as two. So `buildJsonObject`, the one caller, never
|
|
786
|
+
* hands it a key holding a backslash (see {@link mysqlKeyLiteral}).
|
|
541
787
|
*/
|
|
542
788
|
escapeStringLiteral(value) {
|
|
543
789
|
return value.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
544
790
|
},
|
|
545
791
|
buildJsonObject(pairs) {
|
|
546
|
-
const args = pairs.map(([key, expr]) =>
|
|
792
|
+
const args = pairs.map(([key, expr]) => `${mysqlKeyLiteral(key, this)}, ${expr}`);
|
|
547
793
|
return `JSON_OBJECT(${args.join(', ')})`;
|
|
548
794
|
},
|
|
549
795
|
// JSON_ARRAYAGG over an empty group returns NULL → COALESCE to JSON_ARRAY().
|
|
@@ -573,7 +819,9 @@ export const mysqlDialect = {
|
|
|
573
819
|
// came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
|
|
574
820
|
// mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
|
|
575
821
|
// DECIMAL is always a string), so carry the text and keep it.
|
|
576
|
-
|
|
822
|
+
// `int8` is the spelling a code-first (`defineSchema`) schema records for
|
|
823
|
+
// the same BIGINT column, so it needs the same rule.
|
|
824
|
+
if (t === 'bigint' || t === 'int8') {
|
|
577
825
|
return {
|
|
578
826
|
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
579
827
|
decode: (value) => {
|
|
@@ -631,9 +879,9 @@ export const mysqlDialect = {
|
|
|
631
879
|
// parsed value. It is also what `pg` returns for json/jsonb, so this is
|
|
632
880
|
// Postgres parity rather than a MySQL convention.
|
|
633
881
|
//
|
|
634
|
-
// Contrast SQLite
|
|
635
|
-
//
|
|
636
|
-
//
|
|
882
|
+
// Contrast SQLite, which has no JSON column type and stores the document as
|
|
883
|
+
// text: its driver shim parses the text back for a column the schema
|
|
884
|
+
// declares json/jsonb, and its own rule does the same through a join.
|
|
637
885
|
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
638
886
|
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
639
887
|
// bytes. Carry hex and rebuild the buffer.
|
|
@@ -648,6 +896,14 @@ export const mysqlDialect = {
|
|
|
648
896
|
castAggregate(expr, target) {
|
|
649
897
|
return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
|
|
650
898
|
},
|
|
899
|
+
// AVG over an integer column is a DECIMAL with only four more digits of
|
|
900
|
+
// scale (div_precision_increment), so 4/3 came back 1.3333 where PostgreSQL
|
|
901
|
+
// returns 1.3333333333333333. Adding a DOUBLE zero averages in DOUBLE (the
|
|
902
|
+
// `CAST(... AS DOUBLE)` spelling needs 8.0.17; the engine floor is 8.0.0).
|
|
903
|
+
// BIGINT keeps its exact DECIMAL average, like PostgreSQL's numeric one.
|
|
904
|
+
averageArgument(columnRef, columnType) {
|
|
905
|
+
return SMALL_INTEGER_TYPES.has(columnType.toLowerCase()) ? `(${columnRef} + 0E0)` : undefined;
|
|
906
|
+
},
|
|
651
907
|
// MySQL has no bare OFFSET: the grammar requires a LIMIT for OFFSET to
|
|
652
908
|
// attach to, so `offset` without `limit` (valid on Postgres, which the
|
|
653
909
|
// default path is written for) is a syntax error here. The MySQL manual's
|
|
@@ -667,17 +923,16 @@ export const mysqlDialect = {
|
|
|
667
923
|
s += ` OFFSET ${offsetPlaceholder}`;
|
|
668
924
|
return s;
|
|
669
925
|
},
|
|
670
|
-
// No array params in MySQL.
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
926
|
+
// No array params in MySQL. The list is ONE builder param (a MysqlInList),
|
|
927
|
+
// so the SQL text and its cache entry do not depend on the list's length; the
|
|
928
|
+
// shim expands the placeholder into one per element when the statement runs
|
|
929
|
+
// (see expandInLists). Each element binds as an equality operand does, so the
|
|
930
|
+
// list compares under the column's own type and collation rules.
|
|
675
931
|
buildInClause(expr, paramRef, negated) {
|
|
676
|
-
|
|
677
|
-
return `${expr} ${negated ? 'NOT IN' : 'IN'} (${inner})`;
|
|
932
|
+
return `${expr} ${negated ? 'NOT IN' : 'IN'} (${paramRef})`;
|
|
678
933
|
},
|
|
679
934
|
inClauseParam(values) {
|
|
680
|
-
return
|
|
935
|
+
return new MysqlInList(Array.isArray(values) ? values : []);
|
|
681
936
|
},
|
|
682
937
|
// No RETURNING, resultStrategy 'reselect' re-fetches the row.
|
|
683
938
|
buildReturningClause() {
|
|
@@ -701,6 +956,28 @@ export const mysqlDialect = {
|
|
|
701
956
|
params: input.rowValues.flat(),
|
|
702
957
|
};
|
|
703
958
|
},
|
|
959
|
+
buildBulkUpsertStatement(input) {
|
|
960
|
+
// 8.0.19's row alias (`VALUES(col)` is deprecated since 8.0.20). There is
|
|
961
|
+
// no conflict target: ON DUPLICATE KEY UPDATE fires on whichever unique key
|
|
962
|
+
// a row violates, which is why the builder refuses a table where another
|
|
963
|
+
// key's columns are in the data, and why it refuses `ignore` and a
|
|
964
|
+
// conflict predicate (this grammar has no slot for one).
|
|
965
|
+
if (!input.update) {
|
|
966
|
+
throw new UnsupportedFeatureError('createMany onConflict { ignore: true }', 'mysql', 'MySQL cannot tell the inserted rows from the skipped ones afterwards; use onConflict: { target, update }.');
|
|
967
|
+
}
|
|
968
|
+
let n = 0;
|
|
969
|
+
const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
|
|
970
|
+
const params = input.rowValues.flat();
|
|
971
|
+
const update = input.update(params, { target: input.table, incoming: MYSQL_INCOMING_ALIAS });
|
|
972
|
+
if (update.where) {
|
|
973
|
+
throw new UnsupportedFeatureError('A predicate on createMany onConflict', 'mysql', 'ON DUPLICATE KEY UPDATE has no WHERE slot.');
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} AS ${MYSQL_INCOMING_ALIAS} ` +
|
|
977
|
+
`ON DUPLICATE KEY UPDATE ${update.setClauses.join(', ')}`,
|
|
978
|
+
params,
|
|
979
|
+
};
|
|
980
|
+
},
|
|
704
981
|
buildDefaultValuesInsertStatement(input) {
|
|
705
982
|
// MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
|
|
706
983
|
// repeats for any row count, so this is the same statement the column-driven
|
|
@@ -728,12 +1005,39 @@ export const mysqlDialect = {
|
|
|
728
1005
|
// unless a functional/generated index exists.
|
|
729
1006
|
return `LOWER(${column}) LIKE LOWER(${paramRef})`;
|
|
730
1007
|
},
|
|
1008
|
+
/**
|
|
1009
|
+
* `!`, not `\`: with the server's default `sql_mode` a backslash escapes
|
|
1010
|
+
* inside a string literal, so the `ESCAPE '\'` every other engine emits is an
|
|
1011
|
+
* unterminated string here, and only a session running with
|
|
1012
|
+
* `NO_BACKSLASH_ESCAPES` reads it as one backslash. Turbine sets that mode on
|
|
1013
|
+
* the connections it opens, never on a pool the caller supplies (it would
|
|
1014
|
+
* change how the caller's own SQL parses), so the clause has to mean the same
|
|
1015
|
+
* thing in both modes, and `'!'` does. The operand is bound, so a backslash in
|
|
1016
|
+
* the VALUE is a plain character under this escape.
|
|
1017
|
+
*/
|
|
1018
|
+
likeEscapeCharacter: MYSQL_LIKE_ESCAPE,
|
|
1019
|
+
/** Prefix `%`, `_` and the escape character itself with {@link MYSQL_LIKE_ESCAPE}. */
|
|
1020
|
+
escapeLikePattern(value) {
|
|
1021
|
+
return value.replace(MYSQL_LIKE_META_G, `${MYSQL_LIKE_ESCAPE}$&`);
|
|
1022
|
+
},
|
|
731
1023
|
buildJsonContains(column, paramRef) {
|
|
732
1024
|
return `JSON_CONTAINS(${column}, ${paramRef})`;
|
|
733
1025
|
},
|
|
734
1026
|
buildJsonPathExtract(column, pathParamRef) {
|
|
735
1027
|
return `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${pathParamRef}))`;
|
|
736
1028
|
},
|
|
1029
|
+
// MySQL compares two JSON values by JSON type and value, so binding the
|
|
1030
|
+
// operand as JSON text makes `{ path, equals }` exact for every shape:
|
|
1031
|
+
// 5 vs "5" and true vs 1 differ, 5 equals 5.0, null matches JSON null, and
|
|
1032
|
+
// arrays and objects compare structurally (object key order does not matter).
|
|
1033
|
+
// A missing path extracts SQL NULL, which matches nothing. With no path the
|
|
1034
|
+
// whole document is compared.
|
|
1035
|
+
buildJsonPathEquals(input) {
|
|
1036
|
+
const { value } = input;
|
|
1037
|
+
const text = typeof value === 'bigint' ? value.toString() : JSON.stringify(value);
|
|
1038
|
+
const lhs = input.pathRef === undefined ? input.column : `JSON_EXTRACT(${input.column}, ${input.pathRef})`;
|
|
1039
|
+
return `${lhs} = CAST(${input.bind(text)} AS JSON)`;
|
|
1040
|
+
},
|
|
737
1041
|
// ---- Type mapping -------------------------------------------------------
|
|
738
1042
|
typeToTypeScript(dialectType, nullable) {
|
|
739
1043
|
return mysqlTypeToTs(dialectType, nullable);
|
|
@@ -745,6 +1049,24 @@ export const mysqlDialect = {
|
|
|
745
1049
|
buildColumnType(input) {
|
|
746
1050
|
return mysqlColumnType(input.type, input.maxLength);
|
|
747
1051
|
},
|
|
1052
|
+
/**
|
|
1053
|
+
* MySQL declares an enum's labels on the column, `ENUM('a', 'b')`, which
|
|
1054
|
+
* refuses any other value in strict mode. A label's quotes are doubled. A
|
|
1055
|
+
* label holding a backslash is refused: whether `\` escapes in a string
|
|
1056
|
+
* literal depends on the session's NO_BACKSLASH_ESCAPES (Turbine's own
|
|
1057
|
+
* connections set it, other clients do not), so the same DDL would declare
|
|
1058
|
+
* a different label depending on what runs it.
|
|
1059
|
+
*/
|
|
1060
|
+
buildEnumColumn(column, labels) {
|
|
1061
|
+
const literals = labels.map((label) => {
|
|
1062
|
+
if (label.includes('\\')) {
|
|
1063
|
+
throw new ValidationError(`The enum label ${JSON.stringify(label)} on column ${column} holds a backslash, which MySQL reads ` +
|
|
1064
|
+
'differently with and without NO_BACKSLASH_ESCAPES. Use a label without one.');
|
|
1065
|
+
}
|
|
1066
|
+
return `'${label.replace(/'/g, "''")}'`;
|
|
1067
|
+
});
|
|
1068
|
+
return { type: `ENUM(${literals.join(', ')})` };
|
|
1069
|
+
},
|
|
748
1070
|
buildColumnDefinition(input) {
|
|
749
1071
|
const isSerial = /serial/i.test(input.type);
|
|
750
1072
|
const parts = [input.name, this.buildColumnType(input)];
|
|
@@ -828,8 +1150,8 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
828
1150
|
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
829
1151
|
* collision resolution against scalar column fields included.
|
|
830
1152
|
*/
|
|
831
|
-
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
832
|
-
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
1153
|
+
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, logger) {
|
|
1154
|
+
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable, logger);
|
|
833
1155
|
}
|
|
834
1156
|
/**
|
|
835
1157
|
* Introspect a MySQL database into the same {@link SchemaMetadata} shape the
|
|
@@ -838,7 +1160,8 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
|
|
|
838
1160
|
*
|
|
839
1161
|
* @param exec Runs a parameterized (`:p1`, `:p2`, …) query and returns rows.
|
|
840
1162
|
* @param schema The MySQL database (schema) name to introspect.
|
|
841
|
-
* @param options Optional include / exclude table-name filters
|
|
1163
|
+
* @param options Optional include / exclude table-name filters, and the logger
|
|
1164
|
+
* that receives relation-naming notes (console when absent).
|
|
842
1165
|
*/
|
|
843
1166
|
export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
844
1167
|
// ----- Tables -----
|
|
@@ -851,7 +1174,7 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
851
1174
|
COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH
|
|
852
1175
|
FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
|
|
853
1176
|
const columnsByTable = new Map();
|
|
854
|
-
const enums =
|
|
1177
|
+
const enums = nullRecord();
|
|
855
1178
|
for (const c of columnRows) {
|
|
856
1179
|
if (!tableSet.has(c.TABLE_NAME))
|
|
857
1180
|
continue;
|
|
@@ -879,6 +1202,8 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
879
1202
|
tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
|
|
880
1203
|
nullable,
|
|
881
1204
|
hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
|
|
1205
|
+
// The server numbers it, so a create's re-read can use the insert id.
|
|
1206
|
+
isGenerated: /auto_increment/i.test(c.EXTRA),
|
|
882
1207
|
isArray: false,
|
|
883
1208
|
arrayType: undefined,
|
|
884
1209
|
pgArrayType: 'text[]',
|
|
@@ -960,16 +1285,19 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
960
1285
|
}
|
|
961
1286
|
}
|
|
962
1287
|
// ----- Relations -----
|
|
963
|
-
|
|
1288
|
+
// One field per column, as the PostgreSQL introspector requires.
|
|
1289
|
+
for (const [tableName, cols] of columnsByTable)
|
|
1290
|
+
assertDistinctColumnFields(tableName, cols);
|
|
1291
|
+
const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
|
|
964
1292
|
// ----- Assemble TableMetadata -----
|
|
965
|
-
const tables =
|
|
1293
|
+
const tables = nullRecord();
|
|
966
1294
|
for (const tableName of tableNames) {
|
|
967
1295
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
968
|
-
const columnMap =
|
|
969
|
-
const reverseColumnMap =
|
|
1296
|
+
const columnMap = nullRecord();
|
|
1297
|
+
const reverseColumnMap = nullRecord();
|
|
970
1298
|
const dateColumns = new Set();
|
|
971
|
-
const dialectTypes =
|
|
972
|
-
const pgTypes =
|
|
1299
|
+
const dialectTypes = nullRecord();
|
|
1300
|
+
const pgTypes = nullRecord();
|
|
973
1301
|
const allColumns = [];
|
|
974
1302
|
for (const col of columns) {
|
|
975
1303
|
columnMap[col.field] = col.name;
|
|
@@ -991,7 +1319,7 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
991
1319
|
allColumns,
|
|
992
1320
|
primaryKey: pkByTable.get(tableName) ?? [],
|
|
993
1321
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
994
|
-
relations: relationsByTable.get(tableName) ??
|
|
1322
|
+
relations: relationsByTable.get(tableName) ?? nullRecord(),
|
|
995
1323
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
996
1324
|
};
|
|
997
1325
|
}
|
|
@@ -1021,7 +1349,11 @@ export async function introspectMysql(options) {
|
|
|
1021
1349
|
// try/finally runs the finally BEFORE the promise settles, so `pool.end()`
|
|
1022
1350
|
// closed the pool out from under every introspection query and the whole
|
|
1023
1351
|
// call failed with mysql2's "Pool is closed".
|
|
1024
|
-
return await introspectMysqlWith(exec, schemaName, {
|
|
1352
|
+
return await introspectMysqlWith(exec, schemaName, {
|
|
1353
|
+
include: options.include,
|
|
1354
|
+
exclude: options.exclude,
|
|
1355
|
+
logger: options.logger,
|
|
1356
|
+
});
|
|
1025
1357
|
}
|
|
1026
1358
|
finally {
|
|
1027
1359
|
await pool.end();
|
|
@@ -1032,7 +1364,7 @@ export async function introspectMysql(options) {
|
|
|
1032
1364
|
// ---------------------------------------------------------------------------
|
|
1033
1365
|
/** mysql2 connection flags Turbine pins for correct behavior. */
|
|
1034
1366
|
const MYSQL_DRIVER_FLAGS = {
|
|
1035
|
-
// Named placeholders (`:pN`), see paramPlaceholder +
|
|
1367
|
+
// Named placeholders (`:pN`), see paramPlaceholder + bindStatement.
|
|
1036
1368
|
namedPlaceholders: true,
|
|
1037
1369
|
// Safe-int policy matching Postgres int8: number when it fits, decimal string
|
|
1038
1370
|
// otherwise. DECIMAL always comes back as a string. No global state mutated.
|
|
@@ -1158,11 +1490,15 @@ export async function turbineMysql(target, schema, options = {}) {
|
|
|
1158
1490
|
connectionLimit: options.connectionLimit ?? 10,
|
|
1159
1491
|
...MYSQL_DRIVER_FLAGS,
|
|
1160
1492
|
});
|
|
1161
|
-
// Every NEW physical connection: disable backslash string escaping so
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1493
|
+
// Every NEW physical connection: disable backslash string escaping, so a
|
|
1494
|
+
// string literal in raw SQL on a Turbine-owned pool reads the way it does
|
|
1495
|
+
// on PostgreSQL. Nothing Turbine GENERATES depends on it any more: the LIKE
|
|
1496
|
+
// clause names `!` (see mysqlDialect.likeEscapeCharacter), a JSON_OBJECT
|
|
1497
|
+
// key holding a backslash is written in hex (mysqlKeyLiteral), and every
|
|
1498
|
+
// value is a bound parameter, so the SQL means the same thing on a pool
|
|
1499
|
+
// the caller supplies, which keeps the caller's own mode. This stays
|
|
1500
|
+
// because removing it would change how raw SQL already written against an
|
|
1501
|
+
// owned pool parses.
|
|
1166
1502
|
//
|
|
1167
1503
|
// NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
|
|
1168
1504
|
// *raw* (callback-style) connection, its `.query()` returns a non-thenable
|
|
@@ -1175,32 +1511,46 @@ export async function turbineMysql(target, schema, options = {}) {
|
|
|
1175
1511
|
pool = new MysqlPool(mysql2Pool);
|
|
1176
1512
|
owns = true;
|
|
1177
1513
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
...clientConfig
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1514
|
+
let client;
|
|
1515
|
+
try {
|
|
1516
|
+
// Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
|
|
1517
|
+
const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
|
|
1518
|
+
assertSupportedVersion(String(versionRows[0]?.v ?? ''));
|
|
1519
|
+
// Everything that is not a pool-construction option is client config and
|
|
1520
|
+
// rides through untouched, so a new TurbineConfig option works here the day
|
|
1521
|
+
// it lands. The engine-owned keys come last, so no caller can unbind the
|
|
1522
|
+
// dialect.
|
|
1523
|
+
const { connectionLimit: _connectionLimit, ...clientConfig } = options;
|
|
1524
|
+
client = new TurbineClient({
|
|
1525
|
+
...clientConfig,
|
|
1526
|
+
pool,
|
|
1527
|
+
dialect: mysqlDialect,
|
|
1528
|
+
preparedStatements: false,
|
|
1529
|
+
},
|
|
1530
|
+
// The join strategy is the only read path that consults a recorded dialect
|
|
1531
|
+
// type, so a schema that spells a boolean any other way would read 1/0
|
|
1532
|
+
// there and `true`/`false` everywhere else.
|
|
1533
|
+
normalizeBooleanDialectTypes(schema));
|
|
1534
|
+
}
|
|
1535
|
+
catch (err) {
|
|
1536
|
+
// A failed factory hands the caller nothing to close, so a pool it BUILT
|
|
1537
|
+
// is closed here: the version probe's connection would otherwise keep the
|
|
1538
|
+
// process alive (the client constructor refuses a bad config, a static
|
|
1539
|
+
// global filter naming no table or column for one). A pool the caller
|
|
1540
|
+
// supplied is the caller's and stays open.
|
|
1541
|
+
if (owns)
|
|
1542
|
+
await pool.end().catch(() => { });
|
|
1543
|
+
throw err;
|
|
1544
|
+
}
|
|
1196
1545
|
if (owns) {
|
|
1197
1546
|
// Turbine built this pool, so disconnect()/end() must close it. External
|
|
1198
1547
|
// pools (injection) stay the caller's responsibility (disconnect() no-op),
|
|
1199
1548
|
// consistent with turbineHttp / turbineSqlite.
|
|
1200
1549
|
const baseDisconnect = client.disconnect.bind(client);
|
|
1201
|
-
const close = async () => {
|
|
1202
|
-
await baseDisconnect();
|
|
1550
|
+
const close = async (options) => {
|
|
1551
|
+
const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
|
|
1203
1552
|
await pool.end();
|
|
1553
|
+
return result ?? undefined;
|
|
1204
1554
|
};
|
|
1205
1555
|
const patch = client;
|
|
1206
1556
|
patch.disconnect = close;
|