@dbsp/nql 1.4.0 → 1.6.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, 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,8 +43,6 @@ var NqlSemanticException = class extends Error {
43
43
  this.relatedSymbol = relatedSymbol;
44
44
  }
45
45
  };
46
-
47
- // src/compiler/column-validator.ts
48
46
  var ColumnValidator = class _ColumnValidator {
49
47
  constructor(schema) {
50
48
  this.schema = schema;
@@ -52,6 +50,7 @@ var ColumnValidator = class _ColumnValidator {
52
50
  schema;
53
51
  knownCteTables = /* @__PURE__ */ new Set();
54
52
  virtualBindingTables = /* @__PURE__ */ new Map();
53
+ virtualBindingRelationFilters = /* @__PURE__ */ new Map();
55
54
  /**
56
55
  * Register CTE names so validateTable() skips validation for them.
57
56
  * CTE names are not physical tables in the schema, but are valid FROM targets.
@@ -69,12 +68,16 @@ var ColumnValidator = class _ColumnValidator {
69
68
  * Register an NQL binding as a virtual table with a concrete output schema.
70
69
  * Binding columns are logical output names, so matching is exact.
71
70
  */
72
- addVirtualBindingTable(name, columns) {
71
+ addVirtualBindingTable(name, columns, relationFilters) {
73
72
  this.virtualBindingTables.set(name, columns);
73
+ if (relationFilters) {
74
+ this.virtualBindingRelationFilters.set(name, relationFilters);
75
+ }
74
76
  }
75
77
  /** Clear all registered virtual binding tables (used between compilations). */
76
78
  clearVirtualBindingTables() {
77
79
  this.virtualBindingTables.clear();
80
+ this.virtualBindingRelationFilters.clear();
78
81
  }
79
82
  isVirtualBindingTable(name) {
80
83
  return name !== void 0 && this.virtualBindingTables.has(name);
@@ -87,6 +90,173 @@ var ColumnValidator = class _ColumnValidator {
87
90
  if (virtualColumns) return virtualColumns;
88
91
  return this.schema.getTable(name)?.columns.map((column) => column.name);
89
92
  }
93
+ hasPhysicalTable(name) {
94
+ return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
95
+ }
96
+ hasQualifiedRelationLookup() {
97
+ return typeof this.schema.getRelation === "function";
98
+ }
99
+ getRelation(sourceTable, relationName) {
100
+ return this.schema.getRelation?.(`${sourceTable}.${relationName}`) ?? this.schema.getRelationsFrom(sourceTable).find((relation) => relation.name === relationName);
101
+ }
102
+ getRelationsFrom(sourceTable) {
103
+ return this.schema.getRelationsFrom(sourceTable);
104
+ }
105
+ getVirtualBindingRelation(bindingName, relationName) {
106
+ return this.virtualBindingRelationFilters.get(bindingName)?.relations.find((relation) => relation.relation === relationName);
107
+ }
108
+ getVirtualBindingScalarRelation(bindingName, relationName) {
109
+ return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
110
+ }
111
+ static virtualRelationIncludeShape(relation) {
112
+ const relationType = relation.relationType;
113
+ const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relation.targetColumn;
114
+ return {
115
+ type: relationType,
116
+ foreignKey,
117
+ source: relation.sourceTable,
118
+ target: relation.targetTable
119
+ };
120
+ }
121
+ resolveVirtualBindingScalarRelationForInclude(bindingName, relationPath) {
122
+ const relationName = relationPath.join(".");
123
+ if (relationPath.length < 1) {
124
+ throw new NqlSemanticException(
125
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
126
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
127
+ );
128
+ }
129
+ const relation = relationPath[0];
130
+ if (!relation) {
131
+ throw new NqlSemanticException(
132
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
133
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
134
+ );
135
+ }
136
+ const virtualRelation = this.getVirtualBindingScalarRelation(
137
+ bindingName,
138
+ relation
139
+ );
140
+ if (virtualRelation) {
141
+ const resolvedFirstHop = this.getRelation(virtualRelation.sourceTable, relation) ?? _ColumnValidator.virtualRelationIncludeShape(virtualRelation);
142
+ const unsupportedFirstHopReason = explainUnsupportedNqlBindingIncludeHop(
143
+ relation,
144
+ resolvedFirstHop,
145
+ { relation }
146
+ );
147
+ if (unsupportedFirstHopReason) {
148
+ throw new NqlSemanticException(
149
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
150
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): ${unsupportedFirstHopReason}.`
151
+ );
152
+ }
153
+ let sourceTable = virtualRelation.targetTable;
154
+ for (let i = 1; i < relationPath.length; i++) {
155
+ const tailRelation = relationPath[i];
156
+ if (!tailRelation) {
157
+ throw new NqlSemanticException(
158
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
159
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): relation path segment ${i + 1} is empty.`
160
+ );
161
+ }
162
+ const resolvedTail = this.getRelation(sourceTable, tailRelation);
163
+ if (!resolvedTail) {
164
+ throw new NqlSemanticException(
165
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
166
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail relation '${tailRelation}' is not declared on table '${sourceTable}'.`
167
+ );
168
+ }
169
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
170
+ tailRelation,
171
+ resolvedTail,
172
+ { relation: tailRelation }
173
+ );
174
+ if (unsupportedReason) {
175
+ throw new NqlSemanticException(
176
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
177
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail ${unsupportedReason}.`
178
+ );
179
+ }
180
+ sourceTable = resolvedTail.target;
181
+ }
182
+ return virtualRelation;
183
+ }
184
+ const reason = this.explainVirtualBindingScalarRelationRejection(
185
+ bindingName,
186
+ relation
187
+ );
188
+ throw new NqlSemanticException(
189
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
190
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relation}' (ref-#192): ${reason}.`
191
+ );
192
+ }
193
+ explainVirtualBindingRelationRejection(bindingName, relationName) {
194
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
195
+ if (!metadata) {
196
+ return "no binding relation-filter metadata is available";
197
+ }
198
+ if (metadata.unsafeReason) return metadata.unsafeReason;
199
+ const sourceTable = metadata.sourceTable;
200
+ if (!sourceTable) {
201
+ return "the binding source table could not be proven";
202
+ }
203
+ const relation = this.getRelation(sourceTable, relationName);
204
+ if (!relation) {
205
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
206
+ }
207
+ const fkColumns = toColumnList(relation.foreignKey);
208
+ if (relation.type !== "belongsTo") {
209
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
210
+ }
211
+ if (fkColumns.length === 0) {
212
+ return `relation '${relationName}' must have at least one FK column for A-lite binding relation filters`;
213
+ }
214
+ for (const fkColumn of fkColumns) {
215
+ const directProjection = metadata.directProjectionLineage?.find(
216
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, fkColumn)
217
+ );
218
+ if (!directProjection) {
219
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
220
+ return `relation '${relationName}' FK column '${fkColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
221
+ }
222
+ }
223
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
224
+ return `relation '${relationName}' FK columns '${fkColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
225
+ }
226
+ explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
227
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
228
+ if (!metadata) {
229
+ return "no binding relation metadata is available";
230
+ }
231
+ if (metadata.unsafeReason) return metadata.unsafeReason;
232
+ const sourceTable = metadata.sourceTable;
233
+ if (!sourceTable) {
234
+ return "the binding source table could not be proven";
235
+ }
236
+ const relation = this.getRelation(sourceTable, relationName);
237
+ if (!relation) {
238
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
239
+ }
240
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
241
+ relationName,
242
+ relation
243
+ );
244
+ if (unsupportedReason) return unsupportedReason;
245
+ const fkColumns = toColumnList(relation.foreignKey);
246
+ const sourceColumns = relation.type === "belongsTo" ? fkColumns : toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
247
+ for (const sourceColumn of sourceColumns) {
248
+ const directProjection = metadata.directProjectionLineage?.find(
249
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
250
+ );
251
+ if (!directProjection) {
252
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
253
+ const sourceColumnLabel = relation.type === "belongsTo" ? "FK column" : "source key column";
254
+ return `relation '${relationName}' ${sourceColumnLabel} '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
255
+ }
256
+ }
257
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
258
+ return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
259
+ }
90
260
  /**
91
261
  * Convert camelCase to snake_case for column name matching.
92
262
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -156,13 +326,17 @@ var ColumnValidator = class _ColumnValidator {
156
326
  }
157
327
  resolveRelationTarget(sourceTable, relationName) {
158
328
  if (this.virtualBindingTables.has(sourceTable)) {
329
+ const virtualRelation = this.getVirtualBindingRelation(
330
+ sourceTable,
331
+ relationName
332
+ );
333
+ if (virtualRelation) return virtualRelation.targetTable;
159
334
  throw new NqlSemanticException(
160
335
  NqlErrorCodes.SEM_INVALID_SYNTAX,
161
- `Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}). Relation constructs require a physical model table, not a CTE binding.`
336
+ `Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${this.explainVirtualBindingRelationRejection(sourceTable, relationName)}.`
162
337
  );
163
338
  }
164
- const relations = this.schema.getRelationsFrom(sourceTable);
165
- const rel = relations.find((r) => r.name === relationName);
339
+ const rel = this.getRelation(sourceTable, relationName);
166
340
  return rel?.target;
167
341
  }
168
342
  assertNoBindingRelationConstruct(bindingName, construct, detail) {
@@ -180,6 +354,28 @@ var ColumnValidator = class _ColumnValidator {
180
354
  );
181
355
  }
182
356
  };
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
+ }
183
379
  var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
184
380
  "__proto__",
185
381
  "constructor",
@@ -288,6 +484,174 @@ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
288
484
  `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
289
485
  );
290
486
  }
