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/cjs/mysql.js
CHANGED
|
@@ -78,6 +78,7 @@ const errors_js_1 = require("./errors.js");
|
|
|
78
78
|
const introspect_js_1 = require("./introspect.js");
|
|
79
79
|
const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
|
|
80
80
|
const schema_js_1 = require("./schema.js");
|
|
81
|
+
const sql_placeholders_js_1 = require("./sql-placeholders.js");
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
83
|
// Value coercion (params in)
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
@@ -86,7 +87,7 @@ const schema_js_1 = require("./schema.js");
|
|
|
86
87
|
* 1/0 (MySQL has no boolean type; `TINYINT(1)` stores 1/0), `undefined`/`null`
|
|
87
88
|
* → NULL, `Date`/`Uint8Array` pass through (mysql2 formats them), and any
|
|
88
89
|
* remaining object/array → JSON text (matches how Turbine already pre-serializes
|
|
89
|
-
* JSON filter
|
|
90
|
+
* JSON filter params). An `in` list element binds through here too, one by one.
|
|
90
91
|
*/
|
|
91
92
|
function toMysqlParam(value) {
|
|
92
93
|
if (value === undefined || value === null)
|
|
@@ -95,31 +96,136 @@ function toMysqlParam(value) {
|
|
|
95
96
|
case 'boolean':
|
|
96
97
|
return value ? 1 : 0;
|
|
97
98
|
case 'number':
|
|
99
|
+
// MySQL has no NaN or Infinity: the server stored NaN as 0 without a
|
|
100
|
+
// word, and refused Infinity with a raw driver error.
|
|
101
|
+
if (!Number.isFinite(value)) {
|
|
102
|
+
throw new errors_js_1.ValidationError(`MySQL cannot store or compare ${value}: a bound number must be finite. Pass a finite number, or null.`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
98
105
|
case 'bigint':
|
|
99
106
|
case 'string':
|
|
100
107
|
return value;
|
|
101
108
|
}
|
|
102
|
-
if (value instanceof Date)
|
|
109
|
+
if (value instanceof Date) {
|
|
110
|
+
// mysql2 renders an Invalid Date as '0000-00-00 00:00:00.000', which the
|
|
111
|
+
// server then reports as that value instead of the caller's mistake.
|
|
112
|
+
if (Number.isNaN(value.getTime())) {
|
|
113
|
+
throw new errors_js_1.ValidationError('MySQL cannot bind an Invalid Date. Pass a valid Date, or null.');
|
|
114
|
+
}
|
|
103
115
|
return value;
|
|
116
|
+
}
|
|
104
117
|
if (value instanceof Uint8Array)
|
|
105
118
|
return value; // also covers Buffer
|
|
106
119
|
return JSON.stringify(value);
|
|
107
120
|
}
|
|
108
121
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
122
|
+
* An `in` / `notIn` list, as the builder hands it to the shim: ONE param, so
|
|
123
|
+
* the SQL text and its cache entry stay independent of the list's length (see
|
|
124
|
+
* {@link mysqlDialect.buildInClause}). The shim expands it into one placeholder
|
|
125
|
+
* per element, each bound exactly as an equality operand is, which is what
|
|
126
|
+
* makes `{ in: [v] }` match the rows `{ equals: v }` matches on every column
|
|
127
|
+
* type: a `JSON_TABLE` list compared JSON values, so a boolean, a date or time,
|
|
128
|
+
* a binary value, a decimal or bigint written as a string, and every
|
|
129
|
+
* case-folded string missed, and `notIn` then matched every row.
|
|
130
|
+
*/
|
|
131
|
+
class MysqlInList {
|
|
132
|
+
values;
|
|
133
|
+
constructor(values) {
|
|
134
|
+
this.values = values;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** MySQL's limit on placeholders in one prepared statement. */
|
|
138
|
+
const MYSQL_MAX_PLACEHOLDERS = 65_535;
|
|
139
|
+
/** A list of `n` elements is sent padded to the next power of two (by repeating its last element). */
|
|
140
|
+
function paddedListLength(n) {
|
|
141
|
+
return n <= 1 ? n : 2 ** Math.ceil(Math.log2(n));
|
|
142
|
+
}
|
|
143
|
+
/** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
|
|
144
|
+
function refusePlaceholder(message) {
|
|
145
|
+
throw new errors_js_1.ValidationError(message);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The statement text and named binding for one execution: `$n` placeholders
|
|
149
|
+
* (the spelling `rawUnsafe` documents) rewritten to `:pN`, every
|
|
150
|
+
* {@link MysqlInList} expanded, and every other value bound by name. Mapping by
|
|
151
|
+
* NAME makes binding independent of where each placeholder lands in the SQL
|
|
152
|
+
* text, the same guarantee Postgres' numbered `$N` gives. `binding` is
|
|
153
|
+
* `undefined` when nothing is bound, so parameter-less statements (BEGIN,
|
|
154
|
+
* COMMIT, DDL) run through `.query()`.
|
|
114
155
|
*/
|
|
115
|
-
function
|
|
156
|
+
function bindStatement(sql, values) {
|
|
116
157
|
if (values.length === 0)
|
|
117
|
-
return undefined;
|
|
158
|
+
return { sql, binding: undefined };
|
|
159
|
+
const text = (0, sql_placeholders_js_1.rewritePositionalPlaceholders)(sql, 'mysql', values.length, (i) => `:p${i}`, refusePlaceholder);
|
|
118
160
|
const named = {};
|
|
161
|
+
let lists;
|
|
119
162
|
for (let i = 0; i < values.length; i++) {
|
|
120
|
-
|
|
163
|
+
const value = values[i];
|
|
164
|
+
if (value instanceof MysqlInList) {
|
|
165
|
+
lists ??= new Map();
|
|
166
|
+
lists.set(`p${i + 1}`, value);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
named[`p${i + 1}`] = toMysqlParam(value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (lists === undefined)
|
|
173
|
+
return { sql: text, binding: named };
|
|
174
|
+
const expanded = expandInLists(text, lists, named);
|
|
175
|
+
return { sql: expanded, binding: Object.keys(named).length > 0 ? named : undefined };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Replace each list placeholder (`:p3`) with one placeholder per element
|
|
179
|
+
* (`:p3_0, :p3_1, ...`), binding each element into `named`. An empty list
|
|
180
|
+
* becomes a subquery with no rows, so `IN` is false and `NOT IN` true, as
|
|
181
|
+
* Postgres answers `= ANY('{}')` and `!= ALL('{}')`.
|
|
182
|
+
*
|
|
183
|
+
* Padding to a power of two bounds the statement texts a caller can produce
|
|
184
|
+
* (mysql2 prepares and caches one server-side statement per distinct text and
|
|
185
|
+
* connection, and the server caps the total across every client at
|
|
186
|
+
* `max_prepared_stmt_count`): about seventeen lengths per list instead of one
|
|
187
|
+
* per length. A repeated element changes neither `IN` nor `NOT IN`.
|
|
188
|
+
*/
|
|
189
|
+
function expandInLists(sql, lists, named) {
|
|
190
|
+
const tokens = (0, sql_placeholders_js_1.scanPlaceholders)(sql, 'mysql');
|
|
191
|
+
const listTokens = tokens.filter((t) => t.kind === 'named' && lists.has(t.name));
|
|
192
|
+
const others = tokens.length - listTokens.length;
|
|
193
|
+
let padded = 0;
|
|
194
|
+
let exact = 0;
|
|
195
|
+
for (const t of listTokens) {
|
|
196
|
+
const n = lists.get(t.name).values.length;
|
|
197
|
+
padded += paddedListLength(n);
|
|
198
|
+
exact += n;
|
|
199
|
+
}
|
|
200
|
+
if (others + exact > MYSQL_MAX_PLACEHOLDERS) {
|
|
201
|
+
throw new errors_js_1.ValidationError(`An \`in\` / \`notIn\` list on MySQL binds one placeholder per element, and this statement needs ` +
|
|
202
|
+
`${others + exact} where MySQL allows ${MYSQL_MAX_PLACEHOLDERS}. Split the list across several queries.`);
|
|
203
|
+
}
|
|
204
|
+
const pad = others + padded <= MYSQL_MAX_PLACEHOLDERS;
|
|
205
|
+
for (const [name, list] of lists) {
|
|
206
|
+
list.values.forEach((v, j) => {
|
|
207
|
+
named[`${name}_${j}`] = toMysqlParam(v);
|
|
208
|
+
});
|
|
121
209
|
}
|
|
122
|
-
|
|
210
|
+
let out = '';
|
|
211
|
+
let last = 0;
|
|
212
|
+
for (const t of listTokens) {
|
|
213
|
+
const n = lists.get(t.name).values.length;
|
|
214
|
+
let replacement;
|
|
215
|
+
if (n === 0) {
|
|
216
|
+
replacement = 'SELECT NULL FROM DUAL WHERE FALSE';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const parts = [];
|
|
220
|
+
const length = pad ? paddedListLength(n) : n;
|
|
221
|
+
for (let j = 0; j < length; j++)
|
|
222
|
+
parts.push(`:${t.name}_${Math.min(j, n - 1)}`);
|
|
223
|
+
replacement = parts.join(', ');
|
|
224
|
+
}
|
|
225
|
+
out += sql.slice(last, t.start) + replacement;
|
|
226
|
+
last = t.end;
|
|
227
|
+
}
|
|
228
|
+
return out + sql.slice(last);
|
|
123
229
|
}
|
|
124
230
|
// ---------------------------------------------------------------------------
|
|
125
231
|
// Result shaping + error translation
|
|
@@ -254,10 +360,50 @@ function augmentMysqlError(err) {
|
|
|
254
360
|
case 1205:
|
|
255
361
|
target.code = '40001';
|
|
256
362
|
return err;
|
|
257
|
-
default:
|
|
363
|
+
default: {
|
|
364
|
+
// Data exceptions (a bound value that does not fit the column) take the
|
|
365
|
+
// SQLSTATE class 22 path in `wrapPgError`, which builds a ValidationError
|
|
366
|
+
// naming the column and redacts the value-bearing message in 'safe' mode.
|
|
367
|
+
const sqlstate = mysqlDataExceptionState(e.errno, e.sqlState, msg);
|
|
368
|
+
if (sqlstate === undefined)
|
|
369
|
+
return err;
|
|
370
|
+
target.code = sqlstate;
|
|
371
|
+
// "... for column 'name' at row 1", or "... in value for column 't.meta'."
|
|
372
|
+
const column = /for column '([^']+)'/i.exec(msg)?.[1];
|
|
373
|
+
if (column)
|
|
374
|
+
target.column = column.slice(column.lastIndexOf('.') + 1);
|
|
258
375
|
return err;
|
|
376
|
+
}
|
|
259
377
|
}
|
|
260
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* The SQLSTATE a MySQL data exception maps to, or `undefined` when `errno` is
|
|
381
|
+
* not one. MySQL reports several of these under the generic `HY000`, so the
|
|
382
|
+
* common ones are mapped by errno to the code PostgreSQL uses for the same
|
|
383
|
+
* failure; anything else MySQL itself files under class 22 keeps its own code.
|
|
384
|
+
*/
|
|
385
|
+
function mysqlDataExceptionState(errno, sqlState, message) {
|
|
386
|
+
switch (errno) {
|
|
387
|
+
case 1406: // ER_DATA_TOO_LONG
|
|
388
|
+
return '22001';
|
|
389
|
+
case 1264: // ER_WARN_DATA_OUT_OF_RANGE
|
|
390
|
+
case 1690: // ER_DATA_OUT_OF_RANGE
|
|
391
|
+
return '22003';
|
|
392
|
+
case 1366: // ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: "Incorrect integer value: 'abc' for column ..."
|
|
393
|
+
case 1265: // WARN_DATA_TRUNCATED: a value outside an ENUM, a malformed DECIMAL
|
|
394
|
+
return '22P02';
|
|
395
|
+
case 1292: // ER_TRUNCATED_WRONG_VALUE: an invalid datetime, or a non-numeric string compared as a number
|
|
396
|
+
case 1411: // ER_WRONG_VALUE_FOR_TYPE
|
|
397
|
+
return /\b(?:date|time|datetime|timestamp)\b/i.test(message) ? '22007' : '22P02';
|
|
398
|
+
case 1365: // ER_DIVISION_BY_ZERO
|
|
399
|
+
return '22012';
|
|
400
|
+
case 3140: // ER_INVALID_JSON_TEXT
|
|
401
|
+
case 3141: // ER_INVALID_JSON_TEXT_IN_PARAM
|
|
402
|
+
case 3144: // ER_INVALID_JSON_CHARSET
|
|
403
|
+
return '22032';
|
|
404
|
+
}
|
|
405
|
+
return typeof sqlState === 'string' && /^22[0-9A-Z]{3}$/.test(sqlState) ? sqlState : undefined;
|
|
406
|
+
}
|
|
261
407
|
function normalizeQueryArgs(arg, values) {
|
|
262
408
|
if (typeof arg === 'string')
|
|
263
409
|
return { text: arg, params: values ?? [] };
|
|
@@ -271,9 +417,10 @@ function normalizeQueryArgs(arg, values) {
|
|
|
271
417
|
* the `{ p1, p2, … }` object built from the positional params.
|
|
272
418
|
*/
|
|
273
419
|
async function execOne(runner, sql, params) {
|
|
274
|
-
const
|
|
420
|
+
const bound = bindStatement(sql, params);
|
|
421
|
+
const binding = bound.binding;
|
|
275
422
|
try {
|
|
276
|
-
const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
423
|
+
const [result, fields] = binding ? await runner.execute(bound.sql, binding) : await runner.query(bound.sql);
|
|
277
424
|
return shapeResult(result, fields);
|
|
278
425
|
}
|
|
279
426
|
catch (err) {
|
|
@@ -387,6 +534,18 @@ function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
387
534
|
* matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
|
|
388
535
|
*/
|
|
389
536
|
const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
|
|
537
|
+
/** Integer column types narrower than 64 bits, in MySQL and code-first (PostgreSQL) spellings. */
|
|
538
|
+
const SMALL_INTEGER_TYPES = new Set([
|
|
539
|
+
'tinyint',
|
|
540
|
+
'smallint',
|
|
541
|
+
'mediumint',
|
|
542
|
+
'int',
|
|
543
|
+
'integer',
|
|
544
|
+
'int2',
|
|
545
|
+
'int4',
|
|
546
|
+
'serial',
|
|
547
|
+
'smallserial',
|
|
548
|
+
]);
|
|
390
549
|
/**
|
|
391
550
|
* Rewrite every boolean column's recorded dialect type to
|
|
392
551
|
* {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
|
|
@@ -439,6 +598,11 @@ function isMysqlDateType(dialectType) {
|
|
|
439
598
|
}
|
|
440
599
|
/** Map a schema-builder (Postgres-flavored) column type to MySQL DDL. */
|
|
441
600
|
function mysqlColumnType(type, maxLength) {
|
|
601
|
+
// An enum column's type from `buildEnumColumn`, already spelled for MySQL. It
|
|
602
|
+
// returns first: its labels are free text, and one named `int` or `date`
|
|
603
|
+
// would otherwise match a pattern below.
|
|
604
|
+
if (/^ENUM\(/.test(type))
|
|
605
|
+
return type;
|
|
442
606
|
const t = type.toUpperCase();
|
|
443
607
|
if (/BIGSERIAL/.test(t))
|
|
444
608
|
return 'BIGINT AUTO_INCREMENT';
|
|
@@ -462,14 +626,19 @@ function mysqlColumnType(type, maxLength) {
|
|
|
462
626
|
return 'JSON';
|
|
463
627
|
if (/UUID/.test(t))
|
|
464
628
|
return 'CHAR(36)';
|
|
629
|
+
// DATETIME(3), not bare DATETIME: the bare type has zero fractional digits and
|
|
630
|
+
// rounds a JS Date's milliseconds away on write.
|
|
465
631
|
if (/TIMESTAMPTZ|TIMESTAMP|DATETIME/.test(t))
|
|
466
|
-
return 'DATETIME';
|
|
632
|
+
return 'DATETIME(3)';
|
|
467
633
|
if (/\bDATE\b/.test(t))
|
|
468
634
|
return 'DATE';
|
|
469
635
|
if (/\bTIME\b/.test(t))
|
|
470
636
|
return 'TIME';
|
|
637
|
+
// A bounded binary column is VARBINARY(n): a BLOB cannot be a key.
|
|
638
|
+
if (/VARBINARY/.test(t))
|
|
639
|
+
return `VARBINARY(${maxLength ?? 255})`;
|
|
471
640
|
if (/BYTEA|BLOB/.test(t))
|
|
472
|
-
return 'BLOB';
|
|
641
|
+
return maxLength != null ? `VARBINARY(${maxLength})` : 'BLOB';
|
|
473
642
|
if (/VARCHAR/.test(t))
|
|
474
643
|
return maxLength != null ? `VARCHAR(${maxLength})` : 'VARCHAR(255)';
|
|
475
644
|
if (/CHAR/.test(t))
|
|
@@ -483,6 +652,32 @@ function mysqlColumnType(type, maxLength) {
|
|
|
483
652
|
// ---------------------------------------------------------------------------
|
|
484
653
|
// mysqlDialect, the full Dialect contract for MySQL 8
|
|
485
654
|
// ---------------------------------------------------------------------------
|
|
655
|
+
/** The row alias a multi-row upsert gives its incoming rows (`AS ... ON DUPLICATE KEY UPDATE`). */
|
|
656
|
+
const MYSQL_INCOMING_ALIAS = '`turbine_excluded`';
|
|
657
|
+
/**
|
|
658
|
+
* The LIKE escape character on MySQL (see `mysqlDialect.likeEscapeCharacter`).
|
|
659
|
+
* One character, nothing special inside a single-quoted literal in either
|
|
660
|
+
* `sql_mode`, and not a LIKE metacharacter.
|
|
661
|
+
*/
|
|
662
|
+
const MYSQL_LIKE_ESCAPE = '!';
|
|
663
|
+
/** The LIKE metacharacters under {@link MYSQL_LIKE_ESCAPE}: itself, `%` and `_`. */
|
|
664
|
+
const MYSQL_LIKE_META_G = /[!%_]/g;
|
|
665
|
+
/**
|
|
666
|
+
* A `JSON_OBJECT` key as SQL text that reads the same in every `sql_mode`.
|
|
667
|
+
* A key without a backslash is a plain quoted literal. A backslash has no
|
|
668
|
+
* literal spelling that both modes agree on (`'a\\b'` is one backslash by
|
|
669
|
+
* default and two under `NO_BACKSLASH_ESCAPES`, which Turbine's own
|
|
670
|
+
* connections set and a caller-supplied pool may not), so such a key is
|
|
671
|
+
* written as its UTF-8 bytes in hex, which no mode re-reads.
|
|
672
|
+
*/
|
|
673
|
+
function mysqlKeyLiteral(key, dialect) {
|
|
674
|
+
if (!key.includes('\\'))
|
|
675
|
+
return `'${dialect.escapeStringLiteral(key)}'`;
|
|
676
|
+
let hex = '';
|
|
677
|
+
for (const byte of new TextEncoder().encode(key))
|
|
678
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
679
|
+
return `CONVERT(X'${hex}' USING utf8mb4)`;
|
|
680
|
+
}
|
|
486
681
|
/**
|
|
487
682
|
* MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
|
|
488
683
|
* quoting, named `:pN` placeholders (NOT positional `?`, see the module
|
|
@@ -494,6 +689,47 @@ function mysqlColumnType(type, maxLength) {
|
|
|
494
689
|
exports.mysqlDialect = {
|
|
495
690
|
...dialect_js_1.postgresDialect,
|
|
496
691
|
name: 'mysql',
|
|
692
|
+
capabilities: Object.freeze({
|
|
693
|
+
distinctOn: false,
|
|
694
|
+
positionalJson: false,
|
|
695
|
+
nullsOrderingClause: false,
|
|
696
|
+
nullsSortHigh: false,
|
|
697
|
+
insensitiveInList: false,
|
|
698
|
+
jsonDocumentEquality: 'exact',
|
|
699
|
+
jsonExtractTyped: false,
|
|
700
|
+
jsonExactNumericCast: false,
|
|
701
|
+
numericTextComparison: true,
|
|
702
|
+
enforcesForeignKeys: true,
|
|
703
|
+
partitionLimitPushdown: false,
|
|
704
|
+
// A zone-less literal reads in the session time zone; only `date` columns rewrite.
|
|
705
|
+
utcTimestampRewrite: false,
|
|
706
|
+
paramTypeCasts: false,
|
|
707
|
+
namedEnumTypes: false,
|
|
708
|
+
structuredValueColumns: 'scalars-and-text',
|
|
709
|
+
// Counted in a 16-bit field.
|
|
710
|
+
maxBindParameters: 65535,
|
|
711
|
+
maxInsertRows: Number.POSITIVE_INFINITY,
|
|
712
|
+
upsertConflictTarget: 'any-unique-key',
|
|
713
|
+
setClauseOrder: 'left-to-right',
|
|
714
|
+
reselectLocksRows: true,
|
|
715
|
+
integerDivision: 'DIV',
|
|
716
|
+
updateExpressions: true,
|
|
717
|
+
// Caught by the onConflict re-read, which runs in the reselect transaction.
|
|
718
|
+
bulkUpsertAppliesRepeatedKeys: true,
|
|
719
|
+
readOnlyTransactions: true,
|
|
720
|
+
connectionParameters: false,
|
|
721
|
+
backendCancel: false,
|
|
722
|
+
idleTransactionTimeout: false,
|
|
723
|
+
abortsTransactionOnError: false,
|
|
724
|
+
connectionDriver: 'engine-driver',
|
|
725
|
+
// mysql2 execute() prepares each statement text; max_prepared_stmt_count caps them server-wide.
|
|
726
|
+
preparesEveryStatement: true,
|
|
727
|
+
statementSyntax: 'mysql',
|
|
728
|
+
// An inline REFERENCES is parsed and ignored.
|
|
729
|
+
inlineForeignKeys: false,
|
|
730
|
+
// TEXT / BLOB need a prefix length in a key (ER_BLOB_KEY_WITHOUT_LENGTH).
|
|
731
|
+
unboundedTextKeys: false,
|
|
732
|
+
}),
|
|
497
733
|
// No RETURNING → run the write, then re-SELECT by PK/where (Phase-0 reselect).
|
|
498
734
|
resultStrategy: 'reselect',
|
|
499
735
|
supportsReturning: false,
|
|
@@ -514,6 +750,13 @@ exports.mysqlDialect = {
|
|
|
514
750
|
// Postgres-only in this release (flipping it on is a one-line change + tests).
|
|
515
751
|
supportsLateralJoin: false,
|
|
516
752
|
supportsPlanCacheMode: false,
|
|
753
|
+
// FOR UPDATE / FOR SHARE (8.0); there is no NO KEY UPDATE or KEY SHARE.
|
|
754
|
+
rowLockModes: ['update', 'share'],
|
|
755
|
+
// No percentile_cont / percentile_disc.
|
|
756
|
+
supportsPercentileAggregates: false,
|
|
757
|
+
// INSERT ... AS alias ON DUPLICATE KEY UPDATE, with the refusals that
|
|
758
|
+
// keyless conflict resolution needs.
|
|
759
|
+
supportsBulkUpsert: true,
|
|
517
760
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
518
761
|
supportsAdvisoryLock: true,
|
|
519
762
|
// Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
|
|
@@ -546,15 +789,18 @@ exports.mysqlDialect = {
|
|
|
546
789
|
* `NO_BACKSLASH_ESCAPES`, `\` is an escape character inside a string literal,
|
|
547
790
|
* so doubling only `'` (the inherited Postgres rule) leaves a value ending in
|
|
548
791
|
* a backslash able to escape its own closing quote. Escape the backslash
|
|
549
|
-
* first, then the quote; every other byte is safe raw.
|
|
550
|
-
*
|
|
551
|
-
* is
|
|
792
|
+
* first, then the quote; every other byte is safe raw.
|
|
793
|
+
*
|
|
794
|
+
* That is right for the server's default `sql_mode` only: under
|
|
795
|
+
* `NO_BACKSLASH_ESCAPES` (which Turbine sets on the connections it opens) the
|
|
796
|
+
* doubled backslash reads as two. So `buildJsonObject`, the one caller, never
|
|
797
|
+
* hands it a key holding a backslash (see {@link mysqlKeyLiteral}).
|
|
552
798
|
*/
|
|
553
799
|
escapeStringLiteral(value) {
|
|
554
800
|
return value.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
555
801
|
},
|
|
556
802
|
buildJsonObject(pairs) {
|
|
557
|
-
const args = pairs.map(([key, expr]) =>
|
|
803
|
+
const args = pairs.map(([key, expr]) => `${mysqlKeyLiteral(key, this)}, ${expr}`);
|
|
558
804
|
return `JSON_OBJECT(${args.join(', ')})`;
|
|
559
805
|
},
|
|
560
806
|
// JSON_ARRAYAGG over an empty group returns NULL → COALESCE to JSON_ARRAY().
|
|
@@ -584,7 +830,9 @@ exports.mysqlDialect = {
|
|
|
584
830
|
// came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
|
|
585
831
|
// mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
|
|
586
832
|
// DECIMAL is always a string), so carry the text and keep it.
|
|
587
|
-
|
|
833
|
+
// `int8` is the spelling a code-first (`defineSchema`) schema records for
|
|
834
|
+
// the same BIGINT column, so it needs the same rule.
|
|
835
|
+
if (t === 'bigint' || t === 'int8') {
|
|
588
836
|
return {
|
|
589
837
|
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
590
838
|
decode: (value) => {
|
|
@@ -642,9 +890,9 @@ exports.mysqlDialect = {
|
|
|
642
890
|
// parsed value. It is also what `pg` returns for json/jsonb, so this is
|
|
643
891
|
// Postgres parity rather than a MySQL convention.
|
|
644
892
|
//
|
|
645
|
-
// Contrast SQLite
|
|
646
|
-
//
|
|
647
|
-
//
|
|
893
|
+
// Contrast SQLite, which has no JSON column type and stores the document as
|
|
894
|
+
// text: its driver shim parses the text back for a column the schema
|
|
895
|
+
// declares json/jsonb, and its own rule does the same through a join.
|
|
648
896
|
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
649
897
|
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
650
898
|
// bytes. Carry hex and rebuild the buffer.
|
|
@@ -659,6 +907,14 @@ exports.mysqlDialect = {
|
|
|
659
907
|
castAggregate(expr, target) {
|
|
660
908
|
return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
|
|
661
909
|
},
|
|
910
|
+
// AVG over an integer column is a DECIMAL with only four more digits of
|
|
911
|
+
// scale (div_precision_increment), so 4/3 came back 1.3333 where PostgreSQL
|
|
912
|
+
// returns 1.3333333333333333. Adding a DOUBLE zero averages in DOUBLE (the
|
|
913
|
+
// `CAST(... AS DOUBLE)` spelling needs 8.0.17; the engine floor is 8.0.0).
|
|
914
|
+
// BIGINT keeps its exact DECIMAL average, like PostgreSQL's numeric one.
|
|
915
|
+
averageArgument(columnRef, columnType) {
|
|
916
|
+
return SMALL_INTEGER_TYPES.has(columnType.toLowerCase()) ? `(${columnRef} + 0E0)` : undefined;
|
|
917
|
+
},
|
|
662
918
|
// MySQL has no bare OFFSET: the grammar requires a LIMIT for OFFSET to
|
|
663
919
|
// attach to, so `offset` without `limit` (valid on Postgres, which the
|
|
664
920
|
// default path is written for) is a syntax error here. The MySQL manual's
|
|
@@ -678,17 +934,16 @@ exports.mysqlDialect = {
|
|
|
678
934
|
s += ` OFFSET ${offsetPlaceholder}`;
|
|
679
935
|
return s;
|
|
680
936
|
},
|
|
681
|
-
// No array params in MySQL.
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
937
|
+
// No array params in MySQL. The list is ONE builder param (a MysqlInList),
|
|
938
|
+
// so the SQL text and its cache entry do not depend on the list's length; the
|
|
939
|
+
// shim expands the placeholder into one per element when the statement runs
|
|
940
|
+
// (see expandInLists). Each element binds as an equality operand does, so the
|
|
941
|
+
// list compares under the column's own type and collation rules.
|
|
686
942
|
buildInClause(expr, paramRef, negated) {
|
|
687
|
-
|
|
688
|
-
return `${expr} ${negated ? 'NOT IN' : 'IN'} (${inner})`;
|
|
943
|
+
return `${expr} ${negated ? 'NOT IN' : 'IN'} (${paramRef})`;
|
|
689
944
|
},
|
|
690
945
|
inClauseParam(values) {
|
|
691
|
-
return
|
|
946
|
+
return new MysqlInList(Array.isArray(values) ? values : []);
|
|
692
947
|
},
|
|
693
948
|
// No RETURNING, resultStrategy 'reselect' re-fetches the row.
|
|
694
949
|
buildReturningClause() {
|
|
@@ -712,6 +967,28 @@ exports.mysqlDialect = {
|
|
|
712
967
|
params: input.rowValues.flat(),
|
|
713
968
|
};
|
|
714
969
|
},
|
|
970
|
+
buildBulkUpsertStatement(input) {
|
|
971
|
+
// 8.0.19's row alias (`VALUES(col)` is deprecated since 8.0.20). There is
|
|
972
|
+
// no conflict target: ON DUPLICATE KEY UPDATE fires on whichever unique key
|
|
973
|
+
// a row violates, which is why the builder refuses a table where another
|
|
974
|
+
// key's columns are in the data, and why it refuses `ignore` and a
|
|
975
|
+
// conflict predicate (this grammar has no slot for one).
|
|
976
|
+
if (!input.update) {
|
|
977
|
+
throw new errors_js_1.UnsupportedFeatureError('createMany onConflict { ignore: true }', 'mysql', 'MySQL cannot tell the inserted rows from the skipped ones afterwards; use onConflict: { target, update }.');
|
|
978
|
+
}
|
|
979
|
+
let n = 0;
|
|
980
|
+
const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
|
|
981
|
+
const params = input.rowValues.flat();
|
|
982
|
+
const update = input.update(params, { target: input.table, incoming: MYSQL_INCOMING_ALIAS });
|
|
983
|
+
if (update.where) {
|
|
984
|
+
throw new errors_js_1.UnsupportedFeatureError('A predicate on createMany onConflict', 'mysql', 'ON DUPLICATE KEY UPDATE has no WHERE slot.');
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} AS ${MYSQL_INCOMING_ALIAS} ` +
|
|
988
|
+
`ON DUPLICATE KEY UPDATE ${update.setClauses.join(', ')}`,
|
|
989
|
+
params,
|
|
990
|
+
};
|
|
991
|
+
},
|
|
715
992
|
buildDefaultValuesInsertStatement(input) {
|
|
716
993
|
// MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
|
|
717
994
|
// repeats for any row count, so this is the same statement the column-driven
|
|
@@ -739,12 +1016,39 @@ exports.mysqlDialect = {
|
|
|
739
1016
|
// unless a functional/generated index exists.
|
|
740
1017
|
return `LOWER(${column}) LIKE LOWER(${paramRef})`;
|
|
741
1018
|
},
|
|
1019
|
+
/**
|
|
1020
|
+
* `!`, not `\`: with the server's default `sql_mode` a backslash escapes
|
|
1021
|
+
* inside a string literal, so the `ESCAPE '\'` every other engine emits is an
|
|
1022
|
+
* unterminated string here, and only a session running with
|
|
1023
|
+
* `NO_BACKSLASH_ESCAPES` reads it as one backslash. Turbine sets that mode on
|
|
1024
|
+
* the connections it opens, never on a pool the caller supplies (it would
|
|
1025
|
+
* change how the caller's own SQL parses), so the clause has to mean the same
|
|
1026
|
+
* thing in both modes, and `'!'` does. The operand is bound, so a backslash in
|
|
1027
|
+
* the VALUE is a plain character under this escape.
|
|
1028
|
+
*/
|
|
1029
|
+
likeEscapeCharacter: MYSQL_LIKE_ESCAPE,
|
|
1030
|
+
/** Prefix `%`, `_` and the escape character itself with {@link MYSQL_LIKE_ESCAPE}. */
|
|
1031
|
+
escapeLikePattern(value) {
|
|
1032
|
+
return value.replace(MYSQL_LIKE_META_G, `${MYSQL_LIKE_ESCAPE}$&`);
|
|
1033
|
+
},
|
|
742
1034
|
buildJsonContains(column, paramRef) {
|
|
743
1035
|
return `JSON_CONTAINS(${column}, ${paramRef})`;
|
|
744
1036
|
},
|
|
745
1037
|
buildJsonPathExtract(column, pathParamRef) {
|
|
746
1038
|
return `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${pathParamRef}))`;
|
|
747
1039
|
},
|
|
1040
|
+
// MySQL compares two JSON values by JSON type and value, so binding the
|
|
1041
|
+
// operand as JSON text makes `{ path, equals }` exact for every shape:
|
|
1042
|
+
// 5 vs "5" and true vs 1 differ, 5 equals 5.0, null matches JSON null, and
|
|
1043
|
+
// arrays and objects compare structurally (object key order does not matter).
|
|
1044
|
+
// A missing path extracts SQL NULL, which matches nothing. With no path the
|
|
1045
|
+
// whole document is compared.
|
|
1046
|
+
buildJsonPathEquals(input) {
|
|
1047
|
+
const { value } = input;
|
|
1048
|
+
const text = typeof value === 'bigint' ? value.toString() : JSON.stringify(value);
|
|
1049
|
+
const lhs = input.pathRef === undefined ? input.column : `JSON_EXTRACT(${input.column}, ${input.pathRef})`;
|
|
1050
|
+
return `${lhs} = CAST(${input.bind(text)} AS JSON)`;
|
|
1051
|
+
},
|
|
748
1052
|
// ---- Type mapping -------------------------------------------------------
|
|
749
1053
|
typeToTypeScript(dialectType, nullable) {
|
|
750
1054
|
return mysqlTypeToTs(dialectType, nullable);
|
|
@@ -756,6 +1060,24 @@ exports.mysqlDialect = {
|
|
|
756
1060
|
buildColumnType(input) {
|
|
757
1061
|
return mysqlColumnType(input.type, input.maxLength);
|
|
758
1062
|
},
|
|
1063
|
+
/**
|
|
1064
|
+
* MySQL declares an enum's labels on the column, `ENUM('a', 'b')`, which
|
|
1065
|
+
* refuses any other value in strict mode. A label's quotes are doubled. A
|
|
1066
|
+
* label holding a backslash is refused: whether `\` escapes in a string
|
|
1067
|
+
* literal depends on the session's NO_BACKSLASH_ESCAPES (Turbine's own
|
|
1068
|
+
* connections set it, other clients do not), so the same DDL would declare
|
|
1069
|
+
* a different label depending on what runs it.
|
|
1070
|
+
*/
|
|
1071
|
+
buildEnumColumn(column, labels) {
|
|
1072
|
+
const literals = labels.map((label) => {
|
|
1073
|
+
if (label.includes('\\')) {
|
|
1074
|
+
throw new errors_js_1.ValidationError(`The enum label ${JSON.stringify(label)} on column ${column} holds a backslash, which MySQL reads ` +
|
|
1075
|
+
'differently with and without NO_BACKSLASH_ESCAPES. Use a label without one.');
|
|
1076
|
+
}
|
|
1077
|
+
return `'${label.replace(/'/g, "''")}'`;
|
|
1078
|
+
});
|
|
1079
|
+
return { type: `ENUM(${literals.join(', ')})` };
|
|
1080
|
+
},
|
|
759
1081
|
buildColumnDefinition(input) {
|
|
760
1082
|
const isSerial = /serial/i.test(input.type);
|
|
761
1083
|
const parts = [input.name, this.buildColumnType(input)];
|
|
@@ -839,8 +1161,8 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
|
839
1161
|
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
840
1162
|
* collision resolution against scalar column fields included.
|
|
841
1163
|
*/
|
|
842
|
-
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
843
|
-
return (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
1164
|
+
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, logger) {
|
|
1165
|
+
return (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable, logger);
|
|
844
1166
|
}
|
|
845
1167
|
/**
|
|
846
1168
|
* Introspect a MySQL database into the same {@link SchemaMetadata} shape the
|
|
@@ -849,7 +1171,8 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
|
|
|
849
1171
|
*
|
|
850
1172
|
* @param exec Runs a parameterized (`:p1`, `:p2`, …) query and returns rows.
|
|
851
1173
|
* @param schema The MySQL database (schema) name to introspect.
|
|
852
|
-
* @param options Optional include / exclude table-name filters
|
|
1174
|
+
* @param options Optional include / exclude table-name filters, and the logger
|
|
1175
|
+
* that receives relation-naming notes (console when absent).
|
|
853
1176
|
*/
|
|
854
1177
|
async function introspectMysqlWith(exec, schema, options = {}) {
|
|
855
1178
|
// ----- Tables -----
|
|
@@ -862,7 +1185,7 @@ async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
862
1185
|
COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH
|
|
863
1186
|
FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
|
|
864
1187
|
const columnsByTable = new Map();
|
|
865
|
-
const enums =
|
|
1188
|
+
const enums = (0, introspect_js_1.nullRecord)();
|
|
866
1189
|
for (const c of columnRows) {
|
|
867
1190
|
if (!tableSet.has(c.TABLE_NAME))
|
|
868
1191
|
continue;
|
|
@@ -890,6 +1213,8 @@ async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
890
1213
|
tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
|
|
891
1214
|
nullable,
|
|
892
1215
|
hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
|
|
1216
|
+
// The server numbers it, so a create's re-read can use the insert id.
|
|
1217
|
+
isGenerated: /auto_increment/i.test(c.EXTRA),
|
|
893
1218
|
isArray: false,
|
|
894
1219
|
arrayType: undefined,
|
|
895
1220
|
pgArrayType: 'text[]',
|
|
@@ -971,16 +1296,19 @@ async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
971
1296
|
}
|
|
972
1297
|
}
|
|
973
1298
|
// ----- Relations -----
|
|
974
|
-
|
|
1299
|
+
// One field per column, as the PostgreSQL introspector requires.
|
|
1300
|
+
for (const [tableName, cols] of columnsByTable)
|
|
1301
|
+
(0, introspect_js_1.assertDistinctColumnFields)(tableName, cols);
|
|
1302
|
+
const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
|
|
975
1303
|
// ----- Assemble TableMetadata -----
|
|
976
|
-
const tables =
|
|
1304
|
+
const tables = (0, introspect_js_1.nullRecord)();
|
|
977
1305
|
for (const tableName of tableNames) {
|
|
978
1306
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
979
|
-
const columnMap =
|
|
980
|
-
const reverseColumnMap =
|
|
1307
|
+
const columnMap = (0, introspect_js_1.nullRecord)();
|
|
1308
|
+
const reverseColumnMap = (0, introspect_js_1.nullRecord)();
|
|
981
1309
|
const dateColumns = new Set();
|
|
982
|
-
const dialectTypes =
|
|
983
|
-
const pgTypes =
|
|
1310
|
+
const dialectTypes = (0, introspect_js_1.nullRecord)();
|
|
1311
|
+
const pgTypes = (0, introspect_js_1.nullRecord)();
|
|
984
1312
|
const allColumns = [];
|
|
985
1313
|
for (const col of columns) {
|
|
986
1314
|
columnMap[col.field] = col.name;
|
|
@@ -1002,7 +1330,7 @@ async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
1002
1330
|
allColumns,
|
|
1003
1331
|
primaryKey: pkByTable.get(tableName) ?? [],
|
|
1004
1332
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
1005
|
-
relations: relationsByTable.get(tableName) ??
|
|
1333
|
+
relations: relationsByTable.get(tableName) ?? (0, introspect_js_1.nullRecord)(),
|
|
1006
1334
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
1007
1335
|
};
|
|
1008
1336
|
}
|
|
@@ -1032,7 +1360,11 @@ async function introspectMysql(options) {
|
|
|
1032
1360
|
// try/finally runs the finally BEFORE the promise settles, so `pool.end()`
|
|
1033
1361
|
// closed the pool out from under every introspection query and the whole
|
|
1034
1362
|
// call failed with mysql2's "Pool is closed".
|
|
1035
|
-
return await introspectMysqlWith(exec, schemaName, {
|
|
1363
|
+
return await introspectMysqlWith(exec, schemaName, {
|
|
1364
|
+
include: options.include,
|
|
1365
|
+
exclude: options.exclude,
|
|
1366
|
+
logger: options.logger,
|
|
1367
|
+
});
|
|
1036
1368
|
}
|
|
1037
1369
|
finally {
|
|
1038
1370
|
await pool.end();
|
|
@@ -1043,7 +1375,7 @@ async function introspectMysql(options) {
|
|
|
1043
1375
|
// ---------------------------------------------------------------------------
|
|
1044
1376
|
/** mysql2 connection flags Turbine pins for correct behavior. */
|
|
1045
1377
|
const MYSQL_DRIVER_FLAGS = {
|
|
1046
|
-
// Named placeholders (`:pN`), see paramPlaceholder +
|
|
1378
|
+
// Named placeholders (`:pN`), see paramPlaceholder + bindStatement.
|
|
1047
1379
|
namedPlaceholders: true,
|
|
1048
1380
|
// Safe-int policy matching Postgres int8: number when it fits, decimal string
|
|
1049
1381
|
// otherwise. DECIMAL always comes back as a string. No global state mutated.
|
|
@@ -1169,11 +1501,15 @@ async function turbineMysql(target, schema, options = {}) {
|
|
|
1169
1501
|
connectionLimit: options.connectionLimit ?? 10,
|
|
1170
1502
|
...MYSQL_DRIVER_FLAGS,
|
|
1171
1503
|
});
|
|
1172
|
-
// Every NEW physical connection: disable backslash string escaping so
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1504
|
+
// Every NEW physical connection: disable backslash string escaping, so a
|
|
1505
|
+
// string literal in raw SQL on a Turbine-owned pool reads the way it does
|
|
1506
|
+
// on PostgreSQL. Nothing Turbine GENERATES depends on it any more: the LIKE
|
|
1507
|
+
// clause names `!` (see mysqlDialect.likeEscapeCharacter), a JSON_OBJECT
|
|
1508
|
+
// key holding a backslash is written in hex (mysqlKeyLiteral), and every
|
|
1509
|
+
// value is a bound parameter, so the SQL means the same thing on a pool
|
|
1510
|
+
// the caller supplies, which keeps the caller's own mode. This stays
|
|
1511
|
+
// because removing it would change how raw SQL already written against an
|
|
1512
|
+
// owned pool parses.
|
|
1177
1513
|
//
|
|
1178
1514
|
// NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
|
|
1179
1515
|
// *raw* (callback-style) connection, its `.query()` returns a non-thenable
|
|
@@ -1186,32 +1522,46 @@ async function turbineMysql(target, schema, options = {}) {
|
|
|
1186
1522
|
pool = new MysqlPool(mysql2Pool);
|
|
1187
1523
|
owns = true;
|
|
1188
1524
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
...clientConfig
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1525
|
+
let client;
|
|
1526
|
+
try {
|
|
1527
|
+
// Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
|
|
1528
|
+
const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
|
|
1529
|
+
assertSupportedVersion(String(versionRows[0]?.v ?? ''));
|
|
1530
|
+
// Everything that is not a pool-construction option is client config and
|
|
1531
|
+
// rides through untouched, so a new TurbineConfig option works here the day
|
|
1532
|
+
// it lands. The engine-owned keys come last, so no caller can unbind the
|
|
1533
|
+
// dialect.
|
|
1534
|
+
const { connectionLimit: _connectionLimit, ...clientConfig } = options;
|
|
1535
|
+
client = new client_js_1.TurbineClient({
|
|
1536
|
+
...clientConfig,
|
|
1537
|
+
pool,
|
|
1538
|
+
dialect: exports.mysqlDialect,
|
|
1539
|
+
preparedStatements: false,
|
|
1540
|
+
},
|
|
1541
|
+
// The join strategy is the only read path that consults a recorded dialect
|
|
1542
|
+
// type, so a schema that spells a boolean any other way would read 1/0
|
|
1543
|
+
// there and `true`/`false` everywhere else.
|
|
1544
|
+
normalizeBooleanDialectTypes(schema));
|
|
1545
|
+
}
|
|
1546
|
+
catch (err) {
|
|
1547
|
+
// A failed factory hands the caller nothing to close, so a pool it BUILT
|
|
1548
|
+
// is closed here: the version probe's connection would otherwise keep the
|
|
1549
|
+
// process alive (the client constructor refuses a bad config, a static
|
|
1550
|
+
// global filter naming no table or column for one). A pool the caller
|
|
1551
|
+
// supplied is the caller's and stays open.
|
|
1552
|
+
if (owns)
|
|
1553
|
+
await pool.end().catch(() => { });
|
|
1554
|
+
throw err;
|
|
1555
|
+
}
|
|
1207
1556
|
if (owns) {
|
|
1208
1557
|
// Turbine built this pool, so disconnect()/end() must close it. External
|
|
1209
1558
|
// pools (injection) stay the caller's responsibility (disconnect() no-op),
|
|
1210
1559
|
// consistent with turbineHttp / turbineSqlite.
|
|
1211
1560
|
const baseDisconnect = client.disconnect.bind(client);
|
|
1212
|
-
const close = async () => {
|
|
1213
|
-
await baseDisconnect();
|
|
1561
|
+
const close = async (options) => {
|
|
1562
|
+
const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
|
|
1214
1563
|
await pool.end();
|
|
1564
|
+
return result ?? undefined;
|
|
1215
1565
|
};
|
|
1216
1566
|
const patch = client;
|
|
1217
1567
|
patch.disconnect = close;
|