@prisma-next/sql-relational-core 0.5.0-dev.7 → 0.5.0-dev.71

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 (72) hide show
  1. package/README.md +65 -1
  2. package/dist/{errors-D3xmG4h-.mjs → errors-Cb03_kgU.mjs} +2 -2
  3. package/dist/{errors-D3xmG4h-.mjs.map → errors-Cb03_kgU.mjs.map} +1 -1
  4. package/dist/{errors-ChY_dHam.d.mts → errors-DCg-36g-.d.mts} +2 -2
  5. package/dist/errors-DCg-36g-.d.mts.map +1 -0
  6. package/dist/exports/ast.d.mts +154 -85
  7. package/dist/exports/ast.d.mts.map +1 -1
  8. package/dist/exports/ast.mjs +229 -1289
  9. package/dist/exports/ast.mjs.map +1 -1
  10. package/dist/exports/codec-descriptor-registry.d.mts +17 -0
  11. package/dist/exports/codec-descriptor-registry.d.mts.map +1 -0
  12. package/dist/exports/codec-descriptor-registry.mjs +40 -0
  13. package/dist/exports/codec-descriptor-registry.mjs.map +1 -0
  14. package/dist/exports/errors.d.mts +1 -4
  15. package/dist/exports/errors.mjs +2 -3
  16. package/dist/exports/expression.d.mts +84 -0
  17. package/dist/exports/expression.d.mts.map +1 -0
  18. package/dist/exports/expression.mjs +62 -0
  19. package/dist/exports/expression.mjs.map +1 -0
  20. package/dist/exports/plan.d.mts +3 -2
  21. package/dist/exports/plan.mjs +1 -17
  22. package/dist/exports/query-lane-context.d.mts +2 -3
  23. package/dist/exports/query-lane-context.mjs +1 -1
  24. package/dist/exports/types.d.mts +3 -4
  25. package/dist/exports/types.mjs +1 -1
  26. package/dist/index.d.mts +10 -11
  27. package/dist/index.mjs +6 -5
  28. package/dist/plan-CZ6CFuSX.d.mts +24 -0
  29. package/dist/plan-CZ6CFuSX.d.mts.map +1 -0
  30. package/dist/query-lane-context-DaimN0zf.d.mts +174 -0
  31. package/dist/query-lane-context-DaimN0zf.d.mts.map +1 -0
  32. package/dist/sql-execution-plan-DgcD75jn.d.mts +32 -0
  33. package/dist/sql-execution-plan-DgcD75jn.d.mts.map +1 -0
  34. package/dist/types-BnLrX_Vr.d.mts +23 -0
  35. package/dist/types-BnLrX_Vr.d.mts.map +1 -0
  36. package/dist/{types-k9pir8XY.d.mts → types-Czw4j_wY.d.mts} +18 -25
  37. package/dist/types-Czw4j_wY.d.mts.map +1 -0
  38. package/dist/{types-C3Hg-CVz.d.mts → types-hwPoFZX2.d.mts} +36 -23
  39. package/dist/types-hwPoFZX2.d.mts.map +1 -0
  40. package/dist/types-vA5134SY.mjs +1072 -0
  41. package/dist/types-vA5134SY.mjs.map +1 -0
  42. package/package.json +16 -12
  43. package/src/ast/adapter-types.ts +6 -14
  44. package/src/ast/codec-types.ts +90 -372
  45. package/src/ast/sql-codec-helpers.ts +79 -0
  46. package/src/ast/sql-codecs.ts +285 -125
  47. package/src/ast/types.ts +170 -175
  48. package/src/ast/util.ts +23 -0
  49. package/src/ast/validate-param-refs.ts +39 -0
  50. package/src/codec-descriptor-registry.ts +52 -0
  51. package/src/exports/ast.ts +2 -0
  52. package/src/exports/codec-descriptor-registry.ts +1 -0
  53. package/src/exports/expression.ts +1 -0
  54. package/src/exports/plan.ts +1 -0
  55. package/src/exports/types.ts +1 -0
  56. package/src/expression.ts +134 -0
  57. package/src/index.ts +1 -0
  58. package/src/plan.ts +11 -30
  59. package/src/query-lane-context.ts +35 -55
  60. package/src/runtime-scope.ts +20 -0
  61. package/src/sql-execution-plan.ts +28 -0
  62. package/src/types.ts +9 -22
  63. package/dist/codec-types-DcEITed4.d.mts +0 -144
  64. package/dist/codec-types-DcEITed4.d.mts.map +0 -1
  65. package/dist/errors-ChY_dHam.d.mts.map +0 -1
  66. package/dist/exports/plan.mjs.map +0 -1
  67. package/dist/plan-Cs65hb-E.d.mts +0 -28
  68. package/dist/plan-Cs65hb-E.d.mts.map +0 -1
  69. package/dist/query-lane-context-UlR8vOkd.d.mts +0 -89
  70. package/dist/query-lane-context-UlR8vOkd.d.mts.map +0 -1
  71. package/dist/types-C3Hg-CVz.d.mts.map +0 -1
  72. package/dist/types-k9pir8XY.d.mts.map +0 -1
package/src/ast/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { PlanRefs } from '@prisma-next/contract/types';
2
1
  import type { ParamSpec } from '@prisma-next/operations';
3
2
  import type { SqlLoweringSpec } from '@prisma-next/sql-operations';
4
3
 
@@ -146,54 +145,89 @@ function collectParamRefsWith<TNode extends Expression>(node: TNode): ParamRef[]
146
145
  });
147
146
  }
148
147
 
149
- function sortRefs(
150
- tables: ReadonlySet<string>,
151
- columns: ReadonlyMap<string, { table: string; column: string }>,
152
- ): PlanRefs {
153
- const sortedTables = [...tables].sort((a, b) => a.localeCompare(b));
154
- const sortedColumns = [...columns.values()].sort((a, b) => {
155
- const tableCompare = a.table.localeCompare(b.table);
156
- if (tableCompare !== 0) {
157
- return tableCompare;
158
- }
159
- return a.column.localeCompare(b.column);
160
- });
148
+ function rewriteTableSource(table: TableSource, rewriter: AstRewriter): TableSource {
149
+ return rewriter.tableSource ? rewriter.tableSource(table) : table;
150
+ }
161
151
 
162
- return {
163
- tables: sortedTables,
164
- columns: sortedColumns,
165
- };
166
- }
167
-
168
- function addColumnRefToRefSets(
169
- columnRef: ColumnRef,
170
- tables: Set<string>,
171
- columns: Map<string, { table: string; column: string }>,
172
- ): void {
173
- if (columnRef.table === 'excluded') {
174
- return;
175
- }
176
- tables.add(columnRef.table);
177
- const key = `${columnRef.table}.${columnRef.column}`;
178
- if (!columns.has(key)) {
179
- columns.set(key, {
180
- table: columnRef.table,
181
- column: columnRef.column,
182
- });
152
+ function rewriteProjectionItem(item: ProjectionItem, rewriter: AstRewriter): ProjectionItem {
153
+ const rewrittenExpr =
154
+ item.expr.kind === 'literal'
155
+ ? rewriter.literal
156
+ ? rewriter.literal(item.expr)
157
+ : item.expr
158
+ : item.expr.rewrite(rewriter);
159
+ return new ProjectionItem(item.alias, rewrittenExpr, item.codecId, item.refs);
160
+ }
161
+
162
+ function rewriteInsertValue(value: InsertValue, rewriter: AstRewriter): InsertValue {
163
+ switch (value.kind) {
164
+ case 'param-ref':
165
+ return rewriter.paramRef ? rewriteParamRefForInsert(value, rewriter) : value;
166
+ case 'column-ref':
167
+ return rewriter.columnRef ? rewriteColumnRefForInsert(value, rewriter) : value;
168
+ case 'default-value':
169
+ return value;
170
+ }
171
+ }
172
+
173
+ function rewriteParamRefForInsert(value: ParamRef, rewriter: AstRewriter): InsertValue {
174
+ const rewritten = rewriter.paramRef ? rewriter.paramRef(value) : value;
175
+ return rewritten.kind === 'param-ref' ? rewritten : value;
176
+ }
177
+
178
+ function rewriteColumnRefForInsert(value: ColumnRef, rewriter: AstRewriter): InsertValue {
179
+ const rewritten = rewriter.columnRef ? rewriter.columnRef(value) : value;
180
+ return rewritten.kind === 'column-ref' ? rewritten : value;
181
+ }
182
+
183
+ function rewriteInsertRow(
184
+ row: Readonly<Record<string, InsertValue>>,
185
+ rewriter: AstRewriter,
186
+ ): Record<string, InsertValue> {
187
+ const result: Record<string, InsertValue> = {};
188
+ for (const [key, value] of Object.entries(row)) {
189
+ result[key] = rewriteInsertValue(value, rewriter);
183
190
  }
191
+ return result;
184
192
  }
185
193
 
186
- function mergeRefsInto(
187
- refs: PlanRefs,
188
- tables: Set<string>,
189
- columns: Map<string, { table: string; column: string }>,
190
- ): void {
191
- for (const table of refs.tables ?? []) {
192
- tables.add(table);
194
+ function rewriteUpdateSetValue(
195
+ value: ColumnRef | ParamRef,
196
+ rewriter: AstRewriter,
197
+ ): ColumnRef | ParamRef {
198
+ if (value.kind === 'column-ref') {
199
+ const rewritten = rewriter.columnRef ? rewriter.columnRef(value) : value;
200
+ return rewritten.kind === 'column-ref' ? rewritten : value;
193
201
  }
194
- for (const column of refs.columns ?? []) {
195
- addColumnRefToRefSets(new ColumnRef(column.table, column.column), tables, columns);
202
+ const rewritten = rewriter.paramRef ? rewriter.paramRef(value) : value;
203
+ return rewritten.kind === 'param-ref' ? rewritten : value;
204
+ }
205
+
206
+ function rewriteUpdateSet(
207
+ set: Readonly<Record<string, ColumnRef | ParamRef>>,
208
+ rewriter: AstRewriter,
209
+ ): Record<string, ColumnRef | ParamRef> {
210
+ const result: Record<string, ColumnRef | ParamRef> = {};
211
+ for (const [key, value] of Object.entries(set)) {
212
+ result[key] = rewriteUpdateSetValue(value, rewriter);
213
+ }
214
+ return result;
215
+ }
216
+
217
+ function rewriteOnConflict(onConflict: InsertOnConflict, rewriter: AstRewriter): InsertOnConflict {
218
+ const columns = onConflict.columns.map((columnRef) => {
219
+ const rewritten = rewriter.columnRef ? rewriter.columnRef(columnRef) : columnRef;
220
+ return rewritten.kind === 'column-ref' ? rewritten : columnRef;
221
+ });
222
+
223
+ if (onConflict.action.kind === 'do-nothing') {
224
+ return new InsertOnConflict(columns, new DoNothingConflictAction());
196
225
  }
226
+
227
+ return new InsertOnConflict(
228
+ columns,
229
+ new DoUpdateSetConflictAction(rewriteUpdateSet(onConflict.action.set, rewriter)),
230
+ );
197
231
  }
198
232
 
199
233
  abstract class AstNode {
@@ -205,18 +239,11 @@ abstract class AstNode {
205
239
  }
206
240
 
207
241
  abstract class QueryAst extends AstNode {
208
- abstract collectRefs(): PlanRefs;
209
242
  abstract collectParamRefs(): ParamRef[];
210
243
  abstract toQueryAst(): AnyQueryAst;
211
-
212
- collectColumnRefs(): ColumnRef[] {
213
- const refs = this.collectRefs().columns ?? [];
214
- return refs.map((ref) => new ColumnRef(ref.table, ref.column));
215
- }
216
244
  }
217
245
 
218
246
  abstract class FromSource extends AstNode {
219
- abstract collectRefs(): PlanRefs;
220
247
  abstract rewrite(rewriter: AstRewriter): AnyFromSource;
221
248
  abstract toFromSource(): AnyFromSource;
222
249
  }
@@ -270,13 +297,6 @@ export class TableSource extends FromSource {
270
297
  override toFromSource(): AnyFromSource {
271
298
  return this;
272
299
  }
273
-
274
- override collectRefs(): PlanRefs {
275
- return {
276
- tables: [this.name],
277
- columns: [],
278
- };
279
- }
280
300
  }
281
301
 
282
302
  export interface TableRef {
@@ -300,9 +320,7 @@ export class DerivedTableSource extends FromSource {
300
320
  return new DerivedTableSource(alias, query);
301
321
  }
302
322
 
303
- // Intentionally does not call rewriter.tableSource — derived tables are rewritten
304
- // via their inner query, not intercepted at the FromSource level. A future
305
- // fromSource?(source: AnyFromSource) callback would be needed for that.
323
+ // Intentionally does not call rewriter.tableSource — derived tables are rewritten via their inner query, not intercepted at the FromSource level. A future fromSource?(source: AnyFromSource) callback would be needed for that.
306
324
  override rewrite(rewriter: AstRewriter): AnyFromSource {
307
325
  return new DerivedTableSource(this.alias, this.query.rewrite(rewriter));
308
326
  }
@@ -310,10 +328,6 @@ export class DerivedTableSource extends FromSource {
310
328
  override toFromSource(): AnyFromSource {
311
329
  return this;
312
330
  }
313
-
314
- override collectRefs(): PlanRefs {
315
- return this.query.collectRefs();
316
- }
317
331
  }
318
332
 
319
333
  export class ColumnRef extends Expression {
@@ -376,23 +390,37 @@ export class IdentifierRef extends Expression {
376
390
  }
377
391
  }
378
392
 
393
+ /**
394
+ * Column ref carried by a {@link ParamRef} that was constructed at a column-bound site (e.g. an INSERT/UPDATE column assignment, a `WHERE column = $param` comparison). The encode-side dispatch path uses `refs` to call `contractCodecs.forColumn(refs.table, refs.column)`, which selects the per-instance parameterized codec for the column — required when a parameterized codec id is shared by multiple columns with distinct
395
+ * typeParams (e.g. `vector(1024)` vs. `vector(1536)`).
396
+ *
397
+ * `refs` may legitimately be `undefined` for `ParamRef`s constructed without a column context — the validator pass (`validateParamRefRefs`) treats refs-less ParamRefs as a hard error only when their codec id is parameterized.
398
+ */
399
+ export interface ParamRefBindingRefs {
400
+ readonly table: string;
401
+ readonly column: string;
402
+ }
403
+
379
404
  export class ParamRef extends Expression {
380
405
  readonly kind = 'param-ref' as const;
381
406
  readonly value: unknown;
382
407
  readonly name: string | undefined;
383
408
  readonly codecId: string | undefined;
409
+ readonly refs: ParamRefBindingRefs | undefined;
384
410
 
385
411
  constructor(
386
412
  value: unknown,
387
413
  options?: {
388
414
  name?: string;
389
415
  codecId?: string;
416
+ refs?: ParamRefBindingRefs;
390
417
  },
391
418
  ) {
392
419
  super();
393
420
  this.value = value;
394
421
  this.name = options?.name;
395
422
  this.codecId = options?.codecId;
423
+ this.refs = options?.refs ? Object.freeze({ ...options.refs }) : undefined;
396
424
  this.freeze();
397
425
  }
398
426
 
@@ -401,6 +429,7 @@ export class ParamRef extends Expression {
401
429
  options?: {
402
430
  name?: string;
403
431
  codecId?: string;
432
+ refs?: ParamRefBindingRefs;
404
433
  },
405
434
  ): ParamRef {
406
435
  return new ParamRef(value, options);
@@ -1052,16 +1081,33 @@ export class ProjectionItem extends AstNode {
1052
1081
  readonly kind = 'projection-item' as const;
1053
1082
  readonly alias: string;
1054
1083
  readonly expr: ProjectionExpr;
1084
+ readonly codecId: string | undefined;
1085
+ /**
1086
+ * Column-bound metadata for the projection. Populated by builder paths that promote a top-level field shortcut (`select('email', ...)`) into a bare `IdentifierRef` AST while still knowing the originating `(table, column)`. Decode-side dispatch consults `refs` to call `forColumn(table, column)` so parameterized codec ids resolve to the per-instance codec — required when multiple columns share a codec id with distinct
1087
+ * typeParams (e.g. `varchar(36)` vs. `varchar(255)`). Stays `undefined` for `column-ref` projections (the AST already carries the binding) and for non-column-bound projections (computed expressions, subqueries, raw aliases).
1088
+ */
1089
+ readonly refs: ParamRefBindingRefs | undefined;
1055
1090
 
1056
- constructor(alias: string, expr: ProjectionExpr) {
1091
+ constructor(alias: string, expr: ProjectionExpr, codecId?: string, refs?: ParamRefBindingRefs) {
1057
1092
  super();
1058
1093
  this.alias = alias;
1059
1094
  this.expr = expr;
1095
+ this.codecId = codecId;
1096
+ this.refs = refs ? Object.freeze({ ...refs }) : undefined;
1060
1097
  this.freeze();
1061
1098
  }
1062
1099
 
1063
- static of(alias: string, expr: ProjectionExpr): ProjectionItem {
1064
- return new ProjectionItem(alias, expr);
1100
+ static of(
1101
+ alias: string,
1102
+ expr: ProjectionExpr,
1103
+ codecId?: string,
1104
+ refs?: ParamRefBindingRefs,
1105
+ ): ProjectionItem {
1106
+ return new ProjectionItem(alias, expr, codecId, refs);
1107
+ }
1108
+
1109
+ withCodecId(codecId: string | undefined): ProjectionItem {
1110
+ return new ProjectionItem(this.alias, this.expr, codecId, this.refs);
1065
1111
  }
1066
1112
  }
1067
1113
 
@@ -1218,6 +1264,8 @@ export class SelectAst extends QueryAst {
1218
1264
  ? rewriter.literal(projection.expr)
1219
1265
  : projection.expr
1220
1266
  : projection.expr.rewrite(rewriter),
1267
+ projection.codecId,
1268
+ projection.refs,
1221
1269
  ),
1222
1270
  ),
1223
1271
  where: this.where?.rewrite(rewriter),
@@ -1234,7 +1282,7 @@ export class SelectAst extends QueryAst {
1234
1282
  return rewriter.select ? rewriter.select(rewritten) : rewritten;
1235
1283
  }
1236
1284
 
1237
- override collectColumnRefs(): ColumnRef[] {
1285
+ collectColumnRefs(): ColumnRef[] {
1238
1286
  const refs: ColumnRef[] = [];
1239
1287
  const pushRefs = (columns: ReadonlyArray<ColumnRef>) => {
1240
1288
  refs.push(...columns);
@@ -1322,35 +1370,6 @@ export class SelectAst extends QueryAst {
1322
1370
  return refs;
1323
1371
  }
1324
1372
 
1325
- override collectRefs(): PlanRefs {
1326
- const tables = new Set<string>();
1327
- const columns = new Map<string, { table: string; column: string }>();
1328
-
1329
- const addSource = (source: AnyFromSource) => {
1330
- mergeRefsInto(source.collectRefs(), tables, columns);
1331
- };
1332
-
1333
- addSource(this.from);
1334
-
1335
- for (const join of this.joins ?? []) {
1336
- addSource(join.source);
1337
- if (join.on.kind === 'eq-col-join-on') {
1338
- addColumnRefToRefSets(join.on.left, tables, columns);
1339
- addColumnRefToRefSets(join.on.right, tables, columns);
1340
- } else {
1341
- for (const columnRef of join.on.collectColumnRefs()) {
1342
- addColumnRefToRefSets(columnRef, tables, columns);
1343
- }
1344
- }
1345
- }
1346
-
1347
- for (const columnRef of this.collectColumnRefs()) {
1348
- addColumnRefToRefSets(columnRef, tables, columns);
1349
- }
1350
-
1351
- return sortRefs(tables, columns);
1352
- }
1353
-
1354
1373
  override toQueryAst(): AnyQueryAst {
1355
1374
  return this;
1356
1375
  }
@@ -1418,13 +1437,13 @@ export class InsertAst extends QueryAst {
1418
1437
  readonly table: TableSource;
1419
1438
  readonly rows: ReadonlyArray<Readonly<Record<string, InsertValue>>>;
1420
1439
  readonly onConflict: InsertOnConflict | undefined;
1421
- readonly returning: ReadonlyArray<ColumnRef> | undefined;
1440
+ readonly returning: ReadonlyArray<ProjectionItem> | undefined;
1422
1441
 
1423
1442
  constructor(
1424
1443
  table: TableSource,
1425
1444
  rows: ReadonlyArray<Record<string, InsertValue>> = [{}],
1426
1445
  onConflict?: InsertOnConflict,
1427
- returning?: ReadonlyArray<ColumnRef>,
1446
+ returning?: ReadonlyArray<ProjectionItem>,
1428
1447
  ) {
1429
1448
  super();
1430
1449
  this.table = table;
@@ -1451,7 +1470,7 @@ export class InsertAst extends QueryAst {
1451
1470
  );
1452
1471
  }
1453
1472
 
1454
- withReturning(returning: ReadonlyArray<ColumnRef> | undefined): InsertAst {
1473
+ withReturning(returning: ReadonlyArray<ProjectionItem> | undefined): InsertAst {
1455
1474
  return new InsertAst(
1456
1475
  this.table,
1457
1476
  this.rows.map((row) => ({ ...row })),
@@ -1469,6 +1488,15 @@ export class InsertAst extends QueryAst {
1469
1488
  );
1470
1489
  }
1471
1490
 
1491
+ rewrite(rewriter: AstRewriter): InsertAst {
1492
+ return new InsertAst(
1493
+ rewriteTableSource(this.table, rewriter),
1494
+ this.rows.map((row) => rewriteInsertRow(row, rewriter)),
1495
+ this.onConflict ? rewriteOnConflict(this.onConflict, rewriter) : undefined,
1496
+ this.returning?.map((item) => rewriteProjectionItem(item, rewriter)),
1497
+ );
1498
+ }
1499
+
1472
1500
  override collectParamRefs(): ParamRef[] {
1473
1501
  const refs: ParamRef[] = [];
1474
1502
  for (const row of this.rows) {
@@ -1485,44 +1513,12 @@ export class InsertAst extends QueryAst {
1485
1513
  }
1486
1514
  }
1487
1515
  }
1488
- return refs;
1489
- }
1490
-
1491
- override collectRefs(): PlanRefs {
1492
- const tables = new Set<string>([this.table.name]);
1493
- const columns = new Map<string, { table: string; column: string }>();
1494
-
1495
- const addColumn = (columnRef: ColumnRef) => addColumnRefToRefSets(columnRef, tables, columns);
1496
- const addValue = (value: InsertValue) => {
1497
- if (value.kind === 'column-ref') {
1498
- addColumn(value);
1499
- }
1500
- };
1501
-
1502
- for (const row of this.rows) {
1503
- for (const value of Object.values(row)) {
1504
- addValue(value);
1505
- }
1506
- }
1507
-
1508
- for (const columnRef of this.returning ?? []) {
1509
- addColumn(columnRef);
1510
- }
1511
-
1512
- if (this.onConflict) {
1513
- for (const columnRef of this.onConflict.columns) {
1514
- addColumn(columnRef);
1515
- }
1516
- if (this.onConflict.action.kind === 'do-update-set') {
1517
- for (const value of Object.values(this.onConflict.action.set)) {
1518
- if (value.kind === 'column-ref') {
1519
- addColumn(value);
1520
- }
1521
- }
1516
+ for (const item of this.returning ?? []) {
1517
+ if (item.expr.kind !== 'literal') {
1518
+ refs.push(...item.expr.collectParamRefs());
1522
1519
  }
1523
1520
  }
1524
-
1525
- return sortRefs(tables, columns);
1521
+ return refs;
1526
1522
  }
1527
1523
 
1528
1524
  override toQueryAst(): AnyQueryAst {
@@ -1535,13 +1531,13 @@ export class UpdateAst extends QueryAst {
1535
1531
  readonly table: TableSource;
1536
1532
  readonly set: Readonly<Record<string, ColumnRef | ParamRef>>;
1537
1533
  readonly where: AnyExpression | undefined;
1538
- readonly returning: ReadonlyArray<ColumnRef> | undefined;
1534
+ readonly returning: ReadonlyArray<ProjectionItem> | undefined;
1539
1535
 
1540
1536
  constructor(
1541
1537
  table: TableSource,
1542
1538
  set: Readonly<Record<string, ColumnRef | ParamRef>> = {},
1543
1539
  where?: AnyExpression,
1544
- returning?: ReadonlyArray<ColumnRef>,
1540
+ returning?: ReadonlyArray<ProjectionItem>,
1545
1541
  ) {
1546
1542
  super();
1547
1543
  this.table = table;
@@ -1563,10 +1559,19 @@ export class UpdateAst extends QueryAst {
1563
1559
  return new UpdateAst(this.table, this.set, where, this.returning);
1564
1560
  }
1565
1561
 
1566
- withReturning(returning: ReadonlyArray<ColumnRef> | undefined): UpdateAst {
1562
+ withReturning(returning: ReadonlyArray<ProjectionItem> | undefined): UpdateAst {
1567
1563
  return new UpdateAst(this.table, this.set, this.where, returning);
1568
1564
  }
1569
1565
 
1566
+ rewrite(rewriter: AstRewriter): UpdateAst {
1567
+ return new UpdateAst(
1568
+ rewriteTableSource(this.table, rewriter),
1569
+ rewriteUpdateSet(this.set, rewriter),
1570
+ this.where?.rewrite(rewriter),
1571
+ this.returning?.map((item) => rewriteProjectionItem(item, rewriter)),
1572
+ );
1573
+ }
1574
+
1570
1575
  override collectParamRefs(): ParamRef[] {
1571
1576
  const refs: ParamRef[] = [];
1572
1577
  for (const value of Object.values(this.set)) {
@@ -1577,28 +1582,12 @@ export class UpdateAst extends QueryAst {
1577
1582
  if (this.where) {
1578
1583
  refs.push(...this.where.collectParamRefs());
1579
1584
  }
1580
- return refs;
1581
- }
1582
-
1583
- override collectRefs(): PlanRefs {
1584
- const tables = new Set<string>([this.table.name]);
1585
- const columns = new Map<string, { table: string; column: string }>();
1586
-
1587
- for (const value of Object.values(this.set)) {
1588
- if (value.kind === 'column-ref') {
1589
- addColumnRefToRefSets(value, tables, columns);
1585
+ for (const item of this.returning ?? []) {
1586
+ if (item.expr.kind !== 'literal') {
1587
+ refs.push(...item.expr.collectParamRefs());
1590
1588
  }
1591
1589
  }
1592
-
1593
- for (const columnRef of this.where?.collectColumnRefs() ?? []) {
1594
- addColumnRefToRefSets(columnRef, tables, columns);
1595
- }
1596
-
1597
- for (const columnRef of this.returning ?? []) {
1598
- addColumnRefToRefSets(columnRef, tables, columns);
1599
- }
1600
-
1601
- return sortRefs(tables, columns);
1590
+ return refs;
1602
1591
  }
1603
1592
 
1604
1593
  override toQueryAst(): AnyQueryAst {
@@ -1610,9 +1599,13 @@ export class DeleteAst extends QueryAst {
1610
1599
  readonly kind = 'delete' as const;
1611
1600
  readonly table: TableSource;
1612
1601
  readonly where: AnyExpression | undefined;
1613
- readonly returning: ReadonlyArray<ColumnRef> | undefined;
1602
+ readonly returning: ReadonlyArray<ProjectionItem> | undefined;
1614
1603
 
1615
- constructor(table: TableSource, where?: AnyExpression, returning?: ReadonlyArray<ColumnRef>) {
1604
+ constructor(
1605
+ table: TableSource,
1606
+ where?: AnyExpression,
1607
+ returning?: ReadonlyArray<ProjectionItem>,
1608
+ ) {
1616
1609
  super();
1617
1610
  this.table = table;
1618
1611
  this.where = where;
@@ -1628,27 +1621,29 @@ export class DeleteAst extends QueryAst {
1628
1621
  return new DeleteAst(this.table, where, this.returning);
1629
1622
  }
1630
1623
 
1631
- withReturning(returning: ReadonlyArray<ColumnRef> | undefined): DeleteAst {
1624
+ withReturning(returning: ReadonlyArray<ProjectionItem> | undefined): DeleteAst {
1632
1625
  return new DeleteAst(this.table, this.where, returning);
1633
1626
  }
1634
1627
 
1635
- override collectParamRefs(): ParamRef[] {
1636
- return this.where?.collectParamRefs() ?? [];
1628
+ rewrite(rewriter: AstRewriter): DeleteAst {
1629
+ return new DeleteAst(
1630
+ rewriteTableSource(this.table, rewriter),
1631
+ this.where?.rewrite(rewriter),
1632
+ this.returning?.map((item) => rewriteProjectionItem(item, rewriter)),
1633
+ );
1637
1634
  }
1638
1635
 
1639
- override collectRefs(): PlanRefs {
1640
- const tables = new Set<string>([this.table.name]);
1641
- const columns = new Map<string, { table: string; column: string }>();
1642
-
1643
- for (const columnRef of this.where?.collectColumnRefs() ?? []) {
1644
- addColumnRefToRefSets(columnRef, tables, columns);
1636
+ override collectParamRefs(): ParamRef[] {
1637
+ const refs: ParamRef[] = [];
1638
+ if (this.where) {
1639
+ refs.push(...this.where.collectParamRefs());
1645
1640
  }
1646
-
1647
- for (const columnRef of this.returning ?? []) {
1648
- addColumnRefToRefSets(columnRef, tables, columns);
1641
+ for (const item of this.returning ?? []) {
1642
+ if (item.expr.kind !== 'literal') {
1643
+ refs.push(...item.expr.collectParamRefs());
1644
+ }
1649
1645
  }
1650
-
1651
- return sortRefs(tables, columns);
1646
+ return refs;
1652
1647
  }
1653
1648
 
1654
1649
  override toQueryAst(): AnyQueryAst {
package/src/ast/util.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { AnyQueryAst, ParamRef } from './types';
2
+
1
3
  export function compact<T extends Record<string, unknown>>(o: T): T {
2
4
  const out: Record<string, unknown> = {};
3
5
  for (const [k, v] of Object.entries(o)) {
@@ -7,3 +9,24 @@ export function compact<T extends Record<string, unknown>>(o: T): T {
7
9
  }
8
10
  return out as T;
9
11
  }
12
+
13
+ /**
14
+ * Walks an AST's parameter references in first-encounter order and dedupes
15
+ * by ParamRef identity. The single canonical helper used by every consumer
16
+ * that aligns `plan.params` with metadata-by-index — the SQL builder lane,
17
+ * the SQL ORM client, the SQL runtime encoder, and the Postgres renderer's
18
+ * `$N` index map — so the four walks cannot drift in dedupe semantics.
19
+ *
20
+ * SQLite's `?`-placeholder renderer intentionally does NOT use this helper
21
+ * because it needs one params entry per occurrence in the SQL.
22
+ */
23
+ export function collectOrderedParamRefs(ast: AnyQueryAst): ReadonlyArray<ParamRef> {
24
+ const seen = new Set<ParamRef>();
25
+ const ordered: ParamRef[] = [];
26
+ for (const ref of ast.collectParamRefs()) {
27
+ if (seen.has(ref)) continue;
28
+ seen.add(ref);
29
+ ordered.push(ref);
30
+ }
31
+ return Object.freeze(ordered);
32
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Builder-pipeline validator pass: every {@link ParamRef} whose `codecId` resolves to a *parameterized* descriptor must carry `refs: { table, column }` so encode-side dispatch can call `contractCodecs.forColumn(table, column)`. Refs-less parameterized `ParamRef`s are a hard error — the codec-id-keyed `forCodecId` fallback cannot disambiguate per-instance codecs (e.g. `vector(1024)` vs. `vector(1536)`), so the dispatch
3
+ * path must reject them at validation time rather than silently bind to the wrong instance.
4
+ *
5
+ * Non-parameterized codec ids (the `voidParamsSchema` case) are always dispatch-safe via codec id alone, so refs-less ParamRefs for those ids are accepted unchanged.
6
+ *
7
+ * The pass runs post-build / pre-execute — the natural location is the SQL runtime's `lower()` step, between any `beforeCompile` rewrites and `encodeParams`. See AC-5 in the codec-registry-unification spec.
8
+ */
9
+
10
+ import { runtimeError } from '@prisma-next/framework-components/runtime';
11
+ import type { CodecDescriptorRegistry } from '../query-lane-context';
12
+ import type { AnyQueryAst, ParamRef } from './types';
13
+ import { collectOrderedParamRefs } from './util';
14
+
15
+ /**
16
+ * Validate that every parameterized-codec `ParamRef` in `plan` carries `refs`. Throws `RUNTIME.PARAM_REF_REFS_REQUIRED` (a runtime envelope) naming the codec id and the binding label when the invariant is violated. Returns the plan unchanged on success — callers that prefer a side-effecting assertion can ignore the return value.
17
+ *
18
+ * The `registry` is consulted via `descriptorFor(codecId).isParameterized` — `true` whenever the descriptor's `paramsSchema` is not the singleton `voidParamsSchema`.
19
+ */
20
+ export function validateParamRefRefs(plan: AnyQueryAst, registry: CodecDescriptorRegistry): void {
21
+ for (const ref of collectOrderedParamRefs(plan)) {
22
+ diagnoseRef(ref, registry);
23
+ }
24
+ }
25
+
26
+ function diagnoseRef(ref: ParamRef, registry: CodecDescriptorRegistry): void {
27
+ if (!ref.codecId) return;
28
+ const descriptor = registry.descriptorFor(ref.codecId);
29
+ if (descriptor === undefined) return;
30
+ if (!descriptor.isParameterized) return;
31
+ if (ref.refs) return;
32
+
33
+ const label = ref.name ?? '<anonymous>';
34
+ throw runtimeError(
35
+ 'RUNTIME.PARAM_REF_REFS_REQUIRED',
36
+ `ParamRef '${label}' for parameterized codec '${ref.codecId}' is missing column refs; column-aware dispatch requires { table, column } at the binding site.`,
37
+ { codecId: ref.codecId, paramName: ref.name },
38
+ );
39
+ }
@@ -0,0 +1,52 @@
1
+ import type { CodecDescriptor } from '@prisma-next/framework-components/codec';
2
+ import type { AnyCodecDescriptor } from './ast/codec-types';
3
+ import type { CodecDescriptorRegistry } from './query-lane-context';
4
+
5
+ /**
6
+ * Build a {@link CodecDescriptorRegistry} from a flat descriptor list.
7
+ *
8
+ * Used by:
9
+ * - Each codec-shipping package's `core/registry.ts` to expose a package-scoped registry as the public consumer surface (replacing raw descriptor-array exports). See ADR 208.
10
+ * - The runtime's `buildExecutionContext` to construct the contract-bound combined registry from every contributor's `codecs:` slot.
11
+ *
12
+ * The descriptor map is heterogeneous in `P` — each codec id has its own params shape. The public {@link CodecDescriptorRegistry} interface widens to `CodecDescriptor<unknown>` and consumers narrow per codec id at the call site (the descriptor's `paramsSchema` validates JSON-sourced params before the factory ever sees them, so the runtime narrow is safe). The cast at registration goes through `unknown` because
13
+ * `CodecDescriptor<P>` is invariant in `P` (the `factory` and `renderOutputType` slots use `P` contravariantly).
14
+ */
15
+ export function buildCodecDescriptorRegistry(
16
+ allDescriptors: ReadonlyArray<AnyCodecDescriptor>,
17
+ ): CodecDescriptorRegistry {
18
+ type AnyDescriptor = CodecDescriptor<unknown>;
19
+ const byId = new Map<string, AnyDescriptor>();
20
+ const byTargetType = new Map<string, Array<AnyDescriptor>>();
21
+
22
+ for (const descriptor of allDescriptors) {
23
+ if (byId.has(descriptor.codecId)) {
24
+ throw new Error(
25
+ `Duplicate codec descriptor id: '${descriptor.codecId}' — registered twice during registry construction. ` +
26
+ 'Each codecId must be contributed by exactly one component (target / adapter / extension pack).',
27
+ );
28
+ }
29
+ const widened = descriptor as unknown as AnyDescriptor;
30
+ byId.set(descriptor.codecId, widened);
31
+ for (const targetType of descriptor.targetTypes) {
32
+ const list = byTargetType.get(targetType);
33
+ if (list) {
34
+ list.push(widened);
35
+ } else {
36
+ byTargetType.set(targetType, [widened]);
37
+ }
38
+ }
39
+ }
40
+
41
+ return {
42
+ descriptorFor(codecId: string): AnyDescriptor | undefined {
43
+ return byId.get(codecId);
44
+ },
45
+ *values(): IterableIterator<AnyDescriptor> {
46
+ yield* byId.values();
47
+ },
48
+ byTargetType(targetType: string): readonly AnyDescriptor[] {
49
+ return byTargetType.get(targetType) ?? Object.freeze([]);
50
+ },
51
+ };
52
+ }
@@ -1,6 +1,8 @@
1
1
  export * from '../ast/adapter-types';
2
2
  export * from '../ast/codec-types';
3
3
  export * from '../ast/driver-types';
4
+ export * from '../ast/sql-codec-helpers';
4
5
  export * from '../ast/sql-codecs';
5
6
  export * from '../ast/types';
6
7
  export * from '../ast/util';
8
+ export * from '../ast/validate-param-refs';
@@ -0,0 +1 @@
1
+ export * from '../codec-descriptor-registry';
@@ -0,0 +1 @@
1
+ export * from '../expression';