@dbsp/nql 1.6.0 → 1.7.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,30 @@ var NqlSemanticException = class extends Error {
43
43
  this.relatedSymbol = relatedSymbol;
44
44
  }
45
45
  };
46
+ var DEFAULT_RELATION_TARGET_COLUMN = "id";
47
+ function relationForeignKeys(relation) {
48
+ if (!relation) return void 0;
49
+ const columns = toColumnList(relation.foreignKey);
50
+ return columns.length > 0 ? columns : void 0;
51
+ }
52
+ function relationCardinality(relation) {
53
+ return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
54
+ }
55
+ function scalarRelationJoinColumns(relation) {
56
+ if (!relation) return void 0;
57
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
58
+ return void 0;
59
+ }
60
+ const fkColumns = relationForeignKeys(relation);
61
+ if (!fkColumns) return void 0;
62
+ const sourceKeys = toColumnList(relation.sourceKey);
63
+ const targetKeys = toColumnList(relation.targetKey);
64
+ const sourceJoinColumn = relation.type === "belongsTo" ? fkColumns : sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN];
65
+ const targetJoinColumn = relation.type === "belongsTo" ? targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN] : fkColumns;
66
+ return { sourceJoinColumn, targetJoinColumn };
67
+ }
68
+
69
+ // src/compiler/column-validator.ts
46
70
  var ColumnValidator = class _ColumnValidator {
47
71
  constructor(schema) {
48
72
  this.schema = schema;
@@ -90,6 +114,12 @@ var ColumnValidator = class _ColumnValidator {
90
114
  if (virtualColumns) return virtualColumns;
91
115
  return this.schema.getTable(name)?.columns.map((column) => column.name);
92
116
  }
117
+ getPhysicalTableColumns(name) {
118
+ return this.schema.getTable(name)?.columns.map((column) => column.name);
119
+ }
120
+ getPseudoColumns(name) {
121
+ return this.schema.getTable(name)?.pseudoColumns ?? [];
122
+ }
93
123
  hasPhysicalTable(name) {
94
124
  return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
95
125
  }
@@ -233,10 +263,54 @@ var ColumnValidator = class _ColumnValidator {
233
263
  if (!sourceTable) {
234
264
  return "the binding source table could not be proven";
235
265
  }
266
+ const recursiveReason = this.explainVirtualBindingRecursiveScalarRelationRejection(
267
+ bindingName,
268
+ relationName,
269
+ sourceTable,
270
+ metadata
271
+ );
272
+ if (recursiveReason) return recursiveReason;
236
273
  const relation = this.getRelation(sourceTable, relationName);
237
274
  if (!relation) {
238
275
  return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
239
276
  }
277
+ if (relation.type === "belongsToMany") {
278
+ if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
279
+ return `relation '${relationName}' is recursive/self-referential; binding relation columns for recursive relations require recursive CTE handling and are not supported (ref-#193)`;
280
+ }
281
+ if (typeof relation.through !== "string" || relation.through.length === 0) {
282
+ return `relation '${relationName}' is many-to-many but does not declare a resolvable junction table (ref-#192)`;
283
+ }
284
+ const sourceColumns2 = toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
285
+ const throughSourceColumns = toColumnList(relation.foreignKey);
286
+ const throughTargetColumns = toColumnList(relation.otherKey);
287
+ const targetColumns = toColumnList(relation.targetKey).length > 0 ? toColumnList(relation.targetKey) : ["id"];
288
+ if (sourceColumns2.length !== 1) {
289
+ return `relation '${relationName}' source key is composite; binding many-to-many relation columns require a single source key column (ref-#179)`;
290
+ }
291
+ if (throughSourceColumns.length !== 1) {
292
+ 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)`;
293
+ }
294
+ if (throughTargetColumns.length !== 1) {
295
+ 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)`;
296
+ }
297
+ if (targetColumns.length !== 1) {
298
+ return `relation '${relationName}' target key is composite; binding many-to-many relation columns require a single target key column (ref-#179)`;
299
+ }
300
+ const [sourceColumn] = sourceColumns2;
301
+ if (sourceColumn === void 0) {
302
+ return `relation '${relationName}' source key is missing; binding many-to-many relation columns require a single source key column (ref-#179)`;
303
+ }
304
+ const directProjection = metadata.directProjectionLineage?.find(
305
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
306
+ );
307
+ if (!directProjection) {
308
+ const available3 = this.virtualBindingTables.get(bindingName)?.join(", ");
309
+ return `relation '${relationName}' source key column '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available3 ? ` (available columns: ${available3})` : ""}`;
310
+ }
311
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
312
+ return `relation '${relationName}' many-to-many source column '${sourceColumn}' is not available through binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
313
+ }
240
314
  const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
241
315
  relationName,
242
316
  relation
@@ -257,6 +331,64 @@ var ColumnValidator = class _ColumnValidator {
257
331
  const available = this.virtualBindingTables.get(bindingName)?.join(", ");
258
332
  return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
259
333
  }
334
+ static recursiveDirection(value) {
335
+ if (value === null || typeof value !== "object") return void 0;
336
+ const direction = value.direction;
337
+ if (direction === "up" || direction === "ancestors") return "up";
338
+ if (direction === "down" || direction === "descendants") return "down";
339
+ return void 0;
340
+ }
341
+ explainVirtualBindingRecursiveScalarRelationRejection(bindingName, relationName, sourceTable, metadata) {
342
+ const lowerRelationName = relationName.toLowerCase();
343
+ const pseudoColumn = this.getPseudoColumns(sourceTable).find(
344
+ (candidate) => candidate.ascendantKeyword?.toLowerCase() === lowerRelationName || candidate.descendantKeyword?.toLowerCase() === lowerRelationName
345
+ );
346
+ if (!pseudoColumn) return void 0;
347
+ const direction = pseudoColumn.ascendantKeyword?.toLowerCase() === lowerRelationName ? "up" : "down";
348
+ const recursiveRelations = this.getRelationsFrom(sourceTable).filter(
349
+ (relation) => relation.recursive !== void 0 && relation.source === relation.target && relation.source === sourceTable && _ColumnValidator.recursiveDirection(relation.recursive) === direction
350
+ );
351
+ if (recursiveRelations.length === 0) {
352
+ return `recursive traversal '${relationName}' is missing recursive relation metadata for direction '${direction}' (ref-#193)`;
353
+ }
354
+ for (const relation of recursiveRelations) {
355
+ const relationFkColumns = toColumnList(relation.foreignKey);
356
+ const fkColumns = relationFkColumns.length > 0 ? relationFkColumns : toColumnList(pseudoColumn.foreignKeyColumn);
357
+ if (fkColumns.length !== 1) {
358
+ return `relation '${relation.name}' self-ref FK is composite or missing; binding recursive relation columns require a single self-ref FK column (ref-#193)`;
359
+ }
360
+ const relationTargetColumns = toColumnList(relation.targetKey);
361
+ const targetColumns = relationTargetColumns.length > 0 ? relationTargetColumns : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
362
+ if (targetColumns.length !== 1) {
363
+ return `relation '${relation.name}' target key is composite; binding recursive relation columns require a single target key column (ref-#193)`;
364
+ }
365
+ const selfRefColumn = fkColumns[0];
366
+ const targetKeyColumn = targetColumns[0];
367
+ if (selfRefColumn === void 0 || targetKeyColumn === void 0) {
368
+ 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)`;
369
+ }
370
+ if (pseudoColumn.foreignKeyColumn !== void 0 && !_ColumnValidator.columnsMatch(
371
+ pseudoColumn.foreignKeyColumn,
372
+ selfRefColumn
373
+ ) || pseudoColumn.targetColumn !== void 0 && !_ColumnValidator.columnsMatch(
374
+ pseudoColumn.targetColumn,
375
+ targetKeyColumn
376
+ )) {
377
+ continue;
378
+ }
379
+ const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
380
+ const directProjection = metadata.directProjectionLineage?.find(
381
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, seedColumn)
382
+ );
383
+ if (!directProjection) {
384
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
385
+ const seedLabel = direction === "up" ? "self-ref FK column" : "target key column";
386
+ return `recursive traversal '${relationName}' ${seedLabel} '${seedColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available ? ` (available columns: ${available})` : ""} (ref-#193)`;
387
+ }
388
+ return void 0;
389
+ }
390
+ return `recursive traversal '${relationName}' pseudo metadata does not match a single-column recursive self-reference (ref-#193)`;
391
+ }
260
392
  /**
261
393
  * Convert camelCase to snake_case for column name matching.
262
394
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -288,6 +420,14 @@ var ColumnValidator = class _ColumnValidator {
288
420
  (c) => _ColumnValidator.columnsMatch(column, c.name)
289
421
  )?.name;
290
422
  }
423
+ resolvePhysicalColumnName(table, column) {
424
+ if (column === "*") return column;
425
+ const tableInfo = this.schema.getTable(table);
426
+ if (!tableInfo) return void 0;
427
+ return tableInfo.columns.find(
428
+ (c) => _ColumnValidator.columnsMatch(column, c.name)
429
+ )?.name;
430
+ }
291
431
  validateColumn(table, column) {
292
432
  if (column === "*") return;
293
433
  const virtualColumns = this.virtualBindingTables.get(table);
@@ -354,28 +494,6 @@ var ColumnValidator = class _ColumnValidator {
354
494
  );
355
495
  }
356
496
  };
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
497
  var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
380
498
  "__proto__",
381
499
  "constructor",
@@ -652,6 +770,43 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
652
770
  hops
653
771
  };
654
772
  }
773
+ function resolveBindingRecursiveRelationColumn(ctx, bindingName, traversal, selectedColumn, maxDepth) {
774
+ if (!isBindingTable(ctx, bindingName)) return void 0;
775
+ const traversalName = traversal.toLowerCase();
776
+ const isRecursiveTraversal = ctx.recursiveKeywords.has(traversalName) || traversalName === "ascendant" || traversalName === "descendant";
777
+ const actualBindingName = bindingName;
778
+ if (!ctx.validator) {
779
+ if (!isRecursiveTraversal) return void 0;
780
+ throwBindingRelationColumn182(
781
+ actualBindingName,
782
+ traversalName,
783
+ "model metadata is not available"
784
+ );
785
+ }
786
+ const virtualRelation = ctx.validator.getVirtualBindingScalarRelation(
787
+ actualBindingName,
788
+ traversalName
789
+ );
790
+ if (!isRecursiveTraversal && !virtualRelation?.recursive) {
791
+ return void 0;
792
+ }
793
+ if (!virtualRelation?.recursive) {
794
+ const reason = ctx.validator.explainVirtualBindingScalarRelationRejection(
795
+ actualBindingName,
796
+ traversalName
797
+ );
798
+ throwBindingRelationColumn182(actualBindingName, traversalName, reason);
799
+ }
800
+ ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
801
+ if (maxDepth === void 0) return virtualRelation;
802
+ return {
803
+ ...virtualRelation,
804
+ recursive: {
805
+ ...virtualRelation.recursive,
806
+ maxDepth
807
+ }
808
+ };
809
+ }
655
810
  function assertNoBindingRelationPath(ctx, bindingName, path) {
656
811
  if (!isBindingTable(ctx, bindingName)) return;
657
812
  throw new NqlSemanticException(
@@ -1287,6 +1442,12 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
1287
1442
  ...NQL_SELECT_AGGREGATE_FUNCTIONS,
1288
1443
  ...NQL_SELECT_VALUE_FUNCTIONS
1289
1444
  ]);
1445
+ var UnresolvedSelectAllOutputSchemaError = class extends NqlSemanticException {
1446
+ constructor(message) {
1447
+ super(NqlErrorCodes.SEM_INVALID_SYNTAX, message);
1448
+ this.name = "UnresolvedSelectAllOutputSchemaError";
1449
+ }
1450
+ };
1290
1451
  function compileNestedQuery(query, ctx, fns, bindings) {
1291
1452
  const savedContext = {
1292
1453
  currentFromTable: ctx.currentFromTable,
@@ -1373,6 +1534,21 @@ function relationForeignKeysOnSource(relation) {
1373
1534
  const fkColumns = toColumnList(relation.foreignKey);
1374
1535
  return fkColumns.length > 0 ? fkColumns : void 0;
1375
1536
  }
1537
+ function recursiveDirection(recursive) {
1538
+ if (recursive === null || typeof recursive !== "object") return void 0;
1539
+ const direction = recursive.direction;
1540
+ if (direction === "up" || direction === "ancestors") return "up";
1541
+ if (direction === "down" || direction === "descendants") return "down";
1542
+ return void 0;
1543
+ }
1544
+ function recursiveMaxDepth(recursive) {
1545
+ if (recursive === null || typeof recursive !== "object") return void 0;
1546
+ const maxDepth = recursive.maxDepth;
1547
+ return typeof maxDepth === "number" && Number.isSafeInteger(maxDepth) && maxDepth > 0 ? maxDepth : void 0;
1548
+ }
1549
+ function pseudoColumnMatchesSelfRef(pseudoColumn, sourceTable, selfRefColumn, targetKeyColumn) {
1550
+ return (pseudoColumn.table === void 0 || pseudoColumn.table === sourceTable) && (pseudoColumn.foreignKeyColumn === void 0 || columnsMatch(pseudoColumn.foreignKeyColumn, selfRefColumn)) && (pseudoColumn.targetColumn === void 0 || columnsMatch(pseudoColumn.targetColumn, targetKeyColumn));
1551
+ }
1376
1552
  function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1377
1553
  if (!ctx.validator) return "model metadata is not available";
1378
1554
  if (!ctx.validator.hasQualifiedRelationLookup()) {
@@ -1443,8 +1619,119 @@ function virtualRelationForBinding(relation, sourceTable, directProjectionLineag
1443
1619
  cardinality: "one"
1444
1620
  };
1445
1621
  }
1622
+ function singleResolvedColumn(columns) {
1623
+ return columns.length === 1 ? columns[0] : void 0;
1624
+ }
1625
+ function manyToManyVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1626
+ if (relation?.type !== "belongsToMany") return void 0;
1627
+ if (typeof relation.through !== "string" || relation.through.length === 0 || relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
1628
+ return void 0;
1629
+ }
1630
+ const sourceKeys = toColumnList(relation.sourceKey);
1631
+ const sourceColumn = singleResolvedColumn(
1632
+ sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN]
1633
+ );
1634
+ const targetKeys = toColumnList(relation.targetKey);
1635
+ const targetColumn = singleResolvedColumn(
1636
+ targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN]
1637
+ );
1638
+ const throughSourceColumn = singleResolvedColumn(
1639
+ toColumnList(relation.foreignKey)
1640
+ );
1641
+ const throughTargetColumn = singleResolvedColumn(
1642
+ toColumnList(relation.otherKey)
1643
+ );
1644
+ if (!sourceColumn || !targetColumn || !throughSourceColumn || !throughTargetColumn) {
1645
+ return void 0;
1646
+ }
1647
+ const sourceProjection = findDirectSourceProjection(
1648
+ sourceTable,
1649
+ sourceColumn,
1650
+ directProjectionLineage
1651
+ );
1652
+ if (!sourceProjection) return void 0;
1653
+ return {
1654
+ relation: relation.name,
1655
+ sourceTable,
1656
+ targetTable: relation.target,
1657
+ sourceColumn: [sourceProjection.outputColumn],
1658
+ targetColumn: [targetColumn],
1659
+ hops: [],
1660
+ through: relation.through,
1661
+ throughSourceColumn,
1662
+ throughTargetColumn,
1663
+ cardinality: "many",
1664
+ relationType: "manyToMany"
1665
+ };
1666
+ }
1667
+ function recursiveVirtualRelationForBinding(relation, sourceTable, directProjectionLineage, pseudoColumns) {
1668
+ const direction = recursiveDirection(relation?.recursive);
1669
+ if (!relation || !direction) return void 0;
1670
+ if (relation.source !== relation.target || relation.source !== sourceTable) {
1671
+ return void 0;
1672
+ }
1673
+ const fkColumns = toColumnList(relation.foreignKey);
1674
+ const targetKeys = toColumnList(relation.targetKey);
1675
+ const pseudoColumn = pseudoColumns.find((candidate) => {
1676
+ const candidateFkColumns = fkColumns.length > 0 ? fkColumns : toColumnList(candidate.foreignKeyColumn);
1677
+ const candidateTargetColumns = targetKeys.length > 0 ? targetKeys : candidate.targetColumn !== void 0 ? [candidate.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
1678
+ const candidateSelfRefColumn = singleResolvedColumn(candidateFkColumns);
1679
+ const candidateTargetKeyColumn = singleResolvedColumn(
1680
+ candidateTargetColumns
1681
+ );
1682
+ return candidateSelfRefColumn !== void 0 && candidateTargetKeyColumn !== void 0 && pseudoColumnMatchesSelfRef(
1683
+ candidate,
1684
+ sourceTable,
1685
+ candidateSelfRefColumn,
1686
+ candidateTargetKeyColumn
1687
+ );
1688
+ });
1689
+ if (!pseudoColumn) return void 0;
1690
+ const selfRefColumn = singleResolvedColumn(
1691
+ fkColumns.length > 0 ? fkColumns : toColumnList(pseudoColumn.foreignKeyColumn)
1692
+ );
1693
+ const targetKeyColumn = singleResolvedColumn(
1694
+ targetKeys.length > 0 ? targetKeys : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN]
1695
+ );
1696
+ if (!selfRefColumn || !targetKeyColumn) return void 0;
1697
+ const relationName = direction === "up" ? pseudoColumn.ascendantKeyword : pseudoColumn.descendantKeyword;
1698
+ if (!relationName) return void 0;
1699
+ const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
1700
+ const sourceProjection = findDirectSourceProjection(
1701
+ sourceTable,
1702
+ seedColumn,
1703
+ directProjectionLineage
1704
+ );
1705
+ if (!sourceProjection) return void 0;
1706
+ return {
1707
+ relation: relationName.toLowerCase(),
1708
+ sourceTable,
1709
+ targetTable: relation.target,
1710
+ sourceColumn: [sourceProjection.outputColumn],
1711
+ targetColumn: [direction === "up" ? targetKeyColumn : selfRefColumn],
1712
+ hops: [],
1713
+ cardinality: "many",
1714
+ relationType: relation.type,
1715
+ recursive: {
1716
+ direction,
1717
+ maxDepth: recursiveMaxDepth(relation.recursive) ?? 10,
1718
+ selfRefColumn,
1719
+ targetKeyColumn
1720
+ }
1721
+ };
1722
+ }
1446
1723
  function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1447
1724
  if (!relation) return void 0;
1725
+ if (relation.type === "belongsToMany") {
1726
+ return manyToManyVirtualRelationForBinding(
1727
+ relation,
1728
+ sourceTable,
1729
+ directProjectionLineage
1730
+ );
1731
+ }
1732
+ if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
1733
+ return void 0;
1734
+ }
1448
1735
  if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
1449
1736
  return void 0;
1450
1737
  }
@@ -1493,6 +1780,13 @@ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDepe
1493
1780
  );
1494
1781
  for (const relationName of relationNames ?? []) {
1495
1782
  const relation = ctx.validator?.getRelation(sourceTable, relationName);
1783
+ const recursiveRelation = recursiveVirtualRelationForBinding(
1784
+ relation,
1785
+ sourceTable,
1786
+ outputSchema.directProjectionLineage ?? [],
1787
+ ctx.validator?.getPseudoColumns(sourceTable) ?? []
1788
+ );
1789
+ if (recursiveRelation) scalarRelations.push(recursiveRelation);
1496
1790
  const virtualRelation = virtualRelationForBinding(
1497
1791
  relation,
1498
1792
  sourceTable,
@@ -1527,6 +1821,15 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
1527
1821
  }
1528
1822
  const firstSegment = segments[0];
1529
1823
  if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1824
+ if (segments.length === 2 && resolveBindingRecursiveRelationColumn(
1825
+ ctx,
1826
+ bindingName,
1827
+ firstSegment,
1828
+ segments[1],
1829
+ expr.depthHint
1830
+ )) {
1831
+ return;
1832
+ }
1530
1833
  assertNoBindingRelationConstruct(
1531
1834
  ctx,
1532
1835
  bindingName,
@@ -1757,12 +2060,20 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1757
2060
  const segments = item.expression.segments;
1758
2061
  const firstSegment = segments[0];
1759
2062
  if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1760
- assertNoBindingRelationConstruct(
2063
+ if (segments.length !== 2 || !resolveBindingRecursiveRelationColumn(
1761
2064
  ctx,
1762
2065
  bindingName,
1763
- "use pseudo-column traversals",
1764
- firstSegment
1765
- );
2066
+ firstSegment,
2067
+ segments[1],
2068
+ item.expression.depthHint
2069
+ )) {
2070
+ assertNoBindingRelationConstruct(
2071
+ ctx,
2072
+ bindingName,
2073
+ "use pseudo-column traversals",
2074
+ firstSegment
2075
+ );
2076
+ }
1766
2077
  } else {
1767
2078
  resolveBindingRelationColumn(
1768
2079
  ctx,
@@ -2155,8 +2466,7 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
2155
2466
  if (bindingColumns2 !== void 0) return bindingColumns2;
2156
2467
  const columns = ctx.validator?.getTableColumns(intent.from);
2157
2468
  if (columns !== void 0) return columns;
2158
- throw new NqlSemanticException(
2159
- NqlErrorCodes.SEM_INVALID_SYNTAX,
2469
+ throw new UnresolvedSelectAllOutputSchemaError(
2160
2470
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
2161
2471
  );
2162
2472
  }
@@ -3539,12 +3849,6 @@ function compileMultiSegmentPath(expr, item, ctx) {
3539
3849
  const segments = expr.segments;
3540
3850
  const firstSegmentLower = segments[0].toLowerCase();
3541
3851
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
3542
- assertNoBindingRelationConstruct(
3543
- ctx,
3544
- ctx.currentFromTable,
3545
- "use pseudo-column traversals",
3546
- segments[0]
3547
- );
3548
3852
  const firstSegment = firstSegmentLower;
3549
3853
  const depthHint = expr.depthHint;
3550
3854
  if (depthHint !== void 0) {
@@ -3571,6 +3875,44 @@ function compileMultiSegmentPath(expr, item, ctx) {
3571
3875
  );
3572
3876
  }
3573
3877
  const targetColumn = segments[i];
3878
+ const bindingRelation2 = traversals.length === 1 ? resolveBindingRecursiveRelationColumn(
3879
+ ctx,
3880
+ ctx.currentFromTable,
3881
+ firstSegment,
3882
+ targetColumn,
3883
+ depthHint
3884
+ ) : void 0;
3885
+ if (bindingRelation2) {
3886
+ const relationColumnIntent2 = {
3887
+ kind: "relationColumn",
3888
+ relation: bindingRelation2.relation,
3889
+ column: targetColumn,
3890
+ as: item.alias ?? `${bindingRelation2.relation}.${targetColumn}`
3891
+ };
3892
+ return markNqlTrustedRelationFilter(relationColumnIntent2, {
3893
+ relation: bindingRelation2.relation,
3894
+ targetTable: bindingRelation2.targetTable,
3895
+ sourceColumn: bindingRelation2.sourceColumn,
3896
+ targetColumn: bindingRelation2.targetColumn,
3897
+ hops: bindingRelation2.hops,
3898
+ selectedColumn: targetColumn,
3899
+ ...bindingRelation2.cardinality !== void 0 && {
3900
+ cardinality: bindingRelation2.cardinality
3901
+ },
3902
+ ...bindingRelation2.relationType !== void 0 && {
3903
+ relationType: bindingRelation2.relationType
3904
+ },
3905
+ ...bindingRelation2.recursive !== void 0 && {
3906
+ recursive: bindingRelation2.recursive
3907
+ }
3908
+ });
3909
+ }
3910
+ assertNoBindingRelationConstruct(
3911
+ ctx,
3912
+ ctx.currentFromTable,
3913
+ "use pseudo-column traversals",
3914
+ segments[0]
3915
+ );
3574
3916
  if (ctx.currentFromTable) {
3575
3917
  validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
3576
3918
  }
@@ -3627,12 +3969,24 @@ function compileMultiSegmentPath(expr, item, ctx) {
3627
3969
  sourceColumn: bindingRelation.sourceColumn,
3628
3970
  targetColumn: bindingRelation.targetColumn,
3629
3971
  hops: bindingRelation.hops,
3972
+ ...bindingRelation.through !== void 0 && {
3973
+ through: bindingRelation.through
3974
+ },
3975
+ ...bindingRelation.throughSourceColumn !== void 0 && {
3976
+ throughSourceColumn: bindingRelation.throughSourceColumn
3977
+ },
3978
+ ...bindingRelation.throughTargetColumn !== void 0 && {
3979
+ throughTargetColumn: bindingRelation.throughTargetColumn
3980
+ },
3630
3981
  selectedColumn: column,
3631
3982
  ...bindingRelation.cardinality !== void 0 && {
3632
3983
  cardinality: bindingRelation.cardinality
3633
3984
  },
3634
3985
  ...bindingRelation.relationType !== void 0 && {
3635
3986
  relationType: bindingRelation.relationType
3987
+ },
3988
+ ...bindingRelation.recursive !== void 0 && {
3989
+ recursive: bindingRelation.recursive
3636
3990
  }
3637
3991
  }) : relationColumnIntent;
3638
3992
  }
@@ -3781,16 +4135,17 @@ function allowsInternalParams(options) {
3781
4135
  return internalOptions?.allowInternalParams === true;
3782
4136
  }
3783
4137
  function isUnresolvedSelectAllOutputSchemaError(error) {
3784
- return error instanceof NqlSemanticException && error.message.includes("from SELECT *") && error.message.includes("without a concrete table schema");
4138
+ return error instanceof UnresolvedSelectAllOutputSchemaError;
3785
4139
  }
3786
- function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
4140
+ function programSequenceStepFromResult(result, bindName, final, bindingDependencies, snapshotReadBindings) {
3787
4141
  if (result.query) {
3788
4142
  return {
3789
4143
  kind: "query",
3790
4144
  query: result.query,
3791
4145
  ...bindName !== void 0 && { bindName },
3792
4146
  final,
3793
- bindingDependencies
4147
+ bindingDependencies,
4148
+ ...bindName !== void 0 && snapshotReadBindings.has(bindName) && { snapshot: true }
3794
4149
  };
3795
4150
  }
3796
4151
  if (result.mutation) {
@@ -3810,6 +4165,79 @@ function stringArraysEqual(left, right) {
3810
4165
  function isNqlAstRecord(value) {
3811
4166
  return typeof value === "object" && value !== null;
3812
4167
  }
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;
4220
+ }
4221
+ return projectedColumnCount > 0;
4222
+ }
4223
+ function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding) {
4224
+ if (sourceWasBinding) {
4225
+ return { supported: false, reason: "a binding source" };
4226
+ }
4227
+ if (ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
4228
+ return {
4229
+ supported: false,
4230
+ reason: `no physical model table source '${query.from}'`
4231
+ };
4232
+ }
4233
+ if (!hasDirectPhysicalColumnProjection(query, ctx)) {
4234
+ return {
4235
+ supported: false,
4236
+ reason: "aliased/computed/aggregate columns"
4237
+ };
4238
+ }
4239
+ return { supported: true };
4240
+ }
3813
4241
  function addReadBindingReference(references, readBindingNames, name) {
3814
4242
  if (typeof name === "string" && readBindingNames.has(name)) {
3815
4243
  references.add(name);
@@ -3973,23 +4401,31 @@ function collectTransitiveReadBindingDependencies(directReferences, bindingDepen
3973
4401
  }
3974
4402
  return ordered;
3975
4403
  }
3976
- function rejectReadBindingReferenceAcrossMutation(bindName, definitionIndex, mutationIndex, referenceIndex, statementCount) {
4404
+ function rejectReadBindingReferenceAcrossMutation(bindName, reason) {
3977
4405
  throw new NqlSemanticException(
3978
4406
  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.`
4407
+ `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
4408
  );
3981
4409
  }
3982
- function rejectInvalidReadBindingDependencies(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, referenceIndex, statementCount) {
4410
+ function markReadBindingReferenceAcrossMutation(bindName, snapshotReadBindings, readBindingSnapshotShapes) {
4411
+ const snapshotShape = readBindingSnapshotShapes.get(bindName);
4412
+ if (snapshotShape?.supported !== true) {
4413
+ rejectReadBindingReferenceAcrossMutation(
4414
+ bindName,
4415
+ snapshotShape?.reason ?? "an unavailable output shape"
4416
+ );
4417
+ }
4418
+ snapshotReadBindings.add(bindName);
4419
+ }
4420
+ function markReadBindingDependenciesRequiringSnapshot(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, snapshotReadBindings, readBindingSnapshotShapes) {
3983
4421
  for (const referencedBindName of statementBindingDependencies) {
3984
4422
  const definitionIndex = readBindingDefinitions.get(referencedBindName);
3985
4423
  if (definitionIndex === void 0) continue;
3986
4424
  if (lastMutationStatement > definitionIndex) {
3987
- rejectReadBindingReferenceAcrossMutation(
4425
+ markReadBindingReferenceAcrossMutation(
3988
4426
  referencedBindName,
3989
- definitionIndex,
3990
- lastMutationStatement,
3991
- referenceIndex,
3992
- statementCount
4427
+ snapshotReadBindings,
4428
+ readBindingSnapshotShapes
3993
4429
  );
3994
4430
  }
3995
4431
  }
@@ -4069,6 +4505,8 @@ var NqlCompiler = class {
4069
4505
  const seenBindNames = /* @__PURE__ */ new Map();
4070
4506
  const nqlProgramSequence = [];
4071
4507
  const readBindingDefinitions = /* @__PURE__ */ new Map();
4508
+ const readBindingSnapshotShapes = /* @__PURE__ */ new Map();
4509
+ const snapshotReadBindings = /* @__PURE__ */ new Set();
4072
4510
  const definedBindingNames = /* @__PURE__ */ new Set();
4073
4511
  const bindingDependencies = /* @__PURE__ */ new Map();
4074
4512
  let lastMutationStatement = -1;
@@ -4094,12 +4532,12 @@ var NqlCompiler = class {
4094
4532
  readBindingReferences,
4095
4533
  bindingDependencies
4096
4534
  );
4097
- rejectInvalidReadBindingDependencies(
4535
+ markReadBindingDependenciesRequiringSnapshot(
4098
4536
  statementBindingDependencies,
4099
4537
  readBindingDefinitions,
4100
4538
  lastMutationStatement,
4101
- i,
4102
- program.statements.length
4539
+ snapshotReadBindings,
4540
+ readBindingSnapshotShapes
4103
4541
  );
4104
4542
  lastResult = this.compileSingleStatement(stmt, bindings);
4105
4543
  collectCompileResultReadBindingReferences(
@@ -4111,24 +4549,35 @@ var NqlCompiler = class {
4111
4549
  readBindingReferences,
4112
4550
  bindingDependencies
4113
4551
  );
4114
- rejectInvalidReadBindingDependencies(
4552
+ markReadBindingDependenciesRequiringSnapshot(
4115
4553
  statementBindingDependencies,
4116
4554
  readBindingDefinitions,
4117
4555
  lastMutationStatement,
4118
- i,
4119
- program.statements.length
4556
+ snapshotReadBindings,
4557
+ readBindingSnapshotShapes
4120
4558
  );
4121
4559
  if (stmt.type === "mutationPipeline") {
4122
4560
  lastMutationStatement = i;
4123
4561
  }
4124
4562
  if (bindName) {
4125
4563
  if (lastResult.query) {
4564
+ const sourceWasBinding = definedBindingNames.has(
4565
+ lastResult.query.from
4566
+ );
4126
4567
  const outputSchema = this.registerQueryBindingOutputSchema(
4127
4568
  bindName,
4128
4569
  lastResult.query,
4129
4570
  bindingOutputSchemas,
4130
4571
  statementBindingDependencies
4131
4572
  );
4573
+ readBindingSnapshotShapes.set(
4574
+ bindName,
4575
+ classifyReadBindingSnapshotShape(
4576
+ lastResult.query,
4577
+ this.ctx,
4578
+ sourceWasBinding
4579
+ )
4580
+ );
4132
4581
  bindings.set(bindName, lastResult.query);
4133
4582
  if (outputSchema) {
4134
4583
  this.ctx.validator?.addVirtualBindingTable(
@@ -4176,7 +4625,8 @@ var NqlCompiler = class {
4176
4625
  lastResult,
4177
4626
  bindName,
4178
4627
  i === program.statements.length - 1,
4179
- statementBindingDependencies
4628
+ statementBindingDependencies,
4629
+ snapshotReadBindings
4180
4630
  );
4181
4631
  if (sequenceStep !== void 0) {
4182
4632
  nqlProgramSequence.push(sequenceStep);
@@ -4192,14 +4642,19 @@ var NqlCompiler = class {
4192
4642
  }
4193
4643
  const hasMutationBindings = mutationBindings.size > 0;
4194
4644
  const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
4195
- const hasNqlProgramSequence = nqlProgramSequence.length === program.statements.length;
4645
+ const taggedNqlProgramSequence = nqlProgramSequence.map(
4646
+ (step) => step.kind === "query" && step.bindName !== void 0 && snapshotReadBindings.has(step.bindName) ? { ...step, snapshot: true } : step
4647
+ );
4648
+ const hasNqlProgramSequence = taggedNqlProgramSequence.length === program.statements.length;
4196
4649
  if (bindings.size > 0) {
4197
4650
  return {
4198
4651
  ...lastResult,
4199
4652
  bindings,
4200
4653
  ...hasBindingOutputSchemas && { bindingOutputSchemas },
4201
4654
  ...hasMutationBindings && { mutationBindings },
4202
- ...hasNqlProgramSequence && { nqlProgramSequence }
4655
+ ...hasNqlProgramSequence && {
4656
+ nqlProgramSequence: taggedNqlProgramSequence
4657
+ }
4203
4658
  };
4204
4659
  }
4205
4660
  return lastResult;