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.
Files changed (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. 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. Unqualified it is a dotted field
247
- * reference (`.snake_name`), which bypasses keyword lookup. When an `alias`
248
- * is supplied (the F2 join path) it is qualified (`alias.snake_name`) and the
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
- /** Render a raw column name as a PowQL reference, qualified with `alias` when given. */
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
- return `(.${pk} is null and .${pk} is not null)`;
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(`.${targetJCol}`, chunk, params, targetJColMeta);
880
- const { rows } = await this.exec(`${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { .${sourceJCol} }`, params, timeout, 'findMany');
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
- projectedColumns(select, omit, includePii) {
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 with.
951
- for (const key of pk)
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 projectedColumns}), which already omits PII, so PII never
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) => `.${c}`).join(', ')} }`;
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.projectedColumns(args.select, args.omit, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
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 projectedColumns,
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(`.${sourceJCol}`, chunk, params, sourceJColMeta);
1622
- const powql = `${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { .${sourceJCol}, .${targetJCol} }`;
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
- ...options,
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 cols = targetQi.projectedColumns(options.select, options.omit, includePii);
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 cols;
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.projectedColumns(options.select, options.omit, includePii);
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
- const userCols = targetQi.projectedColumns(options.select, options.omit, includePii);
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). Add
2208
- // it if the user's projection dropped it, and remember to strip it back off.
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 = userCols.includes(pkCol);
2213
- const cols = pkProjected ? userCols : [...userCols, pkCol];
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
- /** The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link plan. */
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
- return plan.cols.map((c) => `${plan.keyPrefix}${c}: ${parentAlias}.${plan.linkName}.${c}`);
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 .${pkCol} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
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(`.${col.name}`);
2758
- proj.push(`.${col.name}`);
2759
- byOrderExprs.set(entry, `.${col.name}`);
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 = `.${col.name}`;
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}`);