@dbsp/nql 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CompiledNqlQuery } from '@dbsp/types';
1
+ import { ColumnType, CompiledNqlQuery } from '@dbsp/types';
2
2
  export { CompiledNqlQuery, DeleteIntent, ExpressionIntent, IncludeIntent, InsertIntent, MutationIntent, OrderByIntent, QueryIntent, SelectAllIntent, SelectFieldsIntent, SelectIntent, SelectWithExpressionsIntent, UpdateIntent, UpsertIntent, WhereAndIntent, WhereComparisonIntent, WhereInIntent, WhereIntent, WhereLikeIntent, WhereNotIntent, WhereNullIntent, WhereOrIntent, WhereRangeIntent } from '@dbsp/types';
3
3
  import * as chevrotain from 'chevrotain';
4
4
  import { Lexer, CstParser, CstNode, IToken } from 'chevrotain';
@@ -454,11 +454,20 @@ interface ColumnValidatorPseudoColumn {
454
454
  readonly ascendantKeyword?: string;
455
455
  readonly descendantKeyword?: string;
456
456
  }
457
+ /**
458
+ * Duck-type column shape carried by `ColumnValidatorSchema.getTable()`.
459
+ * `type`/`originalDbType` are optional so hand-authored test schemas that
460
+ * only supply `name` remain valid — absence makes a column's type
461
+ * unresolvable (untypeable), never silently mismatched.
462
+ */
463
+ interface ColumnValidatorTableColumn {
464
+ readonly name: string;
465
+ readonly type?: ColumnType;
466
+ readonly originalDbType?: string;
467
+ }
457
468
  interface ColumnValidatorSchema {
458
469
  getTable(name: string): {
459
- readonly columns: readonly {
460
- readonly name: string;
461
- }[];
470
+ readonly columns: readonly ColumnValidatorTableColumn[];
462
471
  readonly pseudoColumns?: readonly ColumnValidatorPseudoColumn[];
463
472
  } | undefined;
464
473
  getRelationsFrom(sourceTable: string): readonly ColumnValidatorRelation[];
@@ -518,6 +527,15 @@ declare class NqlCompiler {
518
527
  private registerQueryBindingOutputSchema;
519
528
  private canonicalizeMutationBinding;
520
529
  private getMutationBindingOutputSchema;
530
+ /**
531
+ * Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
532
+ * binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
533
+ * (alias-aware, positional) — NEVER the collapsed `columns` names — so an
534
+ * alias colliding with a real column (`returning email as name`) is
535
+ * flagged 'aliased-returning' rather than silently mistyped as the
536
+ * colliding column's type.
537
+ */
538
+ private buildMutationReturningColumnTypes;
521
539
  private compileSingleStatement;
522
540
  }
523
541
  /**
package/dist/index.js CHANGED
@@ -43,6 +43,45 @@ var NqlSemanticException = class extends Error {
43
43
  this.relatedSymbol = relatedSymbol;
44
44
  }
45
45
  };
46
+
47
+ // src/compiler/binding-column-types.ts
48
+ function freezeColumnTypes(types) {
49
+ for (const info of Object.values(types)) {
50
+ Object.freeze(info);
51
+ }
52
+ return Object.freeze(types);
53
+ }
54
+ function buildBindingColumnTypes(columns, candidates) {
55
+ const attemptsByColumn = /* @__PURE__ */ new Map();
56
+ for (const candidate of candidates) {
57
+ const bucket = attemptsByColumn.get(candidate.column);
58
+ if (bucket) {
59
+ bucket.push(candidate);
60
+ } else {
61
+ attemptsByColumn.set(candidate.column, [candidate]);
62
+ }
63
+ }
64
+ const types = /* @__PURE__ */ Object.create(
65
+ null
66
+ );
67
+ for (const column of columns) {
68
+ const attempts = attemptsByColumn.get(column) ?? [];
69
+ if (attempts.length > 1) {
70
+ return { untypeable: { column, reason: "duplicate-output-name" } };
71
+ }
72
+ const candidate = attempts[0];
73
+ if (candidate?.typed === void 0) {
74
+ return {
75
+ untypeable: {
76
+ column,
77
+ reason: candidate?.untypeable ?? "unresolvable-source"
78
+ }
79
+ };
80
+ }
81
+ types[column] = candidate.typed;
82
+ }
83
+ return { columnTypes: freezeColumnTypes(types) };
84
+ }
46
85
  var DEFAULT_RELATION_TARGET_COLUMN = "id";
