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/powql.js
CHANGED
|
@@ -243,18 +243,32 @@ class PowqlInterface {
|
|
|
243
243
|
return col;
|
|
244
244
|
}
|
|
245
245
|
/**
|
|
246
|
-
* PowQL column reference for a field
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* column name is backtick-quoted if it is a reserved word (a qualified
|
|
250
|
-
* `p.order` does NOT bypass keyword lookup, unlike the dotted `.order`).
|
|
246
|
+
* PowQL column reference for a field: a dotted field reference
|
|
247
|
+
* (`.snake_name`), or `alias.snake_name` when an `alias` is supplied (the F2
|
|
248
|
+
* join path).
|
|
251
249
|
*/
|
|
252
250
|
ref(field, alias) {
|
|
253
251
|
return this.colRefName(this.column(field).name, alias);
|
|
254
252
|
}
|
|
255
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Render a raw column name as a PowQL reference, qualified with `alias` when
|
|
255
|
+
* given.
|
|
256
|
+
*
|
|
257
|
+
* BOTH branches quote now; they just use different rules, because the two
|
|
258
|
+
* positions have different grammars. A QUALIFIED `p.col` does not bypass
|
|
259
|
+
* keyword lookup, so it needs the full {@link quotePowqlIdent}. An
|
|
260
|
+
* UNQUALIFIED `.col` does bypass it, so it uses {@link quotePowqlDotted},
|
|
261
|
+
* which quotes only a name outside the bare-identifier grammar and leaves
|
|
262
|
+
* keywords bare, preserving the ≤0.9 compatibility decision documented on
|
|
263
|
+
* `quotePowqlIdent` while still keeping a column NAME from carrying syntax
|
|
264
|
+
* into the statement. This branch used to interpolate the name RAW, the one
|
|
265
|
+
* identifier site in the engine with no boundary at all.
|
|
266
|
+
*
|
|
267
|
+
* Output is byte-identical for every name the grammar accepts bare, keywords
|
|
268
|
+
* included, so no existing schema sees a different statement.
|
|
269
|
+
*/
|
|
256
270
|
colRefName(name, alias) {
|
|
257
|
-
return alias ? `${alias}.${(0, powdb_js_1.quotePowqlIdent)(name)}` : `.${name}`;
|
|
271
|
+
return alias ? `${alias}.${(0, powdb_js_1.quotePowqlIdent)(name)}` : `.${(0, powdb_js_1.quotePowqlDotted)(name)}`;
|
|
258
272
|
}
|
|
259
273
|
/**
|
|
260
274
|
* Push a value into the param array and return its `$N` placeholder. When the
|
|
@@ -361,7 +375,8 @@ class PowqlInterface {
|
|
|
361
375
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
|
362
376
|
alwaysFalse() {
|
|
363
377
|
const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name;
|
|
364
|
-
|
|
378
|
+
const ref = pk === undefined ? '.__turbine_missing_pk' : this.colRefName(pk);
|
|
379
|
+
return `(${ref} is null and ${ref} is not null)`;
|
|
365
380
|
}
|
|
366
381
|
// -------------------------------------------------------------------------
|
|
367
382
|
// WHERE builder
|
|
@@ -876,8 +891,8 @@ class PowqlInterface {
|
|
|
876
891
|
for (let i = 0; i < targetPks.length; i += junctionChunk) {
|
|
877
892
|
const chunk = targetPks.slice(i, i + junctionChunk);
|
|
878
893
|
const params = [];
|
|
879
|
-
const keyClause = this.inClause(
|
|
880
|
-
const { rows } = await this.exec(`${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} {
|
|
894
|
+
const keyClause = this.inClause(this.colRefName(targetJCol), chunk, params, targetJColMeta);
|
|
895
|
+
const { rows } = await this.exec(`${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { ${this.colRefName(sourceJCol)} }`, params, timeout, 'findMany');
|
|
881
896
|
for (const r of rows) {
|
|
882
897
|
const v = r[sourceJCol];
|
|
883
898
|
if (v != null)
|
|
@@ -922,7 +937,32 @@ class PowqlInterface {
|
|
|
922
937
|
}
|
|
923
938
|
return this.column(field).name;
|
|
924
939
|
}
|
|
925
|
-
|
|
940
|
+
/**
|
|
941
|
+
* The projected column list, plus the PK columns that are in it ONLY because
|
|
942
|
+
* this method put them there.
|
|
943
|
+
*
|
|
944
|
+
* PowDB needs the PK in the FETCH even when the caller excluded it: `upsert`
|
|
945
|
+
* reselects by PK, the m2m loader keys its target map on it (`targetByPk`),
|
|
946
|
+
* and the join path correlates through it. That force-add is right and stays.
|
|
947
|
+
* What was missing is the other half, taking it back off the ENTITY, so
|
|
948
|
+
* `select: { name: true }` returned `{ id, name }` and `omit: { id: true }`
|
|
949
|
+
* returned the column the caller asked to hide. The SQL engines return
|
|
950
|
+
* neither, so this was a cross-engine divergence on a documented-as-shared
|
|
951
|
+
* surface, and it was UNIFORM within PowDB: all five paths leaked (top-level
|
|
952
|
+
* find, batched loader, native join, nested projection, link path). The link
|
|
953
|
+
* path was the only one that even tried, and its strip could never fire: it
|
|
954
|
+
* decided "did the caller ask for the PK" by testing the column list AFTER
|
|
955
|
+
* the force-add, which by construction always contains it. So there was one
|
|
956
|
+
* strip in the code, dead since it shipped, and no query anywhere got the
|
|
957
|
+
* projection it asked for.
|
|
958
|
+
*
|
|
959
|
+
* `forcedPk` is what the strip needs, and it is deliberately NARROW: only the
|
|
960
|
+
* `select` / `omit` force-adds are listed. A PII-tagged PK kept through the
|
|
961
|
+
* DEFAULT projection is NOT, because that one is a deliberate, documented
|
|
962
|
+
* decision the SQL engines make identically (a row that cannot address itself
|
|
963
|
+
* is worse than a key that leaks its own value).
|
|
964
|
+
*/
|
|
965
|
+
projectionPlan(select, omit, includePii) {
|
|
926
966
|
// Same two shape refusals as `resolveProjection` on the SQL engines, with
|
|
927
967
|
// the shared messages, and for a live reason here: this path used to
|
|
928
968
|
// APPLY select-minus-omit while the SQL engines ignored the `omit` half,
|
|
@@ -942,14 +982,20 @@ class PowqlInterface {
|
|
|
942
982
|
}
|
|
943
983
|
const pk = new Set(this.meta.primaryKey);
|
|
944
984
|
let cols = this.meta.columns.map((c) => c.name);
|
|
985
|
+
const forcedPk = [];
|
|
945
986
|
const hasSelect = select && Object.keys(select).length;
|
|
946
987
|
if (hasSelect) {
|
|
947
988
|
const picked = new Set(Object.entries(select)
|
|
948
989
|
.filter(([, v]) => v)
|
|
949
990
|
.map(([k]) => this.projectionColumn(k, 'select')));
|
|
950
|
-
// Always keep the PK so reselect / relation stitching has a key to work
|
|
951
|
-
|
|
991
|
+
// Always keep the PK so reselect / relation stitching has a key to work
|
|
992
|
+
// with; record the ones the caller did NOT name so they can be taken back
|
|
993
|
+
// off the entity once the stitching is done.
|
|
994
|
+
for (const key of pk) {
|
|
995
|
+
if (!picked.has(key))
|
|
996
|
+
forcedPk.push(key);
|
|
952
997
|
picked.add(key);
|
|
998
|
+
}
|
|
953
999
|
cols = cols.filter((c) => picked.has(c));
|
|
954
1000
|
}
|
|
955
1001
|
else if (!includePii) {
|
|
@@ -974,9 +1020,28 @@ class PowqlInterface {
|
|
|
974
1020
|
// target map on the PK (`targetByPk`), so every target collapsed onto the
|
|
975
1021
|
// single bucket "undefined", no parent matched, and the relation came
|
|
976
1022
|
// back `[]` for every row with no error.
|
|
1023
|
+
for (const key of dropped) {
|
|
1024
|
+
if (pk.has(key) && !forcedPk.includes(key))
|
|
1025
|
+
forcedPk.push(key);
|
|
1026
|
+
}
|
|
977
1027
|
cols = cols.filter((c) => !dropped.has(c) || pk.has(c));
|
|
978
1028
|
}
|
|
979
|
-
return cols;
|
|
1029
|
+
return { cols, forcedPk };
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Take the internally-forced PK columns back off the entities, so what the
|
|
1033
|
+
* caller receives matches the `select` / `omit` they wrote. Called only after
|
|
1034
|
+
* every consumer of the key (relation stitching, reselect) has finished with
|
|
1035
|
+
* it. A no-op, and free, when nothing was forced.
|
|
1036
|
+
*/
|
|
1037
|
+
stripForcedPk(entities, forcedPk) {
|
|
1038
|
+
if (forcedPk.length === 0 || entities.length === 0)
|
|
1039
|
+
return;
|
|
1040
|
+
const fields = forcedPk.map((c) => this.meta.reverseColumnMap[c] ?? c);
|
|
1041
|
+
for (const entity of entities) {
|
|
1042
|
+
for (const field of fields)
|
|
1043
|
+
delete entity[field];
|
|
1044
|
+
}
|
|
980
1045
|
}
|
|
981
1046
|
/**
|
|
982
1047
|
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
|
@@ -1005,7 +1070,7 @@ class PowqlInterface {
|
|
|
1005
1070
|
* client-side strip of last resort, not defense-in-depth, for those paths; we
|
|
1006
1071
|
* do NOT reverse-engineer an undocumented projection form. The upsert path is
|
|
1007
1072
|
* different: it has no `returning` and reselects by PK through the read
|
|
1008
|
-
* projection ({@link
|
|
1073
|
+
* projection ({@link projectionPlan}), which already omits PII, so PII never
|
|
1009
1074
|
* crosses the wire there. If a future spec revision lets `returning` take a
|
|
1010
1075
|
* projection, switch the write paths to emit the non-PII list and this strip
|
|
1011
1076
|
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
@@ -1021,7 +1086,7 @@ class PowqlInterface {
|
|
|
1021
1086
|
}
|
|
1022
1087
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
1023
1088
|
projection(cols) {
|
|
1024
|
-
return `{ ${cols.map((c) =>
|
|
1089
|
+
return `{ ${cols.map((c) => this.colRefName(c)).join(', ')} }`;
|
|
1025
1090
|
}
|
|
1026
1091
|
/**
|
|
1027
1092
|
* `order .c1 asc, .c2 desc` clause (empty string when no orderBy). Supports,
|
|
@@ -1243,7 +1308,7 @@ class PowqlInterface {
|
|
|
1243
1308
|
this.assertPagination(this.effectiveLimit(args), args.offset, 'findMany');
|
|
1244
1309
|
if (this.effectiveLimit(args) === 0)
|
|
1245
1310
|
return [];
|
|
1246
|
-
const { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith } = await this.runFind(args, 'findMany');
|
|
1311
|
+
const { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind(args, 'findMany');
|
|
1247
1312
|
const entities = this.shape(rows, native);
|
|
1248
1313
|
if (nestedPlans.length)
|
|
1249
1314
|
this.attachNestedRows(entities, nestedPlans);
|
|
@@ -1252,6 +1317,9 @@ class PowqlInterface {
|
|
|
1252
1317
|
if (residualWith) {
|
|
1253
1318
|
await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1254
1319
|
}
|
|
1320
|
+
// LAST: every consumer of the internally-forced PK (relation stitching,
|
|
1321
|
+
// the loaders' correlation) has finished with it by here.
|
|
1322
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1255
1323
|
return entities;
|
|
1256
1324
|
});
|
|
1257
1325
|
}
|
|
@@ -1273,7 +1341,7 @@ class PowqlInterface {
|
|
|
1273
1341
|
throw new errors_js_1.UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
|
|
1274
1342
|
}
|
|
1275
1343
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
1276
|
-
const cols = this.
|
|
1344
|
+
const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1277
1345
|
// Partition the `with` clause: nested-projection blocks vs loader residue.
|
|
1278
1346
|
// A parent `distinct` never nests (distinct over a row containing a JSON
|
|
1279
1347
|
// array is not a defined comparison), and a relation whose field name
|
|
@@ -1342,14 +1410,14 @@ class PowqlInterface {
|
|
|
1342
1410
|
projection = this.projection(cols);
|
|
1343
1411
|
}
|
|
1344
1412
|
const powql = `${this.qt}${nest ? ' as t0' : ''}${distinct}${filter}${order}${limitClause}${offsetClause} ${projection}`;
|
|
1345
|
-
return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith };
|
|
1413
|
+
return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
|
|
1346
1414
|
}
|
|
1347
1415
|
/** Build + run the findMany select; returns raw rows, the serving wire, the resolved where, and the `with` partition. */
|
|
1348
1416
|
async runFind(args, action = 'findMany') {
|
|
1349
1417
|
const params = [];
|
|
1350
|
-
const { powql, resolvedWhere, nestedPlans, linkPlans, residualWith } = await this.buildFind(args, params);
|
|
1418
|
+
const { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk } = await this.buildFind(args, params);
|
|
1351
1419
|
const { rows, native } = await this.exec(powql, params, args.timeout, action);
|
|
1352
|
-
return { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith };
|
|
1420
|
+
return { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
|
|
1353
1421
|
}
|
|
1354
1422
|
/**
|
|
1355
1423
|
* Diagnostic surface: compile the same PowQL {@link findMany} would run for
|
|
@@ -1385,7 +1453,7 @@ class PowqlInterface {
|
|
|
1385
1453
|
args = { ...args, where: expanded };
|
|
1386
1454
|
}
|
|
1387
1455
|
return this.withMiddleware('findUnique', args, async () => {
|
|
1388
|
-
const { rows, native, nestedPlans, linkPlans, residualWith } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1456
|
+
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1389
1457
|
if (!rows.length)
|
|
1390
1458
|
return null;
|
|
1391
1459
|
const entities = this.shape(rows, native);
|
|
@@ -1395,13 +1463,15 @@ class PowqlInterface {
|
|
|
1395
1463
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1396
1464
|
if (residualWith)
|
|
1397
1465
|
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1466
|
+
// See findMany: the strip is last, after every consumer of the key.
|
|
1467
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1398
1468
|
return entities[0];
|
|
1399
1469
|
});
|
|
1400
1470
|
}
|
|
1401
1471
|
async findFirst(args = {}) {
|
|
1402
1472
|
this.assertNoForceCustomPlan(args);
|
|
1403
1473
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1404
|
-
const { rows, native, nestedPlans, linkPlans, residualWith } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
1474
|
+
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
1405
1475
|
if (!rows.length)
|
|
1406
1476
|
return null;
|
|
1407
1477
|
const entities = this.shape(rows, native);
|
|
@@ -1411,6 +1481,8 @@ class PowqlInterface {
|
|
|
1411
1481
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1412
1482
|
if (residualWith)
|
|
1413
1483
|
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1484
|
+
// See findMany: the strip is last, after every consumer of the key.
|
|
1485
|
+
this.stripForcedPk(entities, forcedPk);
|
|
1414
1486
|
return entities[0];
|
|
1415
1487
|
});
|
|
1416
1488
|
}
|
|
@@ -1498,7 +1570,7 @@ class PowqlInterface {
|
|
|
1498
1570
|
const userSelect = options.select;
|
|
1499
1571
|
const userOmit = options.omit;
|
|
1500
1572
|
// The RAW shape rules, before the force-add below: the forced key makes
|
|
1501
|
-
// an all-falsy select look populated to the child's
|
|
1573
|
+
// an all-falsy select look populated to the child's projectionPlan,
|
|
1502
1574
|
// which would accept here what the nested-projection path refuses. Same
|
|
1503
1575
|
// messages as the SQL engines' assertProjectionShape, same reason.
|
|
1504
1576
|
if (userSelect) {
|
|
@@ -1618,8 +1690,9 @@ class PowqlInterface {
|
|
|
1618
1690
|
for (let i = 0; i < parentKeys.length; i += junctionChunk) {
|
|
1619
1691
|
const chunk = parentKeys.slice(i, i + junctionChunk);
|
|
1620
1692
|
const params = [];
|
|
1621
|
-
const keyClause = this.inClause(
|
|
1622
|
-
const powql = `${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause}
|
|
1693
|
+
const keyClause = this.inClause(this.colRefName(sourceJCol), chunk, params, sourceJColMeta);
|
|
1694
|
+
const powql = `${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} ` +
|
|
1695
|
+
`{ ${this.colRefName(sourceJCol)}, ${this.colRefName(targetJCol)} }`;
|
|
1623
1696
|
const { rows } = await this.exec(powql, params, timeout, 'findMany');
|
|
1624
1697
|
for (const row of rows) {
|
|
1625
1698
|
const sv = String(row[sourceJCol]);
|
|
@@ -1635,6 +1708,41 @@ class PowqlInterface {
|
|
|
1635
1708
|
// (2) Target rows by PK, honouring the relation's own where/with/select/…
|
|
1636
1709
|
const options = (opt === true ? {} : opt);
|
|
1637
1710
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
1711
|
+
// This loader stitches on the TARGET's own primary key, so the PK has to be
|
|
1712
|
+
// in the fetch even when the caller's select/omit excludes it, and has to
|
|
1713
|
+
// come back off afterwards. Exactly the shape `loadRelation` uses for its
|
|
1714
|
+
// correlation column: force it in here, strip it after stitching. It cannot
|
|
1715
|
+
// be left to the projection's internal force-add, because that one is
|
|
1716
|
+
// stripped before findMany returns (see projectionPlan) and the map would
|
|
1717
|
+
// key every target on "undefined".
|
|
1718
|
+
const userSelect = options.select;
|
|
1719
|
+
const userOmit = options.omit;
|
|
1720
|
+
// The RAW shape rules, before the force-add: same reason and same messages
|
|
1721
|
+
// as `loadRelation`, since a forced key makes an all-falsy select look
|
|
1722
|
+
// populated to the child's own projection check.
|
|
1723
|
+
if (userSelect) {
|
|
1724
|
+
if (!Object.values(userSelect).some(Boolean)) {
|
|
1725
|
+
throw new errors_js_1.ValidationError((0, utils_js_1.selectNamesNothingMessage)(targetMeta.name));
|
|
1726
|
+
}
|
|
1727
|
+
if (userOmit && Object.values(userOmit).some(Boolean)) {
|
|
1728
|
+
throw new errors_js_1.ValidationError((0, utils_js_1.selectOmitExclusiveMessage)(targetMeta.name));
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const pkProjected = userSelect ? Boolean(userSelect[targetPkField]) : userOmit ? !userOmit[targetPkField] : true;
|
|
1732
|
+
let fetchOptions = options;
|
|
1733
|
+
if (!pkProjected) {
|
|
1734
|
+
if (userSelect) {
|
|
1735
|
+
fetchOptions = {
|
|
1736
|
+
...options,
|
|
1737
|
+
select: { ...userSelect, [targetPkField]: true },
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
else if (userOmit) {
|
|
1741
|
+
const omitWithoutPk = { ...userOmit };
|
|
1742
|
+
delete omitWithoutPk[targetPkField];
|
|
1743
|
+
fetchOptions = { ...options, omit: omitWithoutPk };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1638
1746
|
const targetByPk = new Map();
|
|
1639
1747
|
const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
|
|
1640
1748
|
const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
|
|
@@ -1645,7 +1753,7 @@ class PowqlInterface {
|
|
|
1645
1753
|
[targetPkField]: { in: chunk },
|
|
1646
1754
|
};
|
|
1647
1755
|
const targets = (await targetQi.findMany({
|
|
1648
|
-
...
|
|
1756
|
+
...fetchOptions,
|
|
1649
1757
|
where,
|
|
1650
1758
|
with: options.with,
|
|
1651
1759
|
timeout: options.timeout ?? timeout,
|
|
@@ -1667,6 +1775,13 @@ class PowqlInterface {
|
|
|
1667
1775
|
}
|
|
1668
1776
|
parent[relName] = children;
|
|
1669
1777
|
}
|
|
1778
|
+
// Stitching is done: take the forced PK back off. Iterating the map rather
|
|
1779
|
+
// than the stitched lists is deliberate, one target can be linked from many
|
|
1780
|
+
// parents and is the SAME object in each, so this touches each entity once.
|
|
1781
|
+
if (!pkProjected) {
|
|
1782
|
+
for (const target of targetByPk.values())
|
|
1783
|
+
delete target[targetPkField];
|
|
1784
|
+
}
|
|
1670
1785
|
}
|
|
1671
1786
|
// -------------------------------------------------------------------------
|
|
1672
1787
|
// Nested relations: native PowQL joins (F2, opt-in via relationLoadStrategy)
|
|
@@ -1793,7 +1908,7 @@ class PowqlInterface {
|
|
|
1793
1908
|
const childKeyCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
1794
1909
|
const parentKeyField = this.meta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
|
|
1795
1910
|
const params = [];
|
|
1796
|
-
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1911
|
+
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
1797
1912
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
1798
1913
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
1799
1914
|
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
@@ -1806,7 +1921,7 @@ class PowqlInterface {
|
|
|
1806
1921
|
// A READ: thread a read-shaped action through the exec seam.
|
|
1807
1922
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
1808
1923
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1809
|
-
const byKey = this.bucketByTpk(targetQi, rows, native);
|
|
1924
|
+
const byKey = this.bucketByTpk(targetQi, rows, native, childForcedPk);
|
|
1810
1925
|
for (const p of parents) {
|
|
1811
1926
|
const key = this.joinKey(p[parentKeyField]);
|
|
1812
1927
|
const matches = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
@@ -1833,7 +1948,7 @@ class PowqlInterface {
|
|
|
1833
1948
|
const targetPkCol = targetMeta.primaryKey[0];
|
|
1834
1949
|
const parentRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
1835
1950
|
const params = [];
|
|
1836
|
-
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1951
|
+
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
1837
1952
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 't', params, options.timeout ?? timeout);
|
|
1838
1953
|
const proj = this.joinProjection(childCols, `j.${(0, powdb_js_1.quotePowqlIdent)(sourceJCol)}`, 't');
|
|
1839
1954
|
const powql = `${targetQi.qt} as t ` +
|
|
@@ -1841,7 +1956,7 @@ class PowqlInterface {
|
|
|
1841
1956
|
`join ${this.qt} as p on j.${(0, powdb_js_1.quotePowqlIdent)(sourceJCol)} = p.${(0, powdb_js_1.quotePowqlIdent)(sourceRefCol)}` +
|
|
1842
1957
|
`${filter} ${proj}`;
|
|
1843
1958
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
1844
|
-
const byKey = this.bucketByTpk(targetQi, rows, native);
|
|
1959
|
+
const byKey = this.bucketByTpk(targetQi, rows, native, childForcedPk);
|
|
1845
1960
|
for (const p of parents) {
|
|
1846
1961
|
const key = this.joinKey(p[parentRefField]);
|
|
1847
1962
|
p[relName] = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
@@ -1853,12 +1968,12 @@ class PowqlInterface {
|
|
|
1853
1968
|
* reserved correlation alias, so refuse rather than silently mis-stitch.
|
|
1854
1969
|
*/
|
|
1855
1970
|
joinChildCols(targetQi, options, includePii = false) {
|
|
1856
|
-
const
|
|
1857
|
-
if (cols.includes('__tpk')) {
|
|
1971
|
+
const plan = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
1972
|
+
if (plan.cols.includes('__tpk')) {
|
|
1858
1973
|
throw new errors_js_1.ValidationError(`[turbine] relation target "${targetQi.table}" has a column named "__tpk", which collides with the reserved ` +
|
|
1859
1974
|
`join correlation alias. Rename the column or load this relation with relationLoadStrategy: 'batched'.`);
|
|
1860
1975
|
}
|
|
1861
|
-
return
|
|
1976
|
+
return plan;
|
|
1862
1977
|
}
|
|
1863
1978
|
/**
|
|
1864
1979
|
* `{ __tpk: <tpkExpr>, <col>: <childAlias>.<col>, … }`. Each child column is
|
|
@@ -1891,12 +2006,15 @@ class PowqlInterface {
|
|
|
1891
2006
|
return parts.length ? ` filter ${parts.join(' and ')}` : '';
|
|
1892
2007
|
}
|
|
1893
2008
|
/** Group join rows by their (normalized) `__tpk`, stripping it and shaping each child. */
|
|
1894
|
-
bucketByTpk(targetQi, rows, native) {
|
|
2009
|
+
bucketByTpk(targetQi, rows, native, forcedPk = []) {
|
|
1895
2010
|
const byKey = new Map();
|
|
1896
2011
|
for (const raw of rows) {
|
|
1897
2012
|
const tpk = this.joinKey(raw.__tpk);
|
|
1898
2013
|
delete raw.__tpk;
|
|
1899
2014
|
const child = targetQi.shape([raw], native)[0];
|
|
2015
|
+
// The correlation runs on `__tpk` (the PARENT's key), so an internally
|
|
2016
|
+
// forced child PK has no consumer past this point and comes straight off.
|
|
2017
|
+
targetQi.stripForcedPk([child], forcedPk);
|
|
1900
2018
|
if (tpk == null)
|
|
1901
2019
|
continue;
|
|
1902
2020
|
const bucket = byKey.get(tpk);
|
|
@@ -1984,7 +2102,7 @@ class PowqlInterface {
|
|
|
1984
2102
|
if (options.limit === 0)
|
|
1985
2103
|
return null;
|
|
1986
2104
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
1987
|
-
const cols = targetQi.
|
|
2105
|
+
const { cols, forcedPk } = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
1988
2106
|
const byName = new Map(targetQi.meta.columns.map((c) => [c.name, c]));
|
|
1989
2107
|
for (const c of cols) {
|
|
1990
2108
|
const ts = (byName.get(c)?.tsType ?? '').replace(/\s*\|\s*null$/i, '').trim();
|
|
@@ -2013,7 +2131,7 @@ class PowqlInterface {
|
|
|
2013
2131
|
return null;
|
|
2014
2132
|
keys.add(child.relName);
|
|
2015
2133
|
}
|
|
2016
|
-
return { relName, rel, options, targetQi, single, cols, children };
|
|
2134
|
+
return { relName, rel, options, targetQi, single, cols, forcedPk, children };
|
|
2017
2135
|
}
|
|
2018
2136
|
/**
|
|
2019
2137
|
* Compile one {@link NestedRelationPlan} into its projection-field block:
|
|
@@ -2084,6 +2202,8 @@ class PowqlInterface {
|
|
|
2084
2202
|
for (const sub of plan.children)
|
|
2085
2203
|
plan.targetQi.attachOneNested(child, sub);
|
|
2086
2204
|
}
|
|
2205
|
+
// After the sub-blocks, for the same reason the top-level strip runs last.
|
|
2206
|
+
plan.targetQi.stripForcedPk(shaped, plan.forcedPk);
|
|
2087
2207
|
row[plan.relName] = plan.single ? (shaped[0] ?? null) : shaped;
|
|
2088
2208
|
}
|
|
2089
2209
|
// -------------------------------------------------------------------------
|
|
@@ -2183,7 +2303,10 @@ class PowqlInterface {
|
|
|
2183
2303
|
if (options.orderBy || options.limit !== undefined || options.offset)
|
|
2184
2304
|
return null;
|
|
2185
2305
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
2186
|
-
|
|
2306
|
+
// `projectionPlan`, not `projectedColumns`: the plan is what carries
|
|
2307
|
+
// `forcedPk`, and reading the column LIST alone is what made this path's
|
|
2308
|
+
// strip dead. See the pkProjected line below.
|
|
2309
|
+
const { cols: userCols, forcedPk } = targetQi.projectionPlan(options.select, options.omit, includePii);
|
|
2187
2310
|
const byName = new Map(targetMeta.columns.map((c) => [c.name, c]));
|
|
2188
2311
|
// Only adopt link paths where a JSON block genuinely cannot serve the case:
|
|
2189
2312
|
// at least one projected child column is bigint/bytes. Otherwise nested
|
|
@@ -2204,13 +2327,20 @@ class PowqlInterface {
|
|
|
2204
2327
|
if (!userCols.every((c) => this.isBareIdent(c)))
|
|
2205
2328
|
return null;
|
|
2206
2329
|
// Always project the target PK for presence detection (an absent to-one yields
|
|
2207
|
-
// Empty at every hop; PK-Empty is the unambiguous "no linked row" signal).
|
|
2208
|
-
//
|
|
2330
|
+
// Empty at every hop; PK-Empty is the unambiguous "no linked row" signal).
|
|
2331
|
+
// `projectionPlan` has already put it in `userCols`, so there is nothing to
|
|
2332
|
+
// add; what is needed is knowing whether the CALLER asked for it.
|
|
2333
|
+
//
|
|
2334
|
+
// This is the line that was wrong. It read `userCols.includes(pkCol)`, and
|
|
2335
|
+
// the projection force-adds the PK, so the answer was ALWAYS true and the
|
|
2336
|
+
// strip in `attachLinkRows` never ran once. `forcedPk` is the caller's
|
|
2337
|
+
// intent rather than the fetched list: it names the PK columns that are in
|
|
2338
|
+
// `userCols` only because the projection put them there.
|
|
2209
2339
|
const pkCol = targetMeta.primaryKey[0];
|
|
2210
2340
|
if (!this.isBareIdent(pkCol))
|
|
2211
2341
|
return null;
|
|
2212
|
-
const pkProjected =
|
|
2213
|
-
const cols =
|
|
2342
|
+
const pkProjected = !forcedPk.includes(pkCol);
|
|
2343
|
+
const cols = userCols;
|
|
2214
2344
|
// Synthetic flat result keys (`l<index>_<col>`) keep the hop fields from
|
|
2215
2345
|
// colliding with real parent columns or each other. Refuse the (astronomically
|
|
2216
2346
|
// unlikely) case where a real parent/child column already uses the prefix.
|
|
@@ -2219,9 +2349,16 @@ class PowqlInterface {
|
|
|
2219
2349
|
return null;
|
|
2220
2350
|
return { relName, linkName: link.name, targetQi, cols, pkCol, pkProjected, keyPrefix };
|
|
2221
2351
|
}
|
|
2222
|
-
/**
|
|
2352
|
+
/**
|
|
2353
|
+
* The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link
|
|
2354
|
+
* plan. Both path segments and the synthetic result key go through
|
|
2355
|
+
* `quotePowqlIdent`, the same identifier boundary every other emission site
|
|
2356
|
+
* uses; verified against the engine that a quoted link hop and a quoted alias
|
|
2357
|
+
* parse exactly like their bare forms and yield the same result-column names.
|
|
2358
|
+
*/
|
|
2223
2359
|
linkPathFields(plan, parentAlias) {
|
|
2224
|
-
|
|
2360
|
+
const link = (0, powdb_js_1.quotePowqlDotted)(plan.linkName);
|
|
2361
|
+
return plan.cols.map((c) => `${(0, powdb_js_1.quotePowqlIdent)(`${plan.keyPrefix}${c}`)}: ${parentAlias}.${link}.${(0, powdb_js_1.quotePowqlDotted)(c)}`);
|
|
2225
2362
|
}
|
|
2226
2363
|
/**
|
|
2227
2364
|
* Reconstruct each link-path relation's child entity from its flat hop fields
|
|
@@ -2581,7 +2718,7 @@ class PowqlInterface {
|
|
|
2581
2718
|
// (verified: "unexpected trailing token … 'returning'"), because it is one
|
|
2582
2719
|
// atomic insert-or-update, not two branches. So upsert alone keeps the
|
|
2583
2720
|
// reselect-by-PK fetch; create/update/delete all use `returning`.
|
|
2584
|
-
await this.exec(`upsert ${this.qt} on
|
|
2721
|
+
await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
|
|
2585
2722
|
const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
|
|
2586
2723
|
const row = await this.reselectByPk(createData[pkField], args.timeout);
|
|
2587
2724
|
if (!row)
|
|
@@ -2754,9 +2891,9 @@ class PowqlInterface {
|
|
|
2754
2891
|
claim(entry, `column "${col.name}"`);
|
|
2755
2892
|
if (col.name !== entry)
|
|
2756
2893
|
claim(col.name, `column "${col.name}"`);
|
|
2757
|
-
groupExprs.push(
|
|
2758
|
-
proj.push(
|
|
2759
|
-
byOrderExprs.set(entry,
|
|
2894
|
+
groupExprs.push(this.colRefName(col.name));
|
|
2895
|
+
proj.push(this.colRefName(col.name));
|
|
2896
|
+
byOrderExprs.set(entry, this.colRefName(col.name));
|
|
2760
2897
|
byReaders.push({ kind: 'plain', resultKey: entry, rowKey: col.name, col });
|
|
2761
2898
|
}
|
|
2762
2899
|
else {
|
|
@@ -2818,7 +2955,7 @@ class PowqlInterface {
|
|
|
2818
2955
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, key, col.name, `groupBy ${fn}`, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
2819
2956
|
}
|
|
2820
2957
|
claim(`${fn}_${col.name}`, `${fn} of column "${col.name}"`);
|
|
2821
|
-
const inner =
|
|
2958
|
+
const inner = this.colRefName(col.name);
|
|
2822
2959
|
proj.push(`${alias}: ${powfn}(${inner})`);
|
|
2823
2960
|
aggReaders.push({ alias, outKey: `${fn}:${key}`, numeric: true });
|
|
2824
2961
|
aggOrderExprs.set(`${fn}:${key}`, `.${alias}`);
|