@dbsp/nql 1.5.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
@@ -1,5 +1,5 @@
1
- import { NQL_INTERNAL_COMPILER_OPTIONS, markNqlTrustedRelationFilter, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
2
- import { NQL_SELECT_AGGREGATE_FUNCTIONS, NQL_SELECT_VALUE_FUNCTIONS, NQL_SELECT_JSON_FUNCTIONS, NQL_SELECT_SCALAR_FUNCTIONS, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction, isParamIntent } from '@dbsp/types';
1
+ import { NQL_INTERNAL_COMPILER_OPTIONS, explainUnsupportedNqlBindingIncludeHop, markNqlTrustedRelationFilter, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
2
+ import { NQL_SELECT_AGGREGATE_FUNCTIONS, NQL_SELECT_VALUE_FUNCTIONS, NQL_SELECT_JSON_FUNCTIONS, NQL_SELECT_SCALAR_FUNCTIONS, toColumnList, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction, isParamIntent } from '@dbsp/types';
3
3
  import { createToken, Lexer, CstParser } from 'chevrotain';
4
4
 
5
5
  // src/compiler/index.ts
@@ -43,6 +43,28 @@ 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
+ }
46
68
 
47
69
  // src/compiler/column-validator.ts
48
70
  var ColumnValidator = class _ColumnValidator {
@@ -92,6 +114,12 @@ var ColumnValidator = class _ColumnValidator {
92
114
  if (virtualColumns) return virtualColumns;
93
115
  return this.schema.getTable(name)?.columns.map((column) => column.name);
94
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
+ }
95
123
  hasPhysicalTable(name) {
96
124
  return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
97
125
  }
@@ -110,6 +138,88 @@ var ColumnValidator = class _ColumnValidator {
110
138
  getVirtualBindingScalarRelation(bindingName, relationName) {
111
139
  return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
112
140
  }
141
+ static virtualRelationIncludeShape(relation) {
142
+ const relationType = relation.relationType;
143
+ const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relation.targetColumn;
144
+ return {
145
+ type: relationType,
146
+ foreignKey,
147
+ source: relation.sourceTable,
148
+ target: relation.targetTable
149
+ };
150
+ }
151
+ resolveVirtualBindingScalarRelationForInclude(bindingName, relationPath) {
152
+ const relationName = relationPath.join(".");
153
+ if (relationPath.length < 1) {
154
+ throw new NqlSemanticException(
155
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
156
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
157
+ );
158
+ }
159
+ const relation = relationPath[0];
160
+ if (!relation) {
161
+ throw new NqlSemanticException(
162
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
163
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
164
+ );
165
+ }
166
+ const virtualRelation = this.getVirtualBindingScalarRelation(
167
+ bindingName,
168
+ relation
169
+ );
170
+ if (virtualRelation) {
171
+ const resolvedFirstHop = this.getRelation(virtualRelation.sourceTable, relation) ?? _ColumnValidator.virtualRelationIncludeShape(virtualRelation);
172
+ const unsupportedFirstHopReason = explainUnsupportedNqlBindingIncludeHop(
173
+ relation,
174
+ resolvedFirstHop,
175
+ { relation }
176
+ );
177
+ if (unsupportedFirstHopReason) {
178
+ throw new NqlSemanticException(
179
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
180
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): ${unsupportedFirstHopReason}.`
181
+ );
182
+ }
183
+ let sourceTable = virtualRelation.targetTable;
184
+ for (let i = 1; i < relationPath.length; i++) {
185
+ const tailRelation = relationPath[i];
186
+ if (!tailRelation) {
187
+ throw new NqlSemanticException(
188
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
189
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): relation path segment ${i + 1} is empty.`
190
+ );
191
+ }
192
+ const resolvedTail = this.getRelation(sourceTable, tailRelation);
193
+ if (!resolvedTail) {
194
+ throw new NqlSemanticException(
195
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
196
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail relation '${tailRelation}' is not declared on table '${sourceTable}'.`
197
+ );
198
+ }
199
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
200
+ tailRelation,
201
+ resolvedTail,
202
+ { relation: tailRelation }
203
+ );
204
+ if (unsupportedReason) {
205
+ throw new NqlSemanticException(
206
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
207
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail ${unsupportedReason}.`
208
+ );
209
+ }
210
+ sourceTable = resolvedTail.target;
211
+ }
212
+ return virtualRelation;
213
+ }
214
+ const reason = this.explainVirtualBindingScalarRelationRejection(
215
+ bindingName,
216
+ relation
217
+ );
218
+ throw new NqlSemanticException(
219
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
220
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relation}' (ref-#192): ${reason}.`
221
+ );
222
+ }
113
223
  explainVirtualBindingRelationRejection(bindingName, relationName) {
114
224
  const metadata = this.virtualBindingRelationFilters.get(bindingName);
115
225
  if (!metadata) {
@@ -124,27 +234,24 @@ var ColumnValidator = class _ColumnValidator {
124
234
  if (!relation) {
125
235
  return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
126
236
  }
127
- const fk = relation.foreignKey;
128
- const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
237
+ const fkColumns = toColumnList(relation.foreignKey);
129
238
  if (relation.type !== "belongsTo") {
130
239
  return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
131
240
  }
132
- if (fkColumns.length !== 1) {
133
- return `relation '${relationName}' must have exactly one FK column for A-lite binding relation filters`;
241
+ if (fkColumns.length === 0) {
242
+ return `relation '${relationName}' must have at least one FK column for A-lite binding relation filters`;
134
243
  }
135
- const fkColumn = fkColumns[0];
136
- if (fkColumn === void 0) {
137
- return `relation '${relationName}' must have exactly one FK column for A-lite binding relation filters`;
138
- }
139
- const directProjection = metadata.directProjectionLineage?.find(
140
- (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, fkColumn)
141
- );
142
- if (!directProjection) {
143
- const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
144
- return `relation '${relationName}' FK column '${fkColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
244
+ for (const fkColumn of fkColumns) {
245
+ const directProjection = metadata.directProjectionLineage?.find(
246
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, fkColumn)
247
+ );
248
+ if (!directProjection) {
249
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
250
+ return `relation '${relationName}' FK column '${fkColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
251
+ }
145
252
  }
146
253
  const available = this.virtualBindingTables.get(bindingName)?.join(", ");
147
- return `relation '${relationName}' FK column '${fkColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
254
+ return `relation '${relationName}' FK columns '${fkColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
148
255
  }
149
256
  explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
150
257
  const metadata = this.virtualBindingRelationFilters.get(bindingName);
@@ -156,33 +263,131 @@ var ColumnValidator = class _ColumnValidator {
156
263
  if (!sourceTable) {
157
264
  return "the binding source table could not be proven";
158
265
  }
266
+ const recursiveReason = this.explainVirtualBindingRecursiveScalarRelationRejection(
267
+ bindingName,
268
+ relationName,
269
+ sourceTable,
270
+ metadata
271
+ );
272
+ if (recursiveReason) return recursiveReason;
159
273
  const relation = this.getRelation(sourceTable, relationName);
160
274
  if (!relation) {
161
275
  return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
162
276
  }
163
- const fk = relation.foreignKey;
164
- const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
165
- if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
166
- return `relation '${relationName}' is '${relation.type ?? "unknown"}'; binding relation columns require a scalar belongsTo/hasOne relation`;
167
- }
168
- if (fkColumns.length !== 1) {
169
- return `relation '${relationName}' must have exactly one FK column for binding relation columns`;
170
- }
171
- const fkColumn = fkColumns[0];
172
- if (fkColumn === void 0) {
173
- return `relation '${relationName}' must have exactly one FK column for binding relation columns`;
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})` : ""}`;
174
313
  }
