@dbsp/nql 1.3.0 → 1.5.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, isNqlBindingRef, getNqlBindingRefName, createNqlBindingRef } from '@dbsp/types/internal';
2
- import { NQL_SELECT_AGGREGATE_FUNCTIONS, NQL_SELECT_JSON_FUNCTIONS, NQL_SELECT_SCALAR_FUNCTIONS, isParamIntent, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction } from '@dbsp/types';
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';
3
3
  import { createToken, Lexer, CstParser } from 'chevrotain';
4
4
 
5
5
  // src/compiler/index.ts
@@ -51,6 +51,8 @@ var ColumnValidator = class _ColumnValidator {
51
51
  }
52
52
  schema;
53
53
  knownCteTables = /* @__PURE__ */ new Set();
54
+ virtualBindingTables = /* @__PURE__ */ new Map();
55
+ virtualBindingRelationFilters = /* @__PURE__ */ new Map();
54
56
  /**
55
57
  * Register CTE names so validateTable() skips validation for them.
56
58
  * CTE names are not physical tables in the schema, but are valid FROM targets.
@@ -64,6 +66,124 @@ var ColumnValidator = class _ColumnValidator {
64
66
  clearKnownCteTables() {
65
67
  this.knownCteTables.clear();
66
68
  }
69
+ /**
70
+ * Register an NQL binding as a virtual table with a concrete output schema.
71
+ * Binding columns are logical output names, so matching is exact.
72
+ */
73
+ addVirtualBindingTable(name, columns, relationFilters) {
74
+ this.virtualBindingTables.set(name, columns);
75
+ if (relationFilters) {
76
+ this.virtualBindingRelationFilters.set(name, relationFilters);
77
+ }
78
+ }
79
+ /** Clear all registered virtual binding tables (used between compilations). */
80
+ clearVirtualBindingTables() {
81
+ this.virtualBindingTables.clear();
82
+ this.virtualBindingRelationFilters.clear();
83
+ }
84
+ isVirtualBindingTable(name) {
85
+ return name !== void 0 && this.virtualBindingTables.has(name);
86
+ }
87
+ getVirtualBindingColumns(name) {
88
+ return this.virtualBindingTables.get(name);
89
+ }
90
+ getTableColumns(name) {
91
+ const virtualColumns = this.virtualBindingTables.get(name);
92
+ if (virtualColumns) return virtualColumns;
93
+ return this.schema.getTable(name)?.columns.map((column) => column.name);
94
+ }
95
+ hasPhysicalTable(name) {
96
+ return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
97
+ }
98
+ hasQualifiedRelationLookup() {
99
+ return typeof this.schema.getRelation === "function";
100
+ }
101
+ getRelation(sourceTable, relationName) {
102
+ return this.schema.getRelation?.(`${sourceTable}.${relationName}`) ?? this.schema.getRelationsFrom(sourceTable).find((relation) => relation.name === relationName);
103
+ }
104
+ getRelationsFrom(sourceTable) {
105
+ return this.schema.getRelationsFrom(sourceTable);
106
+ }
107
+ getVirtualBindingRelation(bindingName, relationName) {
108
+ return this.virtualBindingRelationFilters.get(bindingName)?.relations.find((relation) => relation.relation === relationName);
109
+ }
110
+ getVirtualBindingScalarRelation(bindingName, relationName) {
111
+ return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
112
+ }
113
+ explainVirtualBindingRelationRejection(bindingName, relationName) {
114
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
115
+ if (!metadata) {
116
+ return "no binding relation-filter metadata is available";
117
+ }
118
+ if (metadata.unsafeReason) return metadata.unsafeReason;
119
+ const sourceTable = metadata.sourceTable;
120
+ if (!sourceTable) {
121
+ return "the binding source table could not be proven";
122
+ }
123
+ const relation = this.getRelation(sourceTable, relationName);
124
+ if (!relation) {
125
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
126
+ }
127
+ const fk = relation.foreignKey;
128
+ const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
129
+ if (relation.type !== "belongsTo") {
130
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
131
+ }
132
+ if (fkColumns.length !== 1) {
133
+ return `relation '${relationName}' must have exactly one FK column for A-lite binding relation filters`;
134
+ }
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})` : ""}`;
145
+ }
146
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
147
+ return `relation '${relationName}' FK column '${fkColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
148
+ }
149
+ explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
150
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
151
+ if (!metadata) {
152
+ return "no binding relation metadata is available";
153
+ }
154
+ if (metadata.unsafeReason) return metadata.unsafeReason;
155
+ const sourceTable = metadata.sourceTable;
156
+ if (!sourceTable) {
157
+ return "the binding source table could not be proven";
158
+ }
159
+ const relation = this.getRelation(sourceTable, relationName);
160
+ if (!relation) {
161
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
162
+ }
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`;
174
+ }
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)
178
+ );
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})` : ""}`;
183
+ }
184
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
185
+ return `relation '${relationName}' source column '${sourceColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
186
+ }
67
187
  /**
68
188
  * Convert camelCase to snake_case for column name matching.
69
189
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -79,14 +199,40 @@ var ColumnValidator = class _ColumnValidator {
79
199
  if (queryCol === schemaCol) return true;
80
200
  return _ColumnValidator.toSnakeCase(queryCol) === _ColumnValidator.toSnakeCase(schemaCol);
81
201
  }
202
+ /**
203
+ * Resolve a user-authored column spelling to the model column name.
204
+ * Uses the same exact-or-snake/camel equivalence as validateColumn().
205
+ */
206
+ resolveColumnName(table, column) {
207
+ if (column === "*") return column;
208
+ const virtualColumns = this.virtualBindingTables.get(table);
209
+ if (virtualColumns) {
210
+ return virtualColumns.find((c) => c === column);
211
+ }
212
+ const tableInfo = this.schema.getTable(table);
213
+ if (!tableInfo) return void 0;
214
+ return tableInfo.columns.find(
215
+ (c) => _ColumnValidator.columnsMatch(column, c.name)
216
+ )?.name;
217
+ }
82
218
  validateColumn(table, column) {
83
219
  if (column === "*") return;
220
+ const virtualColumns = this.virtualBindingTables.get(table);
221
+ if (virtualColumns) {
222
+ const exists = virtualColumns.some((c) => c === column);
223
+ if (!exists) {
224
+ const available = virtualColumns.join(", ") || "(none)";
225
+ throw new NqlSemanticException(
226
+ NqlErrorCodes.SEM_UNKNOWN_COLUMN,
227
+ `Column '${column}' is not projected by NQL binding '${table}'. Available columns: ${available}`
228
+ );
229
+ }
230
+ return;
231
+ }
84
232
  const tableInfo = this.schema.getTable(table);
85
233
  if (!tableInfo) return;
86
- const exists = tableInfo.columns.some(
87
- (c) => _ColumnValidator.columnsMatch(column, c.name)
88
- );
89
- if (!exists) {
234
+ const resolvedColumn = this.resolveColumnName(table, column);
235
+ if (resolvedColumn === void 0) {
90
236
  const available = tableInfo.columns.map((c) => c.name).join(", ");
91
237
  throw new NqlSemanticException(
92
238
  NqlErrorCodes.SEM_UNKNOWN_COLUMN,
@@ -95,6 +241,7 @@ var ColumnValidator = class _ColumnValidator {
95
241
  }
96
242
  }
97
243
  validateTable(table) {
244
+ if (this.virtualBindingTables.has(table)) return;
98
245
  if (this.knownCteTables.has(table)) return;
99
246
  const tableInfo = this.schema.getTable(table);
100
247
  if (!tableInfo) {
@@ -105,10 +252,34 @@ var ColumnValidator = class _ColumnValidator {
105
252
  }
106
253
  }
107
254
  resolveRelationTarget(sourceTable, relationName) {
108
- const relations = this.schema.getRelationsFrom(sourceTable);
109
- const rel = relations.find((r) => r.name === relationName);
255
+ if (this.virtualBindingTables.has(sourceTable)) {
256
+ const virtualRelation = this.getVirtualBindingRelation(
257
+ sourceTable,
258
+ relationName
259
+ );
260
+ if (virtualRelation) return virtualRelation.targetTable;
261
+ throw new NqlSemanticException(
262
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
263
+ `Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${this.explainVirtualBindingRelationRejection(sourceTable, relationName)}.`
264
+ );
265
+ }
266
+ const rel = this.getRelation(sourceTable, relationName);
110
267
  return rel?.target;
111
268
  }
269
+ assertNoBindingRelationConstruct(bindingName, construct, detail) {
270
+ if (!this.isVirtualBindingTable(bindingName)) return;
271
+ throw new NqlSemanticException(
272
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
273
+ `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
274
+ );
275
+ }
276
+ assertNoBindingRelationPath(bindingName, path) {
277
+ if (!this.isVirtualBindingTable(bindingName)) return;
278
+ throw new NqlSemanticException(
279
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
280
+ `Query '${bindingName}' reads from an NQL binding and cannot reference relation path '${path}'. Relation paths require a physical model table, not a CTE binding.`
281
+ );
282
+ }
112
283
  };
113
284
  var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
114
285
  "__proto__",
@@ -197,6 +368,125 @@ function expressionToField(expr, aliasContext) {
197
368
  }
198
369
  return null;
199
370
  }
371
+ function isBindingTable(ctx, table) {
372
+ if (table === void 0) return false;
373
+ return ctx.bindingOutputColumns.has(table) || ctx.validator?.isVirtualBindingTable?.(table) === true;
374
+ }
375
+ function bindingColumns(ctx, table) {
376
+ if (table === void 0) return void 0;
377
+ if (ctx.bindingOutputColumns.has(table)) {
378
+ return ctx.bindingOutputColumns.get(table);
379
+ }
380
+ return ctx.validator?.getVirtualBindingColumns?.(table);
381
+ }
382
+ function getKnownBindingColumns(ctx, table) {
383
+ return bindingColumns(ctx, table);
384
+ }
385
+ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
386
+ if (!isBindingTable(ctx, bindingName)) return;
387
+ throw new NqlSemanticException(
388
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
389
+ `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
390
+ );
391
+ }
392
+ function throwBindingRelationFilter182(bindingName, relationName, reason) {
393
+ throw new NqlSemanticException(
394
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
395
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${reason}.`
396
+ );
397
+ }
398
+ function throwBindingRelationColumn182(bindingName, relationName, reason) {
399
+ throw new NqlSemanticException(
400
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
401
+ `Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
402
+ );
403
+ }
404
+ function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
405
+ if (!isBindingTable(ctx, bindingName)) return void 0;
406
+ const actualBindingName = bindingName;
407
+ const relationName = relationPath.join(".");
408
+ if (relationPath.length !== 1) {
409
+ throwBindingRelationFilter182(
410
+ actualBindingName,
411
+ relationName,
412
+ "multi-hop binding relation filters are not supported; the relation path must be a single source-table relation"
413
+ );
414
+ }
415
+ const relation = relationPath[0];
416
+ if (!relation) {
417
+ throwBindingRelationFilter182(
418
+ actualBindingName,
419
+ relationName,
420
+ "the relation path must name a source-table relation"
421
+ );
422
+ }
423
+ const virtualRelation = ctx.validator?.getVirtualBindingRelation(
424
+ actualBindingName,
425
+ relation
426
+ );
427
+ if (virtualRelation) return virtualRelation;
428
+ const reason = ctx.validator?.explainVirtualBindingRelationRejection(
429
+ actualBindingName,
430
+ relation
431
+ ) ?? "model metadata is not available";
432
+ throwBindingRelationFilter182(actualBindingName, relation, reason);
433
+ }
434
+ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedColumn) {
435
+ if (!isBindingTable(ctx, bindingName)) return void 0;
436
+ const actualBindingName = bindingName;
437
+ const relationName = relationPath.join(".");
438
+ if (relationPath.length !== 1) {
439
+ throwBindingRelationColumn182(
440
+ actualBindingName,
441
+ relationName,
442
+ "multi-hop binding relation columns are not supported; the relation path must be a single source-table relation"
443
+ );
444
+ }
445
+ const relation = relationPath[0];
446
+ if (!relation) {
447
+ throwBindingRelationColumn182(
448
+ actualBindingName,
449
+ relationName,
450
+ "the relation path must name a source-table relation"
451
+ );
452
+ }
453
+ const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
454
+ actualBindingName,
455
+ relation
456
+ );
457
+ if (!virtualRelation) {
458
+ const reason = ctx.validator?.explainVirtualBindingScalarRelationRejection(
459
+ actualBindingName,
460
+ relation
461
+ ) ?? "model metadata is not available";
462
+ throwBindingRelationColumn182(actualBindingName, relation, reason);
463
+ }
464
+ ctx.validator?.validateColumn(virtualRelation.targetTable, selectedColumn);
465
+ return virtualRelation;
466
+ }
467
+ function assertNoBindingRelationPath(ctx, bindingName, path) {
468
+ if (!isBindingTable(ctx, bindingName)) return;
469
+ throw new NqlSemanticException(
470
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
471
+ `Query '${bindingName}' reads from an NQL binding and cannot reference relation path '${path}'. Relation paths require a physical model table, not a CTE binding.`
472
+ );
473
+ }
474
+ function validateColumnForTable(ctx, table, column) {
475
+ if (table === void 0 || column === "*") return;
476
+ if (isBindingTable(ctx, table)) {
477
+ const columns = bindingColumns(ctx, table);
478
+ if (columns === void 0) return;
479
+ if (!columns.some((c) => c === column)) {
480
+ const available = columns.join(", ") || "(none)";
481
+ throw new NqlSemanticException(
482
+ NqlErrorCodes.SEM_UNKNOWN_COLUMN,
483
+ `Column '${column}' is not projected by NQL binding '${table}'. Available columns: ${available}`
484
+ );
485
+ }
486
+ return;
487
+ }
488
+ ctx.validator?.validateColumn(table, column);
489
+ }
200
490
  function expressionToValue(expr, ctx) {
201
491
  switch (expr.type) {
202
492
  case "namedParam":
@@ -216,6 +506,22 @@ function expressionToValue(expr, ctx) {
216
506
  case "null":
217
507
  return null;
218
508
  case "path":
509
+ if (ctx) {
510
+ const field = expr.segments.join(".");
511
+ const isBindingReference = expr.segments.length === 1 && isBindingTable(ctx, field);
512
+ if (isBindingReference) {
513
+ return createNqlBindingRef(field);
514
+ }
515
+ const sourceTable = ctx.currentFromTable;
516
+ const isVirtualBindingSource = sourceTable !== void 0 && isBindingTable(ctx, sourceTable);
517
+ if (isVirtualBindingSource) {
518
+ if (field.includes(".")) {
519
+ assertNoBindingRelationPath(ctx, sourceTable, field);
520
+ } else {
521
+ validateColumnForTable(ctx, sourceTable, field);
522
+ }
523
+ }
524
+ }
219
525
  return createNqlBindingRef(expr.segments.join("."));
220
526
  case "function": {
221
527
  return {
@@ -382,6 +688,24 @@ function isAggregateFunction(name) {
382
688
  );
383
689
  }
384
690
  function validateWhereField(ctx, field, aliasContext, originalExpr) {
691
+ if (!aliasContext && !field.includes(".") && ctx.currentHavingAliases?.has(field)) {
692
+ throw new NqlSemanticException(
693
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
694
+ `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.`
695
+ );
696
+ }
697
+ if (!aliasContext && ctx.currentRelationTarget && !field.includes(".")) {
698
+ ctx.validator?.validateColumn(ctx.currentRelationTarget, field);
699
+ return;
700
+ }
701
+ if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
702
+ if (field.includes(".")) {
703
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
704
+ } else {
705
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
706
+ }
707
+ return;
708
+ }
385
709
  if (!ctx.validator) return;
386
710
  if (aliasContext) {
387
711
  const isInnerScope = originalExpr?.type === "path" && originalExpr.segments.length > 1 && originalExpr.segments[0] === aliasContext;
@@ -396,12 +720,14 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
396
720
  }
397
721
  return;
398
722
  }
399
- if (ctx.currentRelationTarget && !field.includes(".")) {
400
- ctx.validator.validateColumn(ctx.currentRelationTarget, field);
723
+ if (ctx.currentFromTable && field.includes(".")) {
724
+ if (isBindingTable(ctx, ctx.currentFromTable)) {
725
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
726
+ }
401
727
  return;
402
728
  }
403
729
  if (ctx.currentFromTable && !field.includes(".")) {
404
- ctx.validator.validateColumn(ctx.currentFromTable, field);
730
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
405
731
  }
406
732
  }
407
733
  function coerceToStringKey(expr, contextLabel, ctx) {
@@ -768,30 +1094,573 @@ function applyIncludeLimit(includes, path, limit) {
768
1094
  }
769
1095
 
770
1096
  // src/compiler/compile-query.ts
1097
+ var activeBindingScopes = /* @__PURE__ */ new WeakMap();
1098
+ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
1099
+ ...NQL_SELECT_AGGREGATE_FUNCTIONS,
1100
+ ...NQL_SELECT_VALUE_FUNCTIONS
1101
+ ]);
1102
+ var DEFAULT_RELATION_TARGET_COLUMN = "id";
771
1103
  function compileNestedQuery(query, ctx, fns, bindings) {
772
1104
  const savedContext = {
773
1105
  currentFromTable: ctx.currentFromTable,
774
- currentRelationTarget: ctx.currentRelationTarget
1106
+ currentRelationTarget: ctx.currentRelationTarget,
1107
+ currentHavingAliases: ctx.currentHavingAliases
775
1108
  };
776
1109
  try {
777
- if (bindings) {
778
- return compileQuery(query, ctx, fns, bindings);
1110
+ const nestedBindings = bindings ?? activeBindingScopes.get(ctx);
1111
+ if (nestedBindings) {
1112
+ return compileQuery(query, ctx, fns, nestedBindings);
779
1113
  }
780
1114
  return fns.compileQuery(query, ctx);
781
1115
  } finally {
782
1116
  ctx.currentFromTable = savedContext.currentFromTable;
783
1117
  ctx.currentRelationTarget = savedContext.currentRelationTarget;
1118
+ ctx.currentHavingAliases = savedContext.currentHavingAliases;
784
1119
  }
785
1120
  }
786
1121
  function compileQuery(query, ctx, fns, bindings) {
1122
+ const hadPreviousBindingScope = activeBindingScopes.has(ctx);
1123
+ const previousBindingScope = activeBindingScopes.get(ctx);
1124
+ if (bindings) {
1125
+ activeBindingScopes.set(ctx, bindings);
1126
+ }
1127
+ try {
1128
+ return compileQueryInternal(query, ctx, fns, bindings);
1129
+ } finally {
1130
+ if (hadPreviousBindingScope && previousBindingScope) {
1131
+ activeBindingScopes.set(ctx, previousBindingScope);
1132
+ } else {
1133
+ activeBindingScopes.delete(ctx);
1134
+ }
1135
+ }
1136
+ }
1137
+ function collectSelectAliasesFromQuery(query) {
1138
+ const aliases = /* @__PURE__ */ new Set();
1139
+ for (const clause of query.clauses) {
1140
+ if (clause.type !== "select") continue;
1141
+ for (const item of clause.items) {
1142
+ if (item.type === "expression" && item.alias) {
1143
+ aliases.add(item.alias);
1144
+ }
1145
+ }
1146
+ }
1147
+ return aliases;
1148
+ }
1149
+ function throwUnsupportedBindingFinal(bindingName, feature) {
1150
+ throw new NqlSemanticException(
1151
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1152
+ `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.`
1153
+ );
1154
+ }
1155
+ function toSnakeCase(name) {
1156
+ return name.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
1157
+ }
1158
+ function columnsMatch(left, right) {
1159
+ return left === right || toSnakeCase(left) === toSnakeCase(right);
1160
+ }
1161
+ function expressionIntentContainsAggregate(value) {
1162
+ if (!value || typeof value !== "object") return false;
1163
+ const record = value;
1164
+ if (record.kind === "aggregate") return true;
1165
+ for (const child of Object.values(record)) {
1166
+ if (Array.isArray(child)) {
1167
+ if (child.some((item) => expressionIntentContainsAggregate(item))) {
1168
+ return true;
1169
+ }
1170
+ } else if (expressionIntentContainsAggregate(child)) {
1171
+ return true;
1172
+ }
1173
+ }
1174
+ return false;
1175
+ }
1176
+ function selectContainsAggregate(select) {
1177
+ if (!select) return false;
1178
+ if (select.type === "aggregate") return true;
1179
+ if (select.type !== "expressions") return false;
1180
+ return select.columns.some(
1181
+ (column) => expressionIntentContainsAggregate(column)
1182
+ );
1183
+ }
1184
+ 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;
1188
+ return void 0;
1189
+ }
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;
1195
+ }
1196
+ function relationCardinality(relation) {
1197
+ return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
1198
+ }
1199
+ function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1200
+ if (!ctx.validator) return "model metadata is not available";
1201
+ if (!ctx.validator.hasQualifiedRelationLookup()) {
1202
+ return "model.getRelation metadata is not available";
1203
+ }
1204
+ if (bindingDependencies.length > 0 || isBindingTable(ctx, intent.from)) {
1205
+ return "the binding body reads from another NQL binding";
1206
+ }
1207
+ if (!ctx.validator.hasPhysicalTable(intent.from)) {
1208
+ return `the binding source '${intent.from}' is not a single real model table`;
1209
+ }
1210
+ if (intent.batchValuesSource) {
1211
+ return "the binding body reads from a batch-values source";
1212
+ }
1213
+ if ((intent.joins?.length ?? 0) > 0) {
1214
+ return "the binding body uses joins";
1215
+ }
1216
+ if ((intent.include?.length ?? 0) > 0) {
1217
+ return "the binding body uses relation includes";
1218
+ }
1219
+ if ((intent.groupBy?.length ?? 0) > 0 || intent.having) {
1220
+ return "the binding body uses GROUP BY or HAVING";
1221
+ }
1222
+ if (selectContainsAggregate(intent.select)) {
1223
+ return "the binding body uses aggregate projections";
1224
+ }
1225
+ return void 0;
1226
+ }
1227
+ function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionLineage) {
1228
+ return directProjectionLineage.find(
1229
+ (projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
1230
+ );
1231
+ }
1232
+ function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1233
+ const fkColumns = relationForeignKeysOnSource(relation);
1234
+ if (!relation || !fkColumns || fkColumns.length !== 1) return void 0;
1235
+ const fkColumn = fkColumns[0];
1236
+ const sourceProjection = findDirectSourceProjection(
1237
+ sourceTable,
1238
+ fkColumn,
1239
+ directProjectionLineage
1240
+ );
1241
+ if (!sourceProjection) return void 0;
1242
+ return {
1243
+ relation: relation.name,
1244
+ sourceTable,
1245
+ targetTable: relation.target,
1246
+ sourceColumn: sourceProjection.outputColumn,
1247
+ targetColumn: relation.targetKey ?? DEFAULT_RELATION_TARGET_COLUMN,
1248
+ cardinality: "one"
1249
+ };
1250
+ }
1251
+ function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1252
+ if (!relation) return void 0;
1253
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
1254
+ return void 0;
1255
+ }
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;
1261
+ const sourceProjection = findDirectSourceProjection(
1262
+ sourceTable,
1263
+ sourceJoinColumn,
1264
+ directProjectionLineage
1265
+ );
1266
+ if (!sourceProjection) return void 0;
1267
+ return {
1268
+ relation: relation.name,
1269
+ sourceTable,
1270
+ targetTable: relation.target,
1271
+ sourceColumn: sourceProjection.outputColumn,
1272
+ targetColumn: targetJoinColumn,
1273
+ cardinality: relationCardinality(relation)
1274
+ };
1275
+ }
1276
+ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
1277
+ const unsafeReason = unsafeBindingRelationReason(
1278
+ intent,
1279
+ ctx,
1280
+ bindingDependencies
1281
+ );
1282
+ if (unsafeReason) {
1283
+ return {
1284
+ unsafeReason,
1285
+ directProjectionLineage: outputSchema.directProjectionLineage,
1286
+ relations: []
1287
+ };
1288
+ }
1289
+ const sourceTable = intent.from;
1290
+ const relations = [];
1291
+ const scalarRelations = [];
1292
+ const relationNames = new Set(
1293
+ ctx.validator?.getRelationsFrom(sourceTable).map((relation) => relation.name)
1294
+ );
1295
+ for (const relationName of relationNames ?? []) {
1296
+ const relation = ctx.validator?.getRelation(sourceTable, relationName);
1297
+ const virtualRelation = virtualRelationForBinding(
1298
+ relation,
1299
+ sourceTable,
1300
+ outputSchema.directProjectionLineage ?? []
1301
+ );
1302
+ if (virtualRelation) relations.push(virtualRelation);
1303
+ const scalarRelation = scalarVirtualRelationForBinding(
1304
+ relation,
1305
+ sourceTable,
1306
+ outputSchema.directProjectionLineage ?? []
1307
+ );
1308
+ if (scalarRelation) scalarRelations.push(scalarRelation);
1309
+ }
1310
+ return {
1311
+ sourceTable,
1312
+ directProjectionLineage: outputSchema.directProjectionLineage,
1313
+ relations,
1314
+ scalarRelations
1315
+ };
1316
+ }
1317
+ function validateBindingFinalPath(expr, ctx, bindingName) {
1318
+ const { segments } = expr;
1319
+ if (segments.length === 1) {
1320
+ if (ctx.currentHavingAliases?.has(segments[0])) {
1321
+ throw new NqlSemanticException(
1322
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1323
+ `HAVING cannot reference SELECT alias '${segments[0]}'. PostgreSQL does not allow SELECT aliases in HAVING; repeat the aggregate expression or filter the result in an outer query.`
1324
+ );
1325
+ }
1326
+ validateColumnForTable(ctx, bindingName, segments[0]);
1327
+ return;
1328
+ }
1329
+ const firstSegment = segments[0];
1330
+ if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1331
+ assertNoBindingRelationConstruct(
1332
+ ctx,
1333
+ bindingName,
1334
+ "use pseudo-column traversals",
1335
+ firstSegment
1336
+ );
1337
+ return;
1338
+ }
1339
+ assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
1340
+ }
1341
+ function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
1342
+ const fn = expr.name.toLowerCase();
1343
+ if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
1344
+ throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
1345
+ }
1346
+ for (const arg of expr.args) {
1347
+ validateBindingFinalExpression(arg, ctx, bindingName, allowRelationFilters);
1348
+ }
1349
+ }
1350
+ function validateBindingFinalExpression(expr, ctx, bindingName, allowRelationFilters = false) {
1351
+ switch (expr.type) {
1352
+ case "path":
1353
+ validateBindingFinalPath(expr, ctx, bindingName);
1354
+ break;
1355
+ case "binary":
1356
+ validateBindingFinalExpression(
1357
+ expr.left,
1358
+ ctx,
1359
+ bindingName,
1360
+ allowRelationFilters
1361
+ );
1362
+ validateBindingFinalExpression(
1363
+ expr.right,
1364
+ ctx,
1365
+ bindingName,
1366
+ allowRelationFilters
1367
+ );
1368
+ break;
1369
+ case "unary":
1370
+ validateBindingFinalExpression(
1371
+ expr.operand,
1372
+ ctx,
1373
+ bindingName,
1374
+ allowRelationFilters
1375
+ );
1376
+ break;
1377
+ case "comparison":
1378
+ validateBindingFinalExpression(
1379
+ expr.left,
1380
+ ctx,
1381
+ bindingName,
1382
+ allowRelationFilters
1383
+ );
1384
+ validateBindingFinalExpression(
1385
+ expr.right,
1386
+ ctx,
1387
+ bindingName,
1388
+ allowRelationFilters
1389
+ );
1390
+ break;
1391
+ case "in":
1392
+ validateBindingFinalExpression(
1393
+ expr.expression,
1394
+ ctx,
1395
+ bindingName,
1396
+ allowRelationFilters
1397
+ );
1398
+ if (Array.isArray(expr.values)) {
1399
+ for (const value of expr.values) {
1400
+ validateBindingFinalExpression(
1401
+ value,
1402
+ ctx,
1403
+ bindingName,
1404
+ allowRelationFilters
1405
+ );
1406
+ }
1407
+ } else if (expr.values.type === "dateRange") ; else ;
1408
+ break;
1409
+ case "between":
1410
+ validateBindingFinalExpression(
1411
+ expr.expression,
1412
+ ctx,
1413
+ bindingName,
1414
+ allowRelationFilters
1415
+ );
1416
+ validateBindingFinalExpression(
1417
+ expr.low,
1418
+ ctx,
1419
+ bindingName,
1420
+ allowRelationFilters
1421
+ );
1422
+ validateBindingFinalExpression(
1423
+ expr.high,
1424
+ ctx,
1425
+ bindingName,
1426
+ allowRelationFilters
1427
+ );
1428
+ break;
1429
+ case "isNull":
1430
+ validateBindingFinalExpression(
1431
+ expr.expression,
1432
+ ctx,
1433
+ bindingName,
1434
+ allowRelationFilters
1435
+ );
1436
+ break;
1437
+ case "function":
1438
+ validateBindingFinalFunction(
1439
+ expr,
1440
+ ctx,
1441
+ bindingName,
1442
+ allowRelationFilters
1443
+ );
1444
+ break;
1445
+ case "case":
1446
+ if (expr.subject) {
1447
+ validateBindingFinalExpression(
1448
+ expr.subject,
1449
+ ctx,
1450
+ bindingName,
1451
+ allowRelationFilters
1452
+ );
1453
+ }
1454
+ for (const when of expr.whenClauses) {
1455
+ validateBindingFinalExpression(
1456
+ when.condition,
1457
+ ctx,
1458
+ bindingName,
1459
+ allowRelationFilters
1460
+ );
1461
+ validateBindingFinalExpression(
1462
+ when.result,
1463
+ ctx,
1464
+ bindingName,
1465
+ allowRelationFilters
1466
+ );
1467
+ }
1468
+ if (expr.elseClause) {
1469
+ validateBindingFinalExpression(
1470
+ expr.elseClause,
1471
+ ctx,
1472
+ bindingName,
1473
+ allowRelationFilters
1474
+ );
1475
+ }
1476
+ break;
1477
+ case "relationFilter":
1478
+ if (allowRelationFilters) {
1479
+ resolveBindingRelationFilter(
1480
+ ctx,
1481
+ bindingName,
1482
+ expr.relation
1483
+ );
1484
+ break;
1485
+ }
1486
+ assertNoBindingRelationConstruct(
1487
+ ctx,
1488
+ bindingName,
1489
+ "use relation filters",
1490
+ expr.relation.join(".")
1491
+ );
1492
+ break;
1493
+ case "any":
1494
+ throwUnsupportedBindingFinal(bindingName, "ANY(:param)");
1495
+ break;
1496
+ case "rangeOp":
1497
+ case "rangeLiteral":
1498
+ throwUnsupportedBindingFinal(bindingName, "PostgreSQL range operators");
1499
+ break;
1500
+ case "jsonAccess":
1501
+ case "jsonComparison":
1502
+ throwUnsupportedBindingFinal(bindingName, "PostgreSQL JSON operators");
1503
+ break;
1504
+ case "window":
1505
+ throwUnsupportedBindingFinal(bindingName, "window functions");
1506
+ break;
1507
+ case "exists":
1508
+ throwUnsupportedBindingFinal(bindingName, "EXISTS subqueries");
1509
+ break;
1510
+ case "subquery":
1511
+ throwUnsupportedBindingFinal(bindingName, "scalar subqueries");
1512
+ break;
1513
+ case "variable":
1514
+ throwUnsupportedBindingFinal(bindingName, `variable '${expr.name}'`);
1515
+ break;
1516
+ case "namedParam":
1517
+ case "string":
1518
+ case "number":
1519
+ case "boolean":
1520
+ case "null":
1521
+ case "dateRange":
1522
+ break;
1523
+ /* v8 ignore next — defensive: default-reject future expression shapes in binding-final queries -- @preserve */
1524
+ default:
1525
+ throwUnsupportedBindingFinal(
1526
+ bindingName,
1527
+ `expression type '${expr.type ?? "unknown"}'`
1528
+ );
1529
+ }
1530
+ }
1531
+ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1532
+ const distinctOn = clause.distinctOn;
1533
+ if (distinctOn !== void 0) {
1534
+ throwUnsupportedBindingFinal(bindingName, "DISTINCT ON");
1535
+ }
1536
+ for (const item of clause.items) {
1537
+ switch (item.type) {
1538
+ case "star":
1539
+ break;
1540
+ case "relationStar":
1541
+ assertNoBindingRelationConstruct(
1542
+ ctx,
1543
+ bindingName,
1544
+ "select relation columns",
1545
+ item.relation.join(".")
1546
+ );
1547
+ break;
1548
+ case "expression":
1549
+ if (item.expression.type === "path" && item.expression.segments.length > 1) {
1550
+ const segments = item.expression.segments;
1551
+ const firstSegment = segments[0];
1552
+ if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1553
+ assertNoBindingRelationConstruct(
1554
+ ctx,
1555
+ bindingName,
1556
+ "use pseudo-column traversals",
1557
+ firstSegment
1558
+ );
1559
+ } else {
1560
+ resolveBindingRelationColumn(
1561
+ ctx,
1562
+ bindingName,
1563
+ segments.slice(0, -1),
1564
+ segments.at(-1)
1565
+ );
1566
+ }
1567
+ } else {
1568
+ validateBindingFinalExpression(item.expression, ctx, bindingName);
1569
+ }
1570
+ break;
1571
+ /* v8 ignore next — defensive: default-reject future select item shapes in binding-final queries -- @preserve */
1572
+ default:
1573
+ throwUnsupportedBindingFinal(
1574
+ bindingName,
1575
+ `SELECT item type '${item.type ?? "unknown"}'`
1576
+ );
1577
+ }
1578
+ }
1579
+ }
1580
+ function validateBindingFinalQuery(query, ctx) {
1581
+ const groupByIndex = query.clauses.findIndex((c) => c.type === "groupBy");
1582
+ const havingAliases = collectSelectAliasesFromQuery(query);
1583
+ for (let i = 0; i < query.clauses.length; i++) {
1584
+ const clause = query.clauses[i];
1585
+ switch (clause.type) {
1586
+ case "where": {
1587
+ if (groupByIndex >= 0 && i > groupByIndex) {
1588
+ const previousHavingAliases = ctx.currentHavingAliases;
1589
+ ctx.currentHavingAliases = havingAliases;
1590
+ try {
1591
+ validateBindingFinalExpression(clause.condition, ctx, query.table);
1592
+ } finally {
1593
+ ctx.currentHavingAliases = previousHavingAliases;
1594
+ }
1595
+ } else {
1596
+ validateBindingFinalExpression(
1597
+ clause.condition,
1598
+ ctx,
1599
+ query.table,
1600
+ true
1601
+ );
1602
+ }
1603
+ break;
1604
+ }
1605
+ case "select":
1606
+ validateBindingFinalSelectClause(clause, ctx, query.table);
1607
+ break;
1608
+ case "groupBy":
1609
+ for (const expr of clause.expressions) {
1610
+ validateBindingFinalExpression(expr, ctx, query.table);
1611
+ }
1612
+ break;
1613
+ case "orderBy":
1614
+ for (const item of clause.items) {
1615
+ validateBindingFinalExpression(item.expression, ctx, query.table);
1616
+ }
1617
+ break;
1618
+ case "limit":
1619
+ if (clause.relation) {
1620
+ assertNoBindingRelationConstruct(
1621
+ ctx,
1622
+ query.table,
1623
+ "use relation include limits",
1624
+ clause.relation
1625
+ );
1626
+ }
1627
+ break;
1628
+ case "offset":
1629
+ case "bind":
1630
+ case "setOperation":
1631
+ break;
1632
+ case "flat":
1633
+ throwUnsupportedBindingFinal(query.table, "flat relation include mode");
1634
+ break;
1635
+ case "lock":
1636
+ throwUnsupportedBindingFinal(
1637
+ query.table,
1638
+ "row-level locks (FOR UPDATE / SKIP LOCKED), because locks over a CTE binding are silently ineffective"
1639
+ );
1640
+ break;
1641
+ /* v8 ignore next — defensive: default-reject future clauses in binding-final queries -- @preserve */
1642
+ default:
1643
+ throwUnsupportedBindingFinal(
1644
+ query.table,
1645
+ `clause '${clause.type ?? "unknown"}'`
1646
+ );
1647
+ }
1648
+ }
1649
+ }
1650
+ function compileQueryInternal(query, ctx, fns, bindings) {
1651
+ const bindingSource = bindings?.get(query.table);
1652
+ const isBindingSource = bindingSource !== void 0 || isBindingTable(ctx, query.table);
1653
+ ctx.currentFromTable = query.table;
1654
+ ctx.validator?.validateTable(query.table);
1655
+ if (isBindingSource) {
1656
+ validateBindingFinalQuery(query, ctx);
1657
+ }
787
1658
  const setClauseIndex = query.clauses.findIndex(
788
1659
  (c) => c.type === "setOperation"
789
1660
  );
790
1661
  if (setClauseIndex >= 0) {
791
1662
  return compileSetOperation(query, setClauseIndex, ctx, fns, bindings);
792
1663
  }
793
- ctx.currentFromTable = query.table;
794
- ctx.validator?.validateTable(query.table);
795
1664
  let groupByIndex = -1;
796
1665
  for (let i = 0; i < query.clauses.length; i++) {
797
1666
  if (query.clauses[i]?.type === "groupBy") {
@@ -799,6 +1668,7 @@ function compileQuery(query, ctx, fns, bindings) {
799
1668
  break;
800
1669
  }
801
1670
  }
1671
+ const havingAliases = collectSelectAliasesFromQuery(query);
802
1672
  const whereConditions = [];
803
1673
  const havingConditions = [];
804
1674
  let select;
@@ -816,13 +1686,33 @@ function compileQuery(query, ctx, fns, bindings) {
816
1686
  const clause = query.clauses[i];
817
1687
  switch (clause.type) {
818
1688
  case "where": {
819
- const condition = resolveBindingsInWhere(
820
- fns.compileExpression(clause.condition, ctx, fns),
821
- bindings
822
- );
823
1689
  if (groupByIndex >= 0 && i > groupByIndex) {
1690
+ const previousHavingAliases = ctx.currentHavingAliases;
1691
+ ctx.currentHavingAliases = havingAliases;
1692
+ const condition = (() => {
1693
+ try {
1694
+ return resolveBindingsInWhere(
1695
+ fns.compileExpression(
1696
+ clause.condition,
1697
+ ctx,
1698
+ fns
1699
+ ),
1700
+ bindings
1701
+ );
1702
+ } finally {
1703
+ ctx.currentHavingAliases = previousHavingAliases;
1704
+ }
1705
+ })();
824
1706
  havingConditions.push(condition);
825
1707
  } else if (currentIncludeBatch && currentIncludeBatch.length > 0) {
1708
+ const condition = resolveBindingsInWhere(
1709
+ fns.compileExpression(
1710
+ clause.condition,
1711
+ ctx,
1712
+ fns
1713
+ ),
1714
+ bindings
1715
+ );
826
1716
  const targetInclude = currentIncludeBatch[currentIncludeBatch.length - 1];
827
1717
  const mutableInclude = targetInclude;
828
1718
  if (targetInclude.where) {
@@ -834,6 +1724,14 @@ function compileQuery(query, ctx, fns, bindings) {
834
1724
  mutableInclude.where = condition;
835
1725
  }
836
1726
  } else {
1727
+ const condition = resolveBindingsInWhere(
1728
+ fns.compileExpression(
1729
+ clause.condition,
1730
+ ctx,
1731
+ fns
1732
+ ),
1733
+ bindings
1734
+ );
837
1735
  whereConditions.push(condition);
838
1736
  }
839
1737
  break;
@@ -881,7 +1779,7 @@ function compileQuery(query, ctx, fns, bindings) {
881
1779
  }
882
1780
  }
883
1781
  }
884
- if (select && select.type === "expressions") {
1782
+ if (select && select.type === "expressions" && !isBindingSource) {
885
1783
  const relationPaths = /* @__PURE__ */ new Set();
886
1784
  for (const expr of select.columns) {
887
1785
  if (expr.kind === "relationColumn") {
@@ -909,6 +1807,12 @@ function compileQuery(query, ctx, fns, bindings) {
909
1807
  }
910
1808
  }
911
1809
  if (includeLimits.size > 0) {
1810
+ if (isBindingSource) {
1811
+ throw new NqlSemanticException(
1812
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1813
+ `Query '${query.table}' reads from an NQL binding and cannot use relation include limits (${[...includeLimits.keys()].join(", ")}). Relation includes require a physical model table, not a CTE binding.`
1814
+ );
1815
+ }
912
1816
  for (const [relation, limitCount] of includeLimits) {
913
1817
  const rootRelation = relation.split(".")[0];
914
1818
  const targetInclude = allIncludes.find(
@@ -935,7 +1839,7 @@ function compileQuery(query, ctx, fns, bindings) {
935
1839
  } else if (havingConditions.length > 1) {
936
1840
  having = { kind: "and", conditions: havingConditions };
937
1841
  }
938
- return {
1842
+ const compiledQuery = {
939
1843
  type: "select",
940
1844
  from: query.table,
941
1845
  ...select !== void 0 && { select },
@@ -949,6 +1853,7 @@ function compileQuery(query, ctx, fns, bindings) {
949
1853
  ...offset !== void 0 && { offset },
950
1854
  ...lock !== void 0 && { lock }
951
1855
  };
1856
+ return compiledQuery;
952
1857
  }
953
1858
  function getExplicitColumnCount(intent) {
954
1859
  if ("kind" in intent && intent.kind === "setOperation") {
@@ -972,6 +1877,272 @@ function getExplicitColumnCount(intent) {
972
1877
  return void 0;
973
1878
  }
974
1879
  }
1880
+ function expressionOutputColumn(expr) {
1881
+ switch (expr.kind) {
1882
+ case "column":
1883
+ if (expr.column === "*") return void 0;
1884
+ return expr.as ?? expr.column;
1885
+ case "columnAlias":
1886
+ return expr.alias;
1887
+ case "relationColumn":
1888
+ if (expr.column === "*") return void 0;
1889
+ return expr.as;
1890
+ case "aggregate":
1891
+ case "function":
1892
+ case "subquery":
1893
+ case "arithmetic":
1894
+ case "literal":
1895
+ case "case":
1896
+ case "jsonExtract":
1897
+ case "jsonPathExtract":
1898
+ case "customOp":
1899
+ case "customFn":
1900
+ case "array":
1901
+ case "unary":
1902
+ case "param":
1903
+ return expr.as;
1904
+ case "coalesce":
1905
+ case "raw":
1906
+ case "pseudoColumn":
1907
+ return expr.as;
1908
+ case "window":
1909
+ return expr.alias;
1910
+ case "comparison":
1911
+ case "jsonContains":
1912
+ case "jsonExists":
1913
+ case "ref":
1914
+ case "cast":
1915
+ case "namedArg":
1916
+ case "star":
1917
+ return void 0;
1918
+ /* v8 ignore next — defensive: exhaustive switch -- @preserve */
1919
+ default:
1920
+ return void 0;
1921
+ }
1922
+ }
1923
+ function addUnique(columns, seen, column) {
1924
+ if (seen.has(column)) return false;
1925
+ seen.add(column);
1926
+ columns.push(column);
1927
+ return true;
1928
+ }
1929
+ function resolveSourceOutputColumns(intent, ctx, bindingName) {
1930
+ const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
1931
+ if (bindingColumns2 !== void 0) return bindingColumns2;
1932
+ const columns = ctx.validator?.getTableColumns(intent.from);
1933
+ if (columns !== void 0) return columns;
1934
+ throw new NqlSemanticException(
1935
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1936
+ `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
1937
+ );
1938
+ }
1939
+ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
1940
+ const columns = [];
1941
+ const seen = /* @__PURE__ */ new Set();
1942
+ const directProjectionLineage = [];
1943
+ const addColumn = (column) => addUnique(columns, seen, column);
1944
+ const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
1945
+ const addDirectProjection = (outputColumn, sourceColumn) => {
1946
+ if (!addColumn(outputColumn)) return;
1947
+ directProjectionLineage.push({
1948
+ kind: "directProjection",
1949
+ sourceTable: intent.from,
1950
+ sourceColumn: resolveSourceColumn(sourceColumn),
1951
+ outputColumn
1952
+ });
1953
+ };
1954
+ const addSourceColumns = () => {
1955
+ for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
1956
+ addDirectProjection(column, column);
1957
+ }
1958
+ };
1959
+ const { select } = intent;
1960
+ if (!select || select.type === "all") {
1961
+ addSourceColumns();
1962
+ const outputSchema2 = { columns, directProjectionLineage };
1963
+ return {
1964
+ columns: outputSchema2.columns,
1965
+ relationFilters: getBindingRelationFilterMetadata(
1966
+ intent,
1967
+ ctx,
1968
+ outputSchema2,
1969
+ bindingDependencies
1970
+ )
1971
+ };
1972
+ }
1973
+ if (select.type === "fields") {
1974
+ for (const field of select.fields) {
1975
+ if (field === "*") {
1976
+ addSourceColumns();
1977
+ } else {
1978
+ addDirectProjection(field, field);
1979
+ }
1980
+ }
1981
+ const outputSchema2 = { columns, directProjectionLineage };
1982
+ return {
1983
+ columns: outputSchema2.columns,
1984
+ relationFilters: getBindingRelationFilterMetadata(
1985
+ intent,
1986
+ ctx,
1987
+ outputSchema2,
1988
+ bindingDependencies
1989
+ )
1990
+ };
1991
+ }
1992
+ if (select.type === "aggregate") {
1993
+ for (const field of select.fields ?? []) {
1994
+ addDirectProjection(field, field);
1995
+ }
1996
+ for (const aggregate of select.aggregates) {
1997
+ if (!aggregate.as) {
1998
+ throw new NqlSemanticException(
1999
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2000
+ `Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
2001
+ );
2002
+ }
2003
+ addColumn(aggregate.as);
2004
+ }
2005
+ const outputSchema2 = { columns, directProjectionLineage };
2006
+ return {
2007
+ columns: outputSchema2.columns,
2008
+ relationFilters: getBindingRelationFilterMetadata(
2009
+ intent,
2010
+ ctx,
2011
+ outputSchema2,
2012
+ bindingDependencies
2013
+ )
2014
+ };
2015
+ }
2016
+ for (const expr of select.columns) {
2017
+ if (expr.kind === "column" && expr.column === "*") {
2018
+ addSourceColumns();
2019
+ continue;
2020
+ }
2021
+ if (expr.kind === "relationColumn" && expr.column === "*") {
2022
+ throw new NqlSemanticException(
2023
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2024
+ `Cannot compute output schema for NQL binding '${bindingName}' from relation SELECT * '${expr.relation}.*'. Use explicit aliases for binding outputs.`
2025
+ );
2026
+ }
2027
+ const outputColumn = expressionOutputColumn(expr);
2028
+ if (!outputColumn) {
2029
+ throw new NqlSemanticException(
2030
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2031
+ `Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
2032
+ );
2033
+ }
2034
+ if (expr.kind === "column") {
2035
+ addDirectProjection(outputColumn, expr.column);
2036
+ } else if (expr.kind === "columnAlias") {
2037
+ addDirectProjection(outputColumn, expr.column);
2038
+ } else {
2039
+ addColumn(outputColumn);
2040
+ }
2041
+ }
2042
+ const outputSchema = { columns, directProjectionLineage };
2043
+ return {
2044
+ columns: outputSchema.columns,
2045
+ relationFilters: getBindingRelationFilterMetadata(
2046
+ intent,
2047
+ ctx,
2048
+ outputSchema,
2049
+ bindingDependencies
2050
+ )
2051
+ };
2052
+ }
2053
+ function validateNqlExpressionPaths(expr, ctx) {
2054
+ switch (expr.type) {
2055
+ case "path":
2056
+ if (expr.segments.length === 1) {
2057
+ if (ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
2058
+ validateColumnForTable(ctx, ctx.currentFromTable, expr.segments[0]);
2059
+ }
2060
+ } else if (ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
2061
+ assertNoBindingRelationPath(
2062
+ ctx,
2063
+ ctx.currentFromTable,
2064
+ expr.segments.join(".")
2065
+ );
2066
+ }
2067
+ break;
2068
+ case "binary":
2069
+ validateNqlExpressionPaths(expr.left, ctx);
2070
+ validateNqlExpressionPaths(expr.right, ctx);
2071
+ break;
2072
+ case "unary":
2073
+ validateNqlExpressionPaths(expr.operand, ctx);
2074
+ break;
2075
+ case "comparison":
2076
+ case "jsonComparison":
2077
+ validateNqlExpressionPaths(expr.left, ctx);
2078
+ validateNqlExpressionPaths(expr.right, ctx);
2079
+ break;
2080
+ case "rangeOp":
2081
+ validateNqlExpressionPaths(expr.left, ctx);
2082
+ if (expr.scalar) {
2083
+ validateNqlExpressionPaths(expr.scalar, ctx);
2084
+ }
2085
+ break;
2086
+ case "in":
2087
+ validateNqlExpressionPaths(expr.expression, ctx);
2088
+ if (Array.isArray(expr.values)) {
2089
+ for (const value of expr.values) {
2090
+ validateNqlExpressionPaths(value, ctx);
2091
+ }
2092
+ }
2093
+ break;
2094
+ case "any":
2095
+ validateNqlExpressionPaths(expr.column, ctx);
2096
+ break;
2097
+ case "between":
2098
+ validateNqlExpressionPaths(expr.expression, ctx);
2099
+ validateNqlExpressionPaths(expr.low, ctx);
2100
+ validateNqlExpressionPaths(expr.high, ctx);
2101
+ break;
2102
+ case "isNull":
2103
+ validateNqlExpressionPaths(expr.expression, ctx);
2104
+ break;
2105
+ case "function":
2106
+ for (const arg of expr.args) {
2107
+ validateNqlExpressionPaths(arg, ctx);
2108
+ }
2109
+ break;
2110
+ case "window":
2111
+ for (const arg of expr.args) {
2112
+ validateNqlExpressionPaths(arg, ctx);
2113
+ }
2114
+ for (const item of expr.orderBy) {
2115
+ validateNqlExpressionPaths(item.expression, ctx);
2116
+ }
2117
+ for (const partitionBy of expr.partitionBy) {
2118
+ validateNqlExpressionPaths(partitionBy, ctx);
2119
+ }
2120
+ break;
2121
+ case "case":
2122
+ if (expr.subject) {
2123
+ validateNqlExpressionPaths(expr.subject, ctx);
2124
+ }
2125
+ for (const when of expr.whenClauses) {
2126
+ validateNqlExpressionPaths(when.condition, ctx);
2127
+ validateNqlExpressionPaths(when.result, ctx);
2128
+ }
2129
+ if (expr.elseClause) {
2130
+ validateNqlExpressionPaths(expr.elseClause, ctx);
2131
+ }
2132
+ break;
2133
+ case "jsonAccess":
2134
+ validateNqlExpressionPaths(expr.base, ctx);
2135
+ break;
2136
+ case "relationFilter":
2137
+ assertNoBindingRelationConstruct(
2138
+ ctx,
2139
+ ctx.currentFromTable,
2140
+ "use relation filters",
2141
+ expr.relation.join(".")
2142
+ );
2143
+ break;
2144
+ }
2145
+ }
975
2146
  function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
976
2147
  const setClause = query.clauses[setClauseIndex];
977
2148
  if (setClauseIndex < query.clauses.length - 1) {
@@ -981,6 +2152,7 @@ function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
981
2152
  );
982
2153
  }
983
2154
  const leftQuery = {
2155
+ type: "query",
984
2156
  table: query.table,
985
2157
  clauses: query.clauses.slice(0, setClauseIndex)
986
2158
  };
@@ -1016,10 +2188,11 @@ function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
1016
2188
  }
1017
2189
  function compileGroupByClause(clause, ctx) {
1018
2190
  return clause.expressions.map((expr) => {
2191
+ validateNqlExpressionPaths(expr, ctx);
1019
2192
  if (expr.type === "path") {
1020
2193
  const field = expr.segments.join(".");
1021
2194
  if (ctx.currentFromTable && !field.includes(".")) {
1022
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
2195
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
1023
2196
  }
1024
2197
  return field;
1025
2198
  }
@@ -1030,10 +2203,13 @@ function compileOrderByClause(clause, ctx) {
1030
2203
  return clause.items.map((item) => compileOrderItem(item, ctx));
1031
2204
  }
1032
2205
  function compileOrderItem(item, ctx) {
2206
+ validateNqlExpressionPaths(item.expression, ctx);
1033
2207
  const field = expressionToField(item.expression);
1034
2208
  if (field) {
1035
- if (ctx.currentFromTable && !field.includes(".") && !field.includes("(")) {
1036
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
2209
+ if (ctx.currentFromTable && field.includes(".") && isBindingTable(ctx, ctx.currentFromTable)) {
2210
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
2211
+ } else if (ctx.currentFromTable && !field.includes(".") && !field.includes("(")) {
2212
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
1037
2213
  }
1038
2214
  return { field, direction: item.direction };
1039
2215
  }
@@ -1318,6 +2494,13 @@ function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
1318
2494
  return intent2;
1319
2495
  }
1320
2496
  }
2497
+ const havingAggregateComparison = compileHavingAggregateComparison(
2498
+ comp,
2499
+ ctx,
2500
+ aliasContext,
2501
+ outerAliases
2502
+ );
2503
+ if (havingAggregateComparison) return havingAggregateComparison;
1321
2504
  const field = expressionToField(comp.left, aliasContext);
1322
2505
  if (!field) {
1323
2506
  throw new NqlSemanticException(
@@ -1344,6 +2527,26 @@ function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
1344
2527
  };
1345
2528
  return intent;
1346
2529
  }
2530
+ function compileHavingAggregateComparison(comp, ctx, aliasContext, outerAliases) {
2531
+ if (ctx.currentHavingAliases === void 0 || aliasContext || comp.left.type !== "function") {
2532
+ return null;
2533
+ }
2534
+ const fn = comp.left.name.toLowerCase();
2535
+ if (fn !== "count" || comp.left.args.length !== 0) {
2536
+ return null;
2537
+ }
2538
+ const exprIntent = {
2539
+ kind: "aggregate",
2540
+ function: "count",
2541
+ field: "*"
2542
+ };
2543
+ return {
2544
+ kind: "expression",
2545
+ expr: exprIntent,
2546
+ operator: mapComparisonOperator(comp.operator),
2547
+ value: resolveFilterValue(comp.right, ctx, aliasContext, outerAliases)
2548
+ };
2549
+ }
1347
2550
  function compileRange(expr, ctx, _fns, aliasContext, outerAliases) {
1348
2551
  const rangeExpr = expr;
1349
2552
  const field = expressionToField(rangeExpr.left, aliasContext);
@@ -1617,11 +2820,18 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
1617
2820
  const relFilter = expr;
1618
2821
  const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
1619
2822
  const prevRelationTarget = ctx.currentRelationTarget;
1620
- if (ctx.currentFromTable && ctx.validator && relFilter.relation[0]) {
1621
- ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
1622
- ctx.currentFromTable,
1623
- relFilter.relation[0]
1624
- );
2823
+ const bindingRelation = ctx.currentFromTable && relFilter.relation[0] ? resolveBindingRelationFilter(
2824
+ ctx,
2825
+ ctx.currentFromTable,
2826
+ relFilter.relation
2827
+ ) : void 0;
2828
+ if (ctx.currentFromTable && relFilter.relation[0]) {
2829
+ if (ctx.validator) {
2830
+ ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2831
+ ctx.currentFromTable,
2832
+ relFilter.relation[0]
2833
+ );
2834
+ }
1625
2835
  }
1626
2836
  const where = compileExpression(
1627
2837
  relFilter.condition,
@@ -1631,13 +2841,27 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
1631
2841
  nestedOuterAliases
1632
2842
  );
1633
2843
  ctx.currentRelationTarget = prevRelationTarget;
1634
- return {
2844
+ const relationFilterIntent = {
1635
2845
  kind: "relationFilter",
1636
2846
  relation: relFilter.relation,
1637
2847
  where,
1638
2848
  mode: relFilter.mode,
1639
- ...relFilter.alias !== void 0 && { alias: relFilter.alias }
2849
+ ...relFilter.alias !== void 0 && { alias: relFilter.alias },
2850
+ ...bindingRelation !== void 0 && {
2851
+ targetTable: bindingRelation.targetTable,
2852
+ sourceColumn: bindingRelation.sourceColumn,
2853
+ targetColumn: bindingRelation.targetColumn
2854
+ }
1640
2855
  };
2856
+ return bindingRelation ? markNqlTrustedRelationFilter(relationFilterIntent, {
2857
+ relation: bindingRelation.relation,
2858
+ targetTable: bindingRelation.targetTable,
2859
+ sourceColumn: bindingRelation.sourceColumn,
2860
+ targetColumn: bindingRelation.targetColumn,
2861
+ ...bindingRelation.cardinality !== void 0 && {
2862
+ cardinality: bindingRelation.cardinality
2863
+ }
2864
+ }) : relationFilterIntent;
1641
2865
  }
1642
2866
  function expandDateRangeList(field, patterns, negated) {
1643
2867
  const conditions = patterns.map((pattern) => {
@@ -1744,23 +2968,31 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
1744
2968
  ...NQL_SELECT_SCALAR_FUNCTIONS
1745
2969
  ]);
1746
2970
  function validateSelectPathExpression(expr, ctx) {
1747
- if (!ctx.currentFromTable || !ctx.validator) return;
2971
+ if (!ctx.currentFromTable) return;
1748
2972
  const { segments } = expr;
1749
2973
  if (segments.length === 1) {
1750
- ctx.validator.validateColumn(ctx.currentFromTable, segments[0]);
2974
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[0]);
1751
2975
  return;
1752
2976
  }
1753
2977
  const firstSegmentLower = segments[0].toLowerCase();
1754
2978
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
2979
+ assertNoBindingRelationConstruct(
2980
+ ctx,
2981
+ ctx.currentFromTable,
2982
+ "use pseudo-column traversals",
2983
+ segments[0]
2984
+ );
1755
2985
  let i = 1;
1756
2986
  while (i < segments.length && ctx.pseudoColumnKeywords.has(segments[i].toLowerCase())) {
1757
2987
  i++;
1758
2988
  }
1759
2989
  if (i < segments.length) {
1760
- ctx.validator.validateColumn(ctx.currentFromTable, segments[i]);
2990
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[i]);
1761
2991
  }
1762
2992
  return;
1763
2993
  }
2994
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, segments.join("."));
2995
+ if (!ctx.validator) return;
1764
2996
  const targetTable = ctx.validator.resolveRelationTarget(
1765
2997
  ctx.currentFromTable,
1766
2998
  segments[0]
@@ -1790,6 +3022,12 @@ function compileSelectClause(clause, ctx, fns) {
1790
3022
  } else if (item.type === "relationStar") {
1791
3023
  hasExpressions = true;
1792
3024
  const relation = item.relation.join(".");
3025
+ assertNoBindingRelationConstruct(
3026
+ ctx,
3027
+ ctx.currentFromTable,
3028
+ "select relation columns",
3029
+ relation
3030
+ );
1793
3031
  expressions.push({
1794
3032
  kind: "relationColumn",
1795
3033
  relation,
@@ -1935,14 +3173,14 @@ function compileSelectExpression(item, ctx, fns) {
1935
3173
  const partitionBy = windowExpr.partitionBy.length > 0 ? windowExpr.partitionBy.map((e) => {
1936
3174
  const f = expressionToValidatedField(e, ctx) ?? expressionToSql(e);
1937
3175
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1938
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
3176
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1939
3177
  }
1940
3178
  return f;
1941
3179
  }) : void 0;
1942
3180
  const orderBy = windowExpr.orderBy.length > 0 ? windowExpr.orderBy.map((o) => {
1943
3181
  const f = expressionToValidatedField(o.expression, ctx) ?? expressionToSql(o.expression);
1944
3182
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1945
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
3183
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1946
3184
  }
1947
3185
  return { field: f, direction: o.direction };
1948
3186
  }) : void 0;
@@ -2060,6 +3298,12 @@ function compileMultiSegmentPath(expr, item, ctx) {
2060
3298
  const segments = expr.segments;
2061
3299
  const firstSegmentLower = segments[0].toLowerCase();
2062
3300
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
3301
+ assertNoBindingRelationConstruct(
3302
+ ctx,
3303
+ ctx.currentFromTable,
3304
+ "use pseudo-column traversals",
3305
+ segments[0]
3306
+ );
2063
3307
  const firstSegment = firstSegmentLower;
2064
3308
  const depthHint = expr.depthHint;
2065
3309
  if (depthHint !== void 0) {
@@ -2087,7 +3331,7 @@ function compileMultiSegmentPath(expr, item, ctx) {
2087
3331
  }
2088
3332
  const targetColumn = segments[i];
2089
3333
  if (ctx.currentFromTable) {
2090
- ctx.validator?.validateColumn(ctx.currentFromTable, targetColumn);
3334
+ validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
2091
3335
  }
2092
3336
  const defaultAlias = segments.map((s) => s.toLowerCase()).join(".");
2093
3337
  if (traversals.length === 1) {
@@ -2109,21 +3353,42 @@ function compileMultiSegmentPath(expr, item, ctx) {
2109
3353
  }
2110
3354
  const column = segments[segments.length - 1];
2111
3355
  const relation = segments.slice(0, -1).join(".");
2112
- if (ctx.currentFromTable && ctx.validator) {
2113
- const targetTable = ctx.validator.resolveRelationTarget(
3356
+ const bindingRelation = resolveBindingRelationColumn(
3357
+ ctx,
3358
+ ctx.currentFromTable,
3359
+ segments.slice(0, -1),
3360
+ column
3361
+ );
3362
+ if (!bindingRelation) {
3363
+ assertNoBindingRelationConstruct(
3364
+ ctx,
2114
3365
  ctx.currentFromTable,
2115
- segments[0]
3366
+ "select relation columns",
3367
+ relation
2116
3368
  );
3369
+ }
3370
+ if (ctx.currentFromTable && ctx.validator) {
3371
+ const targetTable = bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
2117
3372
  if (targetTable) {
2118
3373
  ctx.validator.validateColumn(targetTable, column);
2119
3374
  }
2120
3375
  }
2121
- return {
3376
+ const relationColumnIntent = {
2122
3377
  kind: "relationColumn",
2123
3378
  relation,
2124
3379
  column,
2125
3380
  as: item.alias ?? `${relation}.${column}`
2126
3381
  };
3382
+ return bindingRelation ? markNqlTrustedRelationFilter(relationColumnIntent, {
3383
+ relation: bindingRelation.relation,
3384
+ targetTable: bindingRelation.targetTable,
3385
+ sourceColumn: bindingRelation.sourceColumn,
3386
+ targetColumn: bindingRelation.targetColumn,
3387
+ selectedColumn: column,
3388
+ ...bindingRelation.cardinality !== void 0 && {
3389
+ cardinality: bindingRelation.cardinality
3390
+ }
3391
+ }) : relationColumnIntent;
2127
3392
  }
2128
3393
  function compileCaseExpression(caseExpr, item, ctx, fns) {
2129
3394
  if (caseExpr.subject) {
@@ -2269,6 +3534,220 @@ function allowsInternalParams(options) {
2269
3534
  const internalOptions = options?.[NQL_INTERNAL_COMPILER_OPTIONS];
2270
3535
  return internalOptions?.allowInternalParams === true;
2271
3536
  }
3537
+ function isUnresolvedSelectAllOutputSchemaError(error) {
3538
+ return error instanceof NqlSemanticException && error.message.includes("from SELECT *") && error.message.includes("without a concrete table schema");
3539
+ }
3540
+ function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
3541
+ if (result.query) {
3542
+ return {
3543
+ kind: "query",
3544
+ query: result.query,
3545
+ ...bindName !== void 0 && { bindName },
3546
+ final,
3547
+ bindingDependencies
3548
+ };
3549
+ }
3550
+ if (result.mutation) {
3551
+ return {
3552
+ kind: "mutation",
3553
+ mutation: result.mutation,
3554
+ ...bindName !== void 0 && { bindName },
3555
+ final,
3556
+ bindingDependencies
3557
+ };
3558
+ }
3559
+ return void 0;
3560
+ }
3561
+ function stringArraysEqual(left, right) {
3562
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3563
+ }
3564
+ function isNqlAstRecord(value) {
3565
+ return typeof value === "object" && value !== null;
3566
+ }
3567
+ function addReadBindingReference(references, readBindingNames, name) {
3568
+ if (typeof name === "string" && readBindingNames.has(name)) {
3569
+ references.add(name);
3570
+ }
3571
+ }
3572
+ function withoutLocalCteNames(readBindingNames, ctes) {
3573
+ if (ctes.length === 0) return readBindingNames;
3574
+ const localCteNames = new Set(ctes.map((cte) => cte.name));
3575
+ if (![...localCteNames].some((name) => readBindingNames.has(name))) {
3576
+ return readBindingNames;
3577
+ }
3578
+ return new Set(
3579
+ [...readBindingNames].filter((name) => !localCteNames.has(name))
3580
+ );
3581
+ }
3582
+ function collectQueryReadBindingReferences(query, readBindingNames, references) {
3583
+ addReadBindingReference(references, readBindingNames, query.table);
3584
+ collectNodeReadBindingReferences(query.clauses, readBindingNames, references);
3585
+ }
3586
+ function collectWithQueryReadBindingReferences(withQuery, readBindingNames, references) {
3587
+ const scopedReadBindingNames = withoutLocalCteNames(
3588
+ readBindingNames,
3589
+ withQuery.ctes
3590
+ );
3591
+ for (const cte of withQuery.ctes) {
3592
+ collectQueryReadBindingReferences(
3593
+ cte.query,
3594
+ scopedReadBindingNames,
3595
+ references
3596
+ );
3597
+ }
3598
+ collectQueryReadBindingReferences(
3599
+ withQuery.query,
3600
+ scopedReadBindingNames,
3601
+ references
3602
+ );
3603
+ }
3604
+ function collectInListReadBindingReference(values, readBindingNames, references) {
3605
+ if (!Array.isArray(values) || values.length !== 1) return;
3606
+ const value = values[0];
3607
+ if (!isNqlAstRecord(value) || value.type !== "path") return;
3608
+ const segments = value.segments;
3609
+ if (!Array.isArray(segments) || segments.length !== 1) return;
3610
+ addReadBindingReference(references, readBindingNames, segments[0]);
3611
+ }
3612
+ function collectNodeReadBindingReferences(node, readBindingNames, references) {
3613
+ if (Array.isArray(node)) {
3614
+ for (const item of node) {
3615
+ collectNodeReadBindingReferences(item, readBindingNames, references);
3616
+ }
3617
+ return;
3618
+ }
3619
+ if (!isNqlAstRecord(node)) return;
3620
+ switch (node.type) {
3621
+ case "query":
3622
+ collectQueryReadBindingReferences(
3623
+ node,
3624
+ readBindingNames,
3625
+ references
3626
+ );
3627
+ return;
3628
+ case "withQuery":
3629
+ collectWithQueryReadBindingReferences(
3630
+ node,
3631
+ readBindingNames,
3632
+ references
3633
+ );
3634
+ return;
3635
+ case "mutationPipeline":
3636
+ collectNodeReadBindingReferences(
3637
+ node.mutation,
3638
+ readBindingNames,
3639
+ references
3640
+ );
3641
+ collectNodeReadBindingReferences(
3642
+ node.clauses,
3643
+ readBindingNames,
3644
+ references
3645
+ );
3646
+ return;
3647
+ case "insert_from":
3648
+ case "upsert_from":
3649
+ addReadBindingReference(references, readBindingNames, node.source);
3650
+ collectNodeReadBindingReferences(
3651
+ node.where,
3652
+ readBindingNames,
3653
+ references
3654
+ );
3655
+ return;
3656
+ case "setOperation":
3657
+ addReadBindingReference(references, readBindingNames, node.boundName);
3658
+ collectNodeReadBindingReferences(
3659
+ node.right,
3660
+ readBindingNames,
3661
+ references
3662
+ );
3663
+ return;
3664
+ case "in":
3665
+ collectNodeReadBindingReferences(
3666
+ node.expression,
3667
+ readBindingNames,
3668
+ references
3669
+ );
3670
+ collectInListReadBindingReference(
3671
+ node.values,
3672
+ readBindingNames,
3673
+ references
3674
+ );
3675
+ collectNodeReadBindingReferences(
3676
+ node.values,
3677
+ readBindingNames,
3678
+ references
3679
+ );
3680
+ return;
3681
+ case "subquery":
3682
+ collectNodeReadBindingReferences(
3683
+ node.query,
3684
+ readBindingNames,
3685
+ references
3686
+ );
3687
+ return;
3688
+ }
3689
+ for (const child of Object.values(node)) {
3690
+ collectNodeReadBindingReferences(child, readBindingNames, references);
3691
+ }
3692
+ }
3693
+ function collectStatementReadBindingReferences(stmt, readBindingNames) {
3694
+ const references = /* @__PURE__ */ new Set();
3695
+ if (readBindingNames.size === 0) return references;
3696
+ collectNodeReadBindingReferences(stmt, readBindingNames, references);
3697
+ return references;
3698
+ }
3699
+ function collectCompileResultReadBindingReferences(result, readBindingNames, references) {
3700
+ if (result.query) {
3701
+ addReadBindingReference(references, readBindingNames, result.query.from);
3702
+ }
3703
+ }
3704
+ function addTransitiveBindingDependency(bindName, bindingDependencies, ordered, seen) {
3705
+ if (seen.has(bindName)) return;
3706
+ seen.add(bindName);
3707
+ for (const dependency of bindingDependencies.get(bindName) ?? []) {
3708
+ addTransitiveBindingDependency(
3709
+ dependency,
3710
+ bindingDependencies,
3711
+ ordered,
3712
+ seen
3713
+ );
3714
+ }
3715
+ ordered.push(bindName);
3716
+ }
3717
+ function collectTransitiveReadBindingDependencies(directReferences, bindingDependencies) {
3718
+ const ordered = [];
3719
+ const seen = /* @__PURE__ */ new Set();
3720
+ for (const reference of directReferences) {
3721
+ addTransitiveBindingDependency(
3722
+ reference,
3723
+ bindingDependencies,
3724
+ ordered,
3725
+ seen
3726
+ );
3727
+ }
3728
+ return ordered;
3729
+ }
3730
+ function rejectReadBindingReferenceAcrossMutation(bindName, definitionIndex, mutationIndex, referenceIndex, statementCount) {
3731
+ throw new NqlSemanticException(
3732
+ 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.`
3734
+ );
3735
+ }
3736
+ function rejectInvalidReadBindingDependencies(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, referenceIndex, statementCount) {
3737
+ for (const referencedBindName of statementBindingDependencies) {
3738
+ const definitionIndex = readBindingDefinitions.get(referencedBindName);
3739
+ if (definitionIndex === void 0) continue;
3740
+ if (lastMutationStatement > definitionIndex) {
3741
+ rejectReadBindingReferenceAcrossMutation(
3742
+ referencedBindName,
3743
+ definitionIndex,
3744
+ lastMutationStatement,
3745
+ referenceIndex,
3746
+ statementCount
3747
+ );
3748
+ }
3749
+ }
3750
+ }
2272
3751
  var NqlCompiler = class {
2273
3752
  ctx;
2274
3753
  fns;
@@ -2292,9 +3771,12 @@ var NqlCompiler = class {
2292
3771
  this.ctx = {
2293
3772
  currentFromTable: void 0,
2294
3773
  currentRelationTarget: void 0,
3774
+ currentHavingAliases: void 0,
2295
3775
  pseudoColumnKeywords,
2296
3776
  recursiveKeywords,
2297
3777
  validator,
3778
+ bindingOutputColumns: /* @__PURE__ */ new Map(),
3779
+ bindingRelationFilters: /* @__PURE__ */ new Map(),
2298
3780
  params,
2299
3781
  maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
2300
3782
  allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
@@ -2310,6 +3792,15 @@ var NqlCompiler = class {
2310
3792
  * Compile an NQL program to IntentAST.
2311
3793
  */
2312
3794
  compile(program) {
3795
+ try {
3796
+ return this.compileProgram(program);
3797
+ } finally {
3798
+ this.ctx.bindingOutputColumns.clear();
3799
+ this.ctx.bindingRelationFilters.clear();
3800
+ this.ctx.validator?.clearVirtualBindingTables();
3801
+ }
3802
+ }
3803
+ compileProgram(program) {
2313
3804
  if (program.statements.length === 0) {
2314
3805
  return {};
2315
3806
  }
@@ -2326,30 +3817,124 @@ var NqlCompiler = class {
2326
3817
  }
2327
3818
  }
2328
3819
  const bindings = /* @__PURE__ */ new Map();
3820
+ const bindingOutputSchemas = /* @__PURE__ */ new Map();
2329
3821
  const mutationBindings = /* @__PURE__ */ new Map();
2330
3822
  const materializedBindStatements = /* @__PURE__ */ new Set();
3823
+ const seenBindNames = /* @__PURE__ */ new Map();
3824
+ const nqlProgramSequence = [];
3825
+ const readBindingDefinitions = /* @__PURE__ */ new Map();
3826
+ const definedBindingNames = /* @__PURE__ */ new Set();
3827
+ const bindingDependencies = /* @__PURE__ */ new Map();
3828
+ let lastMutationStatement = -1;
2331
3829
  let lastResult = {};
2332
3830
  for (let i = 0; i < program.statements.length; i++) {
2333
3831
  const stmt = program.statements[i];
2334
- lastResult = this.compileSingleStatement(stmt, bindings);
2335
3832
  const bindName = extractBindName(stmt);
3833
+ if (bindName) {
3834
+ const previousStatement = seenBindNames.get(bindName);
3835
+ if (previousStatement !== void 0) {
3836
+ throw new NqlSemanticException(
3837
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3838
+ `NQL binding name '${bindName}' is used more than once (statements ${previousStatement + 1} and ${i + 1}). NQL binding names must be unique.`
3839
+ );
3840
+ }
3841
+ seenBindNames.set(bindName, i);
3842
+ }
3843
+ const readBindingReferences = collectStatementReadBindingReferences(
3844
+ stmt,
3845
+ definedBindingNames
3846
+ );
3847
+ let statementBindingDependencies = collectTransitiveReadBindingDependencies(
3848
+ readBindingReferences,
3849
+ bindingDependencies
3850
+ );
3851
+ rejectInvalidReadBindingDependencies(
3852
+ statementBindingDependencies,
3853
+ readBindingDefinitions,
3854
+ lastMutationStatement,
3855
+ i,
3856
+ program.statements.length
3857
+ );
3858
+ lastResult = this.compileSingleStatement(stmt, bindings);
3859
+ collectCompileResultReadBindingReferences(
3860
+ lastResult,
3861
+ definedBindingNames,
3862
+ readBindingReferences
3863
+ );
3864
+ statementBindingDependencies = collectTransitiveReadBindingDependencies(
3865
+ readBindingReferences,
3866
+ bindingDependencies
3867
+ );
3868
+ rejectInvalidReadBindingDependencies(
3869
+ statementBindingDependencies,
3870
+ readBindingDefinitions,
3871
+ lastMutationStatement,
3872
+ i,
3873
+ program.statements.length
3874
+ );
3875
+ if (stmt.type === "mutationPipeline") {
3876
+ lastMutationStatement = i;
3877
+ }
2336
3878
  if (bindName) {
2337
3879
  if (lastResult.query) {
3880
+ const outputSchema = this.registerQueryBindingOutputSchema(
3881
+ bindName,
3882
+ lastResult.query,
3883
+ bindingOutputSchemas,
3884
+ statementBindingDependencies
3885
+ );
2338
3886
  bindings.set(bindName, lastResult.query);
3887
+ if (outputSchema) {
3888
+ this.ctx.validator?.addVirtualBindingTable(
3889
+ bindName,
3890
+ outputSchema.columns,
3891
+ outputSchema.relationFilters
3892
+ );
3893
+ }
2339
3894
  materializedBindStatements.add(i);
3895
+ readBindingDefinitions.set(bindName, i);
3896
+ definedBindingNames.add(bindName);
3897
+ bindingDependencies.set(bindName, statementBindingDependencies);
2340
3898
  } else if (lastResult.mutation?.returning?.length) {
2341
- mutationBindings.set(bindName, lastResult.mutation);
3899
+ const canonicalBinding = this.canonicalizeMutationBinding(
3900
+ bindName,
3901
+ lastResult.mutation
3902
+ );
3903
+ lastResult = { mutation: canonicalBinding.mutation };
3904
+ mutationBindings.set(bindName, canonicalBinding.mutation);
3905
+ const outputSchema = canonicalBinding.outputSchema;
3906
+ const bindingFields = outputSchema?.columns ?? canonicalBinding.mutation.returning ?? [];
3907
+ this.ctx.bindingOutputColumns.set(bindName, outputSchema?.columns);
2342
3908
  bindings.set(bindName, {
2343
3909
  type: "select",
2344
- from: lastResult.mutation.table,
3910
+ from: canonicalBinding.mutation.table,
2345
3911
  select: {
2346
3912
  type: "fields",
2347
- fields: [...lastResult.mutation.returning]
3913
+ fields: [...bindingFields]
2348
3914
  }
2349
3915
  });
3916
+ if (outputSchema) {
3917
+ bindingOutputSchemas.set(bindName, outputSchema);
3918
+ this.ctx.validator?.addVirtualBindingTable(
3919
+ bindName,
3920
+ outputSchema.columns,
3921
+ outputSchema.relationFilters
3922
+ );
3923
+ }
2350
3924
  materializedBindStatements.add(i);
3925
+ definedBindingNames.add(bindName);
3926
+ bindingDependencies.set(bindName, []);
2351
3927
  }
2352
3928
  }
3929
+ const sequenceStep = programSequenceStepFromResult(
3930
+ lastResult,
3931
+ bindName,
3932
+ i === program.statements.length - 1,
3933
+ statementBindingDependencies
3934
+ );
3935
+ if (sequenceStep !== void 0) {
3936
+ nqlProgramSequence.push(sequenceStep);
3937
+ }
2353
3938
  }
2354
3939
  for (let i = 0; i < program.statements.length - 1; i++) {
2355
3940
  const bindName = extractBindName(program.statements[i]);
@@ -2360,15 +3945,79 @@ var NqlCompiler = class {
2360
3945
  }
2361
3946
  }
2362
3947
  const hasMutationBindings = mutationBindings.size > 0;
3948
+ const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
3949
+ const hasNqlProgramSequence = nqlProgramSequence.length === program.statements.length;
2363
3950
  if (bindings.size > 0) {
2364
3951
  return {
2365
3952
  ...lastResult,
2366
3953
  bindings,
2367
- ...hasMutationBindings && { mutationBindings }
3954
+ ...hasBindingOutputSchemas && { bindingOutputSchemas },
3955
+ ...hasMutationBindings && { mutationBindings },
3956
+ ...hasNqlProgramSequence && { nqlProgramSequence }
2368
3957
  };
2369
3958
  }
2370
3959
  return lastResult;
2371
3960
  }
3961
+ registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas, bindingDependencies) {
3962
+ try {
3963
+ const outputSchema = getQueryOutputSchema(
3964
+ query,
3965
+ this.ctx,
3966
+ bindName,
3967
+ bindingDependencies
3968
+ );
3969
+ bindingOutputSchemas.set(bindName, outputSchema);
3970
+ this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
3971
+ if (outputSchema.relationFilters) {
3972
+ this.ctx.bindingRelationFilters.set(
3973
+ bindName,
3974
+ outputSchema.relationFilters
3975
+ );
3976
+ }
3977
+ return outputSchema;
3978
+ } catch (error) {
3979
+ if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {
3980
+ this.ctx.bindingOutputColumns.set(bindName, void 0);
3981
+ return void 0;
3982
+ }
3983
+ throw error;
3984
+ }
3985
+ }
3986
+ canonicalizeMutationBinding(bindName, mutation) {
3987
+ const outputSchema = this.getMutationBindingOutputSchema(
3988
+ bindName,
3989
+ mutation
3990
+ );
3991
+ const returning = mutation.returning;
3992
+ if (returning === void 0 || returning.length === 0 || returning.includes("*") || outputSchema === void 0 || stringArraysEqual(returning, outputSchema.columns)) {
3993
+ return { mutation, outputSchema };
3994
+ }
3995
+ return {
3996
+ mutation: {
3997
+ ...mutation,
3998
+ returning: outputSchema.columns
3999
+ },
4000
+ outputSchema
4001
+ };
4002
+ }
4003
+ getMutationBindingOutputSchema(bindName, mutation) {
4004
+ const returning = mutation.returning;
4005
+ if (!returning || returning.length === 0) return void 0;
4006
+ if (returning.includes("*")) {
4007
+ const columns = this.ctx.validator?.getTableColumns(mutation.table);
4008
+ if (columns !== void 0) return { columns };
4009
+ if (!this.ctx.validator) return void 0;
4010
+ throw new NqlSemanticException(
4011
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
4012
+ `Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
4013
+ );
4014
+ }
4015
+ return {
4016
+ columns: returning.map(
4017
+ (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
4018
+ )
4019
+ };
4020
+ }
2372
4021
  compileSingleStatement(stmt, bindings) {
2373
4022
  if (stmt.type === "withQuery") {
2374
4023
  return compileWithQuery(stmt, this.ctx, this.fns);