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/cjs/mysql.js
CHANGED
|
@@ -124,10 +124,58 @@ function toNamedBinding(values) {
|
|
|
124
124
|
// ---------------------------------------------------------------------------
|
|
125
125
|
// Result shaping + error translation
|
|
126
126
|
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* mysql2's `MYSQL_TYPE_TINY`. A result field with this type and a display width
|
|
129
|
+
* of 1 IS the `BOOLEAN` / `TINYINT(1)` column; see {@link MYSQL_BOOLEAN_TYPE}.
|
|
130
|
+
*/
|
|
131
|
+
const MYSQL_FIELD_TYPE_TINY = 1;
|
|
132
|
+
/**
|
|
133
|
+
* Turn `TINYINT(1)` cells back into real booleans, in place.
|
|
134
|
+
*
|
|
135
|
+
* MySQL stores a boolean as 1/0 and mysql2 returns the integer. Turbine still
|
|
136
|
+
* generates `ok: boolean` for the column ({@link mysqlTypeToTs}), so
|
|
137
|
+
* `row.ok === true` was false, `JSON.stringify(row)` emitted `1`, and a value
|
|
138
|
+
* the ORM itself wrote as `true` came back as a number.
|
|
139
|
+
*
|
|
140
|
+
* Done from the RESULT FIELDS rather than from mysql2's `typeCast` option on
|
|
141
|
+
* purpose. `typeCast` lives in {@link MYSQL_DRIVER_FLAGS}, which only applies
|
|
142
|
+
* to pools TURBINE builds; an injected mysql2 pool (a supported, documented
|
|
143
|
+
* entry point) would keep returning numbers at the top level while the join
|
|
144
|
+
* strategy's `jsonWireRule` returned booleans, i.e. fixing it there would have
|
|
145
|
+
* created exactly the strategy-dependent split this is meant to close. The
|
|
146
|
+
* field metadata is on every result from every pool.
|
|
147
|
+
*
|
|
148
|
+
* An expression column is never touched: `ok + 0` comes back as
|
|
149
|
+
* MYSQL_TYPE_LONGLONG with a different width, and only an exact
|
|
150
|
+
* (TINY, width 1) field is coerced.
|
|
151
|
+
*/
|
|
152
|
+
function coerceBooleanColumns(rows, fields) {
|
|
153
|
+
if (!Array.isArray(fields) || rows.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
const names = [];
|
|
156
|
+
for (const f of fields) {
|
|
157
|
+
if (!f || f.columnType !== MYSQL_FIELD_TYPE_TINY || f.columnLength !== 1)
|
|
158
|
+
continue;
|
|
159
|
+
if (typeof f.name !== 'string')
|
|
160
|
+
continue;
|
|
161
|
+
names.push(f.name);
|
|
162
|
+
}
|
|
163
|
+
if (names.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
for (const name of names) {
|
|
167
|
+
const v = row[name];
|
|
168
|
+
if (v === 1 || v === 0)
|
|
169
|
+
row[name] = v === 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
127
173
|
/** Shape a mysql2 `[result]` into a `pg.QueryResult`-like object. */
|
|
128
|
-
function shapeResult(result) {
|
|
174
|
+
function shapeResult(result, fields) {
|
|
129
175
|
if (Array.isArray(result)) {
|
|
130
|
-
|
|
176
|
+
const rows = result;
|
|
177
|
+
coerceBooleanColumns(rows, fields);
|
|
178
|
+
return { rows, rowCount: rows.length };
|
|
131
179
|
}
|
|
132
180
|
// Write statement → ResultSetHeader (no rows). Expose insertId/lastID for the
|
|
133
181
|
// `reselect` strategy to re-fetch the auto-increment row.
|
|
@@ -164,6 +212,19 @@ function augmentMysqlError(err) {
|
|
|
164
212
|
const parts = key.split('.');
|
|
165
213
|
target.constraint = parts.length > 1 ? parts[parts.length - 1] : key;
|
|
166
214
|
}
|
|
215
|
+
// THE conflicting row value is in that message, between the quotes after
|
|
216
|
+
// "Duplicate entry". MySQL has no `detail` field, so 'safe' mode had
|
|
217
|
+
// nothing to redact and returned this error verbatim as `.cause`, putting
|
|
218
|
+
// the value into every log line that renders the cause chain. Flag it so
|
|
219
|
+
// the message is withheld in 'safe' mode (see markValueBearingMessage);
|
|
220
|
+
// the constraint NAME survives on `target.constraint` and in the typed
|
|
221
|
+
// UniqueConstraintError's own message, so nothing diagnostic is lost.
|
|
222
|
+
//
|
|
223
|
+
// Deliberately only 1062: the other MySQL codes handled here report
|
|
224
|
+
// column/constraint NAMES only ("Column 'x' cannot be null", "Check
|
|
225
|
+
// constraint 'c' is violated", the FK constraint definition), so flagging
|
|
226
|
+
// them would withhold a message that never carried a value.
|
|
227
|
+
(0, errors_js_1.markValueBearingMessage)(err);
|
|
167
228
|
return err;
|
|
168
229
|
}
|
|
169
230
|
// ER_ROW_IS_REFERENCED_2 / ER_NO_REFERENCED_ROW_2 → foreign key violation
|
|
@@ -212,8 +273,8 @@ function normalizeQueryArgs(arg, values) {
|
|
|
212
273
|
async function execOne(runner, sql, params) {
|
|
213
274
|
const binding = toNamedBinding(params);
|
|
214
275
|
try {
|
|
215
|
-
const [result] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
216
|
-
return shapeResult(result);
|
|
276
|
+
const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
|
|
277
|
+
return shapeResult(result, fields);
|
|
217
278
|
}
|
|
218
279
|
catch (err) {
|
|
219
280
|
throw augmentMysqlError(err);
|
|
@@ -290,7 +351,11 @@ function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
290
351
|
const t = dialectType.toLowerCase();
|
|
291
352
|
const full = (columnType ?? '').toLowerCase();
|
|
292
353
|
let base;
|
|
293
|
-
|
|
354
|
+
// `tinyint(1)` can arrive either way: as (DATA_TYPE, COLUMN_TYPE) from the
|
|
355
|
+
// introspector's first pass, or as a single dialect type, because that is the
|
|
356
|
+
// spelling the introspector RECORDS for a boolean column (see the note there)
|
|
357
|
+
// and the `typeToTypeScript` dialect hook passes only the one string.
|
|
358
|
+
if (t === MYSQL_BOOLEAN_TYPE || (t === 'tinyint' && full === MYSQL_BOOLEAN_TYPE))
|
|
294
359
|
base = 'boolean';
|
|
295
360
|
else if (/^(tinyint|smallint|mediumint|int|integer|bigint)$/.test(t))
|
|
296
361
|
base = 'number';
|
|
@@ -310,6 +375,63 @@ function mysqlTypeToTs(dialectType, nullable, columnType) {
|
|
|
310
375
|
base = 'unknown';
|
|
311
376
|
return nullable ? `${base} | null` : base;
|
|
312
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* The dialect-type spelling Turbine records for a MySQL boolean column.
|
|
380
|
+
*
|
|
381
|
+
* MySQL has no boolean type: `BOOLEAN` is an alias for `TINYINT(1)`, and
|
|
382
|
+
* `information_schema` reports DATA_TYPE `tinyint` for it, which is the same
|
|
383
|
+
* DATA_TYPE a genuine one-byte integer column has. The display width in
|
|
384
|
+
* COLUMN_TYPE is the ONLY thing that separates them, so the introspector keeps
|
|
385
|
+
* the full spelling for this one type rather than the bare DATA_TYPE. Anything
|
|
386
|
+
* reading a dialect type has to accept both spellings; the two places that
|
|
387
|
+
* matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
|
|
388
|
+
*/
|
|
389
|
+
const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
|
|
390
|
+
/**
|
|
391
|
+
* Rewrite every boolean column's recorded dialect type to
|
|
392
|
+
* {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
|
|
393
|
+
* {@link mysqlDialect.jsonWireRule} on.
|
|
394
|
+
*
|
|
395
|
+
* WHY. Only the join strategy consults the recorded type, so a column whose
|
|
396
|
+
* type is spelled some other way reads as a boolean at the top level (the shim
|
|
397
|
+
* decides that from the RESULT FIELDS, which every pool has) and as 1/0 through
|
|
398
|
+
* a `with` join, which is the strategy-dependent split the boolean rule exists
|
|
399
|
+
* to close. Two schemas spell it differently and neither is wrong:
|
|
400
|
+
*
|
|
401
|
+
* - a metadata file generated before Turbine started keeping the display width
|
|
402
|
+
* records the bare DATA_TYPE `tinyint` (with `tsType: 'boolean'`, which is
|
|
403
|
+
* what identifies it), so `turbine generate` is NOT required to get the fix;
|
|
404
|
+
* - a code-first `defineSchema` column records the Postgres spelling
|
|
405
|
+
* `boolean` / `bool`, which the DDL generator already emits as `TINYINT(1)`.
|
|
406
|
+
*
|
|
407
|
+
* A genuine one-byte integer column is never touched: it carries
|
|
408
|
+
* `tsType: 'number'`, and the `tsType` is the gate. Copy-on-write, because the
|
|
409
|
+
* argument is usually a shared exported constant and may also be handed to
|
|
410
|
+
* another engine's factory; an already-correct schema is returned unchanged and
|
|
411
|
+
* pays nothing.
|
|
412
|
+
*/
|
|
413
|
+
function normalizeBooleanDialectTypes(schema) {
|
|
414
|
+
const needsFix = (col) => {
|
|
415
|
+
if (!/^boolean\b/.test(col.tsType))
|
|
416
|
+
return false;
|
|
417
|
+
const recorded = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
418
|
+
return recorded === 'tinyint' || recorded === 'bool' || recorded === 'boolean';
|
|
419
|
+
};
|
|
420
|
+
let tables;
|
|
421
|
+
for (const [name, table] of Object.entries(schema.tables)) {
|
|
422
|
+
if (!table.columns.some(needsFix))
|
|
423
|
+
continue;
|
|
424
|
+
const columns = table.columns.map((col) => needsFix(col) ? { ...col, dialectType: MYSQL_BOOLEAN_TYPE, pgType: MYSQL_BOOLEAN_TYPE } : col);
|
|
425
|
+
if (!tables)
|
|
426
|
+
tables = { ...schema.tables };
|
|
427
|
+
tables[name] = {
|
|
428
|
+
...table,
|
|
429
|
+
columns,
|
|
430
|
+
pgTypes: Object.fromEntries(columns.map((c) => [c.name, c.pgType])),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return tables ? { ...schema, tables } : schema;
|
|
434
|
+
}
|
|
313
435
|
/** Is a MySQL declared type a date/time type (so values coerce back to `Date`)? */
|
|
314
436
|
function isMysqlDateType(dialectType) {
|
|
315
437
|
const t = dialectType.toLowerCase();
|
|
@@ -477,6 +599,52 @@ exports.mysqlDialect = {
|
|
|
477
599
|
if (t === 'decimal' || t === 'numeric') {
|
|
478
600
|
return { sql: (ref) => `CAST(${ref} AS CHAR)`, decode: (value) => value };
|
|
479
601
|
}
|
|
602
|
+
// A boolean is 1/0 on the wire, and the driver shim turns that back into
|
|
603
|
+
// true/false on every direct read (see coerceBooleanColumns). JSON_OBJECT
|
|
604
|
+
// renders the same column as a JSON number and has no column metadata to
|
|
605
|
+
// consult, so without this the join strategy alone would keep returning
|
|
606
|
+
// 1/0: the same strategy-dependent type flip the json rule below exists to
|
|
607
|
+
// prevent. Carried as text because the decode half only runs on a string
|
|
608
|
+
// cell, and a plain CAST is enough here (unlike SQLite, MySQL enforces the
|
|
609
|
+
// column type, so the value is always an integer and `Number` reproduces
|
|
610
|
+
// the driver's own reading of anything that is not 0 or 1).
|
|
611
|
+
if (t === MYSQL_BOOLEAN_TYPE) {
|
|
612
|
+
return {
|
|
613
|
+
sql: (ref) => `CAST(${ref} AS CHAR)`,
|
|
614
|
+
decode: (value) => {
|
|
615
|
+
if (value === '1')
|
|
616
|
+
return true;
|
|
617
|
+
if (value === '0')
|
|
618
|
+
return false;
|
|
619
|
+
if (typeof value !== 'string' || !/^-?\d+$/.test(value))
|
|
620
|
+
return value;
|
|
621
|
+
return Number(value);
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// A `json` column gets NO rule, and that is the fix rather than the absence
|
|
626
|
+
// of one. It used to read differently depending on the relation strategy:
|
|
627
|
+
// the driver handed back text while JSON_OBJECT, which knows the column is
|
|
628
|
+
// JSON, embedded it as a real nested value, so parsing the row yielded an
|
|
629
|
+
// object. Under the default `'auto'` the strategy is decided per query by
|
|
630
|
+
// row counts and index coverage, so adding a `limit` to
|
|
631
|
+
// `findMany({ with: { org: true } })` flipped `org.meta` from a string to
|
|
632
|
+
// an object with no other change.
|
|
633
|
+
//
|
|
634
|
+
// A `CAST(… AS CHAR)` carrier closes that, but only for a pool Turbine
|
|
635
|
+
// built: the text half of it came from a `typeCast` override in
|
|
636
|
+
// MYSQL_DRIVER_FLAGS, and an injected mysql2 pool (a documented entry
|
|
637
|
+
// point) never sees those flags, so it read the column as an object at the
|
|
638
|
+
// top level and as a string through the join. That trades one split for
|
|
639
|
+
// another. So the agreement is built on what BOTH pool shapes already do:
|
|
640
|
+
// mysql2 parses a `json` column by default, JSON_OBJECT embeds it parsed,
|
|
641
|
+
// and with the `typeCast` override gone every read path lands on the same
|
|
642
|
+
// parsed value. It is also what `pg` returns for json/jsonb, so this is
|
|
643
|
+
// Postgres parity rather than a MySQL convention.
|
|
644
|
+
//
|
|
645
|
+
// Contrast SQLite and SQL Server, which return TEXT for the same schema:
|
|
646
|
+
// neither has a JSON column type, so the value there really is a string in
|
|
647
|
+
// a text column and there is nothing to parse it back from.
|
|
480
648
|
// Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
|
|
481
649
|
// `base64:type15:…` marker string, so the caller got that text instead of
|
|
482
650
|
// bytes. Carry hex and rebuild the buffer.
|
|
@@ -711,11 +879,14 @@ async function introspectMysqlWith(exec, schema, options = {}) {
|
|
|
711
879
|
}
|
|
712
880
|
}
|
|
713
881
|
const maxLen = c.CHARACTER_MAXIMUM_LENGTH != null ? num(c.CHARACTER_MAXIMUM_LENGTH) : undefined;
|
|
882
|
+
// `tinyint(1)` is the one type whose DATA_TYPE is not enough to identify it
|
|
883
|
+
// (see MYSQL_BOOLEAN_TYPE); everything else keeps the bare DATA_TYPE.
|
|
884
|
+
const recordedType = c.COLUMN_TYPE.toLowerCase() === MYSQL_BOOLEAN_TYPE ? MYSQL_BOOLEAN_TYPE : dataType;
|
|
714
885
|
const col = {
|
|
715
886
|
name: c.COLUMN_NAME,
|
|
716
887
|
field: (0, schema_js_1.snakeToCamel)(c.COLUMN_NAME),
|
|
717
|
-
dialectType:
|
|
718
|
-
pgType:
|
|
888
|
+
dialectType: recordedType,
|
|
889
|
+
pgType: recordedType,
|
|
719
890
|
tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
|
|
720
891
|
nullable,
|
|
721
892
|
hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
|
|
@@ -881,12 +1052,18 @@ const MYSQL_DRIVER_FLAGS = {
|
|
|
881
1052
|
// DATETIME/TIMESTAMP → JS Date interpreted as UTC (matches Postgres).
|
|
882
1053
|
dateStrings: false,
|
|
883
1054
|
timezone: 'Z',
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
|
|
1055
|
+
//
|
|
1056
|
+
// NOTE: deliberately NO `typeCast` JSON override here. There used to be one,
|
|
1057
|
+
// pinning a `json` column to raw text so the nested-relation parser always
|
|
1058
|
+
// took its JSON.parse path. It could only ever apply to pools TURBINE builds,
|
|
1059
|
+
// and pool injection is a documented entry point, so it did not decide how a
|
|
1060
|
+
// `json` column reads; it decided that the two pool shapes read it
|
|
1061
|
+
// DIFFERENTLY (owned: string, injected: object). Every flag above is
|
|
1062
|
+
// idempotent in that sense (a placeholder style, an integer policy, a time
|
|
1063
|
+
// zone) and none of them changes a value's TYPE. Letting mysql2 parse a
|
|
1064
|
+
// `json` column is what both pool shapes do by default, and it is also what
|
|
1065
|
+
// `pg` does with json/jsonb, so the parsed value is the one answer that is
|
|
1066
|
+
// reachable from every entry point. See `jsonWireRule` for the other half.
|
|
890
1067
|
};
|
|
891
1068
|
/** Parse a `mysql://user:pass@host:port/db` connection string into mysql2 config. */
|
|
892
1069
|
function parseMysqlConfig(connectionString) {
|
|
@@ -906,7 +1083,8 @@ function parseMysqlConfig(connectionString) {
|
|
|
906
1083
|
return config;
|
|
907
1084
|
}
|
|
908
1085
|
catch {
|
|
909
|
-
|
|
1086
|
+
// Never echo the value, see malformedConnectionStringMessage.
|
|
1087
|
+
throw new errors_js_1.ConnectionError((0, errors_js_1.malformedConnectionStringMessage)('MySQL', 'mysql://user:password@localhost:3306/app'));
|
|
910
1088
|
}
|
|
911
1089
|
}
|
|
912
1090
|
/**
|
|
@@ -962,8 +1140,11 @@ function isMysql2Pool(x) {
|
|
|
962
1140
|
* `disconnect()` is a no-op, advanced config like SSL lives here).
|
|
963
1141
|
*
|
|
964
1142
|
* When Turbine builds the pool (string/config), it pins the correct mysql2 flags
|
|
965
|
-
* (named placeholders, bignum, UTC dates
|
|
966
|
-
*
|
|
1143
|
+
* (named placeholders, bignum, UTC dates), probes `SELECT VERSION()` to reject
|
|
1144
|
+
* MySQL < 8.0 / MariaDB, and `disconnect()` closes the pool it created. None of
|
|
1145
|
+
* those flags changes a column's TYPE, so an injected pool reads every value
|
|
1146
|
+
* the same way (see {@link MYSQL_DRIVER_FLAGS}); `namedPlaceholders: true` is
|
|
1147
|
+
* the one an injected pool must set for itself.
|
|
967
1148
|
*
|
|
968
1149
|
* @example
|
|
969
1150
|
* ```ts
|
|
@@ -1018,7 +1199,11 @@ async function turbineMysql(target, schema, options = {}) {
|
|
|
1018
1199
|
pool,
|
|
1019
1200
|
dialect: exports.mysqlDialect,
|
|
1020
1201
|
preparedStatements: false,
|
|
1021
|
-
},
|
|
1202
|
+
},
|
|
1203
|
+
// The join strategy is the only read path that consults a recorded dialect
|
|
1204
|
+
// type, so a schema that spells a boolean any other way would read 1/0
|
|
1205
|
+
// there and `true`/`false` everywhere else.
|
|
1206
|
+
normalizeBooleanDialectTypes(schema));
|
|
1022
1207
|
if (owns) {
|
|
1023
1208
|
// Turbine built this pool, so disconnect()/end() must close it. External
|
|
1024
1209
|
// pools (injection) stay the caller's responsibility (disconnect() no-op),
|
package/dist/cjs/nested-write.js
CHANGED
|
@@ -318,8 +318,13 @@ function belongsToCorrelationWhere(ctx, rel, parentRow, parentTable) {
|
|
|
318
318
|
*/
|
|
319
319
|
function scopeWhereToParent(target, correlation) {
|
|
320
320
|
for (const key of Object.keys(correlation)) {
|
|
321
|
+
// Branded as Turbine's own: this `AND` has a FIXED arity of two, chosen
|
|
322
|
+
// here rather than reachable from a request body, so it must not cost the
|
|
323
|
+
// statement its server-side prepared name the way a caller-written
|
|
324
|
+
// combinator array does. Same reason the global-filter merge and the
|
|
325
|
+
// batched loader's correlation merge are branded.
|
|
321
326
|
if (Object.hasOwn(target, key))
|
|
322
|
-
return { AND: [target, correlation] };
|
|
327
|
+
return (0, utils_js_1.markInternalCombinator)({ AND: [target, correlation] });
|
|
323
328
|
}
|
|
324
329
|
return { ...target, ...correlation };
|
|
325
330
|
}
|
|
@@ -172,7 +172,8 @@ async function runPipelined(client, queries, options = {}) {
|
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
174
|
if (!transactional && pipelineError) {
|
|
175
|
-
// In non-transactional mode,
|
|
175
|
+
// In non-transactional mode, report every slot: what each query returned
|
|
176
|
+
// or the error it failed with.
|
|
176
177
|
const partialResults = [];
|
|
177
178
|
for (let i = 0; i < queries.length; i++) {
|
|
178
179
|
const qErr = queryErrors[i];
|
|
@@ -189,8 +190,21 @@ async function runPipelined(client, queries, options = {}) {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
// A REAL PipelineError, not the first driver error with a `results`
|
|
194
|
+
// property bolted onto it. The old shape happened to satisfy
|
|
195
|
+
// `err.results`, which is what the docs show callers reading, while
|
|
196
|
+
// leaving `err instanceof PipelineError` and `err.code ===
|
|
197
|
+
// 'TURBINE_E014'` permanently false: E014 was defined, documented, and
|
|
198
|
+
// never constructed. The driver error survives as `.cause` and as the
|
|
199
|
+
// first `error` slot, and `failedIndex` / `failedTag` (recorded on it by
|
|
200
|
+
// `onErrorMessage`) move onto the typed error's own fields.
|
|
201
|
+
const first = pipelineError;
|
|
202
|
+
reject(new errors_js_1.PipelineError({
|
|
203
|
+
results: partialResults,
|
|
204
|
+
failedIndex: first.failedIndex,
|
|
205
|
+
failedTag: first.failedTag,
|
|
206
|
+
cause: pipelineError,
|
|
207
|
+
}));
|
|
194
208
|
return;
|
|
195
209
|
}
|
|
196
210
|
// All succeeded, transform results
|
package/dist/cjs/pipeline.js
CHANGED
|
@@ -34,10 +34,21 @@ const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
|
|
|
34
34
|
*/
|
|
35
35
|
async function runSequential(client, queries, options = {}) {
|
|
36
36
|
const { transactional = true } = options;
|
|
37
|
+
if (!transactional)
|
|
38
|
+
return runIndependent(client, queries);
|
|
37
39
|
try {
|
|
38
|
-
|
|
40
|
+
// Transaction control is wrapped too. It used to be bare, so a connection
|
|
41
|
+
// that died between checkout and BEGIN (or at COMMIT) surfaced the raw
|
|
42
|
+
// driver error while every statement between them surfaced a typed
|
|
43
|
+
// TurbineError, and a caller branching on `err.code` saw neither
|
|
44
|
+
// `TURBINE_E004` nor a retryable flag for the one failure that is most
|
|
45
|
+
// worth retrying.
|
|
46
|
+
try {
|
|
39
47
|
await client.query('BEGIN');
|
|
40
48
|
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
51
|
+
}
|
|
41
52
|
const results = [];
|
|
42
53
|
for (const q of queries) {
|
|
43
54
|
let raw;
|
|
@@ -49,23 +60,78 @@ async function runSequential(client, queries, options = {}) {
|
|
|
49
60
|
}
|
|
50
61
|
results.push(q.transform(raw));
|
|
51
62
|
}
|
|
52
|
-
|
|
63
|
+
try {
|
|
53
64
|
await client.query('COMMIT');
|
|
54
65
|
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
68
|
+
}
|
|
55
69
|
return results;
|
|
56
70
|
}
|
|
57
71
|
catch (err) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Best-effort rollback
|
|
64
|
-
}
|
|
72
|
+
try {
|
|
73
|
+
await client.query('ROLLBACK');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best-effort rollback
|
|
65
77
|
}
|
|
66
78
|
throw err;
|
|
67
79
|
}
|
|
68
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Sequential fallback for `{ transactional: false }`: each query is
|
|
83
|
+
* INDEPENDENT, which is what that option promises and what the real pipeline
|
|
84
|
+
* path already did. A failing query records its slot and the batch keeps going
|
|
85
|
+
* (there is no transaction to poison), and if anything failed the whole batch
|
|
86
|
+
* rejects with a `PipelineError` carrying every slot.
|
|
87
|
+
*
|
|
88
|
+
* It used to `throw wrapPgError(err)` on the first failure instead. That both
|
|
89
|
+
* abandoned the remaining queries, contradicting the option's own docstring,
|
|
90
|
+
* and rejected with the raw driver error, so `err instanceof PipelineError` and
|
|
91
|
+
* `err.code === 'TURBINE_E014'` were permanently false on the one path that
|
|
92
|
+
* documents them, and `err.results` did not exist at all. The first failure is
|
|
93
|
+
* still reachable, as `.cause` and as the first `error` slot.
|
|
94
|
+
*/
|
|
95
|
+
async function runIndependent(client, queries) {
|
|
96
|
+
const slots = [];
|
|
97
|
+
let firstError;
|
|
98
|
+
let failedIndex;
|
|
99
|
+
let failedTag;
|
|
100
|
+
const fail = (index, error) => {
|
|
101
|
+
slots.push({ status: 'error', error });
|
|
102
|
+
if (firstError !== undefined)
|
|
103
|
+
return;
|
|
104
|
+
firstError = error;
|
|
105
|
+
failedIndex = index;
|
|
106
|
+
failedTag = queries[index]?.tag;
|
|
107
|
+
};
|
|
108
|
+
for (let i = 0; i < queries.length; i++) {
|
|
109
|
+
const q = queries[i];
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = await client.query(q.sql, q.params);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const wrapped = (0, errors_js_1.wrapPgError)(err);
|
|
116
|
+
fail(i, wrapped instanceof Error ? wrapped : new Error(String(wrapped)));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// A transform throw is a slot failure too, not a batch abort: that is what
|
|
120
|
+
// the real pipeline path's `finalize` does with the same situation. It is
|
|
121
|
+
// deliberately NOT run through wrapPgError, since it never came from the
|
|
122
|
+
// driver.
|
|
123
|
+
try {
|
|
124
|
+
slots.push({ status: 'ok', value: q.transform(raw) });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
fail(i, err instanceof Error ? err : new Error(String(err)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (firstError === undefined) {
|
|
131
|
+
return slots.map((slot) => slot.value);
|
|
132
|
+
}
|
|
133
|
+
throw new errors_js_1.PipelineError({ results: slots, failedIndex, failedTag, cause: firstError });
|
|
134
|
+
}
|
|
69
135
|
// ---------------------------------------------------------------------------
|
|
70
136
|
// Pipeline executor (public)
|
|
71
137
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/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/cjs/powdb.js
CHANGED
|
@@ -99,6 +99,7 @@ exports.isJsonColumn = isJsonColumn;
|
|
|
99
99
|
exports.powqlColumnType = powqlColumnType;
|
|
100
100
|
exports.isPowdbDatetimeColumn = isPowdbDatetimeColumn;
|
|
101
101
|
exports.quotePowqlIdent = quotePowqlIdent;
|
|
102
|
+
exports.quotePowqlDotted = quotePowqlDotted;
|
|
102
103
|
exports.deriveDesiredLinks = deriveDesiredLinks;
|
|
103
104
|
exports.powdbLinkStatement = powdbLinkStatement;
|
|
104
105
|
exports.powqlSchemaDDL = powqlSchemaDDL;
|
|
@@ -223,7 +224,8 @@ function parsePowdbUrl(connectionString) {
|
|
|
223
224
|
u = new URL(connectionString);
|
|
224
225
|
}
|
|
225
226
|
catch {
|
|
226
|
-
|
|
227
|
+
// Never echo the value, see malformedConnectionStringMessage.
|
|
228
|
+
throw new errors_js_1.ConnectionError((0, errors_js_1.malformedConnectionStringMessage)('PowDB', 'powdb://user:password@127.0.0.1:5433/app'));
|
|
227
229
|
}
|
|
228
230
|
if (u.protocol !== 'powdb:') {
|
|
229
231
|
throw new errors_js_1.ConnectionError(`[turbine] PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
|
|
@@ -646,6 +648,36 @@ function quotePowqlIdent(name) {
|
|
|
646
648
|
}
|
|
647
649
|
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
648
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
|
|
653
|
+
* falls outside the bare-identifier grammar, and only that.
|
|
654
|
+
*
|
|
655
|
+
* A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
|
|
656
|
+
* `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
|
|
657
|
+
* version and stays bare here, which is the ≤0.9 compatibility decision
|
|
658
|
+
* {@link quotePowqlIdent} documents and which this must not undo.
|
|
659
|
+
*
|
|
660
|
+
* What it does NOT excuse is interpolating the name RAW, which is what these
|
|
661
|
+
* sites used to do. Keyword-ness is a parsing question; a name outside
|
|
662
|
+
* `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
|
|
663
|
+
* thing that can carry PowQL syntax into a statement whose values are all bound
|
|
664
|
+
* as `$N` params. Reaching it needs a hostile column name (an introspected
|
|
665
|
+
* database, a generator, a migration authored elsewhere) since names come from
|
|
666
|
+
* schema metadata, but "the names are trusted" is not the invariant the rest of
|
|
667
|
+
* this engine is written to. So: bare when the grammar allows it (byte-identical
|
|
668
|
+
* output for every ordinary and every keyword name), quoted when it does not,
|
|
669
|
+
* where the bare form was a parse error anyway. Verified against the engine that
|
|
670
|
+
* a quoted dotted reference parses everywhere the bare one does and yields the
|
|
671
|
+
* same result-column name.
|
|
672
|
+
*/
|
|
673
|
+
function quotePowqlDotted(name) {
|
|
674
|
+
if (POWQL_BARE_IDENT.test(name))
|
|
675
|
+
return name;
|
|
676
|
+
if (name.includes('`')) {
|
|
677
|
+
throw new errors_js_1.ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
|
|
678
|
+
}
|
|
679
|
+
return `\`${name}\``;
|
|
680
|
+
}
|
|
649
681
|
/**
|
|
650
682
|
* Derive the entity links Turbine would declare from a schema's relations. One
|
|
651
683
|
* link per single-column hasMany / hasOne / belongsTo relation, owned by the
|
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -70,14 +70,28 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
70
70
|
/** Resolve a camelCase field name (or raw snake) to its column metadata. */
|
|
71
71
|
private column;
|
|
72
72
|
/**
|
|
73
|
-
* PowQL column reference for a field
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* column name is backtick-quoted if it is a reserved word (a qualified
|
|
77
|
-
* `p.order` does NOT bypass keyword lookup, unlike the dotted `.order`).
|
|
73
|
+
* PowQL column reference for a field: a dotted field reference
|
|
74
|
+
* (`.snake_name`), or `alias.snake_name` when an `alias` is supplied (the F2
|
|
75
|
+
* join path).
|
|
78
76
|
*/
|
|
79
77
|
private ref;
|
|
80
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Render a raw column name as a PowQL reference, qualified with `alias` when
|
|
80
|
+
* given.
|
|
81
|
+
*
|
|
82
|
+
* BOTH branches quote now; they just use different rules, because the two
|
|
83
|
+
* positions have different grammars. A QUALIFIED `p.col` does not bypass
|
|
84
|
+
* keyword lookup, so it needs the full {@link quotePowqlIdent}. An
|
|
85
|
+
* UNQUALIFIED `.col` does bypass it, so it uses {@link quotePowqlDotted},
|
|
86
|
+
* which quotes only a name outside the bare-identifier grammar and leaves
|
|
87
|
+
* keywords bare, preserving the ≤0.9 compatibility decision documented on
|
|
88
|
+
* `quotePowqlIdent` while still keeping a column NAME from carrying syntax
|
|
89
|
+
* into the statement. This branch used to interpolate the name RAW, the one
|
|
90
|
+
* identifier site in the engine with no boundary at all.
|
|
91
|
+
*
|
|
92
|
+
* Output is byte-identical for every name the grammar accepts bare, keywords
|
|
93
|
+
* included, so no existing schema sees a different statement.
|
|
94
|
+
*/
|
|
81
95
|
private colRefName;
|
|
82
96
|
/**
|
|
83
97
|
* Push a value into the param array and return its `$N` placeholder. When the
|
|
@@ -316,7 +330,39 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
316
330
|
* for a misspelling that is not there. Same message as the SQL engines.
|
|
317
331
|
*/
|
|
318
332
|
private projectionColumn;
|
|
319
|
-
|
|
333
|
+
/**
|
|
334
|
+
* The projected column list, plus the PK columns that are in it ONLY because
|
|
335
|
+
* this method put them there.
|
|
336
|
+
*
|
|
337
|
+
* PowDB needs the PK in the FETCH even when the caller excluded it: `upsert`
|
|
338
|
+
* reselects by PK, the m2m loader keys its target map on it (`targetByPk`),
|
|
339
|
+
* and the join path correlates through it. That force-add is right and stays.
|
|
340
|
+
* What was missing is the other half, taking it back off the ENTITY, so
|
|
341
|
+
* `select: { name: true }` returned `{ id, name }` and `omit: { id: true }`
|
|
342
|
+
* returned the column the caller asked to hide. The SQL engines return
|
|
343
|
+
* neither, so this was a cross-engine divergence on a documented-as-shared
|
|
344
|
+
* surface, and it was UNIFORM within PowDB: all five paths leaked (top-level
|
|
345
|
+
* find, batched loader, native join, nested projection, link path). The link
|
|
346
|
+
* path was the only one that even tried, and its strip could never fire: it
|
|
347
|
+
* decided "did the caller ask for the PK" by testing the column list AFTER
|
|
348
|
+
* the force-add, which by construction always contains it. So there was one
|
|
349
|
+
* strip in the code, dead since it shipped, and no query anywhere got the
|
|
350
|
+
* projection it asked for.
|
|
351
|
+
*
|
|
352
|
+
* `forcedPk` is what the strip needs, and it is deliberately NARROW: only the
|
|
353
|
+
* `select` / `omit` force-adds are listed. A PII-tagged PK kept through the
|
|
354
|
+
* DEFAULT projection is NOT, because that one is a deliberate, documented
|
|
355
|
+
* decision the SQL engines make identically (a row that cannot address itself
|
|
356
|
+
* is worse than a key that leaks its own value).
|
|
357
|
+
*/
|
|
358
|
+
private projectionPlan;
|
|
359
|
+
/**
|
|
360
|
+
* Take the internally-forced PK columns back off the entities, so what the
|
|
361
|
+
* caller receives matches the `select` / `omit` they wrote. Called only after
|
|
362
|
+
* every consumer of the key (relation stitching, reselect) has finished with
|
|
363
|
+
* it. A no-op, and free, when nothing was forced.
|
|
364
|
+
*/
|
|
365
|
+
private stripForcedPk;
|
|
320
366
|
/**
|
|
321
367
|
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
|
322
368
|
* with no `pii: true` column, so untagged tables keep their prior projection.
|
|
@@ -337,7 +383,7 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
337
383
|
* client-side strip of last resort, not defense-in-depth, for those paths; we
|
|
338
384
|
* do NOT reverse-engineer an undocumented projection form. The upsert path is
|
|
339
385
|
* different: it has no `returning` and reselects by PK through the read
|
|
340
|
-
* projection ({@link
|
|
386
|
+
* projection ({@link projectionPlan}), which already omits PII, so PII never
|
|
341
387
|
* crosses the wire there. If a future spec revision lets `returning` take a
|
|
342
388
|
* projection, switch the write paths to emit the non-PII list and this strip
|
|
343
389
|
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
@@ -641,7 +687,13 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
641
687
|
* DECLARED link that verifiably matches (`findMatchingLink`).
|
|
642
688
|
*/
|
|
643
689
|
private planLinkPathRelation;
|
|
644
|
-
/**
|
|
690
|
+
/**
|
|
691
|
+
* The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link
|
|
692
|
+
* plan. Both path segments and the synthetic result key go through
|
|
693
|
+
* `quotePowqlIdent`, the same identifier boundary every other emission site
|
|
694
|
+
* uses; verified against the engine that a quoted link hop and a quoted alias
|
|
695
|
+
* parse exactly like their bare forms and yield the same result-column names.
|
|
696
|
+
*/
|
|
645
697
|
private linkPathFields;
|
|
646
698
|
/**
|
|
647
699
|
* Reconstruct each link-path relation's child entity from its flat hop fields
|