175
- const sourceColumn = relation.type === "belongsTo" ? fkColumn : relation.sourceKey ?? "id";
176
- const directProjection = metadata.directProjectionLineage?.find(
177
- (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
314
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
315
+ relationName,
316
+ relation
178
317
  );
179
- if (!directProjection) {
180
- const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
181
- const sourceColumnLabel = relation.type === "belongsTo" ? "FK column" : "source key column";
182
- return `relation '${relationName}' ${sourceColumnLabel} '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
318
+ if (unsupportedReason) return unsupportedReason;
319
+ const fkColumns = toColumnList(relation.foreignKey);
320
+ const sourceColumns = relation.type === "belongsTo" ? fkColumns : toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
321
+ for (const sourceColumn of sourceColumns) {
322
+ const directProjection = metadata.directProjectionLineage?.find(
323
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
324
+ );
325
+ if (!directProjection) {
326
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
327
+ const sourceColumnLabel = relation.type === "belongsTo" ? "FK column" : "source key column";
328
+ return `relation '${relationName}' ${sourceColumnLabel} '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
329
+ }
183
330
  }
184
331
  const available = this.virtualBindingTables.get(bindingName)?.join(", ");
185
- return `relation '${relationName}' source column '${sourceColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
332
+ return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
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)`;
186
391
  }