47
86
  function relationForeignKeys(relation) {
48
87
  if (!relation) return void 0;
@@ -117,6 +156,25 @@ var ColumnValidator = class _ColumnValidator {
117
156
  getPhysicalTableColumns(name) {
118
157
  return this.schema.getTable(name)?.columns.map((column) => column.name);
119
158
  }
159
+ /**
160
+ * Resolve a physical column's neutral type info for binding-snapshot
161
+ * type propagation (#213). Returns undefined when the table/column is
162
+ * unknown OR the schema did not supply a `type` for it — the caller
163
+ * treats undefined as untypeable, never falls back to a guess.
164
+ */
165
+ getTableColumnType(table, column) {
166
+ const resolvedName = this.resolvePhysicalColumnName(table, column);
167
+ if (resolvedName === void 0) return void 0;
168
+ const columnInfo = this.schema.getTable(table)?.columns.find((candidate) => candidate.name === resolvedName);
169
+ if (columnInfo?.type === void 0) return void 0;
170
+ return {
171
+ kind: "column",
172
+ type: columnInfo.type,
173
+ ...columnInfo.originalDbType !== void 0 && {
174
+ originalDbType: columnInfo.originalDbType
175
+ }
176
+ };
177
+ }
120
178
  getPseudoColumns(name) {
121
179
  return this.schema.getTable(name)?.pseudoColumns ?? [];
122
180
  }
@@ -1118,15 +1176,29 @@ function assignMutationValue(target, column, value, ctx) {
1118
1176
  function compileMutationPipeline(pipeline, ctx, fns, bindings) {
1119
1177
  ctx.currentFromTable = pipeline.mutation.table;
1120
1178
  const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
1179
+ ctx.currentFromTable = pipeline.mutation.table;
1121
1180
  let returning;
1181
+ let returningItems;
1122
1182
  for (const clause of pipeline.clauses) {
1123
1183
  if (clause.type === "select") {
1124
- returning = extractReturningColumns(clause, ctx);
1184
+ returningItems = extractReturningItems(clause, ctx);
1185
+ returning = returningItems === "star" ? ["*"] : returningItems.map((item) => item.output);
1125
1186
  }
1126
1187
  }
1188
+ ctx.lastMutationReturningItems = returningItems;
1127
1189
  if (returning) {
1190
+ const aliasAwareReturningItems = returningItems !== void 0 && returningItems !== "star" && returningItems.some((item) => item.aliased) ? returningItems.map((item) => ({
1191
+ source: item.source,
1192
+ output: item.output
1193
+ })) : void 0;
1128
1194
  return {
1129
- mutation: { ...mutation, returning }
1195
+ mutation: {
1196
+ ...mutation,
1197
+ returning,
1198
+ ...aliasAwareReturningItems !== void 0 && {
1199
+ returningItems: aliasAwareReturningItems
1200
+ }
1201
+ }
1130
1202
  };
1131
1203
  }
1132
1204
  return { mutation };
@@ -1311,23 +1383,49 @@ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
1311
1383
  }
1312
1384
  };
1313
1385
  }
1314
- function extractReturningColumns(clause, ctx) {
1315
- const columns = [];
1386
+ function extractReturningItems(clause, ctx) {
1387
+ const items = [];
1388
+ const outputs = /* @__PURE__ */ new Set();
1316
1389
  for (const item of clause.items) {
1317
1390
  if (item.type === "star") {
1318
- return ["*"];
1391
+ if (clause.items.length > 1) {
1392
+ throw new NqlSemanticException(
1393
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1394
+ "Mutation RETURNING cannot mix `select *` with explicit projection items."
1395
+ );
1396
+ }
1397
+ return "star";
1319
1398
  }
1320
1399
  if (item.type === "expression") {
1321
1400
  const field = expressionToField(item.expression);
1322
1401
  if (field) {
1402
+ const aliased = item.alias !== void 0;
1403
+ if (aliased && field.includes(".")) {
1404
+ throw new NqlSemanticException(
1405
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1406
+ `Mutation RETURNING alias cannot use dotted source '${field}'.`
1407
+ );
1408
+ }
1323
1409
  if (ctx.currentFromTable && !field.includes(".")) {
1324
1410
  ctx.validator?.validateColumn(ctx.currentFromTable, field);
1325
1411
  }
1326
- columns.push(item.alias ?? field);
1412
+ const output = item.alias ?? field;
1413
+ if (outputs.has(output)) {
1414
+ throw new NqlSemanticException(
1415
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1416
+ `Mutation RETURNING has duplicate output name '${output}'.`
1417
+ );
1418
+ }
1419
+ outputs.add(output);
1420
+ items.push({
1421
+ source: field,
1422
+ output,
1423
+ aliased
1424
+ });
1327
1425
  }
1328
1426
  }
1329
1427
  }
1330
- return columns;
1428
+ return items;
1331
1429
  }
1332
1430
  function resolveBindingsInWhere(where, bindings) {
1333
1431
  if (!bindings || bindings.size === 0) return where;
@@ -2470,39 +2568,74 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
2470
2568
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
2471
2569
  );
2472
2570
  }
2473
- function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
2571
+ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = [], sourceBindingSchemas) {
2474
2572
  const columns = [];
2475
2573
  const seen = /* @__PURE__ */ new Set();
2476
2574
  const directProjectionLineage = [];
2575
+ const typeCandidates = [];
2576
+ const sourceIsPhysicalTable = ctx.validator?.hasPhysicalTable(intent.from) ?? false;
2577
+ const sourceBindingSchema = sourceIsPhysicalTable ? void 0 : sourceBindingSchemas?.get(intent.from);
2477
2578
  const addColumn = (column) => addUnique(columns, seen, column);
2478
2579
  const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
2479
2580
  const addDirectProjection = (outputColumn, sourceColumn) => {
2581
+ const resolvedSourceColumn = resolveSourceColumn(sourceColumn);
2582
+ let typeInfo;
2583
+ let untypeableReason = "unresolvable-source";
2584
+ if (sourceIsPhysicalTable) {
2585
+ typeInfo = ctx.validator?.getTableColumnType(
2586
+ intent.from,
2587
+ resolvedSourceColumn
2588
+ );
2589
+ } else if (sourceBindingSchema?.columnTypes) {
2590
+ typeInfo = sourceBindingSchema.columnTypes[resolvedSourceColumn];
2591
+ } else if (sourceBindingSchema?.columnTypesUnavailable) {
2592
+ untypeableReason = sourceBindingSchema.columnTypesUnavailable.reason;
2593
+ }
2594
+ typeCandidates.push(
2595
+ typeInfo !== void 0 ? { column: outputColumn, typed: typeInfo } : { column: outputColumn, untypeable: untypeableReason }
2596
+ );
2480
2597
  if (!addColumn(outputColumn)) return;
2481
2598
  directProjectionLineage.push({
2482
2599
  kind: "directProjection",
2483
2600
  sourceTable: intent.from,
2484
- sourceColumn: resolveSourceColumn(sourceColumn),
2601
+ sourceColumn: resolvedSourceColumn,
2485
2602
  outputColumn
2486
2603
  });
2487
2604
  };
2605
+ const addUntypedColumn = (outputColumn, reason) => {
2606
+ typeCandidates.push({ column: outputColumn, untypeable: reason });
2607
+ addColumn(outputColumn);
2608
+ };
2609
+ const addCountAggregateColumn = (outputColumn) => {
2610
+ typeCandidates.push({
2611
+ column: outputColumn,
2612
+ typed: { kind: "aggregate", fn: "count" }
2613
+ });
2614
+ addColumn(outputColumn);
2615
+ };
2488
2616
  const addSourceColumns = () => {
2489
2617
  for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
2490
2618
  addDirectProjection(column, column);
2491
2619
  }
2492
2620
  };
2493
- const { select } = intent;
2494
- if (!select || select.type === "all") {
2495
- addSourceColumns();
2496
- const outputSchema2 = { columns, directProjectionLineage };
2621
+ const finalizeOutputSchema = () => {
2622
+ const outputSchema = { columns, directProjectionLineage };
2623
+ const typesResult = buildBindingColumnTypes(columns, typeCandidates);
2497
2624
  return {
2498
- columns: outputSchema2.columns,
2625
+ columns: outputSchema.columns,
2499
2626
  relationFilters: getBindingRelationFilterMetadata(
2500
2627
  intent,
2501
2628
  ctx,
2502
- outputSchema2,
2629
+ outputSchema,
2503
2630
  bindingDependencies
2504
- )
2631
+ ),
2632
+ ..."columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable }
2505
2633
  };
2634
+ };
2635
+ const { select } = intent;
2636
+ if (!select || select.type === "all") {
2637
+ addSourceColumns();
2638
+ return finalizeOutputSchema();
2506
2639
  }
2507
2640
  if (select.type === "fields") {
2508
2641
  for (const field of select.fields) {
@@ -2512,16 +2645,7 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2512
2645
  addDirectProjection(field, field);
2513
2646
  }
2514
2647
  }
2515
- const outputSchema2 = { columns, directProjectionLineage };
2516
- return {
2517
- columns: outputSchema2.columns,
2518
- relationFilters: getBindingRelationFilterMetadata(
2519
- intent,
2520
- ctx,
2521
- outputSchema2,
2522
- bindingDependencies
2523
- )
2524
- };
2648
+ return finalizeOutputSchema();
2525
2649
  }
