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/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
|
package/dist/powql.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import { randomUUID } from 'node:crypto';
|
|
38
38
|
import { NotFoundError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
39
39
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
|
-
import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
|
|
40
|
+
import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
|
|
41
41
|
import { assertAggregatePiiOptIn } from './query/aggregates.js';
|
|
42
42
|
import { expandCompoundUniqueWhere } from './query/compound-unique.js';
|
|
43
43
|
import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
@@ -207,18 +207,32 @@ export class PowqlInterface {
|
|
|
207
207
|
return col;
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
|
-
* PowQL column reference for a field
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* column name is backtick-quoted if it is a reserved word (a qualified
|
|
214
|
-
* `p.order` does NOT bypass keyword lookup, unlike the dotted `.order`).
|
|
210
|
+
* PowQL column reference for a field: a dotted field reference
|
|
211
|
+
* (`.snake_name`), or `alias.snake_name` when an `alias` is supplied (the F2
|
|
212
|
+
* join path).
|
|
215
213
|
*/
|
|
216
214
|
ref(field, alias) {
|
|
217
215
|
return this.colRefName(this.column(field).name, alias);
|
|
218
216
|
}
|
|
219
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Render a raw column name as a PowQL reference, qualified with `alias` when
|
|
219
|
+
* given.
|
|
220
|
+
*
|
|
221
|
+
* BOTH branches quote now; they just use different rules, because the two
|
|
222
|
+
* positions have different grammars. A QUALIFIED `p.col` does not bypass
|
|
223
|
+
* keyword lookup, so it needs the full {@link quotePowqlIdent}. An
|
|
224
|
+
* UNQUALIFIED `.col` does bypass it, so it uses {@link quotePowqlDotted},
|
|
225
|
+
* which quotes only a name outside the bare-identifier grammar and leaves
|
|
226
|
+
* keywords bare, preserving the ≤0.9 compatibility decision documented on
|
|
227
|
+
* `quotePowqlIdent` while still keeping a column NAME from carrying syntax
|
|
228
|
+
* into the statement. This branch used to interpolate the name RAW, the one
|
|
229
|
+
* identifier site in the engine with no boundary at all.
|
|
230
|
+
*
|
|
231
|
+
* Output is byte-identical for every name the grammar accepts bare, keywords
|
|
232
|
+
* included, so no existing schema sees a different statement.
|
|
233
|
+
*/
|
|
220
234
|
colRefName(name, alias) {
|
|
221
|
-
return alias ? `${alias}.${quotePowqlIdent(name)}` : `.${name}`;
|
|
235
|
+
return alias ? `${alias}.${quotePowqlIdent(name)}` : `.${quotePowqlDotted(name)}`;
|
|
222
236
|
}
|
|
223
237
|
/**
|
|
224
238
|
* Push a value into the param array and return its `$N` placeholder. When the
|
|
@@ -325,7 +339,8 @@ export class PowqlInterface {
|
|
|
325
339
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
|
326
340
|
alwaysFalse() {
|
|
327
341
|
const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name;
|
|
328
|
-
|
|
342
|
+
const ref = pk === undefined ? '.__turbine_missing_pk' : this.colRefName(pk);
|
|
343
|
+
return `(${ref} is null and ${ref} is not null)`;
|
|
329
344
|
}
|
|
330
345
|
// -------------------------------------------------------------------------
|
|
331
346
|
// WHERE builder
|
|
@@ -840,8 +855,8 @@ export class PowqlInterface {
|
|
|
840
855
|
for (let i = 0; i < targetPks.length; i += junctionChunk) {
|
|
841
856
|
const chunk = targetPks.slice(i, i + junctionChunk);
|
|
842
857
|
const params = [];
|
|
843
|
-
const keyClause = this.inClause(
|
|
844
|
-
const { rows } = await this.exec(`${quotePowqlIdent(through.table)} filter ${keyClause} {
|
|
858
|
+
const keyClause = this.inClause(this.colRefName(targetJCol), chunk, params, targetJColMeta);
|
|
859
|
+
const { rows } = await this.exec(`${quotePowqlIdent(through.table)} filter ${keyClause} { ${this.colRefName(sourceJCol)} }`, params, timeout, 'findMany');
|
|
845
860
|
for (const r of rows) {
|
|
846
861
|
const v = r[sourceJCol];
|
|
847
862
|
if (v != null)
|
|
@@ -886,7 +901,32 @@ export class PowqlInterface {
|
|
|
886
901
|
}
|
|
887
902
|
return this.column(field).name;
|
|
888
903
|
}
|
|
889
|
-
|
|
904
|
+
/**
|
|
905
|
+
* The projected column list, plus the PK columns that are in it ONLY because
|
|
906
|
+
* this method put them there.
|
|
907
|
+
*
|
|
908
|
+
* PowDB needs the PK in the FETCH even when the caller excluded it: `upsert`
|
|
909
|
+
* reselects by PK, the m2m loader keys its target map on it (`targetByPk`),
|
|
910
|
+
* and the join path correlates through it. That force-add is right and stays.
|
|
911
|
+
* What was missing is the other half, taking it back off the ENTITY, so
|
|
912
|
+
* `select: { name: true }` returned `{ id, name }` and `omit: { id: true }`
|
|
913
|
+
* returned the column the caller asked to hide. The SQL engines return
|
|
914
|
+
* neither, so this was a cross-engine divergence on a documented-as-shared
|
|
915
|
+
* surface, and it was UNIFORM within PowDB: all five paths leaked (top-level
|
|
916
|
+
* find, batched loader, native join, nested projection, link path). The link
|
|
917
|
+
* path was the only one that even tried, and its strip could never fire: it
|
|
918
|
+
* decided "did the caller ask for the PK" by testing the column list AFTER
|
|
919
|
+
* the force-add, which by construction always contains it. So there was one
|
|
920
|
+
* strip in the code, dead since it shipped, and no query anywhere got the
|
|
921
|
+
* projection it asked for.
|
|
922
|
+
*
|
|
923
|
+
* `forcedPk` is what the strip needs, and it is deliberately NARROW: only the
|
|
924
|
+
* `select` / `omit` force-adds are listed. A PII-tagged PK kept through the
|
|
925
|
+
* DEFAULT projection is NOT, because that one is a deliberate, documented
|
|
926
|
+
* decision the SQL engines make identically (a row that cannot address itself
|
|
927
|
+
* is worse than a key that leaks its own value).
|
|
928
|
+
*/
|
|
929
|
+
projectionPlan(select, omit, includePii) {
|
|
890
930
|
// Same two shape refusals as `resolveProjection` on the SQL engines, with
|
|
891
931
|
// the shared messages, and for a live reason here: this path used to
|
|
892
932
|
// APPLY select-minus-omit while the SQL engines ignored the `omit` half,
|
|
@@ -906,14 +946,20 @@ export class PowqlInterface {
|
|
|
906
946
|
}
|
|
907
947
|
const pk = new Set(this.meta.primaryKey);
|
|
908
948
|
let cols = this.meta.columns.map((c) => c.name);
|
|
949
|
+
const forcedPk = [];
|
|
909
950
|
const hasSelect = select && Object.keys(select).length;
|
|
910
951
|
if (hasSelect) {
|
|
911
952
|
const picked = new Set(Object.entries(select)
|
|
912
953
|
.filter(([, v]) => v)
|
|
913
954
|
.map(([k]) => this.projectionColumn(k, 'select')));
|
|
914
|
-
// Always keep the PK so reselect / relation stitching has a key to work
|
|
915
|
-
|
|
955
|
+
// Always keep the PK so reselect / relation stitching has a key to work
|
|
956
|
+
// with; record the ones the caller did NOT name so they can be taken back
|
|
957
|
+
// off the entity once the stitching is done.
|
|
958
|
+
for (const key of pk) {
|
|
959
|
+
if (!picked.has(key))
|
|
960
|
+
forcedPk.push(key);
|
|
916
961
|
picked.add(key);
|
|
962
|
+
}
|
|
917
963
|
cols = cols.filter((c) => picked.has(c));
|
|
918
964
|
}
|
|
919
965
|
else if (!includePii) {
|
|
@@ -938,9 +984,28 @@ export class PowqlInterface {
|
|
|
938
984
|
// target map on the PK (`targetByPk`), so every target collapsed onto the
|
|
939
985
|
// single bucket "undefined", no parent matched, and the relation came
|
|
940
986
|
// back `[]` for every row with no error.
|
|
987
|
+
for (const key of dropped) {
|
|
988
|
+
if (pk.has(key) && !forcedPk.includes(key))
|
|
989
|
+
forcedPk.push(key);
|
|
990
|
+
}
|
|
941
991
|
cols = cols.filter((c) => !dropped.has(c) || pk.has(c));
|
|
942
992
|
}
|
|
943
|
-
return cols;
|
|
993
|
+
return { cols, forcedPk };
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Take the internally-forced PK columns back off the entities, so what the
|
|
997
|
+
* caller receives matches the `select` / `omit` they wrote. Called only after
|
|
998
|
+
* every consumer of the key (relation stitching, reselect) has finished with
|
|
999
|
+
* it. A no-op, and free, when nothing was forced.
|
|
1000
|
+
*/
|
|
1001
|
+
stripForcedPk(entities, forcedPk) {
|
|
1002
|
+
if (forcedPk.length === 0 || entities.length === 0)
|
|
1003
|
+
return;
|
|
1004
|
+
const fields = forcedPk.map((c) => this.meta.reverseColumnMap[c] ?? c);
|
|
1005
|
+
for (const entity of entities) {
|
|
1006
|
+
for (const field of fields)
|
|
1007
|
+
delete entity[field];
|
|
1008
|
+
}
|
|
944
1009
|
}
|
|
945
1010
|
/**
|
|
946
1011
|
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
|
@@ -969,7 +1034,7 @@ export class PowqlInterface {
|
|
|
969
1034
|
* client-side strip of last resort, not defense-in-depth, for those paths; we
|
|
970
1035
|
* do NOT reverse-engineer an undocumented projection form. The upsert path is
|
|
971
1036
|
* different: it has no `returning` and reselects by PK through the read
|
|
972
|
-
* projection ({@link
|
|
1037
|
+
* projection ({@link projectionPlan}), which already omits PII, so PII never
|
|
973
1038
|
* crosses the wire there. If a future spec revision lets `returning` take a
|
|
974
1039
|
* projection, switch the write paths to emit the non-PII list and this strip
|
|
975
1040
|
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
@@ -985,7 +1050,7 @@ export class PowqlInterface {
|
|
|
985
1050
|
}
|
|
986
1051
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
987
1052
|
projection(cols) {
|
|
988
|
-
return `{ ${cols.map((c) =>
|
|
1053
|
+
return `{ ${cols.map((c) => this.colRefName(c)).join(', ')} }`;
|
|
989
1054
|
}
|
|
990
1055
|
/**
|
|
991
1056
|
* `order .c1 asc, .c2 desc` clause (empty string when no orderBy). Supports,
|
|
@@ -1207,7 +1272,7 @@ export class PowqlInterface {
|
|
|
1207
1272
|
this.assertPagination(this.effectiveLimit(args), args.offset, 'findMany');
|
|
1208
1273
|
if (this.effectiveLimit(args) === 0)
|
|
1209
1274
|
return [];
|
|
1210
|
-
const { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith } = await this.runFind(args, 'findMany');
|
|
1275
|
+
const { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind(args, 'findMany');
|
|
1211
1276
|
const entities = this.shape(rows, native);
|
|
1212
1277
|
if (nestedPlans.length)
|
|
1213
1278
|
this.attachNestedRows(entities, nestedPlans);
|
|
@@ -1216,6 +1281,9 @@ export class PowqlInterface {
|
|
|
1216
1281
|
if (residualWith) {
|
|
1217
1282
|
await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, resolveUnsafeFlag(args.includePii, 'includePii'));
|
|
1218
1283
|
}
|
|
1284
|
+
// LAST: every consumer of the internally-forced PK (relation stitching,
|
|
1285
|
+
// the loaders' correlation) has finished with it by here.
|
|
1286
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1219
1287
|
return entities;
|
|
1220
1288
|
});
|
|
1221
1289
|
}
|
|
@@ -1237,7 +1305,7 @@ export class PowqlInterface {
|
|
|
1237
1305
|
throw new UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
|
|
1238
1306
|
}
|
|
1239
1307
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
1240
|
-
const cols = this.
|
|
1308
|
+
const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, resolveUnsafeFlag(args.includePii, 'includePii'));
|
|
1241
1309
|
// Partition the `with` clause: nested-projection blocks vs loader residue.
|
|
1242
1310
|
// A parent `distinct` never nests (distinct over a row containing a JSON
|
|
1243
1311
|
// array is not a defined comparison), and a relation whose field name
|
|
@@ -1306,14 +1374,14 @@ export class PowqlInterface {
|
|
|
1306
1374
|
projection = this.projection(cols);
|
|
1307
1375
|
}
|
|
1308
1376
|
const powql = `${this.qt}${nest ? ' as t0' : ''}${distinct}${filter}${order}${limitClause}${offsetClause} ${projection}`;
|
|
1309
|
-
return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith };
|
|
1377
|
+
return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
|
|
1310
1378
|
}
|
|
1311
1379
|
/** Build + run the findMany select; returns raw rows, the serving wire, the resolved where, and the `with` partition. */
|
|
1312
1380
|
async runFind(args, action = 'findMany') {
|
|
1313
1381
|
const params = [];
|
|
1314
|
-
const { powql, resolvedWhere, nestedPlans, linkPlans, residualWith } = await this.buildFind(args, params);
|
|
1382
|
+
const { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk } = await this.buildFind(args, params);
|
|
1315
1383
|
const { rows, native } = await this.exec(powql, params, args.timeout, action);
|
|
1316
|
-
return { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith };
|
|
1384
|
+
return { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
|
|
1317
1385
|
}
|
|
1318
1386
|
/**
|
|
1319
1387
|
* Diagnostic surface: compile the same PowQL {@link findMany} would run for
|
|
@@ -1349,7 +1417,7 @@ export class PowqlInterface {
|
|
|
1349
1417
|
args = { ...args, where: expanded };
|
|
1350
1418
|
}
|
|
1351
1419
|
return this.withMiddleware('findUnique', args, async () => {
|
|
1352
|
-
const { rows, native, nestedPlans, linkPlans, residualWith } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1420
|
+
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1353
1421
|
if (!rows.length)
|
|
1354
1422
|
return null;
|
|
1355
1423
|
const entities = this.shape(rows, native);
|
|
@@ -1359,13 +1427,15 @@ export class PowqlInterface {
|
|
|
1359
1427
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1360
1428
|
if (residualWith)
|
|
1361
1429
|
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, resolveUnsafeFlag(args.includePii, 'includePii'));
|
|
1430
|
+
// See findMany: the strip is last, after every consumer of the key.
|
|
1431
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1362
1432
|
return entities[0];
|
|
1363
1433
|
});
|
|
1364
1434
|
}
|
|
1365
1435
|
async findFirst(args = {}) {
|
|
1366
1436
|
this.assertNoForceCustomPlan(args);
|
|
1367
1437
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1368
|
-
const { rows, native, nestedPlans, linkPlans, residualWith } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
1438
|
+
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
1369
1439
|
if (!rows.length)
|
|
1370
1440
|
return null;
|
|
1371
1441
|
const entities = this.shape(rows, native);
|
|
@@ -1375,6 +1445,8 @@ export class PowqlInterface {
|
|
|
1375
1445
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1376
1446
|
if (residualWith)
|
|
1377
1447
|
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, resolveUnsafeFlag(args.includePii, 'includePii'));
|
|
1448
|
+
// See findMany: the strip is last, after every consumer of the key.
|
|
1449
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1378
1450
|
return entities[0];
|
|
1379
1451
|
});
|
|
1380
1452
|
}
|
|
@@ -1462,7 +1534,7 @@ export class PowqlInterface {
|
|
|
1462
1534
|
const userSelect = options.select;
|
|
1463
1535
|
const userOmit = options.omit;
|
|
1464
1536
|
// The RAW shape rules, before the force-add below: the forced key makes
|
|
1465
|
-
// an all-falsy select look populated to the child's
|
|
1537
|
+
// an all-falsy select look populated to the child's projectionPlan,
|
|
1466
1538
|
// which would accept here what the nested-projection path refuses. Same
|
|
1467
1539
|
// messages as the SQL engines' assertProjectionShape, same reason.
|
|
1468
1540
|
if (userSelect) {
|
|
@@ -1582,8 +1654,9 @@ export class PowqlInterface {
|
|
|
1582
1654
|
for (let i = 0; i < parentKeys.length; i += junctionChunk) {
|
|
1583
1655
|
const chunk = parentKeys.slice(i, i + junctionChunk);
|
|
1584
1656
|
const params = [];
|
|
1585
|
-
const keyClause = this.inClause(
|
|
1586
|
-
const powql = `${quotePowqlIdent(through.table)} filter ${keyClause}
|
|
1657
|
+
const keyClause = this.inClause(this.colRefName(sourceJCol), chunk, params, sourceJColMeta);
|
|
1658
|
+
const powql = `${quotePowqlIdent(through.table)} filter ${keyClause} ` +
|
|
1659
|
+
`{ ${this.colRefName(sourceJCol)}, ${this.colRefName(targetJCol)} }`;
|
|
1587
1660
|
const { rows } = await this.exec(powql, params, timeout, 'findMany');
|
|
1588
1661
|
for (const row of rows) {
|
|
1589
1662
|
const sv = String(row[sourceJCol]);
|
|
@@ -1599,6 +1672,41 @@ export class PowqlInterface {
|
|
|
1599
1672
|
// (2) Target rows by PK, honouring the relation's own where/with/select/…
|
|
1600
1673
|
const options = (opt === true ? {} : opt);
|
|
1601
1674
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
1675
|
+
// This loader stitches on the TARGET's own primary key, so the PK has to be
|
|
1676
|
+
// in the fetch even when the caller's select/omit excludes it, and has to
|
|
1677
|
+
// come back off afterwards. Exactly the shape `loadRelation` uses for its
|
|
1678
|
+
// correlation column: force it in here, strip it after stitching. It cannot
|
|
1679
|
+
// be left to the projection's internal force-add, because that one is
|
|
1680
|
+
// stripped before findMany returns (see projectionPlan) and the map would
|
|
1681
|
+
// key every target on "undefined".
|
|
1682
|
+
const userSelect = options.select;
|
|
1683
|
+
const userOmit = options.omit;
|
|
1684
|
+
// The RAW shape rules, before the force-add: same reason and same messages
|
|
1685
|
+
// as `loadRelation`, since a forced key makes an all-falsy select look
|
|
1686
|
+
// populated to the child's own projection check.
|
|
1687
|
+
if (userSelect) {
|
|
1688
|
+
if (!Object.values(userSelect).some(Boolean)) {
|
|
1689
|
+
throw new ValidationError(selectNamesNothingMessage(targetMeta.name));
|
|
1690
|
+
}
|
|
1691
|
+
if (userOmit && Object.values(userOmit).some(Boolean)) {
|
|
1692
|
+
throw new ValidationError(selectOmitExclusiveMessage(targetMeta.name));
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const pkProjected = userSelect ? Boolean(userSelect[targetPkField]) : userOmit ? !userOmit[targetPkField] : true;
|
|
1696
|
+
let fetchOptions = options;
|
|
1697
|
+
if (!pkProjected) {
|
|
1698
|
+
if (userSelect) {
|
|
1699
|
+
fetchOptions = {
|
|
1700
|
+
...options,
|
|
1701
|
+
select: { ...userSelect, [targetPkField]: true },
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
else if (userOmit) {
|
|
1705
|
+
const omitWithoutPk = { ...userOmit };
|
|
1706
|
+
delete omitWithoutPk[targetPkField];
|
|
1707
|
+
fetchOptions = { ...options, omit: omitWithoutPk };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1602
1710
|
const targetByPk = new Map();
|
|
1603
1711
|
const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
|
|
1604
1712
|
const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
|
|
@@ -1609,7 +1717,7 @@ export class PowqlInterface {
|
|
|
1609
1717
|
[targetPkField]: { in: chunk },
|
|
1610
1718
|
};
|
|
1611
1719
|
const targets = (await targetQi.findMany({
|
|
1612
|
-
...
|
|
1720
|
+
...fetchOptions,
|
|
1613
1721
|
where,
|
|
1614
1722
|
with: options.with,
|
|
1615
1723
|
timeout: options.timeout ?? timeout,
|
|
@@ -1631,6 +1739,13 @@ export class PowqlInterface {
|
|
|
1631
1739
|
}
|
|
1632
1740
|
parent[relName] = children;
|
|
1633
1741
|
}
|
|
1742
|
+
// Stitching is done: take the forced PK back off. Iterating the map rather
|
|
1743
|
+
// than the stitched lists is deliberate, one target can be linked from many
|
|
1744
|
+
// parents and is the SAME object in each, so this touches each entity once.
|
|
1745
|
+
if (!pkProjected) {
|
|
1746
|
+
for (const target of targetByPk.values())
|
|
1747
|
+
delete target[targetPkField];
|
|
1748
|
+
}
|
|
1634
1749
|
}
|
|
1635
1750
|
// -------------------------------------------------------------------------
|
|
1636
1751
|
// Nested relations: native PowQL joins (F2, opt-in via relationLoadStrategy)
|
|
@@ -1757,7 +1872,7 @@ export class PowqlInterface {
|
|
|
1757
1872
|
const childKeyCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
1758
1873
|
const parentKeyField = this.meta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
|
|
1759
1874
|
const params = [];
|
|
1760
|
-
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1875
|
+
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
1761
1876
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
1762
1877
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
1763
1878
|
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
@@ -1770,7 +1885,7 @@ export class PowqlInterface {
|
|
|
1770
1885
|
// A READ: thread a read-shaped action through the exec seam.
|
|
1771
1886
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
1772
1887
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1773
|
-
const byKey = this.bucketByTpk(targetQi, rows, native);
|
|
1888
|
+
const byKey = this.bucketByTpk(targetQi, rows, native, childForcedPk);
|
|
1774
1889
|
for (const p of parents) {
|
|
1775
1890
|
const key = this.joinKey(p[parentKeyField]);
|
|
1776
1891
|
const matches = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
@@ -1797,7 +1912,7 @@ export class PowqlInterface {
|
|
|
1797
1912
|
const targetPkCol = targetMeta.primaryKey[0];
|
|
1798
1913
|
const parentRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
1799
1914
|
const params = [];
|
|
1800
|
-
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1915
|
+
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
1801
1916
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 't', params, options.timeout ?? timeout);
|
|
1802
1917
|
const proj = this.joinProjection(childCols, `j.${quotePowqlIdent(sourceJCol)}`, 't');
|
|
1803
1918
|
const powql = `${targetQi.qt} as t ` +
|
|
@@ -1805,7 +1920,7 @@ export class PowqlInterface {
|
|
|
1805
1920
|
`join ${this.qt} as p on j.${quotePowqlIdent(sourceJCol)} = p.${quotePowqlIdent(sourceRefCol)}` +
|
|
1806
1921
|
`${filter} ${proj}`;
|
|
1807
1922
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
1808
|
-
const byKey = this.bucketByTpk(targetQi, rows, native);
|
|
1923
|
+
const byKey = this.bucketByTpk(targetQi, rows, native, childForcedPk);
|
|
1809
1924
|
for (const p of parents) {
|
|
1810
1925
|
const key = this.joinKey(p[parentRefField]);
|
|
1811
1926
|
p[relName] = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
@@ -1817,12 +1932,12 @@ export class PowqlInterface {
|
|
|
1817
1932
|
* reserved correlation alias, so refuse rather than silently mis-stitch.
|
|
1818
1933
|
*/
|
|
1819
1934
|
joinChildCols(targetQi, options, includePii = false) {
|
|
1820
|
-
const
|
|
1821
|
-
if (cols.includes('__tpk')) {
|
|
1935
|
+
const plan = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
1936
|
+
if (plan.cols.includes('__tpk')) {
|
|
1822
1937
|
throw new ValidationError(`[turbine] relation target "${targetQi.table}" has a column named "__tpk", which collides with the reserved ` +
|
|
1823
1938
|
`join correlation alias. Rename the column or load this relation with relationLoadStrategy: 'batched'.`);
|
|
1824
1939
|
}
|
|
1825
|
-
return
|
|
1940
|
+
return plan;
|
|
1826
1941
|
}
|
|
1827
1942
|
/**
|
|
1828
1943
|
* `{ __tpk: <tpkExpr>, <col>: <childAlias>.<col>, … }`. Each child column is
|
|
@@ -1855,12 +1970,15 @@ export class PowqlInterface {
|
|
|
1855
1970
|
return parts.length ? ` filter ${parts.join(' and ')}` : '';
|
|
1856
1971
|
}
|
|
1857
1972
|
/** Group join rows by their (normalized) `__tpk`, stripping it and shaping each child. */
|
|
1858
|
-
bucketByTpk(targetQi, rows, native) {
|
|
1973
|
+
bucketByTpk(targetQi, rows, native, forcedPk = []) {
|
|
1859
1974
|
const byKey = new Map();
|
|
1860
1975
|
for (const raw of rows) {
|
|
1861
1976
|
const tpk = this.joinKey(raw.__tpk);
|
|
1862
1977
|
delete raw.__tpk;
|
|
1863
1978
|
const child = targetQi.shape([raw], native)[0];
|
|
1979
|
+
// The correlation runs on `__tpk` (the PARENT's key), so an internally
|
|
1980
|
+
// forced child PK has no consumer past this point and comes straight off.
|
|
1981
|
+
targetQi.stripForcedPk([child], forcedPk);
|
|
1864
1982
|
if (tpk == null)
|
|
1865
1983
|
continue;
|
|
1866
1984
|
const bucket = byKey.get(tpk);
|
|
@@ -1948,7 +2066,7 @@ export class PowqlInterface {
|
|
|
1948
2066
|
if (options.limit === 0)
|
|
1949
2067
|
return null;
|
|
1950
2068
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
1951
|
-
const cols = targetQi.
|
|
2069
|
+
const { cols, forcedPk } = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
1952
2070
|
const byName = new Map(targetQi.meta.columns.map((c) => [c.name, c]));
|
|
1953
2071
|
for (const c of cols) {
|
|
1954
2072
|
const ts = (byName.get(c)?.tsType ?? '').replace(/\s*\|\s*null$/i, '').trim();
|
|
@@ -1977,7 +2095,7 @@ export class PowqlInterface {
|
|
|
1977
2095
|
return null;
|
|
1978
2096
|
keys.add(child.relName);
|
|
1979
2097
|
}
|
|
1980
|
-
return { relName, rel, options, targetQi, single, cols, children };
|
|
2098
|
+
return { relName, rel, options, targetQi, single, cols, forcedPk, children };
|
|
1981
2099
|
}
|
|
1982
2100
|
/**
|
|
1983
2101
|
* Compile one {@link NestedRelationPlan} into its projection-field block:
|
|
@@ -2048,6 +2166,8 @@ export class PowqlInterface {
|
|
|
2048
2166
|
for (const sub of plan.children)
|
|
2049
2167
|
plan.targetQi.attachOneNested(child, sub);
|
|
2050
2168
|
}
|
|
2169
|
+
// After the sub-blocks, for the same reason the top-level strip runs last.
|
|
2170
|
+
plan.targetQi.stripForcedPk(shaped, plan.forcedPk);
|
|
2051
2171
|
row[plan.relName] = plan.single ? (shaped[0] ?? null) : shaped;
|
|
2052
2172
|
}
|
|
2053
2173
|
// -------------------------------------------------------------------------
|
|
@@ -2147,7 +2267,10 @@ export class PowqlInterface {
|
|
|
2147
2267
|
if (options.orderBy || options.limit !== undefined || options.offset)
|
|
2148
2268
|
return null;
|
|
2149
2269
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
2150
|
-
|
|
2270
|
+
// `projectionPlan`, not `projectedColumns`: the plan is what carries
|
|
2271
|
+
// `forcedPk`, and reading the column LIST alone is what made this path's
|
|
2272
|
+
// strip dead. See the pkProjected line below.
|
|
2273
|
+
const { cols: userCols, forcedPk } = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
2151
2274
|
const byName = new Map(targetMeta.columns.map((c) => [c.name, c]));
|
|
2152
2275
|
// Only adopt link paths where a JSON block genuinely cannot serve the case:
|
|
2153
2276
|
// at least one projected child column is bigint/bytes. Otherwise nested
|
|
@@ -2168,13 +2291,20 @@ export class PowqlInterface {
|
|
|
2168
2291
|
if (!userCols.every((c) => this.isBareIdent(c)))
|
|
2169
2292
|
return null;
|
|
2170
2293
|
// Always project the target PK for presence detection (an absent to-one yields
|
|
2171
|
-
// Empty at every hop; PK-Empty is the unambiguous "no linked row" signal).
|
|
2172
|
-
//
|
|
2294
|
+
// Empty at every hop; PK-Empty is the unambiguous "no linked row" signal).
|
|
2295
|
+
// `projectionPlan` has already put it in `userCols`, so there is nothing to
|
|
2296
|
+
// add; what is needed is knowing whether the CALLER asked for it.
|
|
2297
|
+
//
|
|
2298
|
+
// This is the line that was wrong. It read `userCols.includes(pkCol)`, and
|
|
2299
|
+
// the projection force-adds the PK, so the answer was ALWAYS true and the
|
|
2300
|
+
// strip in `attachLinkRows` never ran once. `forcedPk` is the caller's
|
|
2301
|
+
// intent rather than the fetched list: it names the PK columns that are in
|
|
2302
|
+
// `userCols` only because the projection put them there.
|
|
2173
2303
|
const pkCol = targetMeta.primaryKey[0];
|
|
2174
2304
|
if (!this.isBareIdent(pkCol))
|
|
2175
2305
|
return null;
|
|
2176
|
-
const pkProjected =
|
|
2177
|
-
const cols =
|
|
2306
|
+
const pkProjected = !forcedPk.includes(pkCol);
|
|
2307
|
+
const cols = userCols;
|
|
2178
2308
|
// Synthetic flat result keys (`l<index>_<col>`) keep the hop fields from
|
|
2179
2309
|
// colliding with real parent columns or each other. Refuse the (astronomically
|
|
2180
2310
|
// unlikely) case where a real parent/child column already uses the prefix.
|
|
@@ -2183,9 +2313,16 @@ export class PowqlInterface {
|
|
|
2183
2313
|
return null;
|
|
2184
2314
|
return { relName, linkName: link.name, targetQi, cols, pkCol, pkProjected, keyPrefix };
|
|
2185
2315
|
}
|
|
2186
|
-
/**
|
|
2316
|
+
/**
|
|
2317
|
+
* The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link
|
|
2318
|
+
* plan. Both path segments and the synthetic result key go through
|
|
2319
|
+
* `quotePowqlIdent`, the same identifier boundary every other emission site
|
|
2320
|
+
* uses; verified against the engine that a quoted link hop and a quoted alias
|
|
2321
|
+
* parse exactly like their bare forms and yield the same result-column names.
|
|
2322
|
+
*/
|
|
2187
2323
|
linkPathFields(plan, parentAlias) {
|
|
2188
|
-
|
|
2324
|
+
const link = quotePowqlDotted(plan.linkName);
|
|
2325
|
+
return plan.cols.map((c) => `${quotePowqlIdent(`${plan.keyPrefix}${c}`)}: ${parentAlias}.${link}.${quotePowqlDotted(c)}`);
|
|
2189
2326
|
}
|
|
2190
2327
|
/**
|
|
2191
2328
|
* Reconstruct each link-path relation's child entity from its flat hop fields
|
|
@@ -2545,7 +2682,7 @@ export class PowqlInterface {
|
|
|
2545
2682
|
// (verified: "unexpected trailing token … 'returning'"), because it is one
|
|
2546
2683
|
// atomic insert-or-update, not two branches. So upsert alone keeps the
|
|
2547
2684
|
// reselect-by-PK fetch; create/update/delete all use `returning`.
|
|
2548
|
-
await this.exec(`upsert ${this.qt} on
|
|
2685
|
+
await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
|
|
2549
2686
|
const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
|
|
2550
2687
|
const row = await this.reselectByPk(createData[pkField], args.timeout);
|
|
2551
2688
|
if (!row)
|
|
@@ -2718,9 +2855,9 @@ export class PowqlInterface {
|
|
|
2718
2855
|
claim(entry, `column "${col.name}"`);
|
|
2719
2856
|
if (col.name !== entry)
|
|
2720
2857
|
claim(col.name, `column "${col.name}"`);
|
|
2721
|
-
groupExprs.push(
|
|
2722
|
-
proj.push(
|
|
2723
|
-
byOrderExprs.set(entry,
|
|
2858
|
+
groupExprs.push(this.colRefName(col.name));
|
|
2859
|
+
proj.push(this.colRefName(col.name));
|
|
2860
|
+
byOrderExprs.set(entry, this.colRefName(col.name));
|
|
2724
2861
|
byReaders.push({ kind: 'plain', resultKey: entry, rowKey: col.name, col });
|
|
2725
2862
|
}
|
|
2726
2863
|
else {
|
|
@@ -2782,7 +2919,7 @@ export class PowqlInterface {
|
|
|
2782
2919
|
assertAggregatePiiOptIn(this.table, this.meta, key, col.name, `groupBy ${fn}`, resolveUnsafeFlag(args.includePii, 'includePii'));
|
|
2783
2920
|
}
|
|
2784
2921
|
claim(`${fn}_${col.name}`, `${fn} of column "${col.name}"`);
|
|
2785
|
-
const inner =
|
|
2922
|
+
const inner = this.colRefName(col.name);
|
|
2786
2923
|
proj.push(`${alias}: ${powfn}(${inner})`);
|
|
2787
2924
|
aggReaders.push({ alias, outKey: `${fn}:${key}`, numeric: true });
|
|
2788
2925
|
aggOrderExprs.set(`${fn}:${key}`, `.${alias}`);
|