187
392
  /**
188
393
  * Convert camelCase to snake_case for column name matching.
@@ -215,6 +420,14 @@ var ColumnValidator = class _ColumnValidator {
215
420
  (c) => _ColumnValidator.columnsMatch(column, c.name)
216
421
  )?.name;
217
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
+ }
218
431
  validateColumn(table, column) {
219
432
  if (column === "*") return;
220
433
  const virtualColumns = this.virtualBindingTables.get(table);
@@ -401,6 +614,17 @@ function throwBindingRelationColumn182(bindingName, relationName, reason) {
401
614
  `Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
402
615
  );
403
616
  }
617
+ function bindingRelationColumnUnsupportedReason(relationName, relation) {
618
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
619
+ relationName,
620
+ relation
621
+ );
622
+ if (unsupportedReason) return unsupportedReason;
623
+ if (relationCardinality(relation) !== "one") {
624
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; scalar binding relation columns require every hop to be to-one (belongsTo/hasOne) (ref-#192)`;
625
+ }
626
+ return void 0;
627
+ }
404
628
  function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
405
629
  if (!isBindingTable(ctx, bindingName)) return void 0;
406
630
  const actualBindingName = bindingName;
@@ -435,19 +659,19 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
435
659
  if (!isBindingTable(ctx, bindingName)) return void 0;
436
660
  const actualBindingName = bindingName;
437
661
  const relationName = relationPath.join(".");
438
- if (relationPath.length !== 1) {
662
+ const relation = relationPath[0];
663
+ if (!relation) {
439
664
  throwBindingRelationColumn182(
440
665
  actualBindingName,
441
666
  relationName,
442
- "multi-hop binding relation columns are not supported; the relation path must be a single source-table relation"
667
+ "the relation path must name a source-table relation"
443
668
  );
444
669
  }
445
- const relation = relationPath[0];
446
- if (!relation) {
670
+ if (!ctx.validator) {
447
671
  throwBindingRelationColumn182(
448
672
  actualBindingName,
449
673
  relationName,
450
- "the relation path must name a source-table relation"
674
+ "model metadata is not available"
451
675
  );
452
676
  }
453
677
  const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
@@ -459,10 +683,129 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
459
683
  actualBindingName,
460
684
  relation
461
685
  ) ?? "model metadata is not available";
462
- throwBindingRelationColumn182(actualBindingName, relation, reason);
686
+ throwBindingRelationColumn182(actualBindingName, relationName, reason);
687
+ }
688
+ if (relationPath.length === 1) {
689
+ ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
690
+ return virtualRelation;
691
+ }
692
+ const firstHop = ctx.validator.getRelation(
693
+ virtualRelation.sourceTable,
694
+ relation
695
+ );
696
+ if (!firstHop) {
697
+ throwBindingRelationColumn182(
698
+ actualBindingName,
699
+ relationName,
700
+ `relation '${relation}' is not declared on source table '${virtualRelation.sourceTable}'`
701
+ );
463
702
  }
464
- ctx.validator?.validateColumn(virtualRelation.targetTable, selectedColumn);
465
- return virtualRelation;
703
+ const firstHopReason = bindingRelationColumnUnsupportedReason(
704
+ relation,
705
+ firstHop
706
+ );
707
+ if (firstHopReason) {
708
+ throwBindingRelationColumn182(
709
+ actualBindingName,
710
+ relationName,
711
+ firstHopReason
712
+ );
713
+ }
714
+ let sourceTable = virtualRelation.targetTable;
715
+ const hops = [];
716
+ for (let i = 1; i < relationPath.length; i++) {
717
+ const tailRelation = relationPath[i];
718
+ if (!tailRelation) {
719
+ throwBindingRelationColumn182(
720
+ actualBindingName,
721
+ relationName,
722
+ `relation path segment ${i + 1} is empty (ref-#192)`
723
+ );
724
+ }
725
+ const resolvedTail = ctx.validator.getRelation(sourceTable, tailRelation);
726
+ if (!resolvedTail) {
727
+ throwBindingRelationColumn182(
728
+ actualBindingName,
729
+ relationName,
730
+ `tail relation '${tailRelation}' is not declared on table '${sourceTable}' (ref-#192)`
731
+ );
732
+ }
733
+ const tailReason = bindingRelationColumnUnsupportedReason(
734
+ tailRelation,
735
+ resolvedTail
736
+ );
737
+ if (tailReason) {
738
+ throwBindingRelationColumn182(
739
+ actualBindingName,
740
+ relationName,
741
+ `tail ${tailReason}`
742
+ );
743
+ }
744
+ const joinColumns = scalarRelationJoinColumns(resolvedTail);
745
+ if (!joinColumns) {
746
+ throwBindingRelationColumn182(
747
+ actualBindingName,
748
+ relationName,
749
+ `tail relation '${tailRelation}' cannot be resolved to a scalar join (ref-#192)`
750
+ );
751
+ }
752
+ if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
753
+ throwBindingRelationColumn182(
754
+ actualBindingName,
755
+ relationName,
756
+ `tail relation '${tailRelation}' has mismatched join column counts (${joinColumns.sourceJoinColumn.length} source, ${joinColumns.targetJoinColumn.length} target) (ref-#179)`
757
+ );
758
+ }
759
+ hops.push({
760
+ target: resolvedTail.target,
761
+ fkColumn: joinColumns.sourceJoinColumn,
762
+ joinColumn: joinColumns.targetJoinColumn
763
+ });
764
+ sourceTable = resolvedTail.target;
765
+ }
766
+ ctx.validator.validateColumn(sourceTable, selectedColumn);
767
+ return {
768
+ ...virtualRelation,
769
+ relation: relationName,
770
+ hops
771
+ };
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
+ };
466
809
  }
467
810
  function assertNoBindingRelationPath(ctx, bindingName, path) {
468
811
  if (!isBindingTable(ctx, bindingName)) return;
@@ -1099,7 +1442,12 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
1099
1442
  ...NQL_SELECT_AGGREGATE_FUNCTIONS,
1100
1443
  ...NQL_SELECT_VALUE_FUNCTIONS
1101
1444
  ]);
1102
- var DEFAULT_RELATION_TARGET_COLUMN = "id";
1445
+ var UnresolvedSelectAllOutputSchemaError = class extends NqlSemanticException {
1446
+ constructor(message) {
1447
+ super(NqlErrorCodes.SEM_INVALID_SYNTAX, message);
1448
+ this.name = "UnresolvedSelectAllOutputSchemaError";
1449
+ }
1450
+ };
1103
1451
  function compileNestedQuery(query, ctx, fns, bindings) {
1104
1452
  const savedContext = {
1105
1453
  currentFromTable: ctx.currentFromTable,
@@ -1182,19 +1530,24 @@ function selectContainsAggregate(select) {
1182
1530
  );
1183
1531
  }
1184
1532
  function relationForeignKeysOnSource(relation) {
1185
- if (!relation || relation.type !== "belongsTo") return void 0;
1186
- if (typeof relation.foreignKey === "string") return [relation.foreignKey];
1187
- if (Array.isArray(relation.foreignKey)) return relation.foreignKey;
1533
+ if (relation?.type !== "belongsTo") return void 0;
1534
+ const fkColumns = toColumnList(relation.foreignKey);
1535
+ return fkColumns.length > 0 ? fkColumns : void 0;
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";
1188
1542
  return void 0;
1189
1543
  }
1190
- function relationForeignKeys(relation) {
1191
- if (!relation) return void 0;
1192
- if (typeof relation.foreignKey === "string") return [relation.foreignKey];
1193
- if (Array.isArray(relation.foreignKey)) return relation.foreignKey;
1194
- return void 0;
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;
1195
1548
  }
1196
- function relationCardinality(relation) {
1197
- return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
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));
1198
1551
  }
1199
1552
  function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1200
1553
  if (!ctx.validator) return "model metadata is not available";
@@ -1229,13 +1582,71 @@ function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionL
1229
1582
  (projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
1230
1583
  );
1231
1584
  }
1585
+ function findDirectSourceProjections(sourceTable, sourceColumns, directProjectionLineage) {
1586
+ const projections = [];
1587
+ for (const sourceColumn of sourceColumns) {
1588
+ const projection = findDirectSourceProjection(
1589
+ sourceTable,
1590
+ sourceColumn,
1591
+ directProjectionLineage
1592
+ );
1593
+ if (!projection) return void 0;
1594
+ projections.push(projection);
1595
+ }
1596
+ return projections;
1597
+ }
1232
1598
  function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1233
1599
  const fkColumns = relationForeignKeysOnSource(relation);
1234
- if (!relation || !fkColumns || fkColumns.length !== 1) return void 0;
1235
- const fkColumn = fkColumns[0];
1600
+ if (!relation || !fkColumns) return void 0;
1601
+ const sourceProjections = findDirectSourceProjections(
1602
+ sourceTable,
1603
+ fkColumns,
1604
+ directProjectionLineage
1605
+ );
1606
+ if (!sourceProjections) return void 0;
1607
+ const targetKey = toColumnList(relation.targetKey);
1608
+ const targetColumns = targetKey.length > 0 ? targetKey : [DEFAULT_RELATION_TARGET_COLUMN];
1609
+ if (targetColumns.length !== fkColumns.length) return void 0;
1610
+ return {
1611
+ relation: relation.name,
1612
+ sourceTable,
1613
+ targetTable: relation.target,
1614
+ sourceColumn: sourceProjections.map(
1615
+ (projection) => projection.outputColumn
1616
+ ),
1617
+ targetColumn: targetColumns,
1618
+ hops: [],
1619
+ cardinality: "one"
1620
+ };
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
+ }
1236
1647
  const sourceProjection = findDirectSourceProjection(
1237
1648
  sourceTable,
1238
- fkColumn,
1649
+ sourceColumn,
1239
1650
  directProjectionLineage
1240
1651
  );
1241
1652
  if (!sourceProjection) return void 0;
@@ -1243,34 +1654,109 @@ function virtualRelationForBinding(relation, sourceTable, directProjectionLineag
1243
1654
  relation: relation.name,
1244
1655
  sourceTable,
1245
1656
  targetTable: relation.target,
1246
- sourceColumn: sourceProjection.outputColumn,
1247
- targetColumn: relation.targetKey ?? DEFAULT_RELATION_TARGET_COLUMN,
1248
- cardinality: "one"
1657
+ sourceColumn: [sourceProjection.outputColumn],
1658
+ targetColumn: [targetColumn],
1659
+ hops: [],
1660
+ through: relation.through,
1661
+ throughSourceColumn,
1662
+ throughTargetColumn,
1663
+ cardinality: "many",
1664
+ relationType: "manyToMany"
1249
1665
  };
1250
1666
  }
1251
- function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1252
- if (!relation) return void 0;
1253
- if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
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) {
1254
1671
  return void 0;
1255
1672
  }
1256
- const fkColumns = relationForeignKeys(relation);
1257
- if (!fkColumns || fkColumns.length !== 1) return void 0;
1258
- const fkColumn = fkColumns[0];
1259
- const sourceJoinColumn = relation.type === "belongsTo" ? fkColumn : relation.sourceKey ?? DEFAULT_RELATION_TARGET_COLUMN;
1260
- const targetJoinColumn = relation.type === "belongsTo" ? relation.targetKey ?? DEFAULT_RELATION_TARGET_COLUMN : fkColumn;
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;
1261
1700
  const sourceProjection = findDirectSourceProjection(
1262
1701
  sourceTable,
1263
- sourceJoinColumn,
1702
+ seedColumn,
1264
1703
  directProjectionLineage
1265
1704
  );
1266
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
+ }
1723
+ function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
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
+ }
1735
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
1736
+ return void 0;
1737
+ }
1738
+ const joinColumns = scalarRelationJoinColumns(relation);
1739
+ if (!joinColumns) return void 0;
1740
+ if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
1741
+ return void 0;
1742
+ }
1743
+ const sourceProjections = findDirectSourceProjections(
1744
+ sourceTable,
1745
+ joinColumns.sourceJoinColumn,
1746
+ directProjectionLineage
1747
+ );
1748
+ if (!sourceProjections) return void 0;
1267
1749
  return {
1268
1750
  relation: relation.name,
1269
1751
  sourceTable,
1270
1752
  targetTable: relation.target,
1271
- sourceColumn: sourceProjection.outputColumn,
1272
- targetColumn: targetJoinColumn,
1273
- cardinality: relationCardinality(relation)
1753
+ sourceColumn: sourceProjections.map(
1754
+ (projection) => projection.outputColumn
1755
+ ),
1756
+ targetColumn: joinColumns.targetJoinColumn,
1757
+ hops: [],
1758
+ cardinality: relationCardinality(relation),
1759
+ relationType: relation.type
1274
1760
  };
1275
1761
  }
1276
1762
  function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
@@ -1294,6 +1780,13 @@ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDepe
1294
1780
  );
1295
1781
  for (const relationName of relationNames ?? []) {
1296
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);
1297
1790
  const virtualRelation = virtualRelationForBinding(
1298
1791
  relation,
1299
1792
  sourceTable,
@@ -1328,6 +1821,15 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
1328
1821
  }
1329
1822
  const firstSegment = segments[0];
1330
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
+ }
1331
1833
  assertNoBindingRelationConstruct(
1332
1834
  ctx,
1333
1835
  bindingName,
@@ -1338,6 +1840,19 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
1338
1840
  }
1339
1841
  assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
1340
1842
  }
1843
+ function resolveBindingRelationInclude(ctx, bindingName, relationPath) {
1844
+ if (!isBindingTable(ctx, bindingName)) return void 0;
1845
+ if (!bindingName || !ctx.validator) {
1846
+ throw new NqlSemanticException(
1847
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1848
+ `Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
1849
+ );
1850
+ }
1851
+ return ctx.validator.resolveVirtualBindingScalarRelationForInclude(
1852
+ bindingName,
1853
+ relationPath
1854
+ );
1855
+ }
1341
1856
  function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
1342
1857
  const fn = expr.name.toLowerCase();
1343
1858
  if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
@@ -1538,24 +2053,27 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1538
2053
  case "star":
1539
2054
  break;
1540
2055
  case "relationStar":
1541
- assertNoBindingRelationConstruct(
1542
- ctx,
1543
- bindingName,
1544
- "select relation columns",
1545
- item.relation.join(".")
1546
- );
2056
+ resolveBindingRelationInclude(ctx, bindingName, item.relation);
1547
2057
  break;
1548
2058
  case "expression":
1549
2059
  if (item.expression.type === "path" && item.expression.segments.length > 1) {
1550
2060
  const segments = item.expression.segments;
1551
2061
  const firstSegment = segments[0];
1552
2062
  if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1553
- assertNoBindingRelationConstruct(
2063
+ if (segments.length !== 2 || !resolveBindingRecursiveRelationColumn(
1554
2064
  ctx,
1555
2065
  bindingName,
1556
- "use pseudo-column traversals",
1557
- firstSegment
1558
- );
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
+ }
1559
2077
  } else {
1560
2078
  resolveBindingRelationColumn(
1561
2079
  ctx,
@@ -1779,21 +2297,38 @@ function compileQueryInternal(query, ctx, fns, bindings) {
1779
2297
  }
1780
2298
  }
1781
2299
  }
1782
- if (select && select.type === "expressions" && !isBindingSource) {
2300
+ if (select && select.type === "expressions") {
1783
2301
  const relationPaths = /* @__PURE__ */ new Set();
1784
2302
  for (const expr of select.columns) {
1785
2303
  if (expr.kind === "relationColumn") {
1786
- relationPaths.add(expr.relation);
2304
+ if (!isBindingSource || expr.column === "*") {
2305
+ relationPaths.add(expr.relation);
2306
+ }
1787
2307
  }
1788
2308
  }
1789
2309
  if (relationPaths.size > 0) {
1790
- const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
1791
- for (const inc of nestedIncludes) {
1792
- const exists = allIncludes.some(
1793
- (existing) => existing.relation === inc.relation
1794
- );
1795
- if (!exists) {
1796
- allIncludes.push(inc);
2310
+ if (isBindingSource) {
2311
+ for (const relation of relationPaths) {
2312
+ resolveBindingRelationInclude(ctx, query.table, relation.split("."));
2313
+ }
2314
+ const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
2315
+ for (const inc of nestedIncludes) {
2316
+ const exists = allIncludes.some(
2317
+ (existing) => existing.relation === inc.relation
2318
+ );
2319
+ if (!exists) {
2320
+ allIncludes.push(inc);
2321
+ }
2322
+ }
2323
+ } else {
2324
+ const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
2325
+ for (const inc of nestedIncludes) {
2326
+ const exists = allIncludes.some(
2327
+ (existing) => existing.relation === inc.relation
2328
+ );
2329
+ if (!exists) {
2330
+ allIncludes.push(inc);
2331
+ }
1797
2332
  }
1798
2333
  }
1799
2334
  }
@@ -1931,8 +2466,7 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
1931
2466
  if (bindingColumns2 !== void 0) return bindingColumns2;
1932
2467
  const columns = ctx.validator?.getTableColumns(intent.from);
1933
2468
  if (columns !== void 0) return columns;
1934
- throw new NqlSemanticException(
1935
- NqlErrorCodes.SEM_INVALID_SYNTAX,
2469
+ throw new UnresolvedSelectAllOutputSchemaError(
1936
2470
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
1937
2471
  );
1938
2472
  }
@@ -2858,6 +3392,7 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2858
3392
  targetTable: bindingRelation.targetTable,
2859
3393
  sourceColumn: bindingRelation.sourceColumn,
2860
3394
  targetColumn: bindingRelation.targetColumn,
3395
+ hops: bindingRelation.hops,
2861
3396
  ...bindingRelation.cardinality !== void 0 && {
2862
3397
  cardinality: bindingRelation.cardinality
2863
3398
  }
@@ -2967,6 +3502,20 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
2967
3502
  ...NQL_SELECT_JSON_FUNCTIONS,
2968
3503
  ...NQL_SELECT_SCALAR_FUNCTIONS
2969
3504
  ]);
3505
+ function resolveBindingRelationInclude2(ctx, bindingName, relationPath) {
3506
+ if (!isBindingTable(ctx, bindingName)) return false;
3507
+ if (!bindingName || !ctx.validator) {
3508
+ throw new NqlSemanticException(
3509
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3510
+ `Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
3511
+ );
3512
+ }
3513
+ ctx.validator.resolveVirtualBindingScalarRelationForInclude(
3514
+ bindingName,
3515
+ relationPath
3516
+ );
3517
+ return true;
3518
+ }
2970
3519
  function validateSelectPathExpression(expr, ctx) {
2971
3520
  if (!ctx.currentFromTable) return;
2972
3521
  const { segments } = expr;
@@ -3022,12 +3571,14 @@ function compileSelectClause(clause, ctx, fns) {
3022
3571
  } else if (item.type === "relationStar") {
3023
3572
  hasExpressions = true;
3024
3573
  const relation = item.relation.join(".");
3025
- assertNoBindingRelationConstruct(
3026
- ctx,
3027
- ctx.currentFromTable,
3028
- "select relation columns",
3029
- relation
3030
- );
3574
+ if (!resolveBindingRelationInclude2(ctx, ctx.currentFromTable, item.relation)) {
3575
+ assertNoBindingRelationConstruct(
3576
+ ctx,
3577
+ ctx.currentFromTable,
3578
+ "select relation columns",
3579
+ relation
3580
+ );
3581
+ }
3031
3582
  expressions.push({
3032
3583
  kind: "relationColumn",
3033
3584
  relation,
@@ -3298,12 +3849,6 @@ function compileMultiSegmentPath(expr, item, ctx) {
3298
3849
  const segments = expr.segments;
3299
3850
  const firstSegmentLower = segments[0].toLowerCase();
3300
3851
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
3301
- assertNoBindingRelationConstruct(
3302
- ctx,
3303
- ctx.currentFromTable,
3304
- "use pseudo-column traversals",
3305
- segments[0]
3306
- );
3307
3852
  const firstSegment = firstSegmentLower;
3308
3853
  const depthHint = expr.depthHint;
3309
3854
  if (depthHint !== void 0) {
@@ -3330,6 +3875,44 @@ function compileMultiSegmentPath(expr, item, ctx) {
3330
3875
  );
3331
3876
  }
3332
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
+ );
3333
3916
  if (ctx.currentFromTable) {
3334
3917
  validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
3335
3918
  }
@@ -3368,7 +3951,8 @@ function compileMultiSegmentPath(expr, item, ctx) {
3368
3951
  );
3369
3952
  }
3370
3953
  if (ctx.currentFromTable && ctx.validator) {
3371
- const targetTable = bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
3954
+ const lastBindingHop = bindingRelation?.hops[bindingRelation.hops.length - 1];
3955
+ const targetTable = lastBindingHop?.target ?? bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
3372
3956
  if (targetTable) {
3373
3957
  ctx.validator.validateColumn(targetTable, column);
3374
3958
  }
@@ -3384,9 +3968,25 @@ function compileMultiSegmentPath(expr, item, ctx) {
3384
3968
  targetTable: bindingRelation.targetTable,
3385
3969
  sourceColumn: bindingRelation.sourceColumn,
3386
3970
  targetColumn: bindingRelation.targetColumn,
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
+ },
3387
3981
  selectedColumn: column,
3388
3982
  ...bindingRelation.cardinality !== void 0 && {
3389
3983
  cardinality: bindingRelation.cardinality
3984
+ },
3985
+ ...bindingRelation.relationType !== void 0 && {
3986
+ relationType: bindingRelation.relationType
3987
+ },
3988
+ ...bindingRelation.recursive !== void 0 && {
3989
+ recursive: bindingRelation.recursive
3390
3990
  }
3391
3991
  }) : relationColumnIntent;
3392
3992
  }
@@ -3535,16 +4135,17 @@ function allowsInternalParams(options) {
3535
4135
  return internalOptions?.allowInternalParams === true;
3536
4136
  }
3537
4137
  function isUnresolvedSelectAllOutputSchemaError(error) {
3538
- return error instanceof NqlSemanticException && error.message.includes("from SELECT *") && error.message.includes("without a concrete table schema");
4138
+ return error instanceof UnresolvedSelectAllOutputSchemaError;
3539
4139
  }
3540
- function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
4140
+ function programSequenceStepFromResult(result, bindName, final, bindingDependencies, snapshotReadBindings) {
3541
4141
  if (result.query) {
3542
4142
  return {
3543
4143
  kind: "query",
3544
4144
  query: result.query,
3545
4145
  ...bindName !== void 0 && { bindName },
3546
4146
  final,
3547
- bindingDependencies
4147
+ bindingDependencies,
4148
+ ...bindName !== void 0 && snapshotReadBindings.has(bindName) && { snapshot: true }
3548
4149
  };
3549
4150
  }
3550
4151
  if (result.mutation) {
@@ -3564,6 +4165,79 @@ function stringArraysEqual(left, right) {
3564
4165
  function isNqlAstRecord(value) {
3565
4166
  return typeof value === "object" && value !== null;
3566
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
+ }
3567
4241
  function addReadBindingReference(references, readBindingNames, name) {
3568
4242
  if (typeof name === "string" && readBindingNames.has(name)) {
3569
4243
  references.add(name);
@@ -3727,23 +4401,31 @@ function collectTransitiveReadBindingDependencies(directReferences, bindingDepen
3727
4401
  }
3728
4402
  return ordered;
3729
4403
  }
3730
- function rejectReadBindingReferenceAcrossMutation(bindName, definitionIndex, mutationIndex, referenceIndex, statementCount) {
4404
+ function rejectReadBindingReferenceAcrossMutation(bindName, reason) {
3731
4405
  throw new NqlSemanticException(
3732
4406
  NqlErrorCodes.SEM_INVALID_SYNTAX,
3733
- `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.`
3734
4408
  );
3735
4409
  }
3736
- 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) {
3737
4421
  for (const referencedBindName of statementBindingDependencies) {
3738
4422
  const definitionIndex = readBindingDefinitions.get(referencedBindName);
3739
4423
  if (definitionIndex === void 0) continue;
3740
4424
  if (lastMutationStatement > definitionIndex) {
3741
- rejectReadBindingReferenceAcrossMutation(
4425
+ markReadBindingReferenceAcrossMutation(
3742
4426
  referencedBindName,
3743
- definitionIndex,
3744
- lastMutationStatement,
3745
- referenceIndex,
3746
- statementCount
4427
+ snapshotReadBindings,
4428
+ readBindingSnapshotShapes
3747
4429
  );
3748
4430
  }
3749
4431
  }
@@ -3823,6 +4505,8 @@ var NqlCompiler = class {
3823
4505
  const seenBindNames = /* @__PURE__ */ new Map();
3824
4506
  const nqlProgramSequence = [];
3825
4507
  const readBindingDefinitions = /* @__PURE__ */ new Map();
4508
+ const readBindingSnapshotShapes = /* @__PURE__ */ new Map();
4509
+ const snapshotReadBindings = /* @__PURE__ */ new Set();
3826
4510
  const definedBindingNames = /* @__PURE__ */ new Set();
3827
4511
  const bindingDependencies = /* @__PURE__ */ new Map();
3828
4512
  let lastMutationStatement = -1;
@@ -3848,12 +4532,12 @@ var NqlCompiler = class {
3848
4532
  readBindingReferences,
3849
4533
  bindingDependencies
3850
4534
  );
3851
- rejectInvalidReadBindingDependencies(
4535
+ markReadBindingDependenciesRequiringSnapshot(
3852
4536
  statementBindingDependencies,
3853
4537
  readBindingDefinitions,
3854
4538
  lastMutationStatement,
3855
- i,
3856
- program.statements.length
4539
+ snapshotReadBindings,
4540
+ readBindingSnapshotShapes
3857
4541
  );
3858
4542
  lastResult = this.compileSingleStatement(stmt, bindings);
3859
4543
  collectCompileResultReadBindingReferences(
@@ -3865,24 +4549,35 @@ var NqlCompiler = class {
3865
4549
  readBindingReferences,
3866
4550
  bindingDependencies
3867
4551
  );
3868
- rejectInvalidReadBindingDependencies(
4552
+ markReadBindingDependenciesRequiringSnapshot(
3869
4553
  statementBindingDependencies,
3870
4554
  readBindingDefinitions,
3871
4555
  lastMutationStatement,
3872
- i,
3873
- program.statements.length
4556
+ snapshotReadBindings,
4557
+ readBindingSnapshotShapes
3874
4558
  );
3875
4559
  if (stmt.type === "mutationPipeline") {
3876
4560
  lastMutationStatement = i;
3877
4561
  }
3878
4562
  if (bindName) {
3879
4563
  if (lastResult.query) {
4564
+ const sourceWasBinding = definedBindingNames.has(
4565
+ lastResult.query.from
4566
+ );
3880
4567
  const outputSchema = this.registerQueryBindingOutputSchema(
3881
4568
  bindName,
3882
4569
  lastResult.query,
3883
4570
  bindingOutputSchemas,
3884
4571
  statementBindingDependencies
3885
4572
  );
4573
+ readBindingSnapshotShapes.set(
4574
+ bindName,
4575
+ classifyReadBindingSnapshotShape(
4576
+ lastResult.query,
4577
+ this.ctx,
4578
+ sourceWasBinding
4579
+ )
4580
+ );
3886
4581
  bindings.set(bindName, lastResult.query);
3887
4582
  if (outputSchema) {
3888
4583
  this.ctx.validator?.addVirtualBindingTable(
@@ -3930,7 +4625,8 @@ var NqlCompiler = class {
3930
4625
  lastResult,
3931
4626
  bindName,
3932
4627
  i === program.statements.length - 1,
3933
- statementBindingDependencies
4628
+ statementBindingDependencies,
4629
+ snapshotReadBindings
3934
4630
  );
3935
4631
  if (sequenceStep !== void 0) {
3936
4632
  nqlProgramSequence.push(sequenceStep);
@@ -3946,14 +4642,19 @@ var NqlCompiler = class {
3946
4642
  }
3947
4643
  const hasMutationBindings = mutationBindings.size > 0;
3948
4644
  const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
3949
- 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;
3950
4649
  if (bindings.size > 0) {
3951
4650
  return {
3952
4651
  ...lastResult,
3953
4652
  bindings,
3954
4653
  ...hasBindingOutputSchemas && { bindingOutputSchemas },
3955
4654
  ...hasMutationBindings && { mutationBindings },
3956
- ...hasNqlProgramSequence && { nqlProgramSequence }
4655
+ ...hasNqlProgramSequence && {
4656
+ nqlProgramSequence: taggedNqlProgramSequence
4657
+ }
3957
4658
  };
3958
4659
  }
3959
4660
  return lastResult;