2526
2650
  if (select.type === "aggregate") {
2527
2651
  for (const field of select.fields ?? []) {
@@ -2534,18 +2658,13 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2534
2658
  `Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
2535
2659
  );
2536
2660
  }
2537
- addColumn(aggregate.as);
2661
+ if (aggregate.function === "count") {
2662
+ addCountAggregateColumn(aggregate.as);
2663
+ } else {
2664
+ addUntypedColumn(aggregate.as, "unsupported-aggregate");
2665
+ }
2538
2666
  }
2539
- const outputSchema2 = { columns, directProjectionLineage };
2540
- return {
2541
- columns: outputSchema2.columns,
2542
- relationFilters: getBindingRelationFilterMetadata(
2543
- intent,
2544
- ctx,
2545
- outputSchema2,
2546
- bindingDependencies
2547
- )
2548
- };
2667
+ return finalizeOutputSchema();
2549
2668
  }
2550
2669
  for (const expr of select.columns) {
2551
2670
  if (expr.kind === "column" && expr.column === "*") {
@@ -2569,20 +2688,16 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2569
2688
  addDirectProjection(outputColumn, expr.column);
2570
2689
  } else if (expr.kind === "columnAlias") {
2571
2690
  addDirectProjection(outputColumn, expr.column);
2691
+ } else if (expr.kind === "aggregate" && expr.function === "count") {
2692
+ addCountAggregateColumn(outputColumn);
2572
2693
  } else {
2573
- addColumn(outputColumn);
2694
+ let reason = "computed-expression";
2695
+ if (expr.kind === "relationColumn") reason = "relation-column";
2696
+ else if (expr.kind === "aggregate") reason = "unsupported-aggregate";
2697
+ addUntypedColumn(outputColumn, reason);
2574
2698
  }
2575
2699
  }
2576
- const outputSchema = { columns, directProjectionLineage };
2577
- return {
2578
- columns: outputSchema.columns,
2579
- relationFilters: getBindingRelationFilterMetadata(
2580
- intent,
2581
- ctx,
2582
- outputSchema,
2583
- bindingDependencies
2584
- )
2585
- };
2700
+ return finalizeOutputSchema();
2586
2701
  }
2587
2702
  function validateNqlExpressionPaths(expr, ctx) {
2588
2703
  switch (expr.type) {
@@ -4162,81 +4277,39 @@ function programSequenceStepFromResult(result, bindName, final, bindingDependenc
4162
4277
  function stringArraysEqual(left, right) {
4163
4278
  return left.length === right.length && left.every((value, index) => value === right[index]);
4164
4279
  }
4280
+ function mutationReturningItemsMatchReturning(returning, returningItems) {
4281
+ return returningItems.length === returning.length && returningItems.every((item, index) => item.output === returning[index]);
4282
+ }
4165
4283
  function isNqlAstRecord(value) {
4166
4284
  return typeof value === "object" && value !== null;
4167
4285
  }
4168
- function isExactPhysicalProjectionColumn(query, ctx, sourceColumn, outputColumn) {
4169
- const resolvedColumn = ctx.validator?.resolvePhysicalColumnName(
4170
- query.from,
4171
- sourceColumn
4172
- );
4173
- return resolvedColumn !== void 0 && sourceColumn === resolvedColumn && outputColumn === resolvedColumn;
4174
- }
4175
- function hasDirectPhysicalColumnProjection(query, ctx) {
4176
- const physicalColumns = ctx.validator?.getPhysicalTableColumns(query.from);
4177
- if (physicalColumns === void 0) return false;
4178
- const select = query.select;
4179
- if (!select || select.type === "all") return physicalColumns.length > 0;
4180
- let projectedColumnCount = 0;
4181
- const addStarProjection = () => {
4182
- projectedColumnCount += physicalColumns.length;
4183
- return physicalColumns.length > 0;
4184
- };
4185
- const addDirectProjection = (sourceColumn, outputColumn) => {
4186
- projectedColumnCount++;
4187
- return isExactPhysicalProjectionColumn(
4188
- query,
4189
- ctx,
4190
- sourceColumn,
4191
- outputColumn
4192
- );
4193
- };
4194
- if (select.type === "fields") {
4195
- for (const field of select.fields) {
4196
- if (field === "*") {
4197
- if (!addStarProjection()) return false;
4198
- } else if (!addDirectProjection(field, field)) {
4199
- return false;
4200
- }
4201
- }
4202
- return projectedColumnCount > 0;
4203
- }
4204
- if (select.type === "aggregate") return false;
4205
- for (const expr of select.columns) {
4206
- if (expr.kind === "column" && expr.column === "*") {
4207
- if (!addStarProjection()) return false;
4208
- continue;
4209
- }
4210
- if (expr.kind === "column") {
4211
- const outputColumn = expr.as ?? expr.column;
4212
- if (!addDirectProjection(expr.column, outputColumn)) return false;
4213
- continue;
4214
- }
4215
- if (expr.kind === "columnAlias") {
4216
- if (!addDirectProjection(expr.column, expr.alias)) return false;
4217
- continue;
4218
- }
4219
- return false;
4286
+ function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding, outputSchema) {
4287
+ if (!sourceWasBinding && ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
4288
+ return {
4289
+ supported: false,
4290
+ reason: `no physical model table source '${query.from}'`
4291
+ };
4220
4292
  }
4221
- return projectedColumnCount > 0;
4222
- }
4223
- function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding) {
4224
- if (sourceWasBinding) {
4225
- return { supported: false, reason: "a binding source" };
4293
+ if (outputSchema?.columnTypes) {
4294
+ return { supported: true };
4226
4295
  }
4227
- if (ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
4296
+ const untypeable = outputSchema?.columnTypesUnavailable;
4297
+ if (untypeable?.reason === "unsupported-aggregate") {
4228
4298
  return {
4229
4299
  supported: false,
4230
- reason: `no physical model table source '${query.from}'`
4300
+ reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
4231
4301
  };
4232
4302
  }
4233
- if (!hasDirectPhysicalColumnProjection(query, ctx)) {
4303
+ if (untypeable) {
4234
4304
  return {
4235
4305
  supported: false,
4236
- reason: "aliased/computed/aggregate columns"
4306
+ reason: `${untypeable.reason} column '${untypeable.column}'`
4237
4307
  };
4238
4308
  }
4239
- return { supported: true };
4309
+ if (sourceWasBinding) {
4310
+ return { supported: false, reason: "a binding source" };
4311
+ }
4312
+ return { supported: false, reason: "aliased/computed/aggregate columns" };
4240
4313
  }
4241
4314
  function addReadBindingReference(references, readBindingNames, name) {
4242
4315
  if (typeof name === "string" && readBindingNames.has(name)) {
@@ -4479,6 +4552,7 @@ var NqlCompiler = class {
4479
4552
  } finally {
4480
4553
  this.ctx.bindingOutputColumns.clear();
4481
4554
  this.ctx.bindingRelationFilters.clear();
4555
+ this.ctx.lastMutationReturningItems = void 0;
4482
4556
  this.ctx.validator?.clearVirtualBindingTables();
4483
4557
  }
4484
4558
  }
@@ -4575,7 +4649,8 @@ var NqlCompiler = class {
4575
4649
  classifyReadBindingSnapshotShape(
4576
4650
  lastResult.query,
4577
4651
  this.ctx,
4578
- sourceWasBinding
4652
+ sourceWasBinding,
4653
+ outputSchema
4579
4654
  )
4580
4655
  );
4581
4656
  bindings.set(bindName, lastResult.query);
@@ -4665,7 +4740,12 @@ var NqlCompiler = class {
4665
4740
  query,
4666
4741
  this.ctx,
4667
4742
  bindName,
4668
- bindingDependencies
4743
+ bindingDependencies,
4744
+ // #213 B2: earlier bindings are already registered here (this
4745
+ // map is populated in program-statement order), so a
4746
+ // transitive `from` = binding chains through the SOURCE's
4747
+ // typed schema instead of staying untypeable.
4748
+ bindingOutputSchemas
4669
4749
  );
4670
4750
  bindingOutputSchemas.set(bindName, outputSchema);
4671
4751
  this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
@@ -4690,7 +4770,50 @@ var NqlCompiler = class {
4690
4770
  mutation
4691
4771
  );
4692
4772
  const returning = mutation.returning;
4693
- if (returning === void 0 || returning.length === 0 || returning.includes("*") || outputSchema === void 0 || stringArraysEqual(returning, outputSchema.columns)) {
4773
+ const returningItems = mutation.returningItems;
4774
+ if (returning === void 0 || returning.length === 0 || returning.includes("*") || outputSchema === void 0) {
4775
+ return { mutation, outputSchema };
4776
+ }
4777
+ const seenOutputs = /* @__PURE__ */ new Set();
4778
+ for (const column of outputSchema.columns) {
4779
+ if (seenOutputs.has(column)) {
4780
+ throw new NqlSemanticException(
4781
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
4782
+ `Mutation RETURNING resolves to duplicate output name '${column}' after column canonicalization for binding '${bindName}'.`
4783
+ );
4784
+ }
4785
+ seenOutputs.add(column);
4786
+ }
4787
+ if (returningItems !== void 0) {
4788
+ const internalItems = this.ctx.lastMutationReturningItems !== void 0 && this.ctx.lastMutationReturningItems !== "star" ? this.ctx.lastMutationReturningItems : void 0;
4789
+ const canonicalItems = returningItems.map((item, index) => {
4790
+ const source = this.ctx.validator?.resolveColumnName(mutation.table, item.source) ?? item.source;
4791
+ const internalItem = internalItems?.[index];
4792
+ const output = internalItem?.aliased ? item.output : source;
4793
+ return source === item.source && output === item.output ? item : { source, output };
4794
+ });
4795
+ if (!mutationReturningItemsMatchReturning(
4796
+ outputSchema.columns,
4797
+ canonicalItems
4798
+ )) {
4799
+ throw new NqlSemanticException(
4800
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
4801
+ `Mutation RETURNING item outputs drifted during canonicalization for binding '${bindName}'.`
4802
+ );
4803
+ }
4804
+ if (stringArraysEqual(returning, outputSchema.columns) && canonicalItems.every((item, index) => item === returningItems[index])) {
4805
+ return { mutation, outputSchema };
4806
+ }
4807
+ return {
4808
+ mutation: {
4809
+ ...mutation,
4810
+ returning: outputSchema.columns,
4811
+ returningItems: canonicalItems
4812
+ },
4813
+ outputSchema
4814
+ };
4815
+ }
4816
+ if (stringArraysEqual(returning, outputSchema.columns)) {
4694
4817
  return { mutation, outputSchema };
4695
4818
  }
4696
4819
  return {
@@ -4705,20 +4828,62 @@ var NqlCompiler = class {
4705
4828
  const returning = mutation.returning;
4706
4829
  if (!returning || returning.length === 0) return void 0;
4707
4830
  if (returning.includes("*")) {
4708
- const columns = this.ctx.validator?.getTableColumns(mutation.table);
4709
- if (columns !== void 0) return { columns };
4831
+ const columns2 = this.ctx.validator?.getTableColumns(mutation.table);
4832
+ if (columns2 !== void 0) {
4833
+ return {
4834
+ columns: columns2,
4835
+ ...this.buildMutationReturningColumnTypes(mutation.table, columns2)
4836
+ };
4837
+ }
4710
4838
  if (!this.ctx.validator) return void 0;
4711
4839
  throw new NqlSemanticException(
4712
4840
  NqlErrorCodes.SEM_INVALID_SYNTAX,
4713
4841
  `Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
4714
4842
  );
4715
4843
  }
4844
+ const columns = returning.map(
4845
+ (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
4846
+ );
4847
+ const returningItems = mutation.returningItems;
4848
+ if (returningItems !== void 0) {
4849
+ const internalItems = this.ctx.lastMutationReturningItems !== void 0 && this.ctx.lastMutationReturningItems !== "star" ? this.ctx.lastMutationReturningItems : void 0;
4850
+ const columns2 = returningItems.map((item, index) => {
4851
+ const source = this.ctx.validator?.resolveColumnName(mutation.table, item.source) ?? item.source;
4852
+ return internalItems?.[index]?.aliased ? item.output : source;
4853
+ });
4854
+ return {
4855
+ columns: columns2,
4856
+ ...this.buildMutationReturningColumnTypes(mutation.table, columns2)
4857
+ };
4858
+ }
4716
4859
  return {
4717
- columns: returning.map(
4718
- (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
4719
- )
4860
+ columns,
4861
+ ...this.buildMutationReturningColumnTypes(mutation.table, columns)
4720
4862
  };
4721
4863
  }
4864
+ /**
4865
+ * Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
4866
+ * binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
4867
+ * (alias-aware, positional) — NEVER the collapsed `columns` names — so an
4868
+ * alias colliding with a real column (`returning email as name`) is
4869
+ * flagged 'aliased-returning' rather than silently mistyped as the
4870
+ * colliding column's type.
4871
+ */
4872
+ buildMutationReturningColumnTypes(table, columns) {
4873
+ const items = this.ctx.lastMutationReturningItems;
4874
+ const candidates = columns.map(
4875
+ (column, index) => {
4876
+ const item = items !== void 0 && items !== "star" ? items[index] : void 0;
4877
+ if (item?.aliased) {
4878
+ return { column, untypeable: "aliased-returning" };
4879
+ }
4880
+ const typeInfo = this.ctx.validator?.getTableColumnType(table, column);
4881
+ return typeInfo !== void 0 ? { column, typed: typeInfo } : { column, untypeable: "unresolvable-source" };
4882
+ }
4883
+ );
4884
+ const typesResult = buildBindingColumnTypes(columns, candidates);
4885
+ return "columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable };
4886
+ }
4722
4887
  compileSingleStatement(stmt, bindings) {
4723
4888
  if (stmt.type === "withQuery") {
4724
4889
  return compileWithQuery(stmt, this.ctx, this.fns);