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/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. Unqualified it is a dotted field
74
- * reference (`.snake_name`), which bypasses keyword lookup. When an `alias`
75
- * is supplied (the F2 join path) it is qualified (`alias.snake_name`) and the
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
- /** Render a raw column name as a PowQL reference, qualified with `alias` when given. */
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
- private projectedColumns;
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 projectedColumns}), which already omits PII, so PII never
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
- /** The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link plan. */
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. Unqualified it is a dotted field
211
- * reference (`.snake_name`), which bypasses keyword lookup. When an `alias`
212
- * is supplied (the F2 join path) it is qualified (`alias.snake_name`) and the
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
- /** Render a raw column name as a PowQL reference, qualified with `alias` when given. */
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
- return `(.${pk} is null and .${pk} is not null)`;
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(`.${targetJCol}`, chunk, params, targetJColMeta);
844
- const { rows } = await this.exec(`${quotePowqlIdent(through.table)} filter ${keyClause} { .${sourceJCol} }`, params, timeout, 'findMany');
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
- projectedColumns(select, omit, includePii) {
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 with.
915
- for (const key of pk)
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 projectedColumns}), which already omits PII, so PII never
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) => `.${c}`).join(', ')} }`;
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.projectedColumns(args.select, args.omit, resolveUnsafeFlag(args.includePii, 'includePii'));
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 projectedColumns,
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(`.${sourceJCol}`, chunk, params, sourceJColMeta);
1586
- const powql = `${quotePowqlIdent(through.table)} filter ${keyClause} { .${sourceJCol}, .${targetJCol} }`;
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
- ...options,
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 cols = targetQi.projectedColumns(options.select, options.omit, includePii);
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 cols;
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.projectedColumns(options.select, options.omit, includePii);
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
- const userCols = targetQi.projectedColumns(options.select, options.omit, includePii);
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). Add
2172
- // it if the user's projection dropped it, and remember to strip it back off.
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 = userCols.includes(pkCol);
2177
- const cols = pkProjected ? userCols : [...userCols, pkCol];
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
- /** The flat `l<i>_<col>: t0.<linkName>.<col>` projection fields for one link plan. */
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
- return plan.cols.map((c) => `${plan.keyPrefix}${c}: ${parentAlias}.${plan.linkName}.${c}`);
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 .${pkCol} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
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(`.${col.name}`);
2722
- proj.push(`.${col.name}`);
2723
- byOrderExprs.set(entry, `.${col.name}`);
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 = `.${col.name}`;
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}`);