487
+ function throwBindingRelationFilter182(bindingName, relationName, reason) {
488
+ throw new NqlSemanticException(
489
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
490
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${reason}.`
491
+ );
492
+ }
493
+ function throwBindingRelationColumn182(bindingName, relationName, reason) {
494
+ throw new NqlSemanticException(
495
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
496
+ `Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
497
+ );
498
+ }
499
+ function bindingRelationColumnUnsupportedReason(relationName, relation) {
500
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
501
+ relationName,
502
+ relation
503
+ );
504
+ if (unsupportedReason) return unsupportedReason;
505
+ if (relationCardinality(relation) !== "one") {
506
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; scalar binding relation columns require every hop to be to-one (belongsTo/hasOne) (ref-#192)`;
507
+ }
508
+ return void 0;
509
+ }
510
+ function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
511
+ if (!isBindingTable(ctx, bindingName)) return void 0;
512
+ const actualBindingName = bindingName;
513
+ const relationName = relationPath.join(".");
514
+ if (relationPath.length !== 1) {
515
+ throwBindingRelationFilter182(
516
+ actualBindingName,
517
+ relationName,
518
+ "multi-hop binding relation filters are not supported; the relation path must be a single source-table relation"
519
+ );
520
+ }
521
+ const relation = relationPath[0];
522
+ if (!relation) {
523
+ throwBindingRelationFilter182(
524
+ actualBindingName,
525
+ relationName,
526
+ "the relation path must name a source-table relation"
527
+ );
528
+ }
529
+ const virtualRelation = ctx.validator?.getVirtualBindingRelation(
530
+ actualBindingName,
531
+ relation
532
+ );
533
+ if (virtualRelation) return virtualRelation;
534
+ const reason = ctx.validator?.explainVirtualBindingRelationRejection(
535
+ actualBindingName,
536
+ relation
537
+ ) ?? "model metadata is not available";
538
+ throwBindingRelationFilter182(actualBindingName, relation, reason);
539
+ }
540
+ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedColumn) {
541
+ if (!isBindingTable(ctx, bindingName)) return void 0;
542
+ const actualBindingName = bindingName;
543
+ const relationName = relationPath.join(".");
544
+ const relation = relationPath[0];
545
+ if (!relation) {
546
+ throwBindingRelationColumn182(
547
+ actualBindingName,
548
+ relationName,
549
+ "the relation path must name a source-table relation"
550
+ );
551
+ }
552
+ if (!ctx.validator) {
553
+ throwBindingRelationColumn182(
554
+ actualBindingName,
555
+ relationName,
556
+ "model metadata is not available"
557
+ );
558
+ }
559
+ const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
560
+ actualBindingName,
561
+ relation
562
+ );
563
+ if (!virtualRelation) {
564
+ const reason = ctx.validator?.explainVirtualBindingScalarRelationRejection(
565
+ actualBindingName,
566
+ relation
567
+ ) ?? "model metadata is not available";
568
+ throwBindingRelationColumn182(actualBindingName, relationName, reason);
569
+ }
570
+ if (relationPath.length === 1) {
571
+ ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
572
+ return virtualRelation;
573
+ }
574
+ const firstHop = ctx.validator.getRelation(
575
+ virtualRelation.sourceTable,
576
+ relation
577
+ );
578
+ if (!firstHop) {
579
+ throwBindingRelationColumn182(
580
+ actualBindingName,
581
+ relationName,
582
+ `relation '${relation}' is not declared on source table '${virtualRelation.sourceTable}'`
583
+ );
584
+ }
585
+ const firstHopReason = bindingRelationColumnUnsupportedReason(
586
+ relation,
587
+ firstHop
588
+ );
589
+ if (firstHopReason) {
590
+ throwBindingRelationColumn182(
591
+ actualBindingName,
592
+ relationName,
593
+ firstHopReason
594
+ );
595
+ }
596
+ let sourceTable = virtualRelation.targetTable;
597
+ const hops = [];
598
+ for (let i = 1; i < relationPath.length; i++) {
599
+ const tailRelation = relationPath[i];
600
+ if (!tailRelation) {
601
+ throwBindingRelationColumn182(
602
+ actualBindingName,
603
+ relationName,
604
+ `relation path segment ${i + 1} is empty (ref-#192)`
605
+ );
606
+ }
607
+ const resolvedTail = ctx.validator.getRelation(sourceTable, tailRelation);
608
+ if (!resolvedTail) {
609
+ throwBindingRelationColumn182(
610
+ actualBindingName,
611
+ relationName,
612
+ `tail relation '${tailRelation}' is not declared on table '${sourceTable}' (ref-#192)`
613
+ );
614
+ }
615
+ const tailReason = bindingRelationColumnUnsupportedReason(
616
+ tailRelation,
617
+ resolvedTail
618
+ );
619
+ if (tailReason) {
620
+ throwBindingRelationColumn182(
621
+ actualBindingName,
622
+ relationName,
623
+ `tail ${tailReason}`
624
+ );
625
+ }
626
+ const joinColumns = scalarRelationJoinColumns(resolvedTail);
627
+ if (!joinColumns) {
628
+ throwBindingRelationColumn182(
629
+ actualBindingName,
630
+ relationName,
631
+ `tail relation '${tailRelation}' cannot be resolved to a scalar join (ref-#192)`
632
+ );
633
+ }
634
+ if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
635
+ throwBindingRelationColumn182(
636
+ actualBindingName,
637
+ relationName,
638
+ `tail relation '${tailRelation}' has mismatched join column counts (${joinColumns.sourceJoinColumn.length} source, ${joinColumns.targetJoinColumn.length} target) (ref-#179)`
639
+ );
640
+ }
641
+ hops.push({
642
+ target: resolvedTail.target,
643
+ fkColumn: joinColumns.sourceJoinColumn,
644
+ joinColumn: joinColumns.targetJoinColumn
645
+ });
646
+ sourceTable = resolvedTail.target;
647
+ }
648
+ ctx.validator.validateColumn(sourceTable, selectedColumn);
649
+ return {
650
+ ...virtualRelation,
651
+ relation: relationName,
652
+ hops
653
+ };
654
+ }
291
655
  function assertNoBindingRelationPath(ctx, bindingName, path) {
292
656
  if (!isBindingTable(ctx, bindingName)) return;
293
657
  throw new NqlSemanticException(
@@ -518,6 +882,10 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
518
882
  `HAVING cannot reference SELECT alias '${field}'. PostgreSQL does not allow SELECT aliases in HAVING; repeat the aggregate expression or filter the result in an outer query.`
519
883
  );
520
884
  }
885
+ if (!aliasContext && ctx.currentRelationTarget && !field.includes(".")) {
886
+ ctx.validator?.validateColumn(ctx.currentRelationTarget, field);
887
+ return;
888
+ }
521
889
  if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
522
890
  if (field.includes(".")) {
523
891
  assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
@@ -540,10 +908,6 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
540
908
  }
541
909
  return;
542
910
  }
543
- if (ctx.currentRelationTarget && !field.includes(".")) {
544
- ctx.validator.validateColumn(ctx.currentRelationTarget, field);
545
- return;
546
- }
547
911
  if (ctx.currentFromTable && field.includes(".")) {
548
912
  if (isBindingTable(ctx, ctx.currentFromTable)) {
549
913
  assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
@@ -975,6 +1339,180 @@ function throwUnsupportedBindingFinal(bindingName, feature) {
975
1339
  `Query '${bindingName}' reads from an NQL binding and cannot use ${feature} in a binding-final query (#183). Binding-final queries are restricted to portable common-ground SQL over projected binding columns: SELECT (including plain DISTINCT), WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, and UNION/INTERSECT/EXCEPT.`
976
1340
  );
977
1341
  }
1342
+ function toSnakeCase(name) {
1343
+ return name.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
1344
+ }
1345
+ function columnsMatch(left, right) {
1346
+ return left === right || toSnakeCase(left) === toSnakeCase(right);
1347
+ }
1348
+ function expressionIntentContainsAggregate(value) {
1349
+ if (!value || typeof value !== "object") return false;
1350
+ const record = value;
1351
+ if (record.kind === "aggregate") return true;
1352
+ for (const child of Object.values(record)) {
1353
+ if (Array.isArray(child)) {
1354
+ if (child.some((item) => expressionIntentContainsAggregate(item))) {
1355
+ return true;
1356
+ }
1357
+ } else if (expressionIntentContainsAggregate(child)) {
1358
+ return true;
1359
+ }
1360
+ }
1361
+ return false;
1362
+ }
1363
+ function selectContainsAggregate(select) {
1364
+ if (!select) return false;
1365
+ if (select.type === "aggregate") return true;
1366
+ if (select.type !== "expressions") return false;
1367
+ return select.columns.some(
1368
+ (column) => expressionIntentContainsAggregate(column)
1369
+ );
1370
+ }
1371
+ function relationForeignKeysOnSource(relation) {
1372
+ if (relation?.type !== "belongsTo") return void 0;
1373
+ const fkColumns = toColumnList(relation.foreignKey);
1374
+ return fkColumns.length > 0 ? fkColumns : void 0;
1375
+ }
1376
+ function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1377
+ if (!ctx.validator) return "model metadata is not available";
1378
+ if (!ctx.validator.hasQualifiedRelationLookup()) {
1379
+ return "model.getRelation metadata is not available";
1380
+ }
1381
+ if (bindingDependencies.length > 0 || isBindingTable(ctx, intent.from)) {
1382
+ return "the binding body reads from another NQL binding";
1383
+ }
1384
+ if (!ctx.validator.hasPhysicalTable(intent.from)) {
1385
+ return `the binding source '${intent.from}' is not a single real model table`;
1386
+ }
1387
+ if (intent.batchValuesSource) {
1388
+ return "the binding body reads from a batch-values source";
1389
+ }
1390
+ if ((intent.joins?.length ?? 0) > 0) {
1391
+ return "the binding body uses joins";
1392
+ }
1393
+ if ((intent.include?.length ?? 0) > 0) {
1394
+ return "the binding body uses relation includes";
1395
+ }
1396
+ if ((intent.groupBy?.length ?? 0) > 0 || intent.having) {
1397
+ return "the binding body uses GROUP BY or HAVING";
1398
+ }
1399
+ if (selectContainsAggregate(intent.select)) {
1400
+ return "the binding body uses aggregate projections";
1401
+ }
1402
+ return void 0;
1403
+ }
1404
+ function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionLineage) {
1405
+ return directProjectionLineage.find(
1406
+ (projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
1407
+ );
1408
+ }
1409
+ function findDirectSourceProjections(sourceTable, sourceColumns, directProjectionLineage) {
1410
+ const projections = [];
1411
+ for (const sourceColumn of sourceColumns) {
1412
+ const projection = findDirectSourceProjection(
1413
+ sourceTable,
1414
+ sourceColumn,
1415
+ directProjectionLineage
1416
+ );
1417
+ if (!projection) return void 0;
1418
+ projections.push(projection);
1419
+ }
1420
+ return projections;
1421
+ }
1422
+ function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1423
+ const fkColumns = relationForeignKeysOnSource(relation);
1424
+ if (!relation || !fkColumns) return void 0;
1425
+ const sourceProjections = findDirectSourceProjections(
1426
+ sourceTable,
1427
+ fkColumns,
1428
+ directProjectionLineage
1429
+ );
1430
+ if (!sourceProjections) return void 0;
1431
+ const targetKey = toColumnList(relation.targetKey);
1432
+ const targetColumns = targetKey.length > 0 ? targetKey : [DEFAULT_RELATION_TARGET_COLUMN];
1433
+ if (targetColumns.length !== fkColumns.length) return void 0;
1434
+ return {
1435
+ relation: relation.name,
1436
+ sourceTable,
1437
+ targetTable: relation.target,
1438
+ sourceColumn: sourceProjections.map(
1439
+ (projection) => projection.outputColumn
1440
+ ),
1441
+ targetColumn: targetColumns,
1442
+ hops: [],
1443
+ cardinality: "one"
1444
+ };
1445
+ }
1446
+ function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1447
+ if (!relation) return void 0;
1448
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
1449
+ return void 0;
1450
+ }
1451
+ const joinColumns = scalarRelationJoinColumns(relation);
1452
+ if (!joinColumns) return void 0;
1453
+ if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
1454
+ return void 0;
1455
+ }
1456
+ const sourceProjections = findDirectSourceProjections(
1457
+ sourceTable,
1458
+ joinColumns.sourceJoinColumn,
1459
+ directProjectionLineage
1460
+ );
1461
+ if (!sourceProjections) return void 0;
1462
+ return {
1463
+ relation: relation.name,
1464
+ sourceTable,
1465
+ targetTable: relation.target,
1466
+ sourceColumn: sourceProjections.map(
1467
+ (projection) => projection.outputColumn
1468
+ ),
1469
+ targetColumn: joinColumns.targetJoinColumn,
1470
+ hops: [],
1471
+ cardinality: relationCardinality(relation),
1472
+ relationType: relation.type
1473
+ };
1474
+ }
1475
+ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
1476
+ const unsafeReason = unsafeBindingRelationReason(
1477
+ intent,
1478
+ ctx,
1479
+ bindingDependencies
1480
+ );
1481
+ if (unsafeReason) {
1482
+ return {
1483
+ unsafeReason,
1484
+ directProjectionLineage: outputSchema.directProjectionLineage,
1485
+ relations: []
1486
+ };
1487
+ }
1488
+ const sourceTable = intent.from;
1489
+ const relations = [];
1490
+ const scalarRelations = [];
1491
+ const relationNames = new Set(
1492
+ ctx.validator?.getRelationsFrom(sourceTable).map((relation) => relation.name)
1493
+ );
1494
+ for (const relationName of relationNames ?? []) {
1495
+ const relation = ctx.validator?.getRelation(sourceTable, relationName);
1496
+ const virtualRelation = virtualRelationForBinding(
1497
+ relation,
1498
+ sourceTable,
1499
+ outputSchema.directProjectionLineage ?? []
1500
+ );
1501
+ if (virtualRelation) relations.push(virtualRelation);
1502
+ const scalarRelation = scalarVirtualRelationForBinding(
1503
+ relation,
1504
+ sourceTable,
1505
+ outputSchema.directProjectionLineage ?? []
1506
+ );
1507
+ if (scalarRelation) scalarRelations.push(scalarRelation);
1508
+ }
1509
+ return {
1510
+ sourceTable,
1511
+ directProjectionLineage: outputSchema.directProjectionLineage,
1512
+ relations,
1513
+ scalarRelations
1514
+ };
1515
+ }
978
1516
  function validateBindingFinalPath(expr, ctx, bindingName) {
979
1517
  const { segments } = expr;
980
1518
  if (segments.length === 1) {
@@ -999,63 +1537,164 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
999
1537
  }
1000
1538
  assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
1001
1539
  }
1002
- function validateBindingFinalFunction(expr, ctx, bindingName) {
1540
+ function resolveBindingRelationInclude(ctx, bindingName, relationPath) {
1541
+ if (!isBindingTable(ctx, bindingName)) return void 0;
1542
+ if (!bindingName || !ctx.validator) {
1543
+ throw new NqlSemanticException(
1544
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1545
+ `Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
1546
+ );
1547
+ }
1548
+ return ctx.validator.resolveVirtualBindingScalarRelationForInclude(
1549
+ bindingName,
1550
+ relationPath
1551
+ );
1552
+ }
1553
+ function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
1003
1554
  const fn = expr.name.toLowerCase();
1004
1555
  if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
1005
1556
  throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
1006
1557
  }
1007
1558
  for (const arg of expr.args) {
1008
- validateBindingFinalExpression(arg, ctx, bindingName);
1559
+ validateBindingFinalExpression(arg, ctx, bindingName, allowRelationFilters);
1009
1560
  }
1010
1561
  }
1011
- function validateBindingFinalExpression(expr, ctx, bindingName) {
1562
+ function validateBindingFinalExpression(expr, ctx, bindingName, allowRelationFilters = false) {
1012
1563
  switch (expr.type) {
1013
1564
  case "path":
1014
1565
  validateBindingFinalPath(expr, ctx, bindingName);
1015
1566
  break;
1016
1567
  case "binary":
1017
- validateBindingFinalExpression(expr.left, ctx, bindingName);
1018
- validateBindingFinalExpression(expr.right, ctx, bindingName);
1568
+ validateBindingFinalExpression(
1569
+ expr.left,
1570
+ ctx,
1571
+ bindingName,
1572
+ allowRelationFilters
1573
+ );
1574
+ validateBindingFinalExpression(
1575
+ expr.right,
1576
+ ctx,
1577
+ bindingName,
1578
+ allowRelationFilters
1579
+ );
1019
1580
  break;
1020
1581
  case "unary":
1021
- validateBindingFinalExpression(expr.operand, ctx, bindingName);
1582
+ validateBindingFinalExpression(
1583
+ expr.operand,
1584
+ ctx,
1585
+ bindingName,
1586
+ allowRelationFilters
1587
+ );
1022
1588
  break;
1023
1589
  case "comparison":
1024
- validateBindingFinalExpression(expr.left, ctx, bindingName);
1025
- validateBindingFinalExpression(expr.right, ctx, bindingName);
1590
+ validateBindingFinalExpression(
1591
+ expr.left,
1592
+ ctx,
1593
+ bindingName,
1594
+ allowRelationFilters
1595
+ );
1596
+ validateBindingFinalExpression(
1597
+ expr.right,
1598
+ ctx,
1599
+ bindingName,
1600
+ allowRelationFilters
1601
+ );
1026
1602
  break;
1027
1603
  case "in":
1028
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1604
+ validateBindingFinalExpression(
1605
+ expr.expression,
1606
+ ctx,
1607
+ bindingName,
1608
+ allowRelationFilters
1609
+ );
1029
1610
  if (Array.isArray(expr.values)) {
1030
1611
  for (const value of expr.values) {
1031
- validateBindingFinalExpression(value, ctx, bindingName);
1612
+ validateBindingFinalExpression(
1613
+ value,
1614
+ ctx,
1615
+ bindingName,
1616
+ allowRelationFilters
1617
+ );
1032
1618
  }
1033
1619
  } else if (expr.values.type === "dateRange") ; else ;
1034
1620
  break;
1035
1621
  case "between":
1036
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1037
- validateBindingFinalExpression(expr.low, ctx, bindingName);
1038
- validateBindingFinalExpression(expr.high, ctx, bindingName);
1622
+ validateBindingFinalExpression(
1623
+ expr.expression,
1624
+ ctx,
1625
+ bindingName,
1626
+ allowRelationFilters
1627
+ );
1628
+ validateBindingFinalExpression(
1629
+ expr.low,
1630
+ ctx,
1631
+ bindingName,
1632
+ allowRelationFilters
1633
+ );
1634
+ validateBindingFinalExpression(
1635
+ expr.high,
1636
+ ctx,
1637
+ bindingName,
1638
+ allowRelationFilters
1639
+ );
1039
1640
  break;
1040
1641
  case "isNull":
1041
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1642
+ validateBindingFinalExpression(
1643
+ expr.expression,
1644
+ ctx,
1645
+ bindingName,
1646
+ allowRelationFilters
1647
+ );
1042
1648
  break;
1043
1649
  case "function":
1044
- validateBindingFinalFunction(expr, ctx, bindingName);
1650
+ validateBindingFinalFunction(
1651
+ expr,
1652
+ ctx,
1653
+ bindingName,
1654
+ allowRelationFilters
1655
+ );
1045
1656
  break;
1046
1657
  case "case":
1047
1658
  if (expr.subject) {
1048
- validateBindingFinalExpression(expr.subject, ctx, bindingName);
1659
+ validateBindingFinalExpression(
1660
+ expr.subject,
1661
+ ctx,
1662
+ bindingName,
1663
+ allowRelationFilters
1664
+ );
1049
1665
  }
1050
1666
  for (const when of expr.whenClauses) {
1051
- validateBindingFinalExpression(when.condition, ctx, bindingName);
1052
- validateBindingFinalExpression(when.result, ctx, bindingName);
1667
+ validateBindingFinalExpression(
1668
+ when.condition,
1669
+ ctx,
1670
+ bindingName,
1671
+ allowRelationFilters
1672
+ );
1673
+ validateBindingFinalExpression(
1674
+ when.result,
1675
+ ctx,
1676
+ bindingName,
1677
+ allowRelationFilters
1678
+ );
1053
1679
  }
1054
1680
  if (expr.elseClause) {
1055
- validateBindingFinalExpression(expr.elseClause, ctx, bindingName);
1681
+ validateBindingFinalExpression(
1682
+ expr.elseClause,
1683
+ ctx,
1684
+ bindingName,
1685
+ allowRelationFilters
1686
+ );
1056
1687
  }
1057
1688
  break;
1058
1689
  case "relationFilter":
1690
+ if (allowRelationFilters) {
1691
+ resolveBindingRelationFilter(
1692
+ ctx,
1693
+ bindingName,
1694
+ expr.relation
1695
+ );
1696
+ break;
1697
+ }
1059
1698
  assertNoBindingRelationConstruct(
1060
1699
  ctx,
1061
1700
  bindingName,
@@ -1111,12 +1750,7 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1111
1750
  case "star":
1112
1751
  break;
1113
1752
  case "relationStar":
1114
- assertNoBindingRelationConstruct(
1115
- ctx,
1116
- bindingName,
1117
- "select relation columns",
1118
- item.relation.join(".")
1119
- );
1753
+ resolveBindingRelationInclude(ctx, bindingName, item.relation);
1120
1754
  break;
1121
1755
  case "expression":
1122
1756
  if (item.expression.type === "path" && item.expression.segments.length > 1) {
@@ -1130,11 +1764,11 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1130
1764
  firstSegment
1131
1765
  );
1132
1766
  } else {
1133
- assertNoBindingRelationConstruct(
1767
+ resolveBindingRelationColumn(
1134
1768
  ctx,
1135
1769
  bindingName,
1136
- "select relation columns",
1137
- segments.slice(0, -1).join(".")
1770
+ segments.slice(0, -1),
1771
+ segments.at(-1)
1138
1772
  );
1139
1773
  }
1140
1774
  } else {
@@ -1166,7 +1800,12 @@ function validateBindingFinalQuery(query, ctx) {
1166
1800
  ctx.currentHavingAliases = previousHavingAliases;
1167
1801
  }
1168
1802
  } else {
1169
- validateBindingFinalExpression(clause.condition, ctx, query.table);
1803
+ validateBindingFinalExpression(
1804
+ clause.condition,
1805
+ ctx,
1806
+ query.table,
1807
+ true
1808
+ );
1170
1809
  }
1171
1810
  break;
1172
1811
  }
@@ -1351,17 +1990,34 @@ function compileQueryInternal(query, ctx, fns, bindings) {
1351
1990
  const relationPaths = /* @__PURE__ */ new Set();
1352
1991
  for (const expr of select.columns) {
1353
1992
  if (expr.kind === "relationColumn") {
1354
- relationPaths.add(expr.relation);
1993
+ if (!isBindingSource || expr.column === "*") {
1994
+ relationPaths.add(expr.relation);
1995
+ }
1355
1996
  }
1356
1997
  }
1357
1998
  if (relationPaths.size > 0) {
1358
- const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
1359
- for (const inc of nestedIncludes) {
1360
- const exists = allIncludes.some(
1361
- (existing) => existing.relation === inc.relation
1362
- );
1363
- if (!exists) {
1364
- allIncludes.push(inc);
1999
+ if (isBindingSource) {
2000
+ for (const relation of relationPaths) {
2001
+ resolveBindingRelationInclude(ctx, query.table, relation.split("."));
2002
+ }
2003
+ const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
2004
+ for (const inc of nestedIncludes) {
2005
+ const exists = allIncludes.some(
2006
+ (existing) => existing.relation === inc.relation
2007
+ );
2008
+ if (!exists) {
2009
+ allIncludes.push(inc);
2010
+ }
2011
+ }
2012
+ } else {
2013
+ const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
2014
+ for (const inc of nestedIncludes) {
2015
+ const exists = allIncludes.some(
2016
+ (existing) => existing.relation === inc.relation
2017
+ );
2018
+ if (!exists) {
2019
+ allIncludes.push(inc);
2020
+ }
1365
2021
  }
1366
2022
  }
1367
2023
  }
@@ -1489,9 +2145,10 @@ function expressionOutputColumn(expr) {
1489
2145
  }
1490
2146
  }
1491
2147
  function addUnique(columns, seen, column) {
1492
- if (seen.has(column)) return;
2148
+ if (seen.has(column)) return false;
1493
2149
  seen.add(column);
1494
2150
  columns.push(column);
2151
+ return true;
1495
2152
  }
1496
2153
  function resolveSourceOutputColumns(intent, ctx, bindingName) {
1497
2154
  const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
@@ -1503,33 +2160,62 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
1503
2160
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
1504
2161
  );
1505
2162
  }
1506
- function getQueryOutputSchema(intent, ctx, bindingName) {
2163
+ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
1507
2164
  const columns = [];
1508
2165
  const seen = /* @__PURE__ */ new Set();
2166
+ const directProjectionLineage = [];
1509
2167
  const addColumn = (column) => addUnique(columns, seen, column);
2168
+ const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
2169
+ const addDirectProjection = (outputColumn, sourceColumn) => {
2170
+ if (!addColumn(outputColumn)) return;
2171
+ directProjectionLineage.push({
2172
+ kind: "directProjection",
2173
+ sourceTable: intent.from,
2174
+ sourceColumn: resolveSourceColumn(sourceColumn),
2175
+ outputColumn
2176
+ });
2177
+ };
1510
2178
  const addSourceColumns = () => {
1511
2179
  for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
1512
- addColumn(column);
2180
+ addDirectProjection(column, column);
1513
2181
  }
1514
2182
  };
1515
2183
  const { select } = intent;
1516
2184
  if (!select || select.type === "all") {
1517
2185
  addSourceColumns();
1518
- return { columns };
2186
+ const outputSchema2 = { columns, directProjectionLineage };
2187
+ return {
2188
+ columns: outputSchema2.columns,
2189
+ relationFilters: getBindingRelationFilterMetadata(
2190
+ intent,
2191
+ ctx,
2192
+ outputSchema2,
2193
+ bindingDependencies
2194
+ )
2195
+ };
1519
2196
  }
1520
2197
  if (select.type === "fields") {
1521
2198
  for (const field of select.fields) {
1522
2199
  if (field === "*") {
1523
2200
  addSourceColumns();
1524
2201
  } else {
1525
- addColumn(field);
2202
+ addDirectProjection(field, field);
1526
2203
  }
1527
2204
  }
1528
- return { columns };
2205
+ const outputSchema2 = { columns, directProjectionLineage };
2206
+ return {
2207
+ columns: outputSchema2.columns,
2208
+ relationFilters: getBindingRelationFilterMetadata(
2209
+ intent,
2210
+ ctx,
2211
+ outputSchema2,
2212
+ bindingDependencies
2213
+ )
2214
+ };
1529
2215
  }
1530
2216
  if (select.type === "aggregate") {
1531
2217
  for (const field of select.fields ?? []) {
1532
- addColumn(field);
2218
+ addDirectProjection(field, field);
1533
2219
  }
1534
2220
  for (const aggregate of select.aggregates) {
1535
2221
  if (!aggregate.as) {
@@ -1540,7 +2226,16 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
1540
2226
  }
1541
2227
  addColumn(aggregate.as);
1542
2228
  }
1543
- return { columns };
2229
+ const outputSchema2 = { columns, directProjectionLineage };
2230
+ return {
2231
+ columns: outputSchema2.columns,
2232
+ relationFilters: getBindingRelationFilterMetadata(
2233
+ intent,
2234
+ ctx,
2235
+ outputSchema2,
2236
+ bindingDependencies
2237
+ )
2238
+ };
1544
2239
  }
1545
2240
  for (const expr of select.columns) {
1546
2241
  if (expr.kind === "column" && expr.column === "*") {
@@ -1560,9 +2255,24 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
1560
2255
  `Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
1561
2256
  );
1562
2257
  }
1563
- addColumn(outputColumn);
2258
+ if (expr.kind === "column") {
2259
+ addDirectProjection(outputColumn, expr.column);
2260
+ } else if (expr.kind === "columnAlias") {
2261
+ addDirectProjection(outputColumn, expr.column);
2262
+ } else {
2263
+ addColumn(outputColumn);
2264
+ }
1564
2265
  }
1565
- return { columns };
2266
+ const outputSchema = { columns, directProjectionLineage };
2267
+ return {
2268
+ columns: outputSchema.columns,
2269
+ relationFilters: getBindingRelationFilterMetadata(
2270
+ intent,
2271
+ ctx,
2272
+ outputSchema,
2273
+ bindingDependencies
2274
+ )
2275
+ };
1566
2276
  }
1567
2277
  function validateNqlExpressionPaths(expr, ctx) {
1568
2278
  switch (expr.type) {
@@ -2334,13 +3044,12 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2334
3044
  const relFilter = expr;
2335
3045
  const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
2336
3046
  const prevRelationTarget = ctx.currentRelationTarget;
3047
+ const bindingRelation = ctx.currentFromTable && relFilter.relation[0] ? resolveBindingRelationFilter(
3048
+ ctx,
3049
+ ctx.currentFromTable,
3050
+ relFilter.relation
3051
+ ) : void 0;
2337
3052
  if (ctx.currentFromTable && relFilter.relation[0]) {
2338
- assertNoBindingRelationConstruct(
2339
- ctx,
2340
- ctx.currentFromTable,
2341
- "use relation filters",
2342
- relFilter.relation.join(".")
2343
- );
2344
3053
  if (ctx.validator) {
2345
3054
  ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2346
3055
  ctx.currentFromTable,
@@ -2356,13 +3065,28 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2356
3065
  nestedOuterAliases
2357
3066
  );
2358
3067
  ctx.currentRelationTarget = prevRelationTarget;
2359
- return {
3068
+ const relationFilterIntent = {
2360
3069
  kind: "relationFilter",
2361
3070
  relation: relFilter.relation,
2362
3071
  where,
2363
3072
  mode: relFilter.mode,
2364
- ...relFilter.alias !== void 0 && { alias: relFilter.alias }
3073
+ ...relFilter.alias !== void 0 && { alias: relFilter.alias },
3074
+ ...bindingRelation !== void 0 && {
3075
+ targetTable: bindingRelation.targetTable,
3076
+ sourceColumn: bindingRelation.sourceColumn,
3077
+ targetColumn: bindingRelation.targetColumn
3078
+ }
2365
3079
  };
3080
+ return bindingRelation ? markNqlTrustedRelationFilter(relationFilterIntent, {
3081
+ relation: bindingRelation.relation,
3082
+ targetTable: bindingRelation.targetTable,
3083
+ sourceColumn: bindingRelation.sourceColumn,
3084
+ targetColumn: bindingRelation.targetColumn,
3085
+ hops: bindingRelation.hops,
3086
+ ...bindingRelation.cardinality !== void 0 && {
3087
+ cardinality: bindingRelation.cardinality
3088
+ }
3089
+ }) : relationFilterIntent;
2366
3090
  }
2367
3091
  function expandDateRangeList(field, patterns, negated) {
2368
3092
  const conditions = patterns.map((pattern) => {
@@ -2468,6 +3192,20 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
2468
3192
  ...NQL_SELECT_JSON_FUNCTIONS,
2469
3193
  ...NQL_SELECT_SCALAR_FUNCTIONS
2470
3194
  ]);
3195
+ function resolveBindingRelationInclude2(ctx, bindingName, relationPath) {
3196
+ if (!isBindingTable(ctx, bindingName)) return false;
3197
+ if (!bindingName || !ctx.validator) {
3198
+ throw new NqlSemanticException(
3199
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3200
+ `Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
3201
+ );
3202
+ }
3203
+ ctx.validator.resolveVirtualBindingScalarRelationForInclude(
3204
+ bindingName,
3205
+ relationPath
3206
+ );
3207
+ return true;
3208
+ }
2471
3209
  function validateSelectPathExpression(expr, ctx) {
2472
3210
  if (!ctx.currentFromTable) return;
2473
3211
  const { segments } = expr;
@@ -2523,12 +3261,14 @@ function compileSelectClause(clause, ctx, fns) {
2523
3261
  } else if (item.type === "relationStar") {
2524
3262
  hasExpressions = true;
2525
3263
  const relation = item.relation.join(".");
2526
- assertNoBindingRelationConstruct(
2527
- ctx,
2528
- ctx.currentFromTable,
2529
- "select relation columns",
2530
- relation
2531
- );
3264
+ if (!resolveBindingRelationInclude2(ctx, ctx.currentFromTable, item.relation)) {
3265
+ assertNoBindingRelationConstruct(
3266
+ ctx,
3267
+ ctx.currentFromTable,
3268
+ "select relation columns",
3269
+ relation
3270
+ );
3271
+ }
2532
3272
  expressions.push({
2533
3273
  kind: "relationColumn",
2534
3274
  relation,
@@ -2854,27 +3594,47 @@ function compileMultiSegmentPath(expr, item, ctx) {
2854
3594
  }
2855
3595
  const column = segments[segments.length - 1];
2856
3596
  const relation = segments.slice(0, -1).join(".");
2857
- assertNoBindingRelationConstruct(
3597
+ const bindingRelation = resolveBindingRelationColumn(
2858
3598
  ctx,
2859
3599
  ctx.currentFromTable,
2860
- "select relation columns",
2861
- relation
3600
+ segments.slice(0, -1),
3601
+ column
2862
3602
  );
2863
- if (ctx.currentFromTable && ctx.validator) {
2864
- const targetTable = ctx.validator.resolveRelationTarget(
3603
+ if (!bindingRelation) {
3604
+ assertNoBindingRelationConstruct(
3605
+ ctx,
2865
3606
  ctx.currentFromTable,
2866
- segments[0]
3607
+ "select relation columns",
3608
+ relation
2867
3609
  );
3610
+ }
3611
+ if (ctx.currentFromTable && ctx.validator) {
3612
+ const lastBindingHop = bindingRelation?.hops[bindingRelation.hops.length - 1];
3613
+ const targetTable = lastBindingHop?.target ?? bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
2868
3614
  if (targetTable) {
2869
3615
  ctx.validator.validateColumn(targetTable, column);
2870
3616
  }
2871
3617
  }
2872
- return {
3618
+ const relationColumnIntent = {
2873
3619
  kind: "relationColumn",
2874
3620
  relation,
2875
3621
  column,
2876
3622
  as: item.alias ?? `${relation}.${column}`
2877
3623
  };
3624
+ return bindingRelation ? markNqlTrustedRelationFilter(relationColumnIntent, {
3625
+ relation: bindingRelation.relation,
3626
+ targetTable: bindingRelation.targetTable,
3627
+ sourceColumn: bindingRelation.sourceColumn,
3628
+ targetColumn: bindingRelation.targetColumn,
3629
+ hops: bindingRelation.hops,
3630
+ selectedColumn: column,
3631
+ ...bindingRelation.cardinality !== void 0 && {
3632
+ cardinality: bindingRelation.cardinality
3633
+ },
3634
+ ...bindingRelation.relationType !== void 0 && {
3635
+ relationType: bindingRelation.relationType
3636
+ }
3637
+ }) : relationColumnIntent;
2878
3638
  }
2879
3639
  function compileCaseExpression(caseExpr, item, ctx, fns) {
2880
3640
  if (caseExpr.subject) {
@@ -3262,6 +4022,7 @@ var NqlCompiler = class {
3262
4022
  recursiveKeywords,
3263
4023
  validator,
3264
4024
  bindingOutputColumns: /* @__PURE__ */ new Map(),
4025
+ bindingRelationFilters: /* @__PURE__ */ new Map(),
3265
4026
  params,
3266
4027
  maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
3267
4028
  allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
@@ -3281,6 +4042,7 @@ var NqlCompiler = class {
3281
4042
  return this.compileProgram(program);
3282
4043
  } finally {
3283
4044
  this.ctx.bindingOutputColumns.clear();
4045
+ this.ctx.bindingRelationFilters.clear();
3284
4046
  this.ctx.validator?.clearVirtualBindingTables();
3285
4047
  }
3286
4048
  }
@@ -3364,13 +4126,15 @@ var NqlCompiler = class {
3364
4126
  const outputSchema = this.registerQueryBindingOutputSchema(
3365
4127
  bindName,
3366
4128
  lastResult.query,
3367
- bindingOutputSchemas
4129
+ bindingOutputSchemas,
4130
+ statementBindingDependencies
3368
4131
  );
3369
4132
  bindings.set(bindName, lastResult.query);
3370
4133
  if (outputSchema) {
3371
4134
  this.ctx.validator?.addVirtualBindingTable(
3372
4135
  bindName,
3373
- outputSchema.columns
4136
+ outputSchema.columns,
4137
+ outputSchema.relationFilters
3374
4138
  );
3375
4139
  }
3376
4140
  materializedBindStatements.add(i);
@@ -3399,7 +4163,8 @@ var NqlCompiler = class {
3399
4163
  bindingOutputSchemas.set(bindName, outputSchema);
3400
4164
  this.ctx.validator?.addVirtualBindingTable(
3401
4165
  bindName,
3402
- outputSchema.columns
4166
+ outputSchema.columns,
4167
+ outputSchema.relationFilters
3403
4168
  );
3404
4169
  }
3405
4170
  materializedBindStatements.add(i);
@@ -3439,11 +4204,22 @@ var NqlCompiler = class {
3439
4204
  }
3440
4205
  return lastResult;
3441
4206
  }
3442
- registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas) {
4207
+ registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas, bindingDependencies) {
3443
4208
  try {
3444
- const outputSchema = getQueryOutputSchema(query, this.ctx, bindName);
4209
+ const outputSchema = getQueryOutputSchema(
4210
+ query,
4211
+ this.ctx,
4212
+ bindName,
4213
+ bindingDependencies
4214
+ );
3445
4215
  bindingOutputSchemas.set(bindName, outputSchema);
3446
4216
  this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
4217
+ if (outputSchema.relationFilters) {
4218
+ this.ctx.bindingRelationFilters.set(
4219
+ bindName,
4220
+ outputSchema.relationFilters
4221
+ );
4222
+ }
3447
4223
  return outputSchema;
3448
4224
  } catch (error) {
3449
4225
  if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {