@dbsp/nql 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -43,6 +43,69 @@ 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
+ }
85
+ var DEFAULT_RELATION_TARGET_COLUMN = "id";
86
+ function relationForeignKeys(relation) {
87
+ if (!relation) return void 0;
88
+ const columns = toColumnList(relation.foreignKey);
89
+ return columns.length > 0 ? columns : void 0;
90
+ }
91
+ function relationCardinality(relation) {
92
+ return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
93
+ }
94
+ function scalarRelationJoinColumns(relation) {
95
+ if (!relation) return void 0;
96
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
97
+ return void 0;
98
+ }
99
+ const fkColumns = relationForeignKeys(relation);
100
+ if (!fkColumns) return void 0;
101
+ const sourceKeys = toColumnList(relation.sourceKey);
102
+ const targetKeys = toColumnList(relation.targetKey);
103
+ const sourceJoinColumn = relation.type === "belongsTo" ? fkColumns : sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN];
104
+ const targetJoinColumn = relation.type === "belongsTo" ? targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN] : fkColumns;
105
+ return { sourceJoinColumn, targetJoinColumn };
106
+ }
107
+
108
+ // src/compiler/column-validator.ts
46
109
  var ColumnValidator = class _ColumnValidator {
47
110
  constructor(schema) {
48
111
  this.schema = schema;
@@ -90,6 +153,31 @@ var ColumnValidator = class _ColumnValidator {
90
153
  if (virtualColumns) return virtualColumns;
91
154
  return this.schema.getTable(name)?.columns.map((column) => column.name);
92
155
  }
156
+ getPhysicalTableColumns(name) {
157
+ return this.schema.getTable(name)?.columns.map((column) => column.name);
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
+ }
178
+ getPseudoColumns(name) {
179
+ return this.schema.getTable(name)?.pseudoColumns ?? [];
180
+ }
93
181
  hasPhysicalTable(name) {
94
182
  return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
95
183
  }
@@ -233,10 +321,54 @@ var ColumnValidator = class _ColumnValidator {
233
321
  if (!sourceTable) {
234
322
  return "the binding source table could not be proven";
235
323
  }
324
+ const recursiveReason = this.explainVirtualBindingRecursiveScalarRelationRejection(
325
+ bindingName,
326
+ relationName,
327
+ sourceTable,
328
+ metadata
329
+ );
330
+ if (recursiveReason) return recursiveReason;
236
331
  const relation = this.getRelation(sourceTable, relationName);
237
332
  if (!relation) {
238
333
  return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
239
334
  }
335
+ if (relation.type === "belongsToMany") {
336
+ if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
337
+ return `relation '${relationName}' is recursive/self-referential; binding relation columns for recursive relations require recursive CTE handling and are not supported (ref-#193)`;
338
+ }
339
+ if (typeof relation.through !== "string" || relation.through.length === 0) {
340
+ return `relation '${relationName}' is many-to-many but does not declare a resolvable junction table (ref-#192)`;
341
+ }
342
+ const sourceColumns2 = toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
343
+ const throughSourceColumns = toColumnList(relation.foreignKey);
344
+ const throughTargetColumns = toColumnList(relation.otherKey);
345
+ const targetColumns = toColumnList(relation.targetKey).length > 0 ? toColumnList(relation.targetKey) : ["id"];
346
+ if (sourceColumns2.length !== 1) {
347
+ return `relation '${relationName}' source key is composite; binding many-to-many relation columns require a single source key column (ref-#179)`;
348
+ }
349
+ if (throughSourceColumns.length !== 1) {
350
+ return `relation '${relationName}' junction source FK is composite or missing; binding many-to-many relation columns require a single junction source FK column (ref-#179)`;
351
+ }
352
+ if (throughTargetColumns.length !== 1) {
353
+ return `relation '${relationName}' junction target FK is composite or missing; binding many-to-many relation columns require a single junction target FK column (ref-#179)`;
354
+ }
355
+ if (targetColumns.length !== 1) {
356
+ return `relation '${relationName}' target key is composite; binding many-to-many relation columns require a single target key column (ref-#179)`;
357
+ }
358
+ const [sourceColumn] = sourceColumns2;
359
+ if (sourceColumn === void 0) {
360
+ return `relation '${relationName}' source key is missing; binding many-to-many relation columns require a single source key column (ref-#179)`;
361
+ }
362
+ const directProjection = metadata.directProjectionLineage?.find(
363
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
364
+ );
365
+ if (!directProjection) {
366
+ const available3 = this.virtualBindingTables.get(bindingName)?.join(", ");
367
+ return `relation '${relationName}' source key column '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available3 ? ` (available columns: ${available3})` : ""}`;
368
+ }
369
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
370
+ return `relation '${relationName}' many-to-many source column '${sourceColumn}' is not available through binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
371
+ }
240
372
  const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
241
373
  relationName,
242
374
  relation
@@ -257,6 +389,64 @@ var ColumnValidator = class _ColumnValidator {
257
389
  const available = this.virtualBindingTables.get(bindingName)?.join(", ");
258
390
  return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
259
391
  }
392
+ static recursiveDirection(value) {
393
+ if (value === null || typeof value !== "object") return void 0;
394
+ const direction = value.direction;
395
+ if (direction === "up" || direction === "ancestors") return "up";
396
+ if (direction === "down" || direction === "descendants") return "down";
397
+ return void 0;
398
+ }
399
+ explainVirtualBindingRecursiveScalarRelationRejection(bindingName, relationName, sourceTable, metadata) {
400
+ const lowerRelationName = relationName.toLowerCase();
401
+ const pseudoColumn = this.getPseudoColumns(sourceTable).find(
402
+ (candidate) => candidate.ascendantKeyword?.toLowerCase() === lowerRelationName || candidate.descendantKeyword?.toLowerCase() === lowerRelationName
403
+ );
404
+ if (!pseudoColumn) return void 0;
405
+ const direction = pseudoColumn.ascendantKeyword?.toLowerCase() === lowerRelationName ? "up" : "down";
406
+ const recursiveRelations = this.getRelationsFrom(sourceTable).filter(
407
+ (relation) => relation.recursive !== void 0 && relation.source === relation.target && relation.source === sourceTable && _ColumnValidator.recursiveDirection(relation.recursive) === direction
408
+ );
409
+ if (recursiveRelations.length === 0) {
410
+ return `recursive traversal '${relationName}' is missing recursive relation metadata for direction '${direction}' (ref-#193)`;
411
+ }
412
+ for (const relation of recursiveRelations) {
413
+ const relationFkColumns = toColumnList(relation.foreignKey);
414
+ const fkColumns = relationFkColumns.length > 0 ? relationFkColumns : toColumnList(pseudoColumn.foreignKeyColumn);
415
+ if (fkColumns.length !== 1) {
416
+ return `relation '${relation.name}' self-ref FK is composite or missing; binding recursive relation columns require a single self-ref FK column (ref-#193)`;
417
+ }
418
+ const relationTargetColumns = toColumnList(relation.targetKey);
419
+ const targetColumns = relationTargetColumns.length > 0 ? relationTargetColumns : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
420
+ if (targetColumns.length !== 1) {
421
+ return `relation '${relation.name}' target key is composite; binding recursive relation columns require a single target key column (ref-#193)`;
422
+ }
423
+ const selfRefColumn = fkColumns[0];
424
+ const targetKeyColumn = targetColumns[0];
425
+ if (selfRefColumn === void 0 || targetKeyColumn === void 0) {
426
+ return `relation '${relation.name}' recursive metadata is missing a single-column seed; binding recursive relation columns require a single self-ref FK and target key column (ref-#193)`;
427
+ }
428
+ if (pseudoColumn.foreignKeyColumn !== void 0 && !_ColumnValidator.columnsMatch(
429
+ pseudoColumn.foreignKeyColumn,
430
+ selfRefColumn
431
+ ) || pseudoColumn.targetColumn !== void 0 && !_ColumnValidator.columnsMatch(
432
+ pseudoColumn.targetColumn,
433
+ targetKeyColumn
434
+ )) {
435
+ continue;
436
+ }
437
+ const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
438
+ const directProjection = metadata.directProjectionLineage?.find(
439
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, seedColumn)
440
+ );
441
+ if (!directProjection) {
442
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
443
+ const seedLabel = direction === "up" ? "self-ref FK column" : "target key column";
444
+ return `recursive traversal '${relationName}' ${seedLabel} '${seedColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available ? ` (available columns: ${available})` : ""} (ref-#193)`;
445
+ }
446
+ return void 0;
447
+ }
448
+ return `recursive traversal '${relationName}' pseudo metadata does not match a single-column recursive self-reference (ref-#193)`;
449
+ }
260
450
  /**
261
451
  * Convert camelCase to snake_case for column name matching.
262
452
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -288,6 +478,14 @@ var ColumnValidator = class _ColumnValidator {
288
478
  (c) => _ColumnValidator.columnsMatch(column, c.name)
289
479
  )?.name;
290
480
  }
481
+ resolvePhysicalColumnName(table, column) {
482
+ if (column === "*") return column;
483
+ const tableInfo = this.schema.getTable(table);
484
+ if (!tableInfo) return void 0;
485
+ return tableInfo.columns.find(
486
+ (c) => _ColumnValidator.columnsMatch(column, c.name)
487
+ )?.name;
488
+ }
291
489
  validateColumn(table, column) {
292
490
  if (column === "*") return;
293
491
  const virtualColumns = this.virtualBindingTables.get(table);
@@ -354,28 +552,6 @@ var ColumnValidator = class _ColumnValidator {
354
552
  );
355
553
  }
356
554
  };
357
- var DEFAULT_RELATION_TARGET_COLUMN = "id";
358
- function relationForeignKeys(relation) {
359
- if (!relation) return void 0;
360
- const columns = toColumnList(relation.foreignKey);
361
- return columns.length > 0 ? columns : void 0;
362
- }
363
- function relationCardinality(relation) {
364
- return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
365
- }
366
- function scalarRelationJoinColumns(relation) {
367
- if (!relation) return void 0;
368
- if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
369
- return void 0;
370
- }
371
- const fkColumns = relationForeignKeys(relation);
372
- if (!fkColumns) return void 0;
373
- const sourceKeys = toColumnList(relation.sourceKey);
374
- const targetKeys = toColumnList(relation.targetKey);
375
- const sourceJoinColumn = relation.type === "belongsTo" ? fkColumns : sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN];
376
- const targetJoinColumn = relation.type === "belongsTo" ? targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN] : fkColumns;
377
- return { sourceJoinColumn, targetJoinColumn };
378
- }
379
555
  var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
380
556
  "__proto__",
381
557
  "constructor",
@@ -652,6 +828,43 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
652
828
  hops
653
829
  };
654
830
  }
831
+ function resolveBindingRecursiveRelationColumn(ctx, bindingName, traversal, selectedColumn, maxDepth) {
832
+ if (!isBindingTable(ctx, bindingName)) return void 0;
833
+ const traversalName = traversal.toLowerCase();
834
+ const isRecursiveTraversal = ctx.recursiveKeywords.has(traversalName) || traversalName === "ascendant" || traversalName === "descendant";
835
+ const actualBindingName = bindingName;
836
+ if (!ctx.validator) {
837
+ if (!isRecursiveTraversal) return void 0;
838
+ throwBindingRelationColumn182(
839
+ actualBindingName,
840
+ traversalName,
841
+ "model metadata is not available"
842
+ );
843
+ }
844
+ const virtualRelation = ctx.validator.getVirtualBindingScalarRelation(
845
+ actualBindingName,
846
+ traversalName
847
+ );
848
+ if (!isRecursiveTraversal && !virtualRelation?.recursive) {
849
+ return void 0;
850
+ }
851
+ if (!virtualRelation?.recursive) {
852
+ const reason = ctx.validator.explainVirtualBindingScalarRelationRejection(
853
+ actualBindingName,
854
+ traversalName
855
+ );
856
+ throwBindingRelationColumn182(actualBindingName, traversalName, reason);
857
+ }
858
+ ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
859
+ if (maxDepth === void 0) return virtualRelation;
860
+ return {
861
+ ...virtualRelation,
862
+ recursive: {
863
+ ...virtualRelation.recursive,
864
+ maxDepth
865
+ }
866
+ };
867
+ }
655
868
  function assertNoBindingRelationPath(ctx, bindingName, path) {
656
869
  if (!isBindingTable(ctx, bindingName)) return;
657
870
  throw new NqlSemanticException(
@@ -964,11 +1177,14 @@ function compileMutationPipeline(pipeline, ctx, fns, bindings) {
964
1177
  ctx.currentFromTable = pipeline.mutation.table;
965
1178
  const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
966
1179
  let returning;
1180
+ let returningItems;
967
1181
  for (const clause of pipeline.clauses) {
968
1182
  if (clause.type === "select") {
969
- returning = extractReturningColumns(clause, ctx);
1183
+ returningItems = extractReturningItems(clause, ctx);
1184
+ returning = returningItems === "star" ? ["*"] : returningItems.map((item) => item.output);
970
1185
  }
971
1186
  }
1187
+ ctx.lastMutationReturningItems = returningItems;
972
1188
  if (returning) {
973
1189
  return {
974
1190
  mutation: { ...mutation, returning }
@@ -1156,11 +1372,11 @@ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
1156
1372
  }
1157
1373
  };
1158
1374
  }
1159
- function extractReturningColumns(clause, ctx) {
1160
- const columns = [];
1375
+ function extractReturningItems(clause, ctx) {
1376
+ const items = [];
1161
1377
  for (const item of clause.items) {
1162
1378
  if (item.type === "star") {
1163
- return ["*"];
1379
+ return "star";
1164
1380
  }
1165
1381
  if (item.type === "expression") {
1166
1382
  const field = expressionToField(item.expression);
@@ -1168,11 +1384,14 @@ function extractReturningColumns(clause, ctx) {
1168
1384
  if (ctx.currentFromTable && !field.includes(".")) {
1169
1385
  ctx.validator?.validateColumn(ctx.currentFromTable, field);
1170
1386
  }
1171
- columns.push(item.alias ?? field);
1387
+ items.push({
1388
+ output: item.alias ?? field,
1389
+ aliased: item.alias !== void 0
1390
+ });
1172
1391
  }
1173
1392
  }
1174
1393
  }
1175
- return columns;
1394
+ return items;
1176
1395
  }
1177
1396
  function resolveBindingsInWhere(where, bindings) {
1178
1397
  if (!bindings || bindings.size === 0) return where;
@@ -1287,6 +1506,12 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
1287
1506
  ...NQL_SELECT_AGGREGATE_FUNCTIONS,
1288
1507
  ...NQL_SELECT_VALUE_FUNCTIONS
1289
1508
  ]);
1509
+ var UnresolvedSelectAllOutputSchemaError = class extends NqlSemanticException {
1510
+ constructor(message) {
1511
+ super(NqlErrorCodes.SEM_INVALID_SYNTAX, message);
1512
+ this.name = "UnresolvedSelectAllOutputSchemaError";
1513
+ }
1514
+ };
1290
1515
  function compileNestedQuery(query, ctx, fns, bindings) {
1291
1516
  const savedContext = {
1292
1517
  currentFromTable: ctx.currentFromTable,
@@ -1373,6 +1598,21 @@ function relationForeignKeysOnSource(relation) {
1373
1598
  const fkColumns = toColumnList(relation.foreignKey);
1374
1599
  return fkColumns.length > 0 ? fkColumns : void 0;
1375
1600
  }
1601
+ function recursiveDirection(recursive) {
1602
+ if (recursive === null || typeof recursive !== "object") return void 0;
1603
+ const direction = recursive.direction;
1604
+ if (direction === "up" || direction === "ancestors") return "up";
1605
+ if (direction === "down" || direction === "descendants") return "down";
1606
+ return void 0;
1607
+ }
1608
+ function recursiveMaxDepth(recursive) {
1609
+ if (recursive === null || typeof recursive !== "object") return void 0;
1610
+ const maxDepth = recursive.maxDepth;
1611
+ return typeof maxDepth === "number" && Number.isSafeInteger(maxDepth) && maxDepth > 0 ? maxDepth : void 0;
1612
+ }
1613
+ function pseudoColumnMatchesSelfRef(pseudoColumn, sourceTable, selfRefColumn, targetKeyColumn) {
1614
+ return (pseudoColumn.table === void 0 || pseudoColumn.table === sourceTable) && (pseudoColumn.foreignKeyColumn === void 0 || columnsMatch(pseudoColumn.foreignKeyColumn, selfRefColumn)) && (pseudoColumn.targetColumn === void 0 || columnsMatch(pseudoColumn.targetColumn, targetKeyColumn));
1615
+ }
1376
1616
  function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1377
1617
  if (!ctx.validator) return "model metadata is not available";
1378
1618
  if (!ctx.validator.hasQualifiedRelationLookup()) {
@@ -1443,8 +1683,119 @@ function virtualRelationForBinding(relation, sourceTable, directProjectionLineag
1443
1683
  cardinality: "one"
1444
1684
  };
1445
1685
  }
1686
+ function singleResolvedColumn(columns) {
1687
+ return columns.length === 1 ? columns[0] : void 0;
1688
+ }
1689
+ function manyToManyVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1690
+ if (relation?.type !== "belongsToMany") return void 0;
1691
+ if (typeof relation.through !== "string" || relation.through.length === 0 || relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
1692
+ return void 0;
1693
+ }
1694
+ const sourceKeys = toColumnList(relation.sourceKey);
1695
+ const sourceColumn = singleResolvedColumn(
1696
+ sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN]
1697
+ );
1698
+ const targetKeys = toColumnList(relation.targetKey);
1699
+ const targetColumn = singleResolvedColumn(
1700
+ targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN]
1701
+ );
1702
+ const throughSourceColumn = singleResolvedColumn(
1703
+ toColumnList(relation.foreignKey)
1704
+ );
1705
+ const throughTargetColumn = singleResolvedColumn(
1706
+ toColumnList(relation.otherKey)
1707
+ );
1708
+ if (!sourceColumn || !targetColumn || !throughSourceColumn || !throughTargetColumn) {
1709
+ return void 0;
1710
+ }
1711
+ const sourceProjection = findDirectSourceProjection(
1712
+ sourceTable,
1713
+ sourceColumn,
1714
+ directProjectionLineage
1715
+ );
1716
+ if (!sourceProjection) return void 0;
1717
+ return {
1718
+ relation: relation.name,
1719
+ sourceTable,
1720
+ targetTable: relation.target,
1721
+ sourceColumn: [sourceProjection.outputColumn],
1722
+ targetColumn: [targetColumn],
1723
+ hops: [],
1724
+ through: relation.through,
1725
+ throughSourceColumn,
1726
+ throughTargetColumn,
1727
+ cardinality: "many",
1728
+ relationType: "manyToMany"
1729
+ };
1730
+ }
1731
+ function recursiveVirtualRelationForBinding(relation, sourceTable, directProjectionLineage, pseudoColumns) {
1732
+ const direction = recursiveDirection(relation?.recursive);
1733
+ if (!relation || !direction) return void 0;
1734
+ if (relation.source !== relation.target || relation.source !== sourceTable) {
1735
+ return void 0;
1736
+ }
1737
+ const fkColumns = toColumnList(relation.foreignKey);
1738
+ const targetKeys = toColumnList(relation.targetKey);
1739
+ const pseudoColumn = pseudoColumns.find((candidate) => {
1740
+ const candidateFkColumns = fkColumns.length > 0 ? fkColumns : toColumnList(candidate.foreignKeyColumn);
1741
+ const candidateTargetColumns = targetKeys.length > 0 ? targetKeys : candidate.targetColumn !== void 0 ? [candidate.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
1742
+ const candidateSelfRefColumn = singleResolvedColumn(candidateFkColumns);
1743
+ const candidateTargetKeyColumn = singleResolvedColumn(
1744
+ candidateTargetColumns
1745
+ );
1746
+ return candidateSelfRefColumn !== void 0 && candidateTargetKeyColumn !== void 0 && pseudoColumnMatchesSelfRef(
1747
+ candidate,
1748
+ sourceTable,
1749
+ candidateSelfRefColumn,
1750
+ candidateTargetKeyColumn
1751
+ );
1752
+ });
1753
+ if (!pseudoColumn) return void 0;
1754
+ const selfRefColumn = singleResolvedColumn(
1755
+ fkColumns.length > 0 ? fkColumns : toColumnList(pseudoColumn.foreignKeyColumn)
1756
+ );
1757
+ const targetKeyColumn = singleResolvedColumn(
1758
+ targetKeys.length > 0 ? targetKeys : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN]
1759
+ );
1760
+ if (!selfRefColumn || !targetKeyColumn) return void 0;
1761
+ const relationName = direction === "up" ? pseudoColumn.ascendantKeyword : pseudoColumn.descendantKeyword;
1762
+ if (!relationName) return void 0;
1763
+ const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
1764
+ const sourceProjection = findDirectSourceProjection(
1765
+ sourceTable,
1766
+ seedColumn,
1767
+ directProjectionLineage
1768
+ );
1769
+ if (!sourceProjection) return void 0;
1770
+ return {
1771
+ relation: relationName.toLowerCase(),
1772
+ sourceTable,
1773
+ targetTable: relation.target,
1774
+ sourceColumn: [sourceProjection.outputColumn],
1775
+ targetColumn: [direction === "up" ? targetKeyColumn : selfRefColumn],
1776
+ hops: [],
1777
+ cardinality: "many",
1778
+ relationType: relation.type,
1779
+ recursive: {
1780
+ direction,
1781
+ maxDepth: recursiveMaxDepth(relation.recursive) ?? 10,
1782
+ selfRefColumn,
1783
+ targetKeyColumn
1784
+ }
1785
+ };
1786
+ }
1446
1787
  function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1447
1788
  if (!relation) return void 0;
1789
+ if (relation.type === "belongsToMany") {
1790
+ return manyToManyVirtualRelationForBinding(
1791
+ relation,
1792
+ sourceTable,
1793
+ directProjectionLineage
1794
+ );
1795
+ }
1796
+ if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
1797
+ return void 0;
1798
+ }
1448
1799
  if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
1449
1800
  return void 0;
1450
1801
  }
@@ -1493,6 +1844,13 @@ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDepe
1493
1844
  );
1494
1845
  for (const relationName of relationNames ?? []) {
1495
1846
  const relation = ctx.validator?.getRelation(sourceTable, relationName);
1847
+ const recursiveRelation = recursiveVirtualRelationForBinding(
1848
+ relation,
1849
+ sourceTable,
1850
+ outputSchema.directProjectionLineage ?? [],
1851
+ ctx.validator?.getPseudoColumns(sourceTable) ?? []
1852
+ );
1853
+ if (recursiveRelation) scalarRelations.push(recursiveRelation);
1496
1854
  const virtualRelation = virtualRelationForBinding(
1497
1855
  relation,
1498
1856
  sourceTable,
@@ -1527,6 +1885,15 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
1527
1885
  }
1528
1886
  const firstSegment = segments[0];
1529
1887
  if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1888
+ if (segments.length === 2 && resolveBindingRecursiveRelationColumn(
1889
+ ctx,
1890
+ bindingName,
1891
+ firstSegment,
1892
+ segments[1],
1893
+ expr.depthHint
1894
+ )) {
1895
+ return;
1896
+ }
1530
1897
  assertNoBindingRelationConstruct(
1531
1898
  ctx,
1532
1899
  bindingName,
@@ -1757,12 +2124,20 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1757
2124
  const segments = item.expression.segments;
1758
2125
  const firstSegment = segments[0];
1759
2126
  if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1760
- assertNoBindingRelationConstruct(
2127
+ if (segments.length !== 2 || !resolveBindingRecursiveRelationColumn(
1761
2128
  ctx,
1762
2129
  bindingName,
1763
- "use pseudo-column traversals",
1764
- firstSegment
1765
- );
2130
+ firstSegment,
2131
+ segments[1],
2132
+ item.expression.depthHint
2133
+ )) {
2134
+ assertNoBindingRelationConstruct(
2135
+ ctx,
2136
+ bindingName,
2137
+ "use pseudo-column traversals",
2138
+ firstSegment
2139
+ );
2140
+ }
1766
2141
  } else {
1767
2142
  resolveBindingRelationColumn(
1768
2143
  ctx,
@@ -2155,44 +2530,78 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
2155
2530
  if (bindingColumns2 !== void 0) return bindingColumns2;
2156
2531
  const columns = ctx.validator?.getTableColumns(intent.from);
2157
2532
  if (columns !== void 0) return columns;
2158
- throw new NqlSemanticException(
2159
- NqlErrorCodes.SEM_INVALID_SYNTAX,
2533
+ throw new UnresolvedSelectAllOutputSchemaError(
2160
2534
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
2161
2535
  );
2162
2536
  }
2163
- function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
2537
+ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = [], sourceBindingSchemas) {
2164
2538
  const columns = [];
2165
2539
  const seen = /* @__PURE__ */ new Set();
2166
2540
  const directProjectionLineage = [];
2541
+ const typeCandidates = [];
2542
+ const sourceIsPhysicalTable = ctx.validator?.hasPhysicalTable(intent.from) ?? false;
2543
+ const sourceBindingSchema = sourceIsPhysicalTable ? void 0 : sourceBindingSchemas?.get(intent.from);
2167
2544
  const addColumn = (column) => addUnique(columns, seen, column);
2168
2545
  const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
2169
2546
  const addDirectProjection = (outputColumn, sourceColumn) => {
2547
+ const resolvedSourceColumn = resolveSourceColumn(sourceColumn);
2548
+ let typeInfo;
2549
+ let untypeableReason = "unresolvable-source";
2550
+ if (sourceIsPhysicalTable) {
2551
+ typeInfo = ctx.validator?.getTableColumnType(
2552
+ intent.from,
2553
+ resolvedSourceColumn
2554
+ );
2555
+ } else if (sourceBindingSchema?.columnTypes) {
2556
+ typeInfo = sourceBindingSchema.columnTypes[resolvedSourceColumn];
2557
+ } else if (sourceBindingSchema?.columnTypesUnavailable) {
2558
+ untypeableReason = sourceBindingSchema.columnTypesUnavailable.reason;
2559
+ }
2560
+ typeCandidates.push(
2561
+ typeInfo !== void 0 ? { column: outputColumn, typed: typeInfo } : { column: outputColumn, untypeable: untypeableReason }
2562
+ );
2170
2563
  if (!addColumn(outputColumn)) return;
2171
2564
  directProjectionLineage.push({
2172
2565
  kind: "directProjection",
2173
2566
  sourceTable: intent.from,
2174
- sourceColumn: resolveSourceColumn(sourceColumn),
2567
+ sourceColumn: resolvedSourceColumn,
2175
2568
  outputColumn
2176
2569
  });
2177
2570
  };
2571
+ const addUntypedColumn = (outputColumn, reason) => {
2572
+ typeCandidates.push({ column: outputColumn, untypeable: reason });
2573
+ addColumn(outputColumn);
2574
+ };
2575
+ const addCountAggregateColumn = (outputColumn) => {
2576
+ typeCandidates.push({
2577
+ column: outputColumn,
2578
+ typed: { kind: "aggregate", fn: "count" }
2579
+ });
2580
+ addColumn(outputColumn);
2581
+ };
2178
2582
  const addSourceColumns = () => {
2179
2583
  for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
2180
2584
  addDirectProjection(column, column);
2181
2585
  }
2182
2586
  };
2183
- const { select } = intent;
2184
- if (!select || select.type === "all") {
2185
- addSourceColumns();
2186
- const outputSchema2 = { columns, directProjectionLineage };
2587
+ const finalizeOutputSchema = () => {
2588
+ const outputSchema = { columns, directProjectionLineage };
2589
+ const typesResult = buildBindingColumnTypes(columns, typeCandidates);
2187
2590
  return {
2188
- columns: outputSchema2.columns,
2591
+ columns: outputSchema.columns,
2189
2592
  relationFilters: getBindingRelationFilterMetadata(
2190
2593
  intent,
2191
2594
  ctx,
2192
- outputSchema2,
2595
+ outputSchema,
2193
2596
  bindingDependencies
2194
- )
2597
+ ),
2598
+ ..."columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable }
2195
2599
  };
2600
+ };
2601
+ const { select } = intent;
2602
+ if (!select || select.type === "all") {
2603
+ addSourceColumns();
2604
+ return finalizeOutputSchema();
2196
2605
  }
2197
2606
  if (select.type === "fields") {
2198
2607
  for (const field of select.fields) {
@@ -2202,16 +2611,7 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2202
2611
  addDirectProjection(field, field);
2203
2612
  }
2204
2613
  }
2205
- const outputSchema2 = { columns, directProjectionLineage };
2206
- return {
2207
- columns: outputSchema2.columns,
2208
- relationFilters: getBindingRelationFilterMetadata(
2209
- intent,
2210
- ctx,
2211
- outputSchema2,
2212
- bindingDependencies
2213
- )
2214
- };
2614
+ return finalizeOutputSchema();
2215
2615
  }
2216
2616
  if (select.type === "aggregate") {
2217
2617
  for (const field of select.fields ?? []) {
@@ -2224,18 +2624,13 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2224
2624
  `Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
2225
2625
  );
2226
2626
  }
2227
- addColumn(aggregate.as);
2627
+ if (aggregate.function === "count") {
2628
+ addCountAggregateColumn(aggregate.as);
2629
+ } else {
2630
+ addUntypedColumn(aggregate.as, "unsupported-aggregate");
2631
+ }
2228
2632
  }
2229
- const outputSchema2 = { columns, directProjectionLineage };
2230
- return {
2231
- columns: outputSchema2.columns,
2232
- relationFilters: getBindingRelationFilterMetadata(
2233
- intent,
2234
- ctx,
2235
- outputSchema2,
2236
- bindingDependencies
2237
- )
2238
- };
2633
+ return finalizeOutputSchema();
2239
2634
  }
2240
2635
  for (const expr of select.columns) {
2241
2636
  if (expr.kind === "column" && expr.column === "*") {
@@ -2259,20 +2654,16 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
2259
2654
  addDirectProjection(outputColumn, expr.column);
2260
2655
  } else if (expr.kind === "columnAlias") {
2261
2656
  addDirectProjection(outputColumn, expr.column);
2657
+ } else if (expr.kind === "aggregate" && expr.function === "count") {
2658
+ addCountAggregateColumn(outputColumn);
2262
2659
  } else {
2263
- addColumn(outputColumn);
2660
+ let reason = "computed-expression";
2661
+ if (expr.kind === "relationColumn") reason = "relation-column";
2662
+ else if (expr.kind === "aggregate") reason = "unsupported-aggregate";
2663
+ addUntypedColumn(outputColumn, reason);
2264
2664
  }
2265
2665
  }
2266
- const outputSchema = { columns, directProjectionLineage };
2267
- return {
2268
- columns: outputSchema.columns,
2269
- relationFilters: getBindingRelationFilterMetadata(
2270
- intent,
2271
- ctx,
2272
- outputSchema,
2273
- bindingDependencies
2274
- )
2275
- };
2666
+ return finalizeOutputSchema();
2276
2667
  }
2277
2668
  function validateNqlExpressionPaths(expr, ctx) {
2278
2669
  switch (expr.type) {
@@ -3539,12 +3930,6 @@ function compileMultiSegmentPath(expr, item, ctx) {
3539
3930
  const segments = expr.segments;
3540
3931
  const firstSegmentLower = segments[0].toLowerCase();
3541
3932
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
3542
- assertNoBindingRelationConstruct(
3543
- ctx,
3544
- ctx.currentFromTable,
3545
- "use pseudo-column traversals",
3546
- segments[0]
3547
- );
3548
3933
  const firstSegment = firstSegmentLower;
3549
3934
  const depthHint = expr.depthHint;
3550
3935
  if (depthHint !== void 0) {
@@ -3571,6 +3956,44 @@ function compileMultiSegmentPath(expr, item, ctx) {
3571
3956
  );
3572
3957
  }
3573
3958
  const targetColumn = segments[i];
3959
+ const bindingRelation2 = traversals.length === 1 ? resolveBindingRecursiveRelationColumn(
3960
+ ctx,
3961
+ ctx.currentFromTable,
3962
+ firstSegment,
3963
+ targetColumn,
3964
+ depthHint
3965
+ ) : void 0;
3966
+ if (bindingRelation2) {
3967
+ const relationColumnIntent2 = {
3968
+ kind: "relationColumn",
3969
+ relation: bindingRelation2.relation,
3970
+ column: targetColumn,
3971
+ as: item.alias ?? `${bindingRelation2.relation}.${targetColumn}`
3972
+ };
3973
+ return markNqlTrustedRelationFilter(relationColumnIntent2, {
3974
+ relation: bindingRelation2.relation,
3975
+ targetTable: bindingRelation2.targetTable,
3976
+ sourceColumn: bindingRelation2.sourceColumn,
3977
+ targetColumn: bindingRelation2.targetColumn,
3978
+ hops: bindingRelation2.hops,
3979
+ selectedColumn: targetColumn,
3980
+ ...bindingRelation2.cardinality !== void 0 && {
3981
+ cardinality: bindingRelation2.cardinality
3982
+ },
3983
+ ...bindingRelation2.relationType !== void 0 && {
3984
+ relationType: bindingRelation2.relationType
3985
+ },
3986
+ ...bindingRelation2.recursive !== void 0 && {
3987
+ recursive: bindingRelation2.recursive
3988
+ }
3989
+ });
3990
+ }
3991
+ assertNoBindingRelationConstruct(
3992
+ ctx,
3993
+ ctx.currentFromTable,
3994
+ "use pseudo-column traversals",
3995
+ segments[0]
3996
+ );
3574
3997
  if (ctx.currentFromTable) {
3575
3998
  validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
3576
3999
  }
@@ -3627,12 +4050,24 @@ function compileMultiSegmentPath(expr, item, ctx) {
3627
4050
  sourceColumn: bindingRelation.sourceColumn,
3628
4051
  targetColumn: bindingRelation.targetColumn,
3629
4052
  hops: bindingRelation.hops,
4053
+ ...bindingRelation.through !== void 0 && {
4054
+ through: bindingRelation.through
4055
+ },
4056
+ ...bindingRelation.throughSourceColumn !== void 0 && {
4057
+ throughSourceColumn: bindingRelation.throughSourceColumn
4058
+ },
4059
+ ...bindingRelation.throughTargetColumn !== void 0 && {
4060
+ throughTargetColumn: bindingRelation.throughTargetColumn
4061
+ },
3630
4062
  selectedColumn: column,
3631
4063
  ...bindingRelation.cardinality !== void 0 && {
3632
4064
  cardinality: bindingRelation.cardinality
3633
4065
  },
3634
4066
  ...bindingRelation.relationType !== void 0 && {
3635
4067
  relationType: bindingRelation.relationType
4068
+ },
4069
+ ...bindingRelation.recursive !== void 0 && {
4070
+ recursive: bindingRelation.recursive
3636
4071
  }
3637
4072
  }) : relationColumnIntent;
3638
4073
  }
@@ -3781,16 +4216,17 @@ function allowsInternalParams(options) {
3781
4216
  return internalOptions?.allowInternalParams === true;
3782
4217
  }
3783
4218
  function isUnresolvedSelectAllOutputSchemaError(error) {
3784
- return error instanceof NqlSemanticException && error.message.includes("from SELECT *") && error.message.includes("without a concrete table schema");
4219
+ return error instanceof UnresolvedSelectAllOutputSchemaError;
3785
4220
  }
3786
- function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
4221
+ function programSequenceStepFromResult(result, bindName, final, bindingDependencies, snapshotReadBindings) {
3787
4222
  if (result.query) {
3788
4223
  return {
3789
4224
  kind: "query",
3790
4225
  query: result.query,
3791
4226
  ...bindName !== void 0 && { bindName },
3792
4227
  final,
3793
- bindingDependencies
4228
+ bindingDependencies,
4229
+ ...bindName !== void 0 && snapshotReadBindings.has(bindName) && { snapshot: true }
3794
4230
  };
3795
4231
  }
3796
4232
  if (result.mutation) {
@@ -3810,6 +4246,34 @@ function stringArraysEqual(left, right) {
3810
4246
  function isNqlAstRecord(value) {
3811
4247
  return typeof value === "object" && value !== null;
3812
4248
  }
4249
+ function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding, outputSchema) {
4250
+ if (!sourceWasBinding && ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
4251
+ return {
4252
+ supported: false,
4253
+ reason: `no physical model table source '${query.from}'`
4254
+ };
4255
+ }
4256
+ if (outputSchema?.columnTypes) {
4257
+ return { supported: true };
4258
+ }
4259
+ const untypeable = outputSchema?.columnTypesUnavailable;
4260
+ if (untypeable?.reason === "unsupported-aggregate") {
4261
+ return {
4262
+ supported: false,
4263
+ reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
4264
+ };
4265
+ }
4266
+ if (untypeable) {
4267
+ return {
4268
+ supported: false,
4269
+ reason: `${untypeable.reason} column '${untypeable.column}'`
4270
+ };
4271
+ }
4272
+ if (sourceWasBinding) {
4273
+ return { supported: false, reason: "a binding source" };
4274
+ }
4275
+ return { supported: false, reason: "aliased/computed/aggregate columns" };
4276
+ }
3813
4277
  function addReadBindingReference(references, readBindingNames, name) {
3814
4278
  if (typeof name === "string" && readBindingNames.has(name)) {
3815
4279
  references.add(name);
@@ -3973,23 +4437,31 @@ function collectTransitiveReadBindingDependencies(directReferences, bindingDepen
3973
4437
  }
3974
4438
  return ordered;
3975
4439
  }
3976
- function rejectReadBindingReferenceAcrossMutation(bindName, definitionIndex, mutationIndex, referenceIndex, statementCount) {
4440
+ function rejectReadBindingReferenceAcrossMutation(bindName, reason) {
3977
4441
  throw new NqlSemanticException(
3978
4442
  NqlErrorCodes.SEM_INVALID_SYNTAX,
3979
- `read binding referenced across a mutation (#186): binding '${bindName}' is defined by read-only statement ${definitionIndex + 1} of ${statementCount} and referenced by statement ${referenceIndex + 1} after mutation statement ${mutationIndex + 1}. Read-only bindings are not materialized snapshots; move the reference before the mutation or bind the mutation RETURNING result instead.`
4443
+ `read binding referenced across a mutation: unsupported snapshot shape (#186): snapshotting currently supports only a direct single-table column projection; binding '${bindName}' has ${reason}, so it cannot be materialized - move the reference before the mutation, or bind the mutation RETURNING result.`
3980
4444
  );
3981
4445
  }
3982
- function rejectInvalidReadBindingDependencies(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, referenceIndex, statementCount) {
4446
+ function markReadBindingReferenceAcrossMutation(bindName, snapshotReadBindings, readBindingSnapshotShapes) {
4447
+ const snapshotShape = readBindingSnapshotShapes.get(bindName);
4448
+ if (snapshotShape?.supported !== true) {
4449
+ rejectReadBindingReferenceAcrossMutation(
4450
+ bindName,
4451
+ snapshotShape?.reason ?? "an unavailable output shape"
4452
+ );
4453
+ }
4454
+ snapshotReadBindings.add(bindName);
4455
+ }
4456
+ function markReadBindingDependenciesRequiringSnapshot(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, snapshotReadBindings, readBindingSnapshotShapes) {
3983
4457
  for (const referencedBindName of statementBindingDependencies) {
3984
4458
  const definitionIndex = readBindingDefinitions.get(referencedBindName);
3985
4459
  if (definitionIndex === void 0) continue;
3986
4460
  if (lastMutationStatement > definitionIndex) {
3987
- rejectReadBindingReferenceAcrossMutation(
4461
+ markReadBindingReferenceAcrossMutation(
3988
4462
  referencedBindName,
3989
- definitionIndex,
3990
- lastMutationStatement,
3991
- referenceIndex,
3992
- statementCount
4463
+ snapshotReadBindings,
4464
+ readBindingSnapshotShapes
3993
4465
  );
3994
4466
  }
3995
4467
  }
@@ -4043,6 +4515,7 @@ var NqlCompiler = class {
4043
4515
  } finally {
4044
4516
  this.ctx.bindingOutputColumns.clear();
4045
4517
  this.ctx.bindingRelationFilters.clear();
4518
+ this.ctx.lastMutationReturningItems = void 0;
4046
4519
  this.ctx.validator?.clearVirtualBindingTables();
4047
4520
  }
4048
4521
  }
@@ -4069,6 +4542,8 @@ var NqlCompiler = class {
4069
4542
  const seenBindNames = /* @__PURE__ */ new Map();
4070
4543
  const nqlProgramSequence = [];
4071
4544
  const readBindingDefinitions = /* @__PURE__ */ new Map();
4545
+ const readBindingSnapshotShapes = /* @__PURE__ */ new Map();
4546
+ const snapshotReadBindings = /* @__PURE__ */ new Set();
4072
4547
  const definedBindingNames = /* @__PURE__ */ new Set();
4073
4548
  const bindingDependencies = /* @__PURE__ */ new Map();
4074
4549
  let lastMutationStatement = -1;
@@ -4094,12 +4569,12 @@ var NqlCompiler = class {
4094
4569
  readBindingReferences,
4095
4570
  bindingDependencies
4096
4571
  );
4097
- rejectInvalidReadBindingDependencies(
4572
+ markReadBindingDependenciesRequiringSnapshot(
4098
4573
  statementBindingDependencies,
4099
4574
  readBindingDefinitions,
4100
4575
  lastMutationStatement,
4101
- i,
4102
- program.statements.length
4576
+ snapshotReadBindings,
4577
+ readBindingSnapshotShapes
4103
4578
  );
4104
4579
  lastResult = this.compileSingleStatement(stmt, bindings);
4105
4580
  collectCompileResultReadBindingReferences(
@@ -4111,24 +4586,36 @@ var NqlCompiler = class {
4111
4586
  readBindingReferences,
4112
4587
  bindingDependencies
4113
4588
  );
4114
- rejectInvalidReadBindingDependencies(
4589
+ markReadBindingDependenciesRequiringSnapshot(
4115
4590
  statementBindingDependencies,
4116
4591
  readBindingDefinitions,
4117
4592
  lastMutationStatement,
4118
- i,
4119
- program.statements.length
4593
+ snapshotReadBindings,
4594
+ readBindingSnapshotShapes
4120
4595
  );
4121
4596
  if (stmt.type === "mutationPipeline") {
4122
4597
  lastMutationStatement = i;
4123
4598
  }
4124
4599
  if (bindName) {
4125
4600
  if (lastResult.query) {
4601
+ const sourceWasBinding = definedBindingNames.has(
4602
+ lastResult.query.from
4603
+ );
4126
4604
  const outputSchema = this.registerQueryBindingOutputSchema(
4127
4605
  bindName,
4128
4606
  lastResult.query,
4129
4607
  bindingOutputSchemas,
4130
4608
  statementBindingDependencies
4131
4609
  );
4610
+ readBindingSnapshotShapes.set(
4611
+ bindName,
4612
+ classifyReadBindingSnapshotShape(
4613
+ lastResult.query,
4614
+ this.ctx,
4615
+ sourceWasBinding,
4616
+ outputSchema
4617
+ )
4618
+ );
4132
4619
  bindings.set(bindName, lastResult.query);
4133
4620
  if (outputSchema) {
4134
4621
  this.ctx.validator?.addVirtualBindingTable(
@@ -4176,7 +4663,8 @@ var NqlCompiler = class {
4176
4663
  lastResult,
4177
4664
  bindName,
4178
4665
  i === program.statements.length - 1,
4179
- statementBindingDependencies
4666
+ statementBindingDependencies,
4667
+ snapshotReadBindings
4180
4668
  );
4181
4669
  if (sequenceStep !== void 0) {
4182
4670
  nqlProgramSequence.push(sequenceStep);
@@ -4192,14 +4680,19 @@ var NqlCompiler = class {
4192
4680
  }
4193
4681
  const hasMutationBindings = mutationBindings.size > 0;
4194
4682
  const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
4195
- const hasNqlProgramSequence = nqlProgramSequence.length === program.statements.length;
4683
+ const taggedNqlProgramSequence = nqlProgramSequence.map(
4684
+ (step) => step.kind === "query" && step.bindName !== void 0 && snapshotReadBindings.has(step.bindName) ? { ...step, snapshot: true } : step
4685
+ );
4686
+ const hasNqlProgramSequence = taggedNqlProgramSequence.length === program.statements.length;
4196
4687
  if (bindings.size > 0) {
4197
4688
  return {
4198
4689
  ...lastResult,
4199
4690
  bindings,
4200
4691
  ...hasBindingOutputSchemas && { bindingOutputSchemas },
4201
4692
  ...hasMutationBindings && { mutationBindings },
4202
- ...hasNqlProgramSequence && { nqlProgramSequence }
4693
+ ...hasNqlProgramSequence && {
4694
+ nqlProgramSequence: taggedNqlProgramSequence
4695
+ }
4203
4696
  };
4204
4697
  }
4205
4698
  return lastResult;
@@ -4210,7 +4703,12 @@ var NqlCompiler = class {
4210
4703
  query,
4211
4704
  this.ctx,
4212
4705
  bindName,
4213
- bindingDependencies
4706
+ bindingDependencies,
4707
+ // #213 B2: earlier bindings are already registered here (this
4708
+ // map is populated in program-statement order), so a
4709
+ // transitive `from` = binding chains through the SOURCE's
4710
+ // typed schema instead of staying untypeable.
4711
+ bindingOutputSchemas
4214
4712
  );
4215
4713
  bindingOutputSchemas.set(bindName, outputSchema);
4216
4714
  this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
@@ -4250,20 +4748,50 @@ var NqlCompiler = class {
4250
4748
  const returning = mutation.returning;
4251
4749
  if (!returning || returning.length === 0) return void 0;
4252
4750
  if (returning.includes("*")) {
4253
- const columns = this.ctx.validator?.getTableColumns(mutation.table);
4254
- if (columns !== void 0) return { columns };
4751
+ const columns2 = this.ctx.validator?.getTableColumns(mutation.table);
4752
+ if (columns2 !== void 0) {
4753
+ return {
4754
+ columns: columns2,
4755
+ ...this.buildMutationReturningColumnTypes(mutation.table, columns2)
4756
+ };
4757
+ }
4255
4758
  if (!this.ctx.validator) return void 0;
4256
4759
  throw new NqlSemanticException(
4257
4760
  NqlErrorCodes.SEM_INVALID_SYNTAX,
4258
4761
  `Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
4259
4762
  );
4260
4763
  }
4764
+ const columns = returning.map(
4765
+ (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
4766
+ );
4261
4767
  return {
4262
- columns: returning.map(
4263
- (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
4264
- )
4768
+ columns,
4769
+ ...this.buildMutationReturningColumnTypes(mutation.table, columns)
4265
4770
  };
4266
4771
  }
4772
+ /**
4773
+ * Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
4774
+ * binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
4775
+ * (alias-aware, positional) — NEVER the collapsed `columns` names — so an
4776
+ * alias colliding with a real column (`returning email as name`) is
4777
+ * flagged 'aliased-returning' rather than silently mistyped as the
4778
+ * colliding column's type.
4779
+ */
4780
+ buildMutationReturningColumnTypes(table, columns) {
4781
+ const items = this.ctx.lastMutationReturningItems;
4782
+ const candidates = columns.map(
4783
+ (column, index) => {
4784
+ const item = items !== void 0 && items !== "star" ? items[index] : void 0;
4785
+ if (item?.aliased) {
4786
+ return { column, untypeable: "aliased-returning" };
4787
+ }
4788
+ const typeInfo = this.ctx.validator?.getTableColumnType(table, column);
4789
+ return typeInfo !== void 0 ? { column, typed: typeInfo } : { column, untypeable: "unresolvable-source" };
4790
+ }
4791
+ );
4792
+ const typesResult = buildBindingColumnTypes(columns, candidates);
4793
+ return "columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable };
4794
+ }
4267
4795
  compileSingleStatement(stmt, bindings) {
4268
4796
  if (stmt.type === "withQuery") {
4269
4797
  return compileWithQuery(stmt, this.ctx, this.fns);