@dbsp/nql 1.3.0 → 1.4.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, 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,7 @@ var ColumnValidator = class _ColumnValidator {
51
51
  }
52
52
  schema;
53
53
  knownCteTables = /* @__PURE__ */ new Set();
54
+ virtualBindingTables = /* @__PURE__ */ new Map();
54
55
  /**
55
56
  * Register CTE names so validateTable() skips validation for them.
56
57
  * CTE names are not physical tables in the schema, but are valid FROM targets.
@@ -64,6 +65,28 @@ var ColumnValidator = class _ColumnValidator {
64
65
  clearKnownCteTables() {
65
66
  this.knownCteTables.clear();
66
67
  }
68
+ /**
69
+ * Register an NQL binding as a virtual table with a concrete output schema.
70
+ * Binding columns are logical output names, so matching is exact.
71
+ */
72
+ addVirtualBindingTable(name, columns) {
73
+ this.virtualBindingTables.set(name, columns);
74
+ }
75
+ /** Clear all registered virtual binding tables (used between compilations). */
76
+ clearVirtualBindingTables() {
77
+ this.virtualBindingTables.clear();
78
+ }
79
+ isVirtualBindingTable(name) {
80
+ return name !== void 0 && this.virtualBindingTables.has(name);
81
+ }
82
+ getVirtualBindingColumns(name) {
83
+ return this.virtualBindingTables.get(name);
84
+ }
85
+ getTableColumns(name) {
86
+ const virtualColumns = this.virtualBindingTables.get(name);
87
+ if (virtualColumns) return virtualColumns;
88
+ return this.schema.getTable(name)?.columns.map((column) => column.name);
89
+ }
67
90
  /**
68
91
  * Convert camelCase to snake_case for column name matching.
69
92
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -79,14 +102,40 @@ var ColumnValidator = class _ColumnValidator {
79
102
  if (queryCol === schemaCol) return true;
80
103
  return _ColumnValidator.toSnakeCase(queryCol) === _ColumnValidator.toSnakeCase(schemaCol);
81
104
  }
105
+ /**
106
+ * Resolve a user-authored column spelling to the model column name.
107
+ * Uses the same exact-or-snake/camel equivalence as validateColumn().
108
+ */
109
+ resolveColumnName(table, column) {
110
+ if (column === "*") return column;
111
+ const virtualColumns = this.virtualBindingTables.get(table);
112
+ if (virtualColumns) {
113
+ return virtualColumns.find((c) => c === column);
114
+ }
115
+ const tableInfo = this.schema.getTable(table);
116
+ if (!tableInfo) return void 0;
117
+ return tableInfo.columns.find(
118
+ (c) => _ColumnValidator.columnsMatch(column, c.name)
119
+ )?.name;
120
+ }
82
121
  validateColumn(table, column) {
83
122
  if (column === "*") return;
123
+ const virtualColumns = this.virtualBindingTables.get(table);
124
+ if (virtualColumns) {
125
+ const exists = virtualColumns.some((c) => c === column);
126
+ if (!exists) {
127
+ const available = virtualColumns.join(", ") || "(none)";
128
+ throw new NqlSemanticException(
129
+ NqlErrorCodes.SEM_UNKNOWN_COLUMN,
130
+ `Column '${column}' is not projected by NQL binding '${table}'. Available columns: ${available}`
131
+ );
132
+ }
133
+ return;
134
+ }
84
135
  const tableInfo = this.schema.getTable(table);
85
136
  if (!tableInfo) return;
86
- const exists = tableInfo.columns.some(
87
- (c) => _ColumnValidator.columnsMatch(column, c.name)
88
- );
89
- if (!exists) {
137
+ const resolvedColumn = this.resolveColumnName(table, column);
138
+ if (resolvedColumn === void 0) {
90
139
  const available = tableInfo.columns.map((c) => c.name).join(", ");
91
140
  throw new NqlSemanticException(
92
141
  NqlErrorCodes.SEM_UNKNOWN_COLUMN,
@@ -95,6 +144,7 @@ var ColumnValidator = class _ColumnValidator {
95
144
  }
96
145
  }
97
146
  validateTable(table) {
147
+ if (this.virtualBindingTables.has(table)) return;
98
148
  if (this.knownCteTables.has(table)) return;
99
149
  const tableInfo = this.schema.getTable(table);
100
150
  if (!tableInfo) {
@@ -105,10 +155,30 @@ var ColumnValidator = class _ColumnValidator {
105
155
  }
106
156
  }
107
157
  resolveRelationTarget(sourceTable, relationName) {
158
+ if (this.virtualBindingTables.has(sourceTable)) {
159
+ throw new NqlSemanticException(
160
+ 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.`
162
+ );
163
+ }
108
164
  const relations = this.schema.getRelationsFrom(sourceTable);
109
165
  const rel = relations.find((r) => r.name === relationName);
110
166
  return rel?.target;
111
167
  }
168
+ assertNoBindingRelationConstruct(bindingName, construct, detail) {
169
+ if (!this.isVirtualBindingTable(bindingName)) return;
170
+ throw new NqlSemanticException(
171
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
172
+ `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
173
+ );
174
+ }
175
+ assertNoBindingRelationPath(bindingName, path) {
176
+ if (!this.isVirtualBindingTable(bindingName)) return;
177
+ throw new NqlSemanticException(
178
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
179
+ `Query '${bindingName}' reads from an NQL binding and cannot reference relation path '${path}'. Relation paths require a physical model table, not a CTE binding.`
180
+ );
181
+ }
112
182
  };
113
183
  var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
114
184
  "__proto__",
@@ -197,6 +267,50 @@ function expressionToField(expr, aliasContext) {
197
267
  }
198
268
  return null;
199
269
  }
270
+ function isBindingTable(ctx, table) {
271
+ if (table === void 0) return false;
272
+ return ctx.bindingOutputColumns.has(table) || ctx.validator?.isVirtualBindingTable?.(table) === true;
273
+ }
274
+ function bindingColumns(ctx, table) {
275
+ if (table === void 0) return void 0;
276
+ if (ctx.bindingOutputColumns.has(table)) {
277
+ return ctx.bindingOutputColumns.get(table);
278
+ }
279
+ return ctx.validator?.getVirtualBindingColumns?.(table);
280
+ }
281
+ function getKnownBindingColumns(ctx, table) {
282
+ return bindingColumns(ctx, table);
283
+ }
284
+ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
285
+ if (!isBindingTable(ctx, bindingName)) return;
286
+ throw new NqlSemanticException(
287
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
288
+ `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
289
+ );
290
+ }
291
+ function assertNoBindingRelationPath(ctx, bindingName, path) {
292
+ if (!isBindingTable(ctx, bindingName)) return;
293
+ throw new NqlSemanticException(
294
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
295
+ `Query '${bindingName}' reads from an NQL binding and cannot reference relation path '${path}'. Relation paths require a physical model table, not a CTE binding.`
296
+ );
297
+ }
298
+ function validateColumnForTable(ctx, table, column) {
299
+ if (table === void 0 || column === "*") return;
300
+ if (isBindingTable(ctx, table)) {
301
+ const columns = bindingColumns(ctx, table);
302
+ if (columns === void 0) return;
303
+ if (!columns.some((c) => c === column)) {
304
+ const available = columns.join(", ") || "(none)";
305
+ throw new NqlSemanticException(
306
+ NqlErrorCodes.SEM_UNKNOWN_COLUMN,
307
+ `Column '${column}' is not projected by NQL binding '${table}'. Available columns: ${available}`
308
+ );
309
+ }
310
+ return;
311
+ }
312
+ ctx.validator?.validateColumn(table, column);
313
+ }
200
314
  function expressionToValue(expr, ctx) {
201
315
  switch (expr.type) {
202
316
  case "namedParam":
@@ -216,6 +330,22 @@ function expressionToValue(expr, ctx) {
216
330
  case "null":
217
331
  return null;
218
332
  case "path":
333
+ if (ctx) {
334
+ const field = expr.segments.join(".");
335
+ const isBindingReference = expr.segments.length === 1 && isBindingTable(ctx, field);
336
+ if (isBindingReference) {
337
+ return createNqlBindingRef(field);
338
+ }
339
+ const sourceTable = ctx.currentFromTable;
340
+ const isVirtualBindingSource = sourceTable !== void 0 && isBindingTable(ctx, sourceTable);
341
+ if (isVirtualBindingSource) {
342
+ if (field.includes(".")) {
343
+ assertNoBindingRelationPath(ctx, sourceTable, field);
344
+ } else {
345
+ validateColumnForTable(ctx, sourceTable, field);
346
+ }
347
+ }
348
+ }
219
349
  return createNqlBindingRef(expr.segments.join("."));
220
350
  case "function": {
221
351
  return {
@@ -382,6 +512,20 @@ function isAggregateFunction(name) {
382
512
  );
383
513
  }
384
514
  function validateWhereField(ctx, field, aliasContext, originalExpr) {
515
+ if (!aliasContext && !field.includes(".") && ctx.currentHavingAliases?.has(field)) {
516
+ throw new NqlSemanticException(
517
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
518
+ `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
+ );
520
+ }
521
+ if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
522
+ if (field.includes(".")) {
523
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
524
+ } else {
525
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
526
+ }
527
+ return;
528
+ }
385
529
  if (!ctx.validator) return;
386
530
  if (aliasContext) {
387
531
  const isInnerScope = originalExpr?.type === "path" && originalExpr.segments.length > 1 && originalExpr.segments[0] === aliasContext;
@@ -400,8 +544,14 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
400
544
  ctx.validator.validateColumn(ctx.currentRelationTarget, field);
401
545
  return;
402
546
  }
547
+ if (ctx.currentFromTable && field.includes(".")) {
548
+ if (isBindingTable(ctx, ctx.currentFromTable)) {
549
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
550
+ }
551
+ return;
552
+ }
403
553
  if (ctx.currentFromTable && !field.includes(".")) {
404
- ctx.validator.validateColumn(ctx.currentFromTable, field);
554
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
405
555
  }
406
556
  }
407
557
  function coerceToStringKey(expr, contextLabel, ctx) {
@@ -768,30 +918,317 @@ function applyIncludeLimit(includes, path, limit) {
768
918
  }
769
919
 
770
920
  // src/compiler/compile-query.ts
921
+ var activeBindingScopes = /* @__PURE__ */ new WeakMap();
922
+ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
923
+ ...NQL_SELECT_AGGREGATE_FUNCTIONS,
924
+ ...NQL_SELECT_VALUE_FUNCTIONS
925
+ ]);
771
926
  function compileNestedQuery(query, ctx, fns, bindings) {
772
927
  const savedContext = {
773
928
  currentFromTable: ctx.currentFromTable,
774
- currentRelationTarget: ctx.currentRelationTarget
929
+ currentRelationTarget: ctx.currentRelationTarget,
930
+ currentHavingAliases: ctx.currentHavingAliases
775
931
  };
776
932
  try {
777
- if (bindings) {
778
- return compileQuery(query, ctx, fns, bindings);
933
+ const nestedBindings = bindings ?? activeBindingScopes.get(ctx);
934
+ if (nestedBindings) {
935
+ return compileQuery(query, ctx, fns, nestedBindings);
779
936
  }
780
937
  return fns.compileQuery(query, ctx);
781
938
  } finally {
782
939
  ctx.currentFromTable = savedContext.currentFromTable;
783
940
  ctx.currentRelationTarget = savedContext.currentRelationTarget;
941
+ ctx.currentHavingAliases = savedContext.currentHavingAliases;
784
942
  }
785
943
  }
786
944
  function compileQuery(query, ctx, fns, bindings) {
945
+ const hadPreviousBindingScope = activeBindingScopes.has(ctx);
946
+ const previousBindingScope = activeBindingScopes.get(ctx);
947
+ if (bindings) {
948
+ activeBindingScopes.set(ctx, bindings);
949
+ }
950
+ try {
951
+ return compileQueryInternal(query, ctx, fns, bindings);
952
+ } finally {
953
+ if (hadPreviousBindingScope && previousBindingScope) {
954
+ activeBindingScopes.set(ctx, previousBindingScope);
955
+ } else {
956
+ activeBindingScopes.delete(ctx);
957
+ }
958
+ }
959
+ }
960
+ function collectSelectAliasesFromQuery(query) {
961
+ const aliases = /* @__PURE__ */ new Set();
962
+ for (const clause of query.clauses) {
963
+ if (clause.type !== "select") continue;
964
+ for (const item of clause.items) {
965
+ if (item.type === "expression" && item.alias) {
966
+ aliases.add(item.alias);
967
+ }
968
+ }
969
+ }
970
+ return aliases;
971
+ }
972
+ function throwUnsupportedBindingFinal(bindingName, feature) {
973
+ throw new NqlSemanticException(
974
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
975
+ `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
+ );
977
+ }
978
+ function validateBindingFinalPath(expr, ctx, bindingName) {
979
+ const { segments } = expr;
980
+ if (segments.length === 1) {
981
+ if (ctx.currentHavingAliases?.has(segments[0])) {
982
+ throw new NqlSemanticException(
983
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
984
+ `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.`
985
+ );
986
+ }
987
+ validateColumnForTable(ctx, bindingName, segments[0]);
988
+ return;
989
+ }
990
+ const firstSegment = segments[0];
991
+ if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
992
+ assertNoBindingRelationConstruct(
993
+ ctx,
994
+ bindingName,
995
+ "use pseudo-column traversals",
996
+ firstSegment
997
+ );
998
+ return;
999
+ }
1000
+ assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
1001
+ }
1002
+ function validateBindingFinalFunction(expr, ctx, bindingName) {
1003
+ const fn = expr.name.toLowerCase();
1004
+ if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
1005
+ throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
1006
+ }
1007
+ for (const arg of expr.args) {
1008
+ validateBindingFinalExpression(arg, ctx, bindingName);
1009
+ }
1010
+ }
1011
+ function validateBindingFinalExpression(expr, ctx, bindingName) {
1012
+ switch (expr.type) {
1013
+ case "path":
1014
+ validateBindingFinalPath(expr, ctx, bindingName);
1015
+ break;
1016
+ case "binary":
1017
+ validateBindingFinalExpression(expr.left, ctx, bindingName);
1018
+ validateBindingFinalExpression(expr.right, ctx, bindingName);
1019
+ break;
1020
+ case "unary":
1021
+ validateBindingFinalExpression(expr.operand, ctx, bindingName);
1022
+ break;
1023
+ case "comparison":
1024
+ validateBindingFinalExpression(expr.left, ctx, bindingName);
1025
+ validateBindingFinalExpression(expr.right, ctx, bindingName);
1026
+ break;
1027
+ case "in":
1028
+ validateBindingFinalExpression(expr.expression, ctx, bindingName);
1029
+ if (Array.isArray(expr.values)) {
1030
+ for (const value of expr.values) {
1031
+ validateBindingFinalExpression(value, ctx, bindingName);
1032
+ }
1033
+ } else if (expr.values.type === "dateRange") ; else ;
1034
+ break;
1035
+ case "between":
1036
+ validateBindingFinalExpression(expr.expression, ctx, bindingName);
1037
+ validateBindingFinalExpression(expr.low, ctx, bindingName);
1038
+ validateBindingFinalExpression(expr.high, ctx, bindingName);
1039
+ break;
1040
+ case "isNull":
1041
+ validateBindingFinalExpression(expr.expression, ctx, bindingName);
1042
+ break;
1043
+ case "function":
1044
+ validateBindingFinalFunction(expr, ctx, bindingName);
1045
+ break;
1046
+ case "case":
1047
+ if (expr.subject) {
1048
+ validateBindingFinalExpression(expr.subject, ctx, bindingName);
1049
+ }
1050
+ for (const when of expr.whenClauses) {
1051
+ validateBindingFinalExpression(when.condition, ctx, bindingName);
1052
+ validateBindingFinalExpression(when.result, ctx, bindingName);
1053
+ }
1054
+ if (expr.elseClause) {
1055
+ validateBindingFinalExpression(expr.elseClause, ctx, bindingName);
1056
+ }
1057
+ break;
1058
+ case "relationFilter":
1059
+ assertNoBindingRelationConstruct(
1060
+ ctx,
1061
+ bindingName,
1062
+ "use relation filters",
1063
+ expr.relation.join(".")
1064
+ );
1065
+ break;
1066
+ case "any":
1067
+ throwUnsupportedBindingFinal(bindingName, "ANY(:param)");
1068
+ break;
1069
+ case "rangeOp":
1070
+ case "rangeLiteral":
1071
+ throwUnsupportedBindingFinal(bindingName, "PostgreSQL range operators");
1072
+ break;
1073
+ case "jsonAccess":
1074
+ case "jsonComparison":
1075
+ throwUnsupportedBindingFinal(bindingName, "PostgreSQL JSON operators");
1076
+ break;
1077
+ case "window":
1078
+ throwUnsupportedBindingFinal(bindingName, "window functions");
1079
+ break;
1080
+ case "exists":
1081
+ throwUnsupportedBindingFinal(bindingName, "EXISTS subqueries");
1082
+ break;
1083
+ case "subquery":
1084
+ throwUnsupportedBindingFinal(bindingName, "scalar subqueries");
1085
+ break;
1086
+ case "variable":
1087
+ throwUnsupportedBindingFinal(bindingName, `variable '${expr.name}'`);
1088
+ break;
1089
+ case "namedParam":
1090
+ case "string":
1091
+ case "number":
1092
+ case "boolean":
1093
+ case "null":
1094
+ case "dateRange":
1095
+ break;
1096
+ /* v8 ignore next — defensive: default-reject future expression shapes in binding-final queries -- @preserve */
1097
+ default:
1098
+ throwUnsupportedBindingFinal(
1099
+ bindingName,
1100
+ `expression type '${expr.type ?? "unknown"}'`
1101
+ );
1102
+ }
1103
+ }
1104
+ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1105
+ const distinctOn = clause.distinctOn;
1106
+ if (distinctOn !== void 0) {
1107
+ throwUnsupportedBindingFinal(bindingName, "DISTINCT ON");
1108
+ }
1109
+ for (const item of clause.items) {
1110
+ switch (item.type) {
1111
+ case "star":
1112
+ break;
1113
+ case "relationStar":
1114
+ assertNoBindingRelationConstruct(
1115
+ ctx,
1116
+ bindingName,
1117
+ "select relation columns",
1118
+ item.relation.join(".")
1119
+ );
1120
+ break;
1121
+ case "expression":
1122
+ if (item.expression.type === "path" && item.expression.segments.length > 1) {
1123
+ const segments = item.expression.segments;
1124
+ const firstSegment = segments[0];
1125
+ if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
1126
+ assertNoBindingRelationConstruct(
1127
+ ctx,
1128
+ bindingName,
1129
+ "use pseudo-column traversals",
1130
+ firstSegment
1131
+ );
1132
+ } else {
1133
+ assertNoBindingRelationConstruct(
1134
+ ctx,
1135
+ bindingName,
1136
+ "select relation columns",
1137
+ segments.slice(0, -1).join(".")
1138
+ );
1139
+ }
1140
+ } else {
1141
+ validateBindingFinalExpression(item.expression, ctx, bindingName);
1142
+ }
1143
+ break;
1144
+ /* v8 ignore next — defensive: default-reject future select item shapes in binding-final queries -- @preserve */
1145
+ default:
1146
+ throwUnsupportedBindingFinal(
1147
+ bindingName,
1148
+ `SELECT item type '${item.type ?? "unknown"}'`
1149
+ );
1150
+ }
1151
+ }
1152
+ }
1153
+ function validateBindingFinalQuery(query, ctx) {
1154
+ const groupByIndex = query.clauses.findIndex((c) => c.type === "groupBy");
1155
+ const havingAliases = collectSelectAliasesFromQuery(query);
1156
+ for (let i = 0; i < query.clauses.length; i++) {
1157
+ const clause = query.clauses[i];
1158
+ switch (clause.type) {
1159
+ case "where": {
1160
+ if (groupByIndex >= 0 && i > groupByIndex) {
1161
+ const previousHavingAliases = ctx.currentHavingAliases;
1162
+ ctx.currentHavingAliases = havingAliases;
1163
+ try {
1164
+ validateBindingFinalExpression(clause.condition, ctx, query.table);
1165
+ } finally {
1166
+ ctx.currentHavingAliases = previousHavingAliases;
1167
+ }
1168
+ } else {
1169
+ validateBindingFinalExpression(clause.condition, ctx, query.table);
1170
+ }
1171
+ break;
1172
+ }
1173
+ case "select":
1174
+ validateBindingFinalSelectClause(clause, ctx, query.table);
1175
+ break;
1176
+ case "groupBy":
1177
+ for (const expr of clause.expressions) {
1178
+ validateBindingFinalExpression(expr, ctx, query.table);
1179
+ }
1180
+ break;
1181
+ case "orderBy":
1182
+ for (const item of clause.items) {
1183
+ validateBindingFinalExpression(item.expression, ctx, query.table);
1184
+ }
1185
+ break;
1186
+ case "limit":
1187
+ if (clause.relation) {
1188
+ assertNoBindingRelationConstruct(
1189
+ ctx,
1190
+ query.table,
1191
+ "use relation include limits",
1192
+ clause.relation
1193
+ );
1194
+ }
1195
+ break;
1196
+ case "offset":
1197
+ case "bind":
1198
+ case "setOperation":
1199
+ break;
1200
+ case "flat":
1201
+ throwUnsupportedBindingFinal(query.table, "flat relation include mode");
1202
+ break;
1203
+ case "lock":
1204
+ throwUnsupportedBindingFinal(
1205
+ query.table,
1206
+ "row-level locks (FOR UPDATE / SKIP LOCKED), because locks over a CTE binding are silently ineffective"
1207
+ );
1208
+ break;
1209
+ /* v8 ignore next — defensive: default-reject future clauses in binding-final queries -- @preserve */
1210
+ default:
1211
+ throwUnsupportedBindingFinal(
1212
+ query.table,
1213
+ `clause '${clause.type ?? "unknown"}'`
1214
+ );
1215
+ }
1216
+ }
1217
+ }
1218
+ function compileQueryInternal(query, ctx, fns, bindings) {
1219
+ const bindingSource = bindings?.get(query.table);
1220
+ const isBindingSource = bindingSource !== void 0 || isBindingTable(ctx, query.table);
1221
+ ctx.currentFromTable = query.table;
1222
+ ctx.validator?.validateTable(query.table);
1223
+ if (isBindingSource) {
1224
+ validateBindingFinalQuery(query, ctx);
1225
+ }
787
1226
  const setClauseIndex = query.clauses.findIndex(
788
1227
  (c) => c.type === "setOperation"
789
1228
  );
790
1229
  if (setClauseIndex >= 0) {
791
1230
  return compileSetOperation(query, setClauseIndex, ctx, fns, bindings);
792
1231
  }
793
- ctx.currentFromTable = query.table;
794
- ctx.validator?.validateTable(query.table);
795
1232
  let groupByIndex = -1;
796
1233
  for (let i = 0; i < query.clauses.length; i++) {
797
1234
  if (query.clauses[i]?.type === "groupBy") {
@@ -799,6 +1236,7 @@ function compileQuery(query, ctx, fns, bindings) {
799
1236
  break;
800
1237
  }
801
1238
  }
1239
+ const havingAliases = collectSelectAliasesFromQuery(query);
802
1240
  const whereConditions = [];
803
1241
  const havingConditions = [];
804
1242
  let select;
@@ -816,13 +1254,33 @@ function compileQuery(query, ctx, fns, bindings) {
816
1254
  const clause = query.clauses[i];
817
1255
  switch (clause.type) {
818
1256
  case "where": {
819
- const condition = resolveBindingsInWhere(
820
- fns.compileExpression(clause.condition, ctx, fns),
821
- bindings
822
- );
823
1257
  if (groupByIndex >= 0 && i > groupByIndex) {
1258
+ const previousHavingAliases = ctx.currentHavingAliases;
1259
+ ctx.currentHavingAliases = havingAliases;
1260
+ const condition = (() => {
1261
+ try {
1262
+ return resolveBindingsInWhere(
1263
+ fns.compileExpression(
1264
+ clause.condition,
1265
+ ctx,
1266
+ fns
1267
+ ),
1268
+ bindings
1269
+ );
1270
+ } finally {
1271
+ ctx.currentHavingAliases = previousHavingAliases;
1272
+ }
1273
+ })();
824
1274
  havingConditions.push(condition);
825
1275
  } else if (currentIncludeBatch && currentIncludeBatch.length > 0) {
1276
+ const condition = resolveBindingsInWhere(
1277
+ fns.compileExpression(
1278
+ clause.condition,
1279
+ ctx,
1280
+ fns
1281
+ ),
1282
+ bindings
1283
+ );
826
1284
  const targetInclude = currentIncludeBatch[currentIncludeBatch.length - 1];
827
1285
  const mutableInclude = targetInclude;
828
1286
  if (targetInclude.where) {
@@ -834,6 +1292,14 @@ function compileQuery(query, ctx, fns, bindings) {
834
1292
  mutableInclude.where = condition;
835
1293
  }
836
1294
  } else {
1295
+ const condition = resolveBindingsInWhere(
1296
+ fns.compileExpression(
1297
+ clause.condition,
1298
+ ctx,
1299
+ fns
1300
+ ),
1301
+ bindings
1302
+ );
837
1303
  whereConditions.push(condition);
838
1304
  }
839
1305
  break;
@@ -909,6 +1375,12 @@ function compileQuery(query, ctx, fns, bindings) {
909
1375
  }
910
1376
  }
911
1377
  if (includeLimits.size > 0) {
1378
+ if (isBindingSource) {
1379
+ throw new NqlSemanticException(
1380
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1381
+ `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.`
1382
+ );
1383
+ }
912
1384
  for (const [relation, limitCount] of includeLimits) {
913
1385
  const rootRelation = relation.split(".")[0];
914
1386
  const targetInclude = allIncludes.find(
@@ -935,7 +1407,7 @@ function compileQuery(query, ctx, fns, bindings) {
935
1407
  } else if (havingConditions.length > 1) {
936
1408
  having = { kind: "and", conditions: havingConditions };
937
1409
  }
938
- return {
1410
+ const compiledQuery = {
939
1411
  type: "select",
940
1412
  from: query.table,
941
1413
  ...select !== void 0 && { select },
@@ -949,6 +1421,7 @@ function compileQuery(query, ctx, fns, bindings) {
949
1421
  ...offset !== void 0 && { offset },
950
1422
  ...lock !== void 0 && { lock }
951
1423
  };
1424
+ return compiledQuery;
952
1425
  }
953
1426
  function getExplicitColumnCount(intent) {
954
1427
  if ("kind" in intent && intent.kind === "setOperation") {
@@ -972,6 +1445,218 @@ function getExplicitColumnCount(intent) {
972
1445
  return void 0;
973
1446
  }
974
1447
  }
1448
+ function expressionOutputColumn(expr) {
1449
+ switch (expr.kind) {
1450
+ case "column":
1451
+ if (expr.column === "*") return void 0;
1452
+ return expr.as ?? expr.column;
1453
+ case "columnAlias":
1454
+ return expr.alias;
1455
+ case "relationColumn":
1456
+ if (expr.column === "*") return void 0;
1457
+ return expr.as;
1458
+ case "aggregate":
1459
+ case "function":
1460
+ case "subquery":
1461
+ case "arithmetic":
1462
+ case "literal":
1463
+ case "case":
1464
+ case "jsonExtract":
1465
+ case "jsonPathExtract":
1466
+ case "customOp":
1467
+ case "customFn":
1468
+ case "array":
1469
+ case "unary":
1470
+ case "param":
1471
+ return expr.as;
1472
+ case "coalesce":
1473
+ case "raw":
1474
+ case "pseudoColumn":
1475
+ return expr.as;
1476
+ case "window":
1477
+ return expr.alias;
1478
+ case "comparison":
1479
+ case "jsonContains":
1480
+ case "jsonExists":
1481
+ case "ref":
1482
+ case "cast":
1483
+ case "namedArg":
1484
+ case "star":
1485
+ return void 0;
1486
+ /* v8 ignore next — defensive: exhaustive switch -- @preserve */
1487
+ default:
1488
+ return void 0;
1489
+ }
1490
+ }
1491
+ function addUnique(columns, seen, column) {
1492
+ if (seen.has(column)) return;
1493
+ seen.add(column);
1494
+ columns.push(column);
1495
+ }
1496
+ function resolveSourceOutputColumns(intent, ctx, bindingName) {
1497
+ const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
1498
+ if (bindingColumns2 !== void 0) return bindingColumns2;
1499
+ const columns = ctx.validator?.getTableColumns(intent.from);
1500
+ if (columns !== void 0) return columns;
1501
+ throw new NqlSemanticException(
1502
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1503
+ `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
1504
+ );
1505
+ }
1506
+ function getQueryOutputSchema(intent, ctx, bindingName) {
1507
+ const columns = [];
1508
+ const seen = /* @__PURE__ */ new Set();
1509
+ const addColumn = (column) => addUnique(columns, seen, column);
1510
+ const addSourceColumns = () => {
1511
+ for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
1512
+ addColumn(column);
1513
+ }
1514
+ };
1515
+ const { select } = intent;
1516
+ if (!select || select.type === "all") {
1517
+ addSourceColumns();
1518
+ return { columns };
1519
+ }
1520
+ if (select.type === "fields") {
1521
+ for (const field of select.fields) {
1522
+ if (field === "*") {
1523
+ addSourceColumns();
1524
+ } else {
1525
+ addColumn(field);
1526
+ }
1527
+ }
1528
+ return { columns };
1529
+ }
1530
+ if (select.type === "aggregate") {
1531
+ for (const field of select.fields ?? []) {
1532
+ addColumn(field);
1533
+ }
1534
+ for (const aggregate of select.aggregates) {
1535
+ if (!aggregate.as) {
1536
+ throw new NqlSemanticException(
1537
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1538
+ `Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
1539
+ );
1540
+ }
1541
+ addColumn(aggregate.as);
1542
+ }
1543
+ return { columns };
1544
+ }
1545
+ for (const expr of select.columns) {
1546
+ if (expr.kind === "column" && expr.column === "*") {
1547
+ addSourceColumns();
1548
+ continue;
1549
+ }
1550
+ if (expr.kind === "relationColumn" && expr.column === "*") {
1551
+ throw new NqlSemanticException(
1552
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1553
+ `Cannot compute output schema for NQL binding '${bindingName}' from relation SELECT * '${expr.relation}.*'. Use explicit aliases for binding outputs.`
1554
+ );
1555
+ }
1556
+ const outputColumn = expressionOutputColumn(expr);
1557
+ if (!outputColumn) {
1558
+ throw new NqlSemanticException(
1559
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1560
+ `Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
1561
+ );
1562
+ }
1563
+ addColumn(outputColumn);
1564
+ }
1565
+ return { columns };
1566
+ }
1567
+ function validateNqlExpressionPaths(expr, ctx) {
1568
+ switch (expr.type) {
1569
+ case "path":
1570
+ if (expr.segments.length === 1) {
1571
+ if (ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
1572
+ validateColumnForTable(ctx, ctx.currentFromTable, expr.segments[0]);
1573
+ }
1574
+ } else if (ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
1575
+ assertNoBindingRelationPath(
1576
+ ctx,
1577
+ ctx.currentFromTable,
1578
+ expr.segments.join(".")
1579
+ );
1580
+ }
1581
+ break;
1582
+ case "binary":
1583
+ validateNqlExpressionPaths(expr.left, ctx);
1584
+ validateNqlExpressionPaths(expr.right, ctx);
1585
+ break;
1586
+ case "unary":
1587
+ validateNqlExpressionPaths(expr.operand, ctx);
1588
+ break;
1589
+ case "comparison":
1590
+ case "jsonComparison":
1591
+ validateNqlExpressionPaths(expr.left, ctx);
1592
+ validateNqlExpressionPaths(expr.right, ctx);
1593
+ break;
1594
+ case "rangeOp":
1595
+ validateNqlExpressionPaths(expr.left, ctx);
1596
+ if (expr.scalar) {
1597
+ validateNqlExpressionPaths(expr.scalar, ctx);
1598
+ }
1599
+ break;
1600
+ case "in":
1601
+ validateNqlExpressionPaths(expr.expression, ctx);
1602
+ if (Array.isArray(expr.values)) {
1603
+ for (const value of expr.values) {
1604
+ validateNqlExpressionPaths(value, ctx);
1605
+ }
1606
+ }
1607
+ break;
1608
+ case "any":
1609
+ validateNqlExpressionPaths(expr.column, ctx);
1610
+ break;
1611
+ case "between":
1612
+ validateNqlExpressionPaths(expr.expression, ctx);
1613
+ validateNqlExpressionPaths(expr.low, ctx);
1614
+ validateNqlExpressionPaths(expr.high, ctx);
1615
+ break;
1616
+ case "isNull":
1617
+ validateNqlExpressionPaths(expr.expression, ctx);
1618
+ break;
1619
+ case "function":
1620
+ for (const arg of expr.args) {
1621
+ validateNqlExpressionPaths(arg, ctx);
1622
+ }
1623
+ break;
1624
+ case "window":
1625
+ for (const arg of expr.args) {
1626
+ validateNqlExpressionPaths(arg, ctx);
1627
+ }
1628
+ for (const item of expr.orderBy) {
1629
+ validateNqlExpressionPaths(item.expression, ctx);
1630
+ }
1631
+ for (const partitionBy of expr.partitionBy) {
1632
+ validateNqlExpressionPaths(partitionBy, ctx);
1633
+ }
1634
+ break;
1635
+ case "case":
1636
+ if (expr.subject) {
1637
+ validateNqlExpressionPaths(expr.subject, ctx);
1638
+ }
1639
+ for (const when of expr.whenClauses) {
1640
+ validateNqlExpressionPaths(when.condition, ctx);
1641
+ validateNqlExpressionPaths(when.result, ctx);
1642
+ }
1643
+ if (expr.elseClause) {
1644
+ validateNqlExpressionPaths(expr.elseClause, ctx);
1645
+ }
1646
+ break;
1647
+ case "jsonAccess":
1648
+ validateNqlExpressionPaths(expr.base, ctx);
1649
+ break;
1650
+ case "relationFilter":
1651
+ assertNoBindingRelationConstruct(
1652
+ ctx,
1653
+ ctx.currentFromTable,
1654
+ "use relation filters",
1655
+ expr.relation.join(".")
1656
+ );
1657
+ break;
1658
+ }
1659
+ }
975
1660
  function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
976
1661
  const setClause = query.clauses[setClauseIndex];
977
1662
  if (setClauseIndex < query.clauses.length - 1) {
@@ -981,6 +1666,7 @@ function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
981
1666
  );
982
1667
  }
983
1668
  const leftQuery = {
1669
+ type: "query",
984
1670
  table: query.table,
985
1671
  clauses: query.clauses.slice(0, setClauseIndex)
986
1672
  };
@@ -1016,10 +1702,11 @@ function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
1016
1702
  }
1017
1703
  function compileGroupByClause(clause, ctx) {
1018
1704
  return clause.expressions.map((expr) => {
1705
+ validateNqlExpressionPaths(expr, ctx);
1019
1706
  if (expr.type === "path") {
1020
1707
  const field = expr.segments.join(".");
1021
1708
  if (ctx.currentFromTable && !field.includes(".")) {
1022
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
1709
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
1023
1710
  }
1024
1711
  return field;
1025
1712
  }
@@ -1030,10 +1717,13 @@ function compileOrderByClause(clause, ctx) {
1030
1717
  return clause.items.map((item) => compileOrderItem(item, ctx));
1031
1718
  }
1032
1719
  function compileOrderItem(item, ctx) {
1720
+ validateNqlExpressionPaths(item.expression, ctx);
1033
1721
  const field = expressionToField(item.expression);
1034
1722
  if (field) {
1035
- if (ctx.currentFromTable && !field.includes(".") && !field.includes("(")) {
1036
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
1723
+ if (ctx.currentFromTable && field.includes(".") && isBindingTable(ctx, ctx.currentFromTable)) {
1724
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
1725
+ } else if (ctx.currentFromTable && !field.includes(".") && !field.includes("(")) {
1726
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
1037
1727
  }
1038
1728
  return { field, direction: item.direction };
1039
1729
  }
@@ -1318,6 +2008,13 @@ function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
1318
2008
  return intent2;
1319
2009
  }
1320
2010
  }
2011
+ const havingAggregateComparison = compileHavingAggregateComparison(
2012
+ comp,
2013
+ ctx,
2014
+ aliasContext,
2015
+ outerAliases
2016
+ );
2017
+ if (havingAggregateComparison) return havingAggregateComparison;
1321
2018
  const field = expressionToField(comp.left, aliasContext);
1322
2019
  if (!field) {
1323
2020
  throw new NqlSemanticException(
@@ -1344,6 +2041,26 @@ function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
1344
2041
  };
1345
2042
  return intent;
1346
2043
  }
2044
+ function compileHavingAggregateComparison(comp, ctx, aliasContext, outerAliases) {
2045
+ if (ctx.currentHavingAliases === void 0 || aliasContext || comp.left.type !== "function") {
2046
+ return null;
2047
+ }
2048
+ const fn = comp.left.name.toLowerCase();
2049
+ if (fn !== "count" || comp.left.args.length !== 0) {
2050
+ return null;
2051
+ }
2052
+ const exprIntent = {
2053
+ kind: "aggregate",
2054
+ function: "count",
2055
+ field: "*"
2056
+ };
2057
+ return {
2058
+ kind: "expression",
2059
+ expr: exprIntent,
2060
+ operator: mapComparisonOperator(comp.operator),
2061
+ value: resolveFilterValue(comp.right, ctx, aliasContext, outerAliases)
2062
+ };
2063
+ }
1347
2064
  function compileRange(expr, ctx, _fns, aliasContext, outerAliases) {
1348
2065
  const rangeExpr = expr;
1349
2066
  const field = expressionToField(rangeExpr.left, aliasContext);
@@ -1617,11 +2334,19 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
1617
2334
  const relFilter = expr;
1618
2335
  const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
1619
2336
  const prevRelationTarget = ctx.currentRelationTarget;
1620
- if (ctx.currentFromTable && ctx.validator && relFilter.relation[0]) {
1621
- ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2337
+ if (ctx.currentFromTable && relFilter.relation[0]) {
2338
+ assertNoBindingRelationConstruct(
2339
+ ctx,
1622
2340
  ctx.currentFromTable,
1623
- relFilter.relation[0]
2341
+ "use relation filters",
2342
+ relFilter.relation.join(".")
1624
2343
  );
2344
+ if (ctx.validator) {
2345
+ ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2346
+ ctx.currentFromTable,
2347
+ relFilter.relation[0]
2348
+ );
2349
+ }
1625
2350
  }
1626
2351
  const where = compileExpression(
1627
2352
  relFilter.condition,
@@ -1744,23 +2469,31 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
1744
2469
  ...NQL_SELECT_SCALAR_FUNCTIONS
1745
2470
  ]);
1746
2471
  function validateSelectPathExpression(expr, ctx) {
1747
- if (!ctx.currentFromTable || !ctx.validator) return;
2472
+ if (!ctx.currentFromTable) return;
1748
2473
  const { segments } = expr;
1749
2474
  if (segments.length === 1) {
1750
- ctx.validator.validateColumn(ctx.currentFromTable, segments[0]);
2475
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[0]);
1751
2476
  return;
1752
2477
  }
1753
2478
  const firstSegmentLower = segments[0].toLowerCase();
1754
2479
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
2480
+ assertNoBindingRelationConstruct(
2481
+ ctx,
2482
+ ctx.currentFromTable,
2483
+ "use pseudo-column traversals",
2484
+ segments[0]
2485
+ );
1755
2486
  let i = 1;
1756
2487
  while (i < segments.length && ctx.pseudoColumnKeywords.has(segments[i].toLowerCase())) {
1757
2488
  i++;
1758
2489
  }
1759
2490
  if (i < segments.length) {
1760
- ctx.validator.validateColumn(ctx.currentFromTable, segments[i]);
2491
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[i]);
1761
2492
  }
1762
2493
  return;
1763
2494
  }
2495
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, segments.join("."));
2496
+ if (!ctx.validator) return;
1764
2497
  const targetTable = ctx.validator.resolveRelationTarget(
1765
2498
  ctx.currentFromTable,
1766
2499
  segments[0]
@@ -1790,6 +2523,12 @@ function compileSelectClause(clause, ctx, fns) {
1790
2523
  } else if (item.type === "relationStar") {
1791
2524
  hasExpressions = true;
1792
2525
  const relation = item.relation.join(".");
2526
+ assertNoBindingRelationConstruct(
2527
+ ctx,
2528
+ ctx.currentFromTable,
2529
+ "select relation columns",
2530
+ relation
2531
+ );
1793
2532
  expressions.push({
1794
2533
  kind: "relationColumn",
1795
2534
  relation,
@@ -1935,14 +2674,14 @@ function compileSelectExpression(item, ctx, fns) {
1935
2674
  const partitionBy = windowExpr.partitionBy.length > 0 ? windowExpr.partitionBy.map((e) => {
1936
2675
  const f = expressionToValidatedField(e, ctx) ?? expressionToSql(e);
1937
2676
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1938
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
2677
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1939
2678
  }
1940
2679
  return f;
1941
2680
  }) : void 0;
1942
2681
  const orderBy = windowExpr.orderBy.length > 0 ? windowExpr.orderBy.map((o) => {
1943
2682
  const f = expressionToValidatedField(o.expression, ctx) ?? expressionToSql(o.expression);
1944
2683
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1945
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
2684
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1946
2685
  }
1947
2686
  return { field: f, direction: o.direction };
1948
2687
  }) : void 0;
@@ -2060,6 +2799,12 @@ function compileMultiSegmentPath(expr, item, ctx) {
2060
2799
  const segments = expr.segments;
2061
2800
  const firstSegmentLower = segments[0].toLowerCase();
2062
2801
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
2802
+ assertNoBindingRelationConstruct(
2803
+ ctx,
2804
+ ctx.currentFromTable,
2805
+ "use pseudo-column traversals",
2806
+ segments[0]
2807
+ );
2063
2808
  const firstSegment = firstSegmentLower;
2064
2809
  const depthHint = expr.depthHint;
2065
2810
  if (depthHint !== void 0) {
@@ -2087,7 +2832,7 @@ function compileMultiSegmentPath(expr, item, ctx) {
2087
2832
  }
2088
2833
  const targetColumn = segments[i];
2089
2834
  if (ctx.currentFromTable) {
2090
- ctx.validator?.validateColumn(ctx.currentFromTable, targetColumn);
2835
+ validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
2091
2836
  }
2092
2837
  const defaultAlias = segments.map((s) => s.toLowerCase()).join(".");
2093
2838
  if (traversals.length === 1) {
@@ -2109,6 +2854,12 @@ function compileMultiSegmentPath(expr, item, ctx) {
2109
2854
  }
2110
2855
  const column = segments[segments.length - 1];
2111
2856
  const relation = segments.slice(0, -1).join(".");
2857
+ assertNoBindingRelationConstruct(
2858
+ ctx,
2859
+ ctx.currentFromTable,
2860
+ "select relation columns",
2861
+ relation
2862
+ );
2112
2863
  if (ctx.currentFromTable && ctx.validator) {
2113
2864
  const targetTable = ctx.validator.resolveRelationTarget(
2114
2865
  ctx.currentFromTable,
@@ -2269,6 +3020,220 @@ function allowsInternalParams(options) {
2269
3020
  const internalOptions = options?.[NQL_INTERNAL_COMPILER_OPTIONS];
2270
3021
  return internalOptions?.allowInternalParams === true;
2271
3022
  }
3023
+ function isUnresolvedSelectAllOutputSchemaError(error) {
3024
+ return error instanceof NqlSemanticException && error.message.includes("from SELECT *") && error.message.includes("without a concrete table schema");
3025
+ }
3026
+ function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
3027
+ if (result.query) {
3028
+ return {
3029
+ kind: "query",
3030
+ query: result.query,
3031
+ ...bindName !== void 0 && { bindName },
3032
+ final,
3033
+ bindingDependencies
3034
+ };
3035
+ }
3036
+ if (result.mutation) {
3037
+ return {
3038
+ kind: "mutation",
3039
+ mutation: result.mutation,
3040
+ ...bindName !== void 0 && { bindName },
3041
+ final,
3042
+ bindingDependencies
3043
+ };
3044
+ }
3045
+ return void 0;
3046
+ }
3047
+ function stringArraysEqual(left, right) {
3048
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3049
+ }
3050
+ function isNqlAstRecord(value) {
3051
+ return typeof value === "object" && value !== null;
3052
+ }
3053
+ function addReadBindingReference(references, readBindingNames, name) {
3054
+ if (typeof name === "string" && readBindingNames.has(name)) {
3055
+ references.add(name);
3056
+ }
3057
+ }
3058
+ function withoutLocalCteNames(readBindingNames, ctes) {
3059
+ if (ctes.length === 0) return readBindingNames;
3060
+ const localCteNames = new Set(ctes.map((cte) => cte.name));
3061
+ if (![...localCteNames].some((name) => readBindingNames.has(name))) {
3062
+ return readBindingNames;
3063
+ }
3064
+ return new Set(
3065
+ [...readBindingNames].filter((name) => !localCteNames.has(name))
3066
+ );
3067
+ }
3068
+ function collectQueryReadBindingReferences(query, readBindingNames, references) {
3069
+ addReadBindingReference(references, readBindingNames, query.table);
3070
+ collectNodeReadBindingReferences(query.clauses, readBindingNames, references);
3071
+ }
3072
+ function collectWithQueryReadBindingReferences(withQuery, readBindingNames, references) {
3073
+ const scopedReadBindingNames = withoutLocalCteNames(
3074
+ readBindingNames,
3075
+ withQuery.ctes
3076
+ );
3077
+ for (const cte of withQuery.ctes) {
3078
+ collectQueryReadBindingReferences(
3079
+ cte.query,
3080
+ scopedReadBindingNames,
3081
+ references
3082
+ );
3083
+ }
3084
+ collectQueryReadBindingReferences(
3085
+ withQuery.query,
3086
+ scopedReadBindingNames,
3087
+ references
3088
+ );
3089
+ }
3090
+ function collectInListReadBindingReference(values, readBindingNames, references) {
3091
+ if (!Array.isArray(values) || values.length !== 1) return;
3092
+ const value = values[0];
3093
+ if (!isNqlAstRecord(value) || value.type !== "path") return;
3094
+ const segments = value.segments;
3095
+ if (!Array.isArray(segments) || segments.length !== 1) return;
3096
+ addReadBindingReference(references, readBindingNames, segments[0]);
3097
+ }
3098
+ function collectNodeReadBindingReferences(node, readBindingNames, references) {
3099
+ if (Array.isArray(node)) {
3100
+ for (const item of node) {
3101
+ collectNodeReadBindingReferences(item, readBindingNames, references);
3102
+ }
3103
+ return;
3104
+ }
3105
+ if (!isNqlAstRecord(node)) return;
3106
+ switch (node.type) {
3107
+ case "query":
3108
+ collectQueryReadBindingReferences(
3109
+ node,
3110
+ readBindingNames,
3111
+ references
3112
+ );
3113
+ return;
3114
+ case "withQuery":
3115
+ collectWithQueryReadBindingReferences(
3116
+ node,
3117
+ readBindingNames,
3118
+ references
3119
+ );
3120
+ return;
3121
+ case "mutationPipeline":
3122
+ collectNodeReadBindingReferences(
3123
+ node.mutation,
3124
+ readBindingNames,
3125
+ references
3126
+ );
3127
+ collectNodeReadBindingReferences(
3128
+ node.clauses,
3129
+ readBindingNames,
3130
+ references
3131
+ );
3132
+ return;
3133
+ case "insert_from":
3134
+ case "upsert_from":
3135
+ addReadBindingReference(references, readBindingNames, node.source);
3136
+ collectNodeReadBindingReferences(
3137
+ node.where,
3138
+ readBindingNames,
3139
+ references
3140
+ );
3141
+ return;
3142
+ case "setOperation":
3143
+ addReadBindingReference(references, readBindingNames, node.boundName);
3144
+ collectNodeReadBindingReferences(
3145
+ node.right,
3146
+ readBindingNames,
3147
+ references
3148
+ );
3149
+ return;
3150
+ case "in":
3151
+ collectNodeReadBindingReferences(
3152
+ node.expression,
3153
+ readBindingNames,
3154
+ references
3155
+ );
3156
+ collectInListReadBindingReference(
3157
+ node.values,
3158
+ readBindingNames,
3159
+ references
3160
+ );
3161
+ collectNodeReadBindingReferences(
3162
+ node.values,
3163
+ readBindingNames,
3164
+ references
3165
+ );
3166
+ return;
3167
+ case "subquery":
3168
+ collectNodeReadBindingReferences(
3169
+ node.query,
3170
+ readBindingNames,
3171
+ references
3172
+ );
3173
+ return;
3174
+ }
3175
+ for (const child of Object.values(node)) {
3176
+ collectNodeReadBindingReferences(child, readBindingNames, references);
3177
+ }
3178
+ }
3179
+ function collectStatementReadBindingReferences(stmt, readBindingNames) {
3180
+ const references = /* @__PURE__ */ new Set();
3181
+ if (readBindingNames.size === 0) return references;
3182
+ collectNodeReadBindingReferences(stmt, readBindingNames, references);
3183
+ return references;
3184
+ }
3185
+ function collectCompileResultReadBindingReferences(result, readBindingNames, references) {
3186
+ if (result.query) {
3187
+ addReadBindingReference(references, readBindingNames, result.query.from);
3188
+ }
3189
+ }
3190
+ function addTransitiveBindingDependency(bindName, bindingDependencies, ordered, seen) {
3191
+ if (seen.has(bindName)) return;
3192
+ seen.add(bindName);
3193
+ for (const dependency of bindingDependencies.get(bindName) ?? []) {
3194
+ addTransitiveBindingDependency(
3195
+ dependency,
3196
+ bindingDependencies,
3197
+ ordered,
3198
+ seen
3199
+ );
3200
+ }
3201
+ ordered.push(bindName);
3202
+ }
3203
+ function collectTransitiveReadBindingDependencies(directReferences, bindingDependencies) {
3204
+ const ordered = [];
3205
+ const seen = /* @__PURE__ */ new Set();
3206
+ for (const reference of directReferences) {
3207
+ addTransitiveBindingDependency(
3208
+ reference,
3209
+ bindingDependencies,
3210
+ ordered,
3211
+ seen
3212
+ );
3213
+ }
3214
+ return ordered;
3215
+ }
3216
+ function rejectReadBindingReferenceAcrossMutation(bindName, definitionIndex, mutationIndex, referenceIndex, statementCount) {
3217
+ throw new NqlSemanticException(
3218
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3219
+ `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.`
3220
+ );
3221
+ }
3222
+ function rejectInvalidReadBindingDependencies(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, referenceIndex, statementCount) {
3223
+ for (const referencedBindName of statementBindingDependencies) {
3224
+ const definitionIndex = readBindingDefinitions.get(referencedBindName);
3225
+ if (definitionIndex === void 0) continue;
3226
+ if (lastMutationStatement > definitionIndex) {
3227
+ rejectReadBindingReferenceAcrossMutation(
3228
+ referencedBindName,
3229
+ definitionIndex,
3230
+ lastMutationStatement,
3231
+ referenceIndex,
3232
+ statementCount
3233
+ );
3234
+ }
3235
+ }
3236
+ }
2272
3237
  var NqlCompiler = class {
2273
3238
  ctx;
2274
3239
  fns;
@@ -2292,9 +3257,11 @@ var NqlCompiler = class {
2292
3257
  this.ctx = {
2293
3258
  currentFromTable: void 0,
2294
3259
  currentRelationTarget: void 0,
3260
+ currentHavingAliases: void 0,
2295
3261
  pseudoColumnKeywords,
2296
3262
  recursiveKeywords,
2297
3263
  validator,
3264
+ bindingOutputColumns: /* @__PURE__ */ new Map(),
2298
3265
  params,
2299
3266
  maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
2300
3267
  allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
@@ -2310,6 +3277,14 @@ var NqlCompiler = class {
2310
3277
  * Compile an NQL program to IntentAST.
2311
3278
  */
2312
3279
  compile(program) {
3280
+ try {
3281
+ return this.compileProgram(program);
3282
+ } finally {
3283
+ this.ctx.bindingOutputColumns.clear();
3284
+ this.ctx.validator?.clearVirtualBindingTables();
3285
+ }
3286
+ }
3287
+ compileProgram(program) {
2313
3288
  if (program.statements.length === 0) {
2314
3289
  return {};
2315
3290
  }
@@ -2326,30 +3301,121 @@ var NqlCompiler = class {
2326
3301
  }
2327
3302
  }
2328
3303
  const bindings = /* @__PURE__ */ new Map();
3304
+ const bindingOutputSchemas = /* @__PURE__ */ new Map();
2329
3305
  const mutationBindings = /* @__PURE__ */ new Map();
2330
3306
  const materializedBindStatements = /* @__PURE__ */ new Set();
3307
+ const seenBindNames = /* @__PURE__ */ new Map();
3308
+ const nqlProgramSequence = [];
3309
+ const readBindingDefinitions = /* @__PURE__ */ new Map();
3310
+ const definedBindingNames = /* @__PURE__ */ new Set();
3311
+ const bindingDependencies = /* @__PURE__ */ new Map();
3312
+ let lastMutationStatement = -1;
2331
3313
  let lastResult = {};
2332
3314
  for (let i = 0; i < program.statements.length; i++) {
2333
3315
  const stmt = program.statements[i];
2334
- lastResult = this.compileSingleStatement(stmt, bindings);
2335
3316
  const bindName = extractBindName(stmt);
3317
+ if (bindName) {
3318
+ const previousStatement = seenBindNames.get(bindName);
3319
+ if (previousStatement !== void 0) {
3320
+ throw new NqlSemanticException(
3321
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3322
+ `NQL binding name '${bindName}' is used more than once (statements ${previousStatement + 1} and ${i + 1}). NQL binding names must be unique.`
3323
+ );
3324
+ }
3325
+ seenBindNames.set(bindName, i);
3326
+ }
3327
+ const readBindingReferences = collectStatementReadBindingReferences(
3328
+ stmt,
3329
+ definedBindingNames
3330
+ );
3331
+ let statementBindingDependencies = collectTransitiveReadBindingDependencies(
3332
+ readBindingReferences,
3333
+ bindingDependencies
3334
+ );
3335
+ rejectInvalidReadBindingDependencies(
3336
+ statementBindingDependencies,
3337
+ readBindingDefinitions,
3338
+ lastMutationStatement,
3339
+ i,
3340
+ program.statements.length
3341
+ );
3342
+ lastResult = this.compileSingleStatement(stmt, bindings);
3343
+ collectCompileResultReadBindingReferences(
3344
+ lastResult,
3345
+ definedBindingNames,
3346
+ readBindingReferences
3347
+ );
3348
+ statementBindingDependencies = collectTransitiveReadBindingDependencies(
3349
+ readBindingReferences,
3350
+ bindingDependencies
3351
+ );
3352
+ rejectInvalidReadBindingDependencies(
3353
+ statementBindingDependencies,
3354
+ readBindingDefinitions,
3355
+ lastMutationStatement,
3356
+ i,
3357
+ program.statements.length
3358
+ );
3359
+ if (stmt.type === "mutationPipeline") {
3360
+ lastMutationStatement = i;
3361
+ }
2336
3362
  if (bindName) {
2337
3363
  if (lastResult.query) {
3364
+ const outputSchema = this.registerQueryBindingOutputSchema(
3365
+ bindName,
3366
+ lastResult.query,
3367
+ bindingOutputSchemas
3368
+ );
2338
3369
  bindings.set(bindName, lastResult.query);
3370
+ if (outputSchema) {
3371
+ this.ctx.validator?.addVirtualBindingTable(
3372
+ bindName,
3373
+ outputSchema.columns
3374
+ );
3375
+ }
2339
3376
  materializedBindStatements.add(i);
3377
+ readBindingDefinitions.set(bindName, i);
3378
+ definedBindingNames.add(bindName);
3379
+ bindingDependencies.set(bindName, statementBindingDependencies);
2340
3380
  } else if (lastResult.mutation?.returning?.length) {
2341
- mutationBindings.set(bindName, lastResult.mutation);
3381
+ const canonicalBinding = this.canonicalizeMutationBinding(
3382
+ bindName,
3383
+ lastResult.mutation
3384
+ );
3385
+ lastResult = { mutation: canonicalBinding.mutation };
3386
+ mutationBindings.set(bindName, canonicalBinding.mutation);
3387
+ const outputSchema = canonicalBinding.outputSchema;
3388
+ const bindingFields = outputSchema?.columns ?? canonicalBinding.mutation.returning ?? [];
3389
+ this.ctx.bindingOutputColumns.set(bindName, outputSchema?.columns);
2342
3390
  bindings.set(bindName, {
2343
3391
  type: "select",
2344
- from: lastResult.mutation.table,
3392
+ from: canonicalBinding.mutation.table,
2345
3393
  select: {
2346
3394
  type: "fields",
2347
- fields: [...lastResult.mutation.returning]
3395
+ fields: [...bindingFields]
2348
3396
  }
2349
3397
  });
3398
+ if (outputSchema) {
3399
+ bindingOutputSchemas.set(bindName, outputSchema);
3400
+ this.ctx.validator?.addVirtualBindingTable(
3401
+ bindName,
3402
+ outputSchema.columns
3403
+ );
3404
+ }
2350
3405
  materializedBindStatements.add(i);
3406
+ definedBindingNames.add(bindName);
3407
+ bindingDependencies.set(bindName, []);
2351
3408
  }
2352
3409
  }
3410
+ const sequenceStep = programSequenceStepFromResult(
3411
+ lastResult,
3412
+ bindName,
3413
+ i === program.statements.length - 1,
3414
+ statementBindingDependencies
3415
+ );
3416
+ if (sequenceStep !== void 0) {
3417
+ nqlProgramSequence.push(sequenceStep);
3418
+ }
2353
3419
  }
2354
3420
  for (let i = 0; i < program.statements.length - 1; i++) {
2355
3421
  const bindName = extractBindName(program.statements[i]);
@@ -2360,15 +3426,68 @@ var NqlCompiler = class {
2360
3426
  }
2361
3427
  }
2362
3428
  const hasMutationBindings = mutationBindings.size > 0;
3429
+ const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
3430
+ const hasNqlProgramSequence = nqlProgramSequence.length === program.statements.length;
2363
3431
  if (bindings.size > 0) {
2364
3432
  return {
2365
3433
  ...lastResult,
2366
3434
  bindings,
2367
- ...hasMutationBindings && { mutationBindings }
3435
+ ...hasBindingOutputSchemas && { bindingOutputSchemas },
3436
+ ...hasMutationBindings && { mutationBindings },
3437
+ ...hasNqlProgramSequence && { nqlProgramSequence }
2368
3438
  };
2369
3439
  }
2370
3440
  return lastResult;
2371
3441
  }
3442
+ registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas) {
3443
+ try {
3444
+ const outputSchema = getQueryOutputSchema(query, this.ctx, bindName);
3445
+ bindingOutputSchemas.set(bindName, outputSchema);
3446
+ this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
3447
+ return outputSchema;
3448
+ } catch (error) {
3449
+ if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {
3450
+ this.ctx.bindingOutputColumns.set(bindName, void 0);
3451
+ return void 0;
3452
+ }
3453
+ throw error;
3454
+ }
3455
+ }
3456
+ canonicalizeMutationBinding(bindName, mutation) {
3457
+ const outputSchema = this.getMutationBindingOutputSchema(
3458
+ bindName,
3459
+ mutation
3460
+ );
3461
+ const returning = mutation.returning;
3462
+ if (returning === void 0 || returning.length === 0 || returning.includes("*") || outputSchema === void 0 || stringArraysEqual(returning, outputSchema.columns)) {
3463
+ return { mutation, outputSchema };
3464
+ }
3465
+ return {
3466
+ mutation: {
3467
+ ...mutation,
3468
+ returning: outputSchema.columns
3469
+ },
3470
+ outputSchema
3471
+ };
3472
+ }
3473
+ getMutationBindingOutputSchema(bindName, mutation) {
3474
+ const returning = mutation.returning;
3475
+ if (!returning || returning.length === 0) return void 0;
3476
+ if (returning.includes("*")) {
3477
+ const columns = this.ctx.validator?.getTableColumns(mutation.table);
3478
+ if (columns !== void 0) return { columns };
3479
+ if (!this.ctx.validator) return void 0;
3480
+ throw new NqlSemanticException(
3481
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
3482
+ `Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
3483
+ );
3484
+ }
3485
+ return {
3486
+ columns: returning.map(
3487
+ (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
3488
+ )
3489
+ };
3490
+ }
2372
3491
  compileSingleStatement(stmt, bindings) {
2373
3492
  if (stmt.type === "withQuery") {
2374
3493
  return compileWithQuery(stmt, this.ctx, this.fns);