turbine-orm 0.65.0 → 0.66.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 +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
package/dist/mysql.js
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
*/
|
|
65
65
|
import { TurbineClient } from './client.js';
|
|
66
66
|
import { postgresDialect, } from './dialect.js';
|
|
67
|
-
import { ConnectionError, UnsupportedFeatureError } from './errors.js';
|
|
67
|
+
import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, UnsupportedFeatureError, } from './errors.js';
|
|
68
68
|
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
69
69
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
70
70
|
import { isDateType, snakeToCamel, } from './schema.js';
|
|
@@ -114,10 +114,58 @@ function toNamedBinding(values) {
|
|
|
114
114
|
// ---------------------------------------------------------------------------
|
|
115
115
|
// Result shaping + error translation
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* mysql2's `MYSQL_TYPE_TINY`. A result field with this type and a display width
|
|
119
|
+
* of 1 IS the `BOOLEAN` / `TINYINT(1)` column; see {@link MYSQL_BOOLEAN_TYPE}.
|
|
120
|
+
*/
|
|
121
|
+
const MYSQL_FIELD_TYPE_TINY = 1;
|
|
122
|
+
/**
|
|
123
|
+
* Turn `TINYINT(1)` cells back into real booleans, in place.
|
|
124
|
+
*
|
|
125
|
+
* MySQL stores a boolean as 1/0 and mysql2 returns the integer. Turbine still
|
|
126
|
+
* generates `ok: boolean` for the column ({@link mysqlTypeToTs}), so
|
|
127
|
+
* `row.ok === true` was false, `JSON.stringify(row)` emitted `1`, and a value
|
|
128
|
+
* the ORM itself wrote as `true` came back as a number.
|
|
129
|
+
*
|
|
130
|
+
* Done from the RESULT FIELDS rather than from mysql2's `typeCast` option on
|
|
131
|
+
* purpose. `typeCast` lives in {@link MYSQL_DRIVER_FLAGS}, which only applies
|
|
132
|
+
* to pools TURBINE builds; an injected mysql2 pool (a supported, documented
|
|
133
|
+
* entry point) would keep returning numbers at the top level while the join
|
|
134
|
+
* strategy's `jsonWireRule` returned booleans, i.e. fixing it there would have
|
|
135
|
+
* created exactly the strategy-dependent split this is meant to close. The
|
|
136
|
+
* field metadata is on every result from every pool.
|
|
137
|
+
*
|
|
138
|
+
* An expression column is never touched: `ok + 0` comes back as
|
|
139
|
+
* MYSQL_TYPE_LONGLONG with a different width, and only an exact
|
|
140
|
+
* (TINY, width 1) field is coerced.
|
|
141
|
+
*/
|
|
142
|
+
function coerceBooleanColumns(rows, fields) {
|
|
143
|
+
if (!Array.isArray(fields) || rows.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
const names = [];
|
|
146
|
+
for (const f of fields) {
|
|
147
|
+
if (!f || f.columnType !== MYSQL_FIELD_TYPE_TINY || f.columnLength !== 1)
|
|
148
|
+
continue;
|
|
149
|
+
if (typeof f.name !== 'string')
|
|
150
|
+
continue;
|
|
151
|
+
names.push(f.name);
|
|
152
|
+
}
|
|
153
|
+
if (names.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const v = row[name];
|
|
158
|
+
if (v === 1 || v === 0)
|
|
159
|
+
row[name] = v === 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
117
163
|
/** Shape a mysql2 `[result]` into a `pg.QueryResult`-like object. */
|
|
118
|
-
function shapeResult(result) {
|
|
164
|
+
function shapeResult(result, fields) {
|
|
119
165
|
if (Array.isArray(result)) {
|
|
120
|
-
|
|
166
|
+
const rows = result;
|
|
167
|
+
coerceBooleanColumns(rows, fields);
|
|
168
|
+
return { rows, rowCount: rows.length };
|
|
121
169
|
}
|
|
122
170
|
// Write statement → ResultSetHeader (no rows). Expose insertId/lastID for the
|
|
123
171
|
// `reselect` strategy to re-fetch the auto-increment row.
|
|
@@ -154,6 +202,19 @@ function augmentMysqlError(err) {
|
|
|
154
202
|
const parts = key.split('.');
|
|
155
203
|
target.constraint = parts.length > 1 ? parts[parts.length - 1] : key;
|
|
156
204
|
}
|
|
205
|
+
// THE conflicting row value is in that message, between the quotes after
|
|
206
|
+
// "Duplicate entry". MySQL has no `detail` field, so 'safe' mode had
|
|
207
|
+
// nothing to redact and returned this error verbatim as `.cause`, putting
|
|
208
|
+
// the value into every log line that renders the cause chain. Flag it so
|
|
209
|
+
// the message is withheld in 'safe' mode (see markValueBearingMessage);
|
|
210
|
+
// the constraint NAME survives on `target.constraint` and in the typed
|
|
211
|
+
// UniqueConstraintError's own message, so nothing diagnostic is lost.
|
|
212
|
+
//
|
|
213
|
+
// Deliberately only 1062: the other MySQL codes handled here report
|
|
214
|
+
// column/constraint NAMES only ("Column 'x' cannot be null", "Check
|
|
215
|
+
// constraint 'c' is violated", the FK constraint definition), so flagging
|
|
216
|
+
// them would withhold a message that never carried a value.
|
|
217
|
+
markValueBearingMessage(err);
|
|
157
218
|
return err;
|
|
158
219
|
}
|
|
159
220
|
// ER_ROW_IS_REFERENCED_2 / ER_NO_REFERENCED_ROW_2 → foreign key violation
|
|
@@ -202,8 +263,8 @@ function normalizeQueryArgs(arg, values) {
|
|
|
202
263
|
async function execOne(runner, sql, params) {
|
|
203
264
|
const binding = toNamedBinding(params);
|
|
204
265
|
try {
|
|
205
|
-
const [result] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
206
|
-
return shapeResult(result);
|
|
266
|
+
const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
267
|
+
return shapeResult(result, fields);
|
|
207
268
|
}
|
|
208
269
|
catch (err) {
|
|
209
270
|
throw augmentMysqlError(err);
|
|
@@ -279,7 +340,11 @@ export function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
279
340
|
const t = dialectType.toLowerCase();
|
|
280
341
|
const full = (columnType ?? '').toLowerCase();
|
|
281
342
|
let base;
|
|
282
|
-
|
|
343
|
+
// `tinyint(1)` can arrive either way: as (DATA_TYPE, COLUMN_TYPE) from the
|
|
344
|
+
// introspector's first pass, or as a single dialect type, because that is the
|
|
345
|
+
// spelling the introspector RECORDS for a boolean column (see the note there)
|
|
346
|
+
// and the `typeToTypeScript` dialect hook passes only the one string.
|
|
347
|
+
if (t === MYSQL_BOOLEAN_TYPE || (t === 'tinyint' && full === MYSQL_BOOLEAN_TYPE))
|
|
283
348
|
base = 'boolean';
|
|
284
349
|
else if (/^(tinyint|smallint|mediumint|int|integer|bigint)$/.test(t))
|
|
285
350
|
base = 'number';
|
|
@@ -299,6 +364,63 @@ export function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
299
364
|
base = 'unknown';
|
|
300
365
|
return nullable ? `${base} | null` : base;
|
|
301
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* The dialect-type spelling Turbine records for a MySQL boolean column.
|
|
369
|
+
*
|
|
370
|
+
* MySQL has no boolean type: `BOOLEAN` is an alias for `TINYINT(1)`, and
|
|
371
|
+
* `information_schema` reports DATA_TYPE `tinyint` for it, which is the same
|
|
372
|
+
* DATA_TYPE a genuine one-byte integer column has. The display width in
|
|
373
|
+
* COLUMN_TYPE is the ONLY thing that separates them, so the introspector keeps
|
|
374
|
+
* the full spelling for this one type rather than the bare DATA_TYPE. Anything
|
|
375
|
+
* reading a dialect type has to accept both spellings; the two places that
|
|
376
|
+
* matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
|
|
377
|
+
*/
|
|
378
|
+
const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
|
|
379
|
+
/**
|
|
380
|
+
* Rewrite every boolean column's recorded dialect type to
|
|
381
|
+
* {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
|
|
382
|
+
* {@link mysqlDialect.jsonWireRule} on.
|
|
383
|
+
*
|
|
384
|
+
* WHY. Only the join strategy consults the recorded type, so a column whose
|
|
385
|
+
* type is spelled some other way reads as a boolean at the top level (the shim
|
|
386
|
+
* decides that from the RESULT FIELDS, which every pool has) and as 1/0 through
|
|
387
|
+
* a `with` join, which is the strategy-dependent split the boolean rule exists
|
|
388
|
+
* to close. Two schemas spell it differently and neither is wrong:
|
|
389
|
+
*
|
|
390
|
+
* - a metadata file generated before Turbine started keeping the display width
|
|
391
|
+
* records the bare DATA_TYPE `tinyint` (with `tsType: 'boolean'`, which is
|
|
392
|
+
* what identifies it), so `turbine generate` is NOT required to get the fix;
|
|
393
|
+
* - a code-first `defineSchema` column records the Postgres spelling
|
|
394
|
+
* `boolean` / `bool`, which the DDL generator already emits as `TINYINT(1)`.
|
|
395
|
+
*
|
|
396
|
+
* A genuine one-byte integer column is never touched: it carries
|
|
397
|
+
* `tsType: 'number'`, and the `tsType` is the gate. Copy-on-write, because the
|
|
398
|
+
* argument is usually a shared exported constant and may also be handed to
|
|
399
|
+
* another engine's factory; an already-correct schema is returned unchanged and
|
|
400
|
+
* pays nothing.
|
|
401
|
+
*/
|
|
402
|
+
function normalizeBooleanDialectTypes(schema) {
|
|
403
|
+
const needsFix = (col) => {
|
|
404
|
+
if (!/^boolean\b/.test(col.tsType))
|
|
405
|
+
return false;
|
|
406
|
+
const recorded = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
407
|
+
return recorded === 'tinyint' || recorded === 'bool' || recorded === 'boolean';
|
|
408
|
+
};
|
|
409
|
+
let tables;
|
|
410
|
+
for (const [name, table] of Object.entries(schema.tables)) {
|
|
411
|
+
if (!table.columns.some(needsFix))
|
|
412
|
+
continue;
|
|
413
|
+
const columns = table.columns.map((col) => needsFix(col) ? { ...col, dialectType: MYSQL_BOOLEAN_TYPE, pgType: MYSQL_BOOLEAN_TYPE } : col);
|
|
414
|
+
if (!tables)
|
|
415
|
+
tables = { ...schema.tables };
|
|
416
|
+
tables[name] = {
|
|
417
|
+
...table,
|
|
418
|
+
columns,
|
|
419
|
+
pgTypes: Object.fromEntries(columns.map((c) => [c.name, c.pgType])),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return tables ? { ...schema, tables } : schema;
|
|
423
|
+
}
|
|
302
424
|
/** Is a MySQL declared type a date/time type (so values coerce back to `Date`)? */
|
|
303
425
|
function isMysqlDateType(dialectType) {
|
|
304
426
|
const t = dialectType.toLowerCase();
|
|
@@ -466,6 +588,52 @@ export const mysqlDialect = {
|
|
|
466
588
|
if (t === 'decimal' || t === 'numeric') {
|
|
467
589
|
return { sql: (ref) => `CAST(${ref} AS CHAR)`, decode: (value) => value };
|
|
468
590
|
}
|
|
591
|
+
// A boolean is 1/0 on the wire, and the driver shim turns that back into
|
|
592
|
+
// true/false on every direct read (see coerceBooleanColumns). JSON_OBJECT
|
|
593
|
+
// renders the same column as a JSON number and has no column metadata to
|
|
594
|
+
// consult, so without this the join strategy alone would keep returning
|
|
595
|
+
// 1/0: the same strategy-dependent type flip the json rule below exists to
|
|
596
|
+
// prevent. Carried as text because the decode half only runs on a string
|
|
597
|
+
// cell, and a plain CAST is enough here (unlike SQLite, MySQL enforces the
|
|
598
|
+
// column type, so the value is always an integer and `Number` reproduces
|
|
599
|
+
// the driver's own reading of anything that is not 0 or 1).
|
|
600
|
+
if (t === MYSQL_BOOLEAN_TYPE) {
|
|
601
|
+
return {
|
|
602
|
+
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
603
|
+
decode: (value) => {
|
|
604
|
+
if (value === '1')
|
|
605
|
+
return true;
|
|
606
|
+
if (value === '0')
|
|
607
|
+
return false;
|
|
608
|
+
if (typeof value !== 'string' || !/^-?\d+$/.test(value))
|
|
609
|
+
return value;
|
|
610
|
+
return Number(value);
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// A `json` column gets NO rule, and that is the fix rather than the absence
|
|
615
|
+
// of one. It used to read differently depending on the relation strategy:
|
|
616
|
+
// the driver handed back text while JSON_OBJECT, which knows the column is
|
|
617
|
+
// JSON, embedded it as a real nested value, so parsing the row yielded an
|
|
618
|
+
// object. Under the default `'auto'` the strategy is decided per query by
|
|
619
|
+
// row counts and index coverage, so adding a `limit` to
|
|
620
|
+
// `findMany({ with: { org: true } })` flipped `org.meta` from a string to
|
|
621
|
+
// an object with no other change.
|
|
622
|
+
//
|
|
623
|
+
// A `CAST(… AS CHAR)` carrier closes that, but only for a pool Turbine
|
|
624
|
+
// built: the text half of it came from a `typeCast` override in
|
|
625
|
+
// MYSQL_DRIVER_FLAGS, and an injected mysql2 pool (a documented entry
|
|
626
|
+
// point) never sees those flags, so it read the column as an object at the
|
|
627
|
+
// top level and as a string through the join. That trades one split for
|
|
628
|
+
// another. So the agreement is built on what BOTH pool shapes already do:
|
|
629
|
+
// mysql2 parses a `json` column by default, JSON_OBJECT embeds it parsed,
|
|
630
|
+
// and with the `typeCast` override gone every read path lands on the same
|
|
631
|
+
// parsed value. It is also what `pg` returns for json/jsonb, so this is
|
|
632
|
+
// Postgres parity rather than a MySQL convention.
|
|
633
|
+
//
|
|
634
|
+
// Contrast SQLite and SQL Server, which return TEXT for the same schema:
|
|
635
|
+
// neither has a JSON column type, so the value there really is a string in
|
|
636
|
+
// a text column and there is nothing to parse it back from.
|
|
469
637
|
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
470
638
|
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
471
639
|
// bytes. Carry hex and rebuild the buffer.
|
|
@@ -700,11 +868,14 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
700
868
|
}
|
|
701
869
|
}
|
|
702
870
|
const maxLen = c.CHARACTER_MAXIMUM_LENGTH != null ? num(c.CHARACTER_MAXIMUM_LENGTH) : undefined;
|
|
871
|
+
// `tinyint(1)` is the one type whose DATA_TYPE is not enough to identify it
|
|
872
|
+
// (see MYSQL_BOOLEAN_TYPE); everything else keeps the bare DATA_TYPE.
|
|
873
|
+
const recordedType = c.COLUMN_TYPE.toLowerCase() === MYSQL_BOOLEAN_TYPE ? MYSQL_BOOLEAN_TYPE : dataType;
|
|
703
874
|
const col = {
|
|
704
875
|
name: c.COLUMN_NAME,
|
|
705
876
|
field: snakeToCamel(c.COLUMN_NAME),
|
|
706
|
-
dialectType:
|
|
707
|
-
pgType:
|
|
877
|
+
dialectType: recordedType,
|
|
878
|
+
pgType: recordedType,
|
|
708
879
|
tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
|
|
709
880
|
nullable,
|
|
710
881
|
hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
|
|
@@ -870,12 +1041,18 @@ const MYSQL_DRIVER_FLAGS = {
|
|
|
870
1041
|
// DATETIME/TIMESTAMP → JS Date interpreted as UTC (matches Postgres).
|
|
871
1042
|
dateStrings: false,
|
|
872
1043
|
timezone: 'Z',
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
1044
|
+
//
|
|
1045
|
+
// NOTE: deliberately NO `typeCast` JSON override here. There used to be one,
|
|
1046
|
+
// pinning a `json` column to raw text so the nested-relation parser always
|
|
1047
|
+
// took its JSON.parse path. It could only ever apply to pools TURBINE builds,
|
|
1048
|
+
// and pool injection is a documented entry point, so it did not decide how a
|
|
1049
|
+
// `json` column reads; it decided that the two pool shapes read it
|
|
1050
|
+
// DIFFERENTLY (owned: string, injected: object). Every flag above is
|
|
1051
|
+
// idempotent in that sense (a placeholder style, an integer policy, a time
|
|
1052
|
+
// zone) and none of them changes a value's TYPE. Letting mysql2 parse a
|
|
1053
|
+
// `json` column is what both pool shapes do by default, and it is also what
|
|
1054
|
+
// `pg` does with json/jsonb, so the parsed value is the one answer that is
|
|
1055
|
+
// reachable from every entry point. See `jsonWireRule` for the other half.
|
|
879
1056
|
};
|
|
880
1057
|
/** Parse a `mysql://user:pass@host:port/db` connection string into mysql2 config. */
|
|
881
1058
|
function parseMysqlConfig(connectionString) {
|
|
@@ -895,7 +1072,8 @@ function parseMysqlConfig(connectionString) {
|
|
|
895
1072
|
return config;
|
|
896
1073
|
}
|
|
897
1074
|
catch {
|
|
898
|
-
|
|
1075
|
+
// Never echo the value, see malformedConnectionStringMessage.
|
|
1076
|
+
throw new ConnectionError(malformedConnectionStringMessage('MySQL', 'mysql://user:password@localhost:3306/app'));
|
|
899
1077
|
}
|
|
900
1078
|
}
|
|
901
1079
|
/**
|
|
@@ -951,8 +1129,11 @@ function isMysql2Pool(x) {
|
|
|
951
1129
|
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
952
1130
|
*
|
|
953
1131
|
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
|
954
|
-
* (named placeholders, bignum, UTC dates
|
|
955
|
-
*
|
|
1132
|
+
* (named placeholders, bignum, UTC dates), probes `SELECT VERSION()` to reject
|
|
1133
|
+
* MySQL < 8.0 / MariaDB, and `disconnect()` closes the pool it created. None of
|
|
1134
|
+
* those flags changes a column's TYPE, so an injected pool reads every value
|
|
1135
|
+
* the same way (see {@link MYSQL_DRIVER_FLAGS}); `namedPlaceholders: true` is
|
|
1136
|
+
* the one an injected pool must set for itself.
|
|
956
1137
|
*
|
|
957
1138
|
* @example
|
|
958
1139
|
* ```ts
|
|
@@ -1007,7 +1188,11 @@ export async function turbineMysql(target, schema, options = {}) {
|
|
|
1007
1188
|
pool,
|
|
1008
1189
|
dialect: mysqlDialect,
|
|
1009
1190
|
preparedStatements: false,
|
|
1010
|
-
},
|
|
1191
|
+
},
|
|
1192
|
+
// The join strategy is the only read path that consults a recorded dialect
|
|
1193
|
+
// type, so a schema that spells a boolean any other way would read 1/0
|
|
1194
|
+
// there and `true`/`false` everywhere else.
|
|
1195
|
+
normalizeBooleanDialectTypes(schema));
|
|
1011
1196
|
if (owns) {
|
|
1012
1197
|
// Turbine built this pool, so disconnect()/end() must close it. External
|
|
1013
1198
|
// pools (injection) stay the caller's responsibility (disconnect() no-op),
|
package/dist/nested-write.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `NestedWriteContext`.
|
|
13
13
|
*/
|
|
14
14
|
import { CircularRelationError, describeTargetForMessage, NotFoundError, RelationError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
15
|
-
import { resolveColumnName } from './query/utils.js';
|
|
15
|
+
import { markInternalCombinator, resolveColumnName } from './query/utils.js';
|
|
16
16
|
import { normalizeKeyColumns } from './schema.js';
|
|
17
17
|
const MAX_DEPTH = 10;
|
|
18
18
|
const CREATE_ONLY_OPS = new Set(['create', 'connect', 'connectOrCreate']);
|
|
@@ -310,8 +310,13 @@ function belongsToCorrelationWhere(ctx, rel, parentRow, parentTable) {
|
|
|
310
310
|
*/
|
|
311
311
|
function scopeWhereToParent(target, correlation) {
|
|
312
312
|
for (const key of Object.keys(correlation)) {
|
|
313
|
+
// Branded as Turbine's own: this `AND` has a FIXED arity of two, chosen
|
|
314
|
+
// here rather than reachable from a request body, so it must not cost the
|
|
315
|
+
// statement its server-side prepared name the way a caller-written
|
|
316
|
+
// combinator array does. Same reason the global-filter merge and the
|
|
317
|
+
// batched loader's correlation merge are branded.
|
|
313
318
|
if (Object.hasOwn(target, key))
|
|
314
|
-
return { AND: [target, correlation] };
|
|
319
|
+
return markInternalCombinator({ AND: [target, correlation] });
|
|
315
320
|
}
|
|
316
321
|
return { ...target, ...correlation };
|
|
317
322
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import Result from 'pg/lib/result';
|
|
20
20
|
import { prepareValue } from 'pg/lib/utils';
|
|
21
|
-
import { wrapPgError } from './errors.js';
|
|
21
|
+
import { PipelineError, wrapPgError } from './errors.js';
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
// Event names we intercept
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -165,7 +165,8 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
if (!transactional && pipelineError) {
|
|
168
|
-
// In non-transactional mode,
|
|
168
|
+
// In non-transactional mode, report every slot: what each query returned
|
|
169
|
+
// or the error it failed with.
|
|
169
170
|
const partialResults = [];
|
|
170
171
|
for (let i = 0; i < queries.length; i++) {
|
|
171
172
|
const qErr = queryErrors[i];
|
|
@@ -182,8 +183,21 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// A REAL PipelineError, not the first driver error with a `results`
|
|
187
|
+
// property bolted onto it. The old shape happened to satisfy
|
|
188
|
+
// `err.results`, which is what the docs show callers reading, while
|
|
189
|
+
// leaving `err instanceof PipelineError` and `err.code ===
|
|
190
|
+
// 'TURBINE_E014'` permanently false: E014 was defined, documented, and
|
|
191
|
+
// never constructed. The driver error survives as `.cause` and as the
|
|
192
|
+
// first `error` slot, and `failedIndex` / `failedTag` (recorded on it by
|
|
193
|
+
// `onErrorMessage`) move onto the typed error's own fields.
|
|
194
|
+
const first = pipelineError;
|
|
195
|
+
reject(new PipelineError({
|
|
196
|
+
results: partialResults,
|
|
197
|
+
failedIndex: first.failedIndex,
|
|
198
|
+
failedTag: first.failedTag,
|
|
199
|
+
cause: pipelineError,
|
|
200
|
+
}));
|
|
187
201
|
return;
|
|
188
202
|
}
|
|
189
203
|
// All succeeded, transform results
|
package/dist/pipeline.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
|
|
19
19
|
* Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
|
|
20
20
|
*/
|
|
21
|
-
import { wrapPgError } from './errors.js';
|
|
21
|
+
import { PipelineError, wrapPgError } from './errors.js';
|
|
22
22
|
import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.js';
|
|
23
23
|
/**
|
|
24
24
|
* Execute queries sequentially on an already-acquired connection.
|
|
@@ -30,10 +30,21 @@ import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.j
|
|
|
30
30
|
*/
|
|
31
31
|
async function runSequential(client, queries, options = {}) {
|
|
32
32
|
const { transactional = true } = options;
|
|
33
|
+
if (!transactional)
|
|
34
|
+
return runIndependent(client, queries);
|
|
33
35
|
try {
|
|
34
|
-
|
|
36
|
+
// Transaction control is wrapped too. It used to be bare, so a connection
|
|
37
|
+
// that died between checkout and BEGIN (or at COMMIT) surfaced the raw
|
|
38
|
+
// driver error while every statement between them surfaced a typed
|
|
39
|
+
// TurbineError, and a caller branching on `err.code` saw neither
|
|
40
|
+
// `TURBINE_E004` nor a retryable flag for the one failure that is most
|
|
41
|
+
// worth retrying.
|
|
42
|
+
try {
|
|
35
43
|
await client.query('BEGIN');
|
|
36
44
|
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw wrapPgError(err);
|
|
47
|
+
}
|
|
37
48
|
const results = [];
|
|
38
49
|
for (const q of queries) {
|
|
39
50
|
let raw;
|
|
@@ -45,23 +56,78 @@ async function runSequential(client, queries, options = {}) {
|
|
|
45
56
|
}
|
|
46
57
|
results.push(q.transform(raw));
|
|
47
58
|
}
|
|
48
|
-
|
|
59
|
+
try {
|
|
49
60
|
await client.query('COMMIT');
|
|
50
61
|
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
throw wrapPgError(err);
|
|
64
|
+
}
|
|
51
65
|
return results;
|
|
52
66
|
}
|
|
53
67
|
catch (err) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Best-effort rollback
|
|
60
|
-
}
|
|
68
|
+
try {
|
|
69
|
+
await client.query('ROLLBACK');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort rollback
|
|
61
73
|
}
|
|
62
74
|
throw err;
|
|
63
75
|
}
|
|
64
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Sequential fallback for `{ transactional: false }`: each query is
|
|
79
|
+
* INDEPENDENT, which is what that option promises and what the real pipeline
|
|
80
|
+
* path already did. A failing query records its slot and the batch keeps going
|
|
81
|
+
* (there is no transaction to poison), and if anything failed the whole batch
|
|
82
|
+
* rejects with a `PipelineError` carrying every slot.
|
|
83
|
+
*
|
|
84
|
+
* It used to `throw wrapPgError(err)` on the first failure instead. That both
|
|
85
|
+
* abandoned the remaining queries, contradicting the option's own docstring,
|
|
86
|
+
* and rejected with the raw driver error, so `err instanceof PipelineError` and
|
|
87
|
+
* `err.code === 'TURBINE_E014'` were permanently false on the one path that
|
|
88
|
+
* documents them, and `err.results` did not exist at all. The first failure is
|
|
89
|
+
* still reachable, as `.cause` and as the first `error` slot.
|
|
90
|
+
*/
|
|
91
|
+
async function runIndependent(client, queries) {
|
|
92
|
+
const slots = [];
|
|
93
|
+
let firstError;
|
|
94
|
+
let failedIndex;
|
|
95
|
+
let failedTag;
|
|
96
|
+
const fail = (index, error) => {
|
|
97
|
+
slots.push({ status: 'error', error });
|
|
98
|
+
if (firstError !== undefined)
|
|
99
|
+
return;
|
|
100
|
+
firstError = error;
|
|
101
|
+
failedIndex = index;
|
|
102
|
+
failedTag = queries[index]?.tag;
|
|
103
|
+
};
|
|
104
|
+
for (let i = 0; i < queries.length; i++) {
|
|
105
|
+
const q = queries[i];
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = await client.query(q.sql, q.params);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const wrapped = wrapPgError(err);
|
|
112
|
+
fail(i, wrapped instanceof Error ? wrapped : new Error(String(wrapped)));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// A transform throw is a slot failure too, not a batch abort: that is what
|
|
116
|
+
// the real pipeline path's `finalize` does with the same situation. It is
|
|
117
|
+
// deliberately NOT run through wrapPgError, since it never came from the
|
|
118
|
+
// driver.
|
|
119
|
+
try {
|
|
120
|
+
slots.push({ status: 'ok', value: q.transform(raw) });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
fail(i, err instanceof Error ? err : new Error(String(err)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (firstError === undefined) {
|
|
127
|
+
return slots.map((slot) => slot.value);
|
|
128
|
+
}
|
|
129
|
+
throw new PipelineError({ results: slots, failedIndex, failedTag, cause: firstError });
|
|
130
|
+
}
|
|
65
131
|
// ---------------------------------------------------------------------------
|
|
66
132
|
// Pipeline executor (public)
|
|
67
133
|
// ---------------------------------------------------------------------------
|
package/dist/powdb.d.ts
CHANGED
|
@@ -478,6 +478,29 @@ export declare const POWQL_KEYWORDS: ReadonlySet<string>;
|
|
|
478
478
|
* errors when emitted bare, so quoting is strictly an improvement.
|
|
479
479
|
*/
|
|
480
480
|
export declare function quotePowqlIdent(name: string): string;
|
|
481
|
+
/**
|
|
482
|
+
* The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
|
|
483
|
+
* falls outside the bare-identifier grammar, and only that.
|
|
484
|
+
*
|
|
485
|
+
* A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
|
|
486
|
+
* `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
|
|
487
|
+
* version and stays bare here, which is the ≤0.9 compatibility decision
|
|
488
|
+
* {@link quotePowqlIdent} documents and which this must not undo.
|
|
489
|
+
*
|
|
490
|
+
* What it does NOT excuse is interpolating the name RAW, which is what these
|
|
491
|
+
* sites used to do. Keyword-ness is a parsing question; a name outside
|
|
492
|
+
* `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
|
|
493
|
+
* thing that can carry PowQL syntax into a statement whose values are all bound
|
|
494
|
+
* as `$N` params. Reaching it needs a hostile column name (an introspected
|
|
495
|
+
* database, a generator, a migration authored elsewhere) since names come from
|
|
496
|
+
* schema metadata, but "the names are trusted" is not the invariant the rest of
|
|
497
|
+
* this engine is written to. So: bare when the grammar allows it (byte-identical
|
|
498
|
+
* output for every ordinary and every keyword name), quoted when it does not,
|
|
499
|
+
* where the bare form was a parse error anyway. Verified against the engine that
|
|
500
|
+
* a quoted dotted reference parses everywhere the bare one does and yields the
|
|
501
|
+
* same result-column name.
|
|
502
|
+
*/
|
|
503
|
+
export declare function quotePowqlDotted(name: string): string;
|
|
481
504
|
/**
|
|
482
505
|
* Options for {@link powqlSchemaDDL}. Additive: with no options the DDL is
|
|
483
506
|
* emitted unconditionally (pure-function callers / tests); pass `capabilities`
|
package/dist/powdb.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
56
56
|
import { TurbineClient, } from './client.js';
|
|
57
57
|
import { postgresDialect } from './dialect.js';
|
|
58
|
-
import { ConnectionError, NotNullViolationError, ReadOnlyError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
58
|
+
import { ConnectionError, malformedConnectionStringMessage, NotNullViolationError, ReadOnlyError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
59
59
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
60
60
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
61
61
|
import { normalizeKeyColumns } from './schema.js';
|
|
@@ -162,7 +162,8 @@ export function parsePowdbUrl(connectionString) {
|
|
|
162
162
|
u = new URL(connectionString);
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
165
|
-
|
|
165
|
+
// Never echo the value, see malformedConnectionStringMessage.
|
|
166
|
+
throw new ConnectionError(malformedConnectionStringMessage('PowDB', 'powdb://user:password@127.0.0.1:5433/app'));
|
|
166
167
|
}
|
|
167
168
|
if (u.protocol !== 'powdb:') {
|
|
168
169
|
throw new ConnectionError(`[turbine] PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
|
|
@@ -585,6 +586,36 @@ export function quotePowqlIdent(name) {
|
|
|
585
586
|
}
|
|
586
587
|
return POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
587
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
|
|
591
|
+
* falls outside the bare-identifier grammar, and only that.
|
|
592
|
+
*
|
|
593
|
+
* A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
|
|
594
|
+
* `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
|
|
595
|
+
* version and stays bare here, which is the ≤0.9 compatibility decision
|
|
596
|
+
* {@link quotePowqlIdent} documents and which this must not undo.
|
|
597
|
+
*
|
|
598
|
+
* What it does NOT excuse is interpolating the name RAW, which is what these
|
|
599
|
+
* sites used to do. Keyword-ness is a parsing question; a name outside
|
|
600
|
+
* `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
|
|
601
|
+
* thing that can carry PowQL syntax into a statement whose values are all bound
|
|
602
|
+
* as `$N` params. Reaching it needs a hostile column name (an introspected
|
|
603
|
+
* database, a generator, a migration authored elsewhere) since names come from
|
|
604
|
+
* schema metadata, but "the names are trusted" is not the invariant the rest of
|
|
605
|
+
* this engine is written to. So: bare when the grammar allows it (byte-identical
|
|
606
|
+
* output for every ordinary and every keyword name), quoted when it does not,
|
|
607
|
+
* where the bare form was a parse error anyway. Verified against the engine that
|
|
608
|
+
* a quoted dotted reference parses everywhere the bare one does and yields the
|
|
609
|
+
* same result-column name.
|
|
610
|
+
*/
|
|
611
|
+
export function quotePowqlDotted(name) {
|
|
612
|
+
if (POWQL_BARE_IDENT.test(name))
|
|
613
|
+
return name;
|
|
614
|
+
if (name.includes('`')) {
|
|
615
|
+
throw new ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
|
|
616
|
+
}
|
|
617
|
+
return `\`${name}\``;
|
|
618
|
+
}
|
|
588
619
|
/**
|
|
589
620
|
* Derive the entity links Turbine would declare from a schema's relations. One
|
|
590
621
|
* link per single-column hasMany / hasOne / belongsTo relation, owned by the
|