@dbsp/nql 1.2.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 } 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,7 +330,23 @@ function expressionToValue(expr, ctx) {
216
330
  case "null":
217
331
  return null;
218
332
  case "path":
219
- return { $ref: expr.segments.join(".") };
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
+ }
349
+ return createNqlBindingRef(expr.segments.join("."));
220
350
  case "function": {
221
351
  return {
222
352
  $fn: expr.name,
@@ -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) {
@@ -442,1262 +592,1843 @@ function coerceToStringKey(expr, contextLabel, ctx) {
442
592
  );
443
593
  }
444
594
 
445
- // src/compiler/include-builder.ts
446
- function buildNestedIncludes(paths, flatMode) {
447
- const root = { children: /* @__PURE__ */ new Map() };
448
- for (const path of paths) {
449
- const segments = path.split(".");
450
- let node = root;
451
- for (const segment of segments) {
452
- if (!node.children.has(segment)) {
453
- node.children.set(segment, { children: /* @__PURE__ */ new Map() });
454
- }
455
- node = node.children.get(segment);
456
- }
457
- }
458
- function treeToIncludes(node) {
459
- const includes = [];
460
- for (const [relation, child] of node.children) {
461
- const childIncludes = treeToIncludes(child);
462
- const include = {
463
- relation,
464
- ...flatMode ? { strategy: "flat" } : {},
465
- ...childIncludes.length > 0 ? { include: childIncludes } : {}
466
- };
467
- includes.push(include);
595
+ // src/compiler/compile-mutation.ts
596
+ function assignMutationValue(target, column, value, ctx) {
597
+ target[column] = expressionToValue(value, ctx);
598
+ }
599
+ function compileMutationPipeline(pipeline, ctx, fns, bindings) {
600
+ ctx.currentFromTable = pipeline.mutation.table;
601
+ const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
602
+ let returning;
603
+ for (const clause of pipeline.clauses) {
604
+ if (clause.type === "select") {
605
+ returning = extractReturningColumns(clause, ctx);
468
606
  }
469
- return includes;
470
607
  }
471
- return treeToIncludes(root);
472
- }
473
- function applyIncludeLimit(includes, path, limit) {
474
- const segments = path.split(".");
475
- const root = segments[0];
476
- const idx = includes.findIndex((inc) => inc.relation === root);
477
- if (idx === -1) return;
478
- if (segments.length === 1) {
479
- includes[idx] = {
480
- ...includes[idx],
481
- limit,
482
- strategy: "flat"
608
+ if (returning) {
609
+ return {
610
+ mutation: { ...mutation, returning }
483
611
  };
484
- } else {
485
- const nested = [...includes[idx]?.include ?? []];
486
- applyIncludeLimit(nested, segments.slice(1).join("."), limit);
487
- includes[idx] = { ...includes[idx], strategy: "flat", include: nested };
488
612
  }
613
+ return { mutation };
489
614
  }
490
-
491
- // src/compiler/compile-query.ts
492
- function compileNestedQuery(query, ctx, fns, bindings) {
493
- const savedContext = {
494
- currentFromTable: ctx.currentFromTable,
495
- currentRelationTarget: ctx.currentRelationTarget
496
- };
497
- try {
498
- if (bindings) {
499
- return compileQuery(query, ctx, fns, bindings);
500
- }
501
- return fns.compileQuery(query, ctx);
502
- } finally {
503
- ctx.currentFromTable = savedContext.currentFromTable;
504
- ctx.currentRelationTarget = savedContext.currentRelationTarget;
615
+ function compileMutation(mutation, ctx, fns, bindings) {
616
+ switch (mutation.type) {
617
+ case "insert":
618
+ return compileInsert(mutation, ctx);
619
+ case "insert_from":
620
+ return compileInsertFrom(mutation, ctx, fns, bindings);
621
+ case "update":
622
+ return compileUpdate(mutation, ctx, fns, bindings);
623
+ case "delete":
624
+ return compileDelete(mutation, ctx, fns, bindings);
625
+ case "upsert":
626
+ return compileUpsert(mutation, ctx, fns, bindings);
627
+ case "upsert_from":
628
+ return compileUpsertFrom(mutation, ctx, fns, bindings);
505
629
  }
506
630
  }
507
- function compileQuery(query, ctx, fns, bindings) {
508
- const setClauseIndex = query.clauses.findIndex(
509
- (c) => c.type === "setOperation"
510
- );
511
- if (setClauseIndex >= 0) {
512
- return compileSetOperation(query, setClauseIndex, ctx, fns, bindings);
513
- }
514
- ctx.currentFromTable = query.table;
515
- ctx.validator?.validateTable(query.table);
516
- let groupByIndex = -1;
517
- for (let i = 0; i < query.clauses.length; i++) {
518
- if (query.clauses[i]?.type === "groupBy") {
519
- groupByIndex = i;
520
- break;
631
+ function compileInsert(insert, ctx) {
632
+ ctx.validator?.validateTable(insert.table);
633
+ const allColumns = /* @__PURE__ */ new Set();
634
+ for (const row of insert.rows) {
635
+ for (const assignment of row) {
636
+ ctx.validator?.validateColumn(insert.table, assignment.column);
637
+ allColumns.add(assignment.column);
521
638
  }
522
639
  }
523
- const whereConditions = [];
524
- const havingConditions = [];
525
- let select;
526
- let distinct;
527
- const allIncludes = [];
528
- let currentIncludeBatch;
529
- let groupBy;
530
- let orderBy;
531
- let limit;
532
- let offset;
533
- let flatMode = false;
534
- let lock;
535
- const includeLimits = /* @__PURE__ */ new Map();
536
- for (let i = 0; i < query.clauses.length; i++) {
537
- const clause = query.clauses[i];
538
- switch (clause.type) {
539
- case "where": {
540
- const condition = fns.compileExpression(
541
- clause.condition,
542
- ctx,
543
- fns
544
- );
545
- if (groupByIndex >= 0 && i > groupByIndex) {
546
- havingConditions.push(condition);
547
- } else if (currentIncludeBatch && currentIncludeBatch.length > 0) {
548
- const targetInclude = currentIncludeBatch[currentIncludeBatch.length - 1];
549
- const mutableInclude = targetInclude;
550
- if (targetInclude.where) {
551
- mutableInclude.where = {
552
- kind: "and",
553
- conditions: [targetInclude.where, condition]
554
- };
555
- } else {
556
- mutableInclude.where = condition;
557
- }
558
- } else {
559
- whereConditions.push(condition);
560
- }
561
- break;
562
- }
563
- case "select":
564
- select = fns.compileSelectClause(clause, ctx, fns);
565
- distinct = clause.distinct || void 0;
566
- break;
567
- case "flat":
568
- flatMode = true;
569
- break;
570
- case "groupBy":
571
- groupBy = compileGroupByClause(clause, ctx);
572
- currentIncludeBatch = void 0;
573
- break;
574
- case "orderBy":
575
- orderBy = compileOrderByClause(clause, ctx);
576
- break;
577
- case "limit": {
578
- const lc = clause;
579
- const count = resolveIntegerCount(
580
- lc.count,
581
- ctx,
582
- lc.relation ? "per-include limit" : "limit"
583
- );
584
- if (lc.relation) {
585
- const includeLimit = isParamIntent(count) ? count.value : count;
586
- includeLimits.set(lc.relation, includeLimit);
587
- } else {
588
- limit = count;
589
- }
590
- break;
591
- }
592
- case "offset":
593
- offset = resolveIntegerCount(
594
- clause.count,
595
- ctx,
596
- "offset"
597
- );
598
- break;
599
- case "lock": {
600
- const lc = clause;
601
- lock = { strength: lc.strength, waitPolicy: lc.waitPolicy };
602
- break;
640
+ const values = [];
641
+ for (const row of insert.rows) {
642
+ const rowValues = {};
643
+ const rowColumns = /* @__PURE__ */ new Set();
644
+ for (const assignment of row) {
645
+ rowColumns.add(assignment.column);
646
+ assignMutationValue(rowValues, assignment.column, assignment.value, ctx);
647
+ }
648
+ for (const col of allColumns) {
649
+ if (!rowColumns.has(col)) {
650
+ rowValues[col] = void 0;
603
651
  }
604
652
  }
653
+ values.push(rowValues);
605
654
  }
606
- if (select && select.type === "expressions") {
607
- const relationPaths = /* @__PURE__ */ new Set();
608
- for (const expr of select.columns) {
609
- if (expr.kind === "relationColumn") {
610
- relationPaths.add(expr.relation);
611
- }
612
- }
613
- if (relationPaths.size > 0) {
614
- const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
615
- for (const inc of nestedIncludes) {
616
- const exists = allIncludes.some(
617
- (existing) => existing.relation === inc.relation
618
- );
619
- if (!exists) {
620
- allIncludes.push(inc);
621
- }
622
- }
623
- }
624
- }
625
- if (flatMode && allIncludes.length > 0) {
626
- for (let i = 0; i < allIncludes.length; i++) {
627
- const inc = allIncludes[i];
628
- if (!inc.strategy) {
629
- allIncludes[i] = { ...inc, strategy: "flat" };
630
- }
631
- }
632
- }
633
- if (includeLimits.size > 0) {
634
- for (const [relation, limitCount] of includeLimits) {
635
- const rootRelation = relation.split(".")[0];
636
- const targetInclude = allIncludes.find(
637
- (inc) => inc.relation === rootRelation
638
- );
639
- if (!targetInclude) {
640
- throw new Error(
641
- `limit for relation '${relation}' specified but '${rootRelation}' is not included in the query`
642
- );
643
- }
644
- applyIncludeLimit(allIncludes, relation, limitCount);
645
- }
646
- }
647
- const include = allIncludes.length > 0 ? allIncludes : void 0;
648
- let where;
649
- if (whereConditions.length === 1) {
650
- where = whereConditions[0];
651
- } else if (whereConditions.length > 1) {
652
- where = { kind: "and", conditions: whereConditions };
653
- }
654
- let having;
655
- if (havingConditions.length === 1) {
656
- having = havingConditions[0];
657
- } else if (havingConditions.length > 1) {
658
- having = { kind: "and", conditions: havingConditions };
655
+ return {
656
+ type: "insert",
657
+ table: insert.table,
658
+ values
659
+ };
660
+ }
661
+ function compileInsertFrom(insertFrom, ctx, fns, bindings) {
662
+ ctx.validator?.validateTable(insertFrom.table);
663
+ const sourceQuery = bindings?.get(insertFrom.source);
664
+ if (!sourceQuery) {
665
+ ctx.validator?.validateTable(insertFrom.source);
659
666
  }
667
+ ctx.currentFromTable = insertFrom.source;
660
668
  return {
661
- type: "select",
662
- from: query.table,
663
- ...select !== void 0 && { select },
664
- ...where !== void 0 && { where },
665
- ...include !== void 0 && { include },
666
- ...orderBy !== void 0 && { orderBy },
667
- ...groupBy !== void 0 && { groupBy },
668
- ...having !== void 0 && { having },
669
- ...distinct !== void 0 && { distinct },
670
- ...limit !== void 0 && { limit },
671
- ...offset !== void 0 && { offset },
672
- ...lock !== void 0 && { lock }
669
+ type: "insert_from",
670
+ table: insertFrom.table,
671
+ source: insertFrom.source,
672
+ ...sourceQuery !== void 0 && { sourceQuery },
673
+ /* v8 ignore next NQL grammar does not produce explicit column lists for INSERT FROM -- @preserve */
674
+ ...insertFrom.columns !== void 0 && { columns: insertFrom.columns },
675
+ ...insertFrom.where !== void 0 && {
676
+ where: fns.compileExpression(insertFrom.where, ctx, fns)
677
+ },
678
+ ...insertFrom.limit !== void 0 && {
679
+ limit: resolveIntegerCount(insertFrom.limit, ctx, "insert-from limit")
680
+ }
673
681
  };
674
682
  }
675
- function getExplicitColumnCount(intent) {
676
- if ("kind" in intent && intent.kind === "setOperation") {
677
- return getExplicitColumnCount(intent.left);
683
+ function compileUpdate(update, ctx, fns, bindings) {
684
+ ctx.currentFromTable = update.table;
685
+ ctx.validator?.validateTable(update.table);
686
+ const set = {};
687
+ for (const assignment of update.assignments) {
688
+ ctx.validator?.validateColumn(update.table, assignment.column);
689
+ assignMutationValue(set, assignment.column, assignment.value, ctx);
678
690
  }
679
- const q = intent;
680
- if (!q.select) return void 0;
681
- switch (q.select.type) {
682
- case "all":
683
- return void 0;
684
- /* v8 ignore next — NQL compiler never produces 'fields' in set operations (always expressions or all) -- @preserve */
685
- case "fields":
686
- return q.select.fields.length;
687
- /* v8 ignore next — NQL compiler never produces 'aggregate' SelectIntent directly -- @preserve */
688
- case "aggregate":
689
- return (q.select.fields?.length ?? 0) + q.select.aggregates.length;
690
- case "expressions":
691
- return q.select.columns.length;
692
- /* v8 ignore next — defensive: exhaustive switch -- @preserve */
693
- default:
694
- return void 0;
691
+ if (update.where) {
692
+ return {
693
+ type: "update",
694
+ table: update.table,
695
+ set,
696
+ where: resolveBindingsInWhere(
697
+ fns.compileExpression(update.where, ctx, fns),
698
+ bindings
699
+ )
700
+ };
695
701
  }
696
- }
697
- function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
698
- const setClause = query.clauses[setClauseIndex];
699
- if (setClauseIndex < query.clauses.length - 1) {
700
- const trailingTypes = query.clauses.slice(setClauseIndex + 1).map((c) => c.type).join(", ");
702
+ if (!ctx.allowUnfilteredMutations) {
701
703
  throw new Error(
702
- `Clauses after a set operation are not supported (found: ${trailingTypes}). Wrap the set operation in a subquery or move the clause before the set operation.`
704
+ "update without a where clause would affect all rows; pass { allowUnfilteredMutations: true } to the compiler to allow an unfiltered update"
703
705
  );
704
706
  }
705
- const leftQuery = {
706
- table: query.table,
707
- clauses: query.clauses.slice(0, setClauseIndex)
707
+ return {
708
+ type: "update",
709
+ table: update.table,
710
+ set,
711
+ allowAll: true
708
712
  };
709
- const left = compileQuery(leftQuery, ctx, fns, bindings);
710
- let right;
711
- if (setClause.right) {
712
- right = compileNestedQuery(setClause.right, ctx, fns, bindings);
713
- } else if (setClause.boundName) {
714
- const bound = bindings?.get(setClause.boundName);
715
- if (!bound) {
716
- throw new Error(
717
- `Set operation references unbound name '${setClause.boundName}'. Use | bind ${setClause.boundName} in a preceding statement.`
718
- );
719
- }
720
- right = bound;
721
- } else {
722
- throw new Error("Set operation missing right operand");
713
+ }
714
+ function compileDelete(del, ctx, fns, bindings) {
715
+ ctx.currentFromTable = del.table;
716
+ ctx.validator?.validateTable(del.table);
717
+ if (del.where) {
718
+ return {
719
+ type: "delete",
720
+ table: del.table,
721
+ where: resolveBindingsInWhere(
722
+ fns.compileExpression(del.where, ctx, fns),
723
+ bindings
724
+ )
725
+ };
723
726
  }
724
- const leftCount = getExplicitColumnCount(left);
725
- const rightCount = getExplicitColumnCount(right);
726
- if (leftCount !== void 0 && rightCount !== void 0 && leftCount !== rightCount) {
727
+ if (!ctx.allowUnfilteredMutations) {
727
728
  throw new Error(
728
- `${setClause.op.toUpperCase()} requires both sides to have the same number of columns (left: ${leftCount}, right: ${rightCount})`
729
+ "delete without a where clause would affect all rows; pass { allowUnfilteredMutations: true } to the compiler to allow an unfiltered delete"
729
730
  );
730
731
  }
731
732
  return {
732
- kind: "setOperation",
733
- op: setClause.op,
734
- all: setClause.all,
735
- left,
736
- right
733
+ type: "delete",
734
+ table: del.table,
735
+ allowAll: true
737
736
  };
738
737
  }
739
- function compileGroupByClause(clause, ctx) {
740
- return clause.expressions.map((expr) => {
741
- if (expr.type === "path") {
742
- const field = expr.segments.join(".");
743
- if (ctx.currentFromTable && !field.includes(".")) {
744
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
738
+ function compileUpsert(upsert, ctx, fns, bindings) {
739
+ ctx.currentFromTable = upsert.table;
740
+ ctx.validator?.validateTable(upsert.table);
741
+ const values = {};
742
+ for (const assignment of upsert.assignments) {
743
+ ctx.validator?.validateColumn(upsert.table, assignment.column);
744
+ assignMutationValue(values, assignment.column, assignment.value, ctx);
745
+ }
746
+ for (const col of upsert.conflictColumns) {
747
+ ctx.validator?.validateColumn(upsert.table, col);
748
+ }
749
+ return {
750
+ type: "upsert",
751
+ table: upsert.table,
752
+ values: [values],
753
+ onConflict: { columns: upsert.conflictColumns },
754
+ action: {
755
+ type: "doUpdate",
756
+ set: values,
757
+ ...upsert.where !== void 0 && {
758
+ where: resolveBindingsInWhere(
759
+ fns.compileExpression(upsert.where, ctx, fns),
760
+ bindings
761
+ )
745
762
  }
746
- return field;
747
763
  }
748
- return expressionToSql(expr);
749
- });
750
- }
751
- function compileOrderByClause(clause, ctx) {
752
- return clause.items.map((item) => compileOrderItem(item, ctx));
764
+ };
753
765
  }
754
- function compileOrderItem(item, ctx) {
755
- const field = expressionToField(item.expression);
756
- if (field) {
757
- if (ctx.currentFromTable && !field.includes(".") && !field.includes("(")) {
758
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
759
- }
760
- return { field, direction: item.direction };
766
+ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
767
+ ctx.validator?.validateTable(upsertFrom.table);
768
+ const sourceQuery = bindings?.get(upsertFrom.source);
769
+ if (!sourceQuery) {
770
+ ctx.validator?.validateTable(upsertFrom.source);
761
771
  }
762
- if (item.expression.type === "namedParam") {
763
- throw new NqlSemanticException(
764
- NqlErrorCodes.SEM_INVALID_SYNTAX,
765
- `Named parameter :${item.expression.name} cannot be used as an ORDER BY expression because ORDER BY is query structure, not a value`,
766
- void 0,
767
- 'Choose a trusted structural path for dynamic ordering, such as nqlRaw("order by ...") or the query builder after validating the requested column and direction.'
768
- );
772
+ for (const col of upsertFrom.conflictColumns) {
773
+ ctx.validator?.validateColumn(upsertFrom.table, col);
769
774
  }
770
- const sqlExpr = expressionToSql(item.expression);
771
- return { field: sqlExpr, direction: item.direction };
772
- }
773
-
774
- // src/compiler/compile-cte.ts
775
- function compileWithQuery(astNode, ctx, fns) {
776
- const cteNames = /* @__PURE__ */ new Set();
777
- for (const cte of astNode.ctes) {
778
- if (cteNames.has(cte.name)) {
779
- throw new NqlSemanticException(
780
- NqlErrorCodes.SEM_DUPLICATE_BINDING,
781
- `Duplicate CTE name: '${cte.name}'. Each CTE must have a unique name.`
782
- );
775
+ ctx.currentFromTable = upsertFrom.source;
776
+ return {
777
+ type: "upsert_from",
778
+ table: upsertFrom.table,
779
+ source: upsertFrom.source,
780
+ conflictColumns: upsertFrom.conflictColumns,
781
+ ...sourceQuery !== void 0 && { sourceQuery },
782
+ /* v8 ignore start — NQL grammar does not produce explicit column lists for UPSERT FROM -- @preserve */
783
+ ...upsertFrom.columns !== void 0 && {
784
+ columns: upsertFrom.columns
785
+ },
786
+ /* v8 ignore stop -- @preserve */
787
+ ...upsertFrom.where !== void 0 && {
788
+ where: fns.compileExpression(upsertFrom.where, ctx, fns)
789
+ },
790
+ ...upsertFrom.limit !== void 0 && {
791
+ limit: resolveIntegerCount(upsertFrom.limit, ctx, "upsert-from limit")
792
+ }
793
+ };
794
+ }
795
+ function extractReturningColumns(clause, ctx) {
796
+ const columns = [];
797
+ for (const item of clause.items) {
798
+ if (item.type === "star") {
799
+ return ["*"];
800
+ }
801
+ if (item.type === "expression") {
802
+ const field = expressionToField(item.expression);
803
+ if (field) {
804
+ if (ctx.currentFromTable && !field.includes(".")) {
805
+ ctx.validator?.validateColumn(ctx.currentFromTable, field);
806
+ }
807
+ columns.push(item.alias ?? field);
808
+ }
783
809
  }
784
- cteNames.add(cte.name);
785
810
  }
786
- ctx.validator?.addKnownCteTables(cteNames);
787
- try {
788
- const ctes = [];
789
- for (const cte of astNode.ctes) {
790
- const bodyResult = compileNestedQuery(cte.query, ctx, fns);
791
- if ("kind" in bodyResult && bodyResult.kind === "setOperation") {
792
- throw new NqlSemanticException(
793
- NqlErrorCodes.SEM_UNKNOWN_TABLE,
794
- `Set operations (union/intersect/except) in CTE body are not supported yet. Found in CTE '${cte.name}'.`
795
- );
811
+ return columns;
812
+ }
813
+ function resolveBindingsInWhere(where, bindings) {
814
+ if (!bindings || bindings.size === 0) return where;
815
+ if (where.kind === "in") {
816
+ const inWhere = where;
817
+ const inValues = inWhere.subquery ? void 0 : inWhere.values;
818
+ if (inValues && inValues.length === 1) {
819
+ const val = inValues[0];
820
+ if (!isParamIntent(val) && isNqlBindingRef(val)) {
821
+ const ref = getNqlBindingRefName(val);
822
+ if (bindings.has(ref)) {
823
+ const boundQuery = bindings.get(ref);
824
+ const boundSelect = boundQuery.select;
825
+ const selectFields = boundSelect && "fields" in boundSelect ? boundSelect.fields : void 0;
826
+ const cteRef = {
827
+ type: "select",
828
+ from: ref,
829
+ ...selectFields && {
830
+ select: {
831
+ type: "fields",
832
+ fields: selectFields
833
+ }
834
+ }
835
+ };
836
+ return {
837
+ kind: "in",
838
+ field: inWhere.field,
839
+ subquery: cteRef
840
+ };
841
+ }
796
842
  }
797
- ctes.push({
798
- kind: "simpleCte",
799
- name: cte.name,
800
- query: bodyResult
801
- });
802
843
  }
803
- const outerResult = compileQuery(astNode.query, ctx, fns);
804
- if ("kind" in outerResult && outerResult.kind === "setOperation") {
805
- throw new NqlSemanticException(
806
- NqlErrorCodes.SEM_UNKNOWN_TABLE,
807
- "Set operations in the outer query of a WITH clause are not supported yet."
808
- );
844
+ return where;
845
+ }
846
+ if (where.kind === "not") {
847
+ const notWhere = where;
848
+ const resolved = resolveBindingsInWhere(notWhere.condition, bindings);
849
+ return resolved === notWhere.condition ? where : { kind: "not", condition: resolved };
850
+ }
851
+ if (where.kind === "and" || where.kind === "or") {
852
+ const compound = where;
853
+ const resolved = compound.conditions.map(
854
+ (c) => resolveBindingsInWhere(c, bindings)
855
+ );
856
+ const changed = resolved.some((r, i) => r !== compound.conditions[i]);
857
+ return changed ? { kind: compound.kind, conditions: resolved } : where;
858
+ }
859
+ return where;
860
+ }
861
+ function extractBindName(stmt) {
862
+ if (stmt.type === "query") {
863
+ for (const clause of stmt.clauses) {
864
+ if (clause.type === "bind") return clause.name;
865
+ }
866
+ } else if (stmt.type === "mutationPipeline") {
867
+ for (const clause of stmt.clauses) {
868
+ if (clause.type === "bind") return clause.name;
809
869
  }
810
- const cteQueryIntent = {
811
- kind: "cteQuery",
812
- ctes,
813
- query: outerResult
814
- };
815
- return { cteQuery: cteQueryIntent };
816
- } finally {
817
- ctx.validator?.clearKnownCteTables();
818
870
  }
871
+ return void 0;
819
872
  }
820
873
 
821
- // src/compiler/date-range-patterns.ts
822
- var DATE_RANGE_YEAR = /^(\d{4})$/;
823
- var DATE_RANGE_QUARTER = /^(\d{4})-Q([1-4])$/;
824
- var DATE_RANGE_MONTH = /^(\d{4})-(\d{2})$/;
825
- var DATE_RANGE_WEEK = /^(\d{4})-W(\d{2})$/;
826
- function isDateRangePattern(value) {
827
- return DATE_RANGE_YEAR.test(value) || DATE_RANGE_QUARTER.test(value) || DATE_RANGE_MONTH.test(value) || DATE_RANGE_WEEK.test(value);
828
- }
829
- var InvalidDateRangeError = class extends Error {
830
- constructor(message) {
831
- super(message);
832
- this.name = "InvalidDateRangeError";
833
- }
834
- };
835
- function expandDateRange(pattern) {
836
- let match;
837
- match = pattern.match(DATE_RANGE_YEAR);
838
- if (match) {
839
- const year = Number(match[1]);
840
- return {
841
- start: `${year}-01-01`,
842
- end: `${year + 1}-01-01`
843
- };
874
+ // src/compiler/include-builder.ts
875
+ function buildNestedIncludes(paths, flatMode) {
876
+ const root = { children: /* @__PURE__ */ new Map() };
877
+ for (const path of paths) {
878
+ const segments = path.split(".");
879
+ let node = root;
880
+ for (const segment of segments) {
881
+ if (!node.children.has(segment)) {
882
+ node.children.set(segment, { children: /* @__PURE__ */ new Map() });
883
+ }
884
+ node = node.children.get(segment);
885
+ }
844
886
  }
845
- match = pattern.match(DATE_RANGE_QUARTER);
846
- if (match) {
847
- const year = Number(match[1]);
848
- const quarter = Number(match[2]);
849
- const startMonth = (quarter - 1) * 3 + 1;
850
- const endMonth = startMonth + 3;
851
- if (endMonth > 12) {
852
- return {
853
- start: `${year}-${pad(startMonth)}-01`,
854
- end: `${year + 1}-01-01`
887
+ function treeToIncludes(node) {
888
+ const includes = [];
889
+ for (const [relation, child] of node.children) {
890
+ const childIncludes = treeToIncludes(child);
891
+ const include = {
892
+ relation,
893
+ ...flatMode ? { strategy: "flat" } : {},
894
+ ...childIncludes.length > 0 ? { include: childIncludes } : {}
855
895
  };
896
+ includes.push(include);
856
897
  }
857
- return {
858
- start: `${year}-${pad(startMonth)}-01`,
859
- end: `${year}-${pad(endMonth)}-01`
898
+ return includes;
899
+ }
900
+ return treeToIncludes(root);
901
+ }
902
+ function applyIncludeLimit(includes, path, limit) {
903
+ const segments = path.split(".");
904
+ const root = segments[0];
905
+ const idx = includes.findIndex((inc) => inc.relation === root);
906
+ if (idx === -1) return;
907
+ if (segments.length === 1) {
908
+ includes[idx] = {
909
+ ...includes[idx],
910
+ limit,
911
+ strategy: "flat"
860
912
  };
913
+ } else {
914
+ const nested = [...includes[idx]?.include ?? []];
915
+ applyIncludeLimit(nested, segments.slice(1).join("."), limit);
916
+ includes[idx] = { ...includes[idx], strategy: "flat", include: nested };
861
917
  }
862
- match = pattern.match(DATE_RANGE_MONTH);
863
- if (match) {
864
- const year = Number(match[1]);
865
- const month = Number(match[2]);
866
- if (month < 1 || month > 12) {
867
- throw new InvalidDateRangeError(
868
- `Invalid month '${match[2]}' in date range '${pattern}' \u2014 must be 01-12`
869
- );
918
+ }
919
+
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
+ ]);
926
+ function compileNestedQuery(query, ctx, fns, bindings) {
927
+ const savedContext = {
928
+ currentFromTable: ctx.currentFromTable,
929
+ currentRelationTarget: ctx.currentRelationTarget,
930
+ currentHavingAliases: ctx.currentHavingAliases
931
+ };
932
+ try {
933
+ const nestedBindings = bindings ?? activeBindingScopes.get(ctx);
934
+ if (nestedBindings) {
935
+ return compileQuery(query, ctx, fns, nestedBindings);
870
936
  }
871
- if (month === 12) {
872
- return {
873
- start: `${year}-12-01`,
874
- end: `${year + 1}-01-01`
875
- };
937
+ return fns.compileQuery(query, ctx);
938
+ } finally {
939
+ ctx.currentFromTable = savedContext.currentFromTable;
940
+ ctx.currentRelationTarget = savedContext.currentRelationTarget;
941
+ ctx.currentHavingAliases = savedContext.currentHavingAliases;
942
+ }
943
+ }
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);
876
957
  }
877
- return {
878
- start: `${year}-${pad(month)}-01`,
879
- end: `${year}-${pad(month + 1)}-01`
880
- };
881
958
  }
882
- match = pattern.match(DATE_RANGE_WEEK);
883
- if (match) {
884
- const year = Number(match[1]);
885
- const week = Number(match[2]);
886
- const maxWeeks = getISOWeekCount(year);
887
- if (week < 1 || week > maxWeeks) {
888
- throw new InvalidDateRangeError(
889
- `Invalid week 'W${match[2]}' in date range '${pattern}' \u2014 year ${year} has ${maxWeeks} ISO weeks`
890
- );
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
+ }
891
968
  }
892
- const start = isoWeekToDate(year, week);
893
- const end = addDays(start, 7);
894
- return {
895
- start: formatDate(start),
896
- end: formatDate(end)
897
- };
898
969
  }
899
- throw new InvalidDateRangeError(
900
- `Invalid date range '${pattern}' \u2014 expected YYYY, YYYY-QN, YYYY-MM, or YYYY-WNN`
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.`
901
976
  );
902
977
  }
903
- function pad(n) {
904
- return n < 10 ? `0${n}` : `${n}`;
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("."));
905
1001
  }
906
- function formatDate(d) {
907
- return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
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
+ }
908
1010
  }
909
- function addDays(d, days) {
910
- const result = new Date(d.getTime());
911
- result.setUTCDate(result.getUTCDate() + days);
912
- return result;
913
- }
914
- function isoWeekToDate(year, week) {
915
- const jan4 = new Date(Date.UTC(year, 0, 4));
916
- const jan4Dow = jan4.getUTCDay() || 7;
917
- const mondayW1 = new Date(jan4.getTime());
918
- mondayW1.setUTCDate(jan4.getUTCDate() - (jan4Dow - 1));
919
- const result = new Date(mondayW1.getTime());
920
- result.setUTCDate(mondayW1.getUTCDate() + (week - 1) * 7);
921
- return result;
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
+ }
922
1103
  }
923
- function getISOWeekCount(year) {
924
- const jan1 = new Date(Date.UTC(year, 0, 1));
925
- const dec31 = new Date(Date.UTC(year, 11, 31));
926
- const jan1Dow = jan1.getUTCDay();
927
- const dec31Dow = dec31.getUTCDay();
928
- return jan1Dow === 4 || dec31Dow === 4 ? 53 : 52;
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
+ }
929
1152
  }
930
-
931
- // src/compiler/compile-expression.ts
932
- function paramValue(value) {
933
- return value !== null && typeof value === "object" && value.kind === "param" ? value.value : value;
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
+ }
934
1217
  }
935
- var MAX_ANY_ITEMS = 1e4;
936
- function compileLogical(expr, ctx, fns, aliasContext, outerAliases) {
937
- if (expr.type === "binary") {
938
- const binary = expr;
939
- if (binary.operator === "and") {
940
- return {
941
- kind: "and",
942
- conditions: [
943
- compileExpression(binary.left, ctx, fns, aliasContext, outerAliases),
944
- compileExpression(binary.right, ctx, fns, aliasContext, outerAliases)
945
- ]
946
- };
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
+ }
1226
+ const setClauseIndex = query.clauses.findIndex(
1227
+ (c) => c.type === "setOperation"
1228
+ );
1229
+ if (setClauseIndex >= 0) {
1230
+ return compileSetOperation(query, setClauseIndex, ctx, fns, bindings);
1231
+ }
1232
+ let groupByIndex = -1;
1233
+ for (let i = 0; i < query.clauses.length; i++) {
1234
+ if (query.clauses[i]?.type === "groupBy") {
1235
+ groupByIndex = i;
1236
+ break;
947
1237
  }
948
- if (binary.operator === "or") {
949
- return {
950
- kind: "or",
951
- conditions: [
952
- compileExpression(binary.left, ctx, fns, aliasContext, outerAliases),
953
- compileExpression(binary.right, ctx, fns, aliasContext, outerAliases)
954
- ]
955
- };
1238
+ }
1239
+ const havingAliases = collectSelectAliasesFromQuery(query);
1240
+ const whereConditions = [];
1241
+ const havingConditions = [];
1242
+ let select;
1243
+ let distinct;
1244
+ const allIncludes = [];
1245
+ let currentIncludeBatch;
1246
+ let groupBy;
1247
+ let orderBy;
1248
+ let limit;
1249
+ let offset;
1250
+ let flatMode = false;
1251
+ let lock;
1252
+ const includeLimits = /* @__PURE__ */ new Map();
1253
+ for (let i = 0; i < query.clauses.length; i++) {
1254
+ const clause = query.clauses[i];
1255
+ switch (clause.type) {
1256
+ case "where": {
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
+ })();
1274
+ havingConditions.push(condition);
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
+ );
1284
+ const targetInclude = currentIncludeBatch[currentIncludeBatch.length - 1];
1285
+ const mutableInclude = targetInclude;
1286
+ if (targetInclude.where) {
1287
+ mutableInclude.where = {
1288
+ kind: "and",
1289
+ conditions: [targetInclude.where, condition]
1290
+ };
1291
+ } else {
1292
+ mutableInclude.where = condition;
1293
+ }
1294
+ } else {
1295
+ const condition = resolveBindingsInWhere(
1296
+ fns.compileExpression(
1297
+ clause.condition,
1298
+ ctx,
1299
+ fns
1300
+ ),
1301
+ bindings
1302
+ );
1303
+ whereConditions.push(condition);
1304
+ }
1305
+ break;
1306
+ }
1307
+ case "select":
1308
+ select = fns.compileSelectClause(clause, ctx, fns);
1309
+ distinct = clause.distinct || void 0;
1310
+ break;
1311
+ case "flat":
1312
+ flatMode = true;
1313
+ break;
1314
+ case "groupBy":
1315
+ groupBy = compileGroupByClause(clause, ctx);
1316
+ currentIncludeBatch = void 0;
1317
+ break;
1318
+ case "orderBy":
1319
+ orderBy = compileOrderByClause(clause, ctx);
1320
+ break;
1321
+ case "limit": {
1322
+ const lc = clause;
1323
+ const count = resolveIntegerCount(
1324
+ lc.count,
1325
+ ctx,
1326
+ lc.relation ? "per-include limit" : "limit"
1327
+ );
1328
+ if (lc.relation) {
1329
+ const includeLimit = isParamIntent(count) ? count.value : count;
1330
+ includeLimits.set(lc.relation, includeLimit);
1331
+ } else {
1332
+ limit = count;
1333
+ }
1334
+ break;
1335
+ }
1336
+ case "offset":
1337
+ offset = resolveIntegerCount(
1338
+ clause.count,
1339
+ ctx,
1340
+ "offset"
1341
+ );
1342
+ break;
1343
+ case "lock": {
1344
+ const lc = clause;
1345
+ lock = { strength: lc.strength, waitPolicy: lc.waitPolicy };
1346
+ break;
1347
+ }
956
1348
  }
957
- throw new NqlSemanticException(
958
- NqlErrorCodes.SEM_INVALID_SYNTAX,
959
- `Unsupported binary operator in WHERE: ${binary.operator}`
960
- );
961
1349
  }
962
- const unary = expr;
963
- if (unary.operator === "not") {
964
- return {
965
- kind: "not",
966
- condition: compileExpression(
967
- unary.operand,
968
- ctx,
969
- fns,
970
- aliasContext,
971
- outerAliases
972
- )
973
- };
1350
+ if (select && select.type === "expressions") {
1351
+ const relationPaths = /* @__PURE__ */ new Set();
1352
+ for (const expr of select.columns) {
1353
+ if (expr.kind === "relationColumn") {
1354
+ relationPaths.add(expr.relation);
1355
+ }
1356
+ }
1357
+ if (relationPaths.size > 0) {
1358
+ const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
1359
+ for (const inc of nestedIncludes) {
1360
+ const exists = allIncludes.some(
1361
+ (existing) => existing.relation === inc.relation
1362
+ );
1363
+ if (!exists) {
1364
+ allIncludes.push(inc);
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+ if (flatMode && allIncludes.length > 0) {
1370
+ for (let i = 0; i < allIncludes.length; i++) {
1371
+ const inc = allIncludes[i];
1372
+ if (!inc.strategy) {
1373
+ allIncludes[i] = { ...inc, strategy: "flat" };
1374
+ }
1375
+ }
1376
+ }
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
+ }
1384
+ for (const [relation, limitCount] of includeLimits) {
1385
+ const rootRelation = relation.split(".")[0];
1386
+ const targetInclude = allIncludes.find(
1387
+ (inc) => inc.relation === rootRelation
1388
+ );
1389
+ if (!targetInclude) {
1390
+ throw new Error(
1391
+ `limit for relation '${relation}' specified but '${rootRelation}' is not included in the query`
1392
+ );
1393
+ }
1394
+ applyIncludeLimit(allIncludes, relation, limitCount);
1395
+ }
1396
+ }
1397
+ const include = allIncludes.length > 0 ? allIncludes : void 0;
1398
+ let where;
1399
+ if (whereConditions.length === 1) {
1400
+ where = whereConditions[0];
1401
+ } else if (whereConditions.length > 1) {
1402
+ where = { kind: "and", conditions: whereConditions };
1403
+ }
1404
+ let having;
1405
+ if (havingConditions.length === 1) {
1406
+ having = havingConditions[0];
1407
+ } else if (havingConditions.length > 1) {
1408
+ having = { kind: "and", conditions: havingConditions };
1409
+ }
1410
+ const compiledQuery = {
1411
+ type: "select",
1412
+ from: query.table,
1413
+ ...select !== void 0 && { select },
1414
+ ...where !== void 0 && { where },
1415
+ ...include !== void 0 && { include },
1416
+ ...orderBy !== void 0 && { orderBy },
1417
+ ...groupBy !== void 0 && { groupBy },
1418
+ ...having !== void 0 && { having },
1419
+ ...distinct !== void 0 && { distinct },
1420
+ ...limit !== void 0 && { limit },
1421
+ ...offset !== void 0 && { offset },
1422
+ ...lock !== void 0 && { lock }
1423
+ };
1424
+ return compiledQuery;
1425
+ }
1426
+ function getExplicitColumnCount(intent) {
1427
+ if ("kind" in intent && intent.kind === "setOperation") {
1428
+ return getExplicitColumnCount(intent.left);
974
1429
  }
1430
+ const q = intent;
1431
+ if (!q.select) return void 0;
1432
+ switch (q.select.type) {
1433
+ case "all":
1434
+ return void 0;
1435
+ /* v8 ignore next — NQL compiler never produces 'fields' in set operations (always expressions or all) -- @preserve */
1436
+ case "fields":
1437
+ return q.select.fields.length;
1438
+ /* v8 ignore next — NQL compiler never produces 'aggregate' SelectIntent directly -- @preserve */
1439
+ case "aggregate":
1440
+ return (q.select.fields?.length ?? 0) + q.select.aggregates.length;
1441
+ case "expressions":
1442
+ return q.select.columns.length;
1443
+ /* v8 ignore next — defensive: exhaustive switch -- @preserve */
1444
+ default:
1445
+ return void 0;
1446
+ }
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;
975
1501
  throw new NqlSemanticException(
976
1502
  NqlErrorCodes.SEM_INVALID_SYNTAX,
977
- `Unsupported unary operator: ${unary.operator}`
1503
+ `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
978
1504
  );
979
1505
  }
980
- function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
981
- const comp = expr;
982
- if (comp.left.type === "jsonAccess") {
983
- const jsonLeft = comp.left;
984
- const baseField = expressionToField(jsonLeft.base, aliasContext);
985
- if (!baseField) {
986
- throw new NqlSemanticException(
987
- NqlErrorCodes.SEM_INVALID_SYNTAX,
988
- "JSON access base must be a field reference"
989
- );
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);
990
1513
  }
991
- const operator2 = mapComparisonOperator(comp.operator);
992
- const value2 = resolveFilterValue(
993
- comp.right,
994
- ctx,
995
- aliasContext,
996
- outerAliases
997
- );
998
- const intent2 = {
999
- kind: "comparison",
1000
- field: baseField,
1001
- operator: operator2,
1002
- value: value2,
1003
- jsonPath: jsonLeft.path,
1004
- jsonMode: jsonLeft.mode
1005
- };
1006
- return intent2;
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 };
1007
1529
  }
1008
- if (comp.left.type === "function") {
1009
- const fn = comp.left.name.toLowerCase();
1010
- if (fn === "json_extract" || fn === "json_extract_text") {
1011
- if (comp.left.args.length < 2) {
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) {
1012
1536
  throw new NqlSemanticException(
1013
1537
  NqlErrorCodes.SEM_INVALID_SYNTAX,
1014
- `${fn}() requires at least 2 arguments`
1538
+ `Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
1015
1539
  );
1016
1540
  }
1017
- const baseField = expressionToField(comp.left.args[0], aliasContext);
1018
- if (!baseField) {
1019
- throw new NqlSemanticException(
1020
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1021
- `${fn}() first argument must be a field reference`
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(".")
1022
1579
  );
1023
1580
  }
1024
- const keys = comp.left.args.slice(1).map((a) => coerceToStringKey(a, `${fn}() path argument`, ctx));
1025
- const operator2 = mapComparisonOperator(comp.operator);
1026
- const value2 = resolveFilterValue(
1027
- comp.right,
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(
1028
1652
  ctx,
1029
- aliasContext,
1030
- outerAliases
1653
+ ctx.currentFromTable,
1654
+ "use relation filters",
1655
+ expr.relation.join(".")
1031
1656
  );
1032
- const intent2 = {
1033
- kind: "comparison",
1034
- field: baseField,
1035
- operator: operator2,
1036
- value: value2,
1037
- jsonPath: keys,
1038
- jsonMode: fn === "json_extract" ? "json" : "text"
1039
- };
1040
- return intent2;
1041
- }
1042
- }
1043
- const field = expressionToField(comp.left, aliasContext);
1044
- if (!field) {
1045
- throw new NqlSemanticException(
1046
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1047
- "Left side of comparison must be a field reference"
1048
- );
1049
- }
1050
- validateWhereField(ctx, field, aliasContext, comp.left);
1051
- if (comp.operator === "like") {
1052
- const pattern = coerceToStringKey(comp.right, "LIKE pattern", ctx);
1053
- return {
1054
- kind: "like",
1055
- field,
1056
- pattern
1057
- };
1657
+ break;
1058
1658
  }
1059
- const operator = mapComparisonOperator(comp.operator);
1060
- const value = resolveFilterValue(comp.right, ctx, aliasContext, outerAliases);
1061
- const intent = {
1062
- kind: "comparison",
1063
- field,
1064
- operator,
1065
- value
1066
- };
1067
- return intent;
1068
1659
  }
1069
- function compileRange(expr, ctx, _fns, aliasContext, outerAliases) {
1070
- const rangeExpr = expr;
1071
- const field = expressionToField(rangeExpr.left, aliasContext);
1072
- if (!field) {
1073
- throw new NqlSemanticException(
1074
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1075
- "Left side of range operator must be a field reference"
1076
- );
1077
- }
1078
- validateWhereField(ctx, field, aliasContext, rangeExpr.left);
1079
- let rangeValue;
1080
- if (rangeExpr.range) {
1081
- rangeValue = expressionToRangeValue(rangeExpr.range);
1082
- } else if (rangeExpr.scalar) {
1083
- rangeValue = resolveFilterValue(
1084
- rangeExpr.scalar,
1085
- ctx,
1086
- aliasContext,
1087
- outerAliases
1660
+ function compileSetOperation(query, setClauseIndex, ctx, fns, bindings) {
1661
+ const setClause = query.clauses[setClauseIndex];
1662
+ if (setClauseIndex < query.clauses.length - 1) {
1663
+ const trailingTypes = query.clauses.slice(setClauseIndex + 1).map((c) => c.type).join(", ");
1664
+ throw new Error(
1665
+ `Clauses after a set operation are not supported (found: ${trailingTypes}). Wrap the set operation in a subquery or move the clause before the set operation.`
1088
1666
  );
1667
+ }
1668
+ const leftQuery = {
1669
+ type: "query",
1670
+ table: query.table,
1671
+ clauses: query.clauses.slice(0, setClauseIndex)
1672
+ };
1673
+ const left = compileQuery(leftQuery, ctx, fns, bindings);
1674
+ let right;
1675
+ if (setClause.right) {
1676
+ right = compileNestedQuery(setClause.right, ctx, fns, bindings);
1677
+ } else if (setClause.boundName) {
1678
+ const bound = bindings?.get(setClause.boundName);
1679
+ if (!bound) {
1680
+ throw new Error(
1681
+ `Set operation references unbound name '${setClause.boundName}'. Use | bind ${setClause.boundName} in a preceding statement.`
1682
+ );
1683
+ }
1684
+ right = bound;
1089
1685
  } else {
1090
- throw new NqlSemanticException(
1091
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1092
- "Range operator requires either a range literal or scalar value"
1686
+ throw new Error("Set operation missing right operand");
1687
+ }
1688
+ const leftCount = getExplicitColumnCount(left);
1689
+ const rightCount = getExplicitColumnCount(right);
1690
+ if (leftCount !== void 0 && rightCount !== void 0 && leftCount !== rightCount) {
1691
+ throw new Error(
1692
+ `${setClause.op.toUpperCase()} requires both sides to have the same number of columns (left: ${leftCount}, right: ${rightCount})`
1093
1693
  );
1094
1694
  }
1095
- const result = {
1096
- kind: "range",
1097
- field,
1098
- operator: rangeExpr.operator,
1099
- value: rangeValue
1695
+ return {
1696
+ kind: "setOperation",
1697
+ op: setClause.op,
1698
+ all: setClause.all,
1699
+ left,
1700
+ right
1100
1701
  };
1101
- return result;
1102
1702
  }
1103
- function compileMembership(expr, ctx, fns, aliasContext, outerAliases) {
1104
- if (expr.type === "any") {
1105
- const anyExpr = expr;
1106
- const field2 = expressionToField(anyExpr.column, aliasContext);
1107
- if (!field2) {
1108
- throw new NqlSemanticException(
1109
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1110
- "ANY expression must reference a field"
1111
- );
1112
- }
1113
- validateWhereField(ctx, field2, aliasContext, anyExpr.column);
1114
- const valuesParam = resolveNamedParam(ctx, anyExpr.paramName);
1115
- const rawValues = valuesParam.value;
1116
- if (!Array.isArray(rawValues)) {
1117
- throw new NqlSemanticException(
1118
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1119
- `ANY(:${anyExpr.paramName}) requires an array argument`
1120
- );
1703
+ function compileGroupByClause(clause, ctx) {
1704
+ return clause.expressions.map((expr) => {
1705
+ validateNqlExpressionPaths(expr, ctx);
1706
+ if (expr.type === "path") {
1707
+ const field = expr.segments.join(".");
1708
+ if (ctx.currentFromTable && !field.includes(".")) {
1709
+ validateColumnForTable(ctx, ctx.currentFromTable, field);
1710
+ }
1711
+ return field;
1121
1712
  }
1122
- if (rawValues.length > ctx.maxAnyItems) {
1123
- throw new NqlSemanticException(
1124
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1125
- `ANY(:${anyExpr.paramName}) array length ${rawValues.length} exceeds maximum of ${ctx.maxAnyItems}`
1126
- );
1713
+ return expressionToSql(expr);
1714
+ });
1715
+ }
1716
+ function compileOrderByClause(clause, ctx) {
1717
+ return clause.items.map((item) => compileOrderItem(item, ctx));
1718
+ }
1719
+ function compileOrderItem(item, ctx) {
1720
+ validateNqlExpressionPaths(item.expression, ctx);
1721
+ const field = expressionToField(item.expression);
1722
+ if (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);
1127
1727
  }
1128
- const result2 = {
1129
- kind: "any",
1130
- field: field2,
1131
- values: valuesParam
1132
- };
1133
- return result2;
1728
+ return { field, direction: item.direction };
1134
1729
  }
1135
- const inExpr = expr;
1136
- const field = expressionToField(inExpr.expression, aliasContext);
1137
- if (!field) {
1730
+ if (item.expression.type === "namedParam") {
1138
1731
  throw new NqlSemanticException(
1139
1732
  NqlErrorCodes.SEM_INVALID_SYNTAX,
1140
- "IN expression must reference a field"
1733
+ `Named parameter :${item.expression.name} cannot be used as an ORDER BY expression because ORDER BY is query structure, not a value`,
1734
+ void 0,
1735
+ 'Choose a trusted structural path for dynamic ordering, such as nqlRaw("order by ...") or the query builder after validating the requested column and direction.'
1141
1736
  );
1142
1737
  }
1143
- validateWhereField(ctx, field, aliasContext, inExpr.expression);
1144
- let values;
1145
- if (Array.isArray(inExpr.values)) {
1146
- values = inExpr.values.map(
1147
- (v) => resolveFilterValue(v, ctx, aliasContext, outerAliases)
1148
- );
1149
- const dateRangeValues = values.filter(
1150
- (v) => typeof v === "string" && isDateRangePattern(v)
1151
- );
1152
- if (dateRangeValues.length > 0) {
1153
- if (dateRangeValues.length !== values.length) {
1738
+ const sqlExpr = expressionToSql(item.expression);
1739
+ return { field: sqlExpr, direction: item.direction };
1740
+ }
1741
+
1742
+ // src/compiler/compile-cte.ts
1743
+ function compileWithQuery(astNode, ctx, fns) {
1744
+ const cteNames = /* @__PURE__ */ new Set();
1745
+ for (const cte of astNode.ctes) {
1746
+ if (cteNames.has(cte.name)) {
1747
+ throw new NqlSemanticException(
1748
+ NqlErrorCodes.SEM_DUPLICATE_BINDING,
1749
+ `Duplicate CTE name: '${cte.name}'. Each CTE must have a unique name.`
1750
+ );
1751
+ }
1752
+ cteNames.add(cte.name);
1753
+ }
1754
+ ctx.validator?.addKnownCteTables(cteNames);
1755
+ try {
1756
+ const ctes = [];
1757
+ for (const cte of astNode.ctes) {
1758
+ const bodyResult = compileNestedQuery(cte.query, ctx, fns);
1759
+ if ("kind" in bodyResult && bodyResult.kind === "setOperation") {
1154
1760
  throw new NqlSemanticException(
1155
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1156
- "Cannot mix date range patterns with regular values in IN list. Use all date ranges or all literals."
1761
+ NqlErrorCodes.SEM_UNKNOWN_TABLE,
1762
+ `Set operations (union/intersect/except) in CTE body are not supported yet. Found in CTE '${cte.name}'.`
1157
1763
  );
1158
1764
  }
1159
- return expandDateRangeList(field, dateRangeValues, inExpr.negated);
1765
+ ctes.push({
1766
+ kind: "simpleCte",
1767
+ name: cte.name,
1768
+ query: bodyResult
1769
+ });
1160
1770
  }
1161
- } else if ("type" in inExpr.values && inExpr.values.type === "subquery") {
1162
- const subquery = compileNestedQuery(
1163
- inExpr.values.query,
1164
- ctx,
1165
- fns
1166
- );
1167
- const result2 = {
1168
- kind: "in",
1169
- field,
1170
- subquery
1171
- };
1172
- if (inExpr.negated) {
1173
- return { kind: "not", condition: result2 };
1771
+ const outerResult = compileQuery(astNode.query, ctx, fns);
1772
+ if ("kind" in outerResult && outerResult.kind === "setOperation") {
1773
+ throw new NqlSemanticException(
1774
+ NqlErrorCodes.SEM_UNKNOWN_TABLE,
1775
+ "Set operations in the outer query of a WITH clause are not supported yet."
1776
+ );
1174
1777
  }
1175
- return result2;
1176
- } else if ("type" in inExpr.values && inExpr.values.type === "dateRange") {
1177
- return expandDateRangeList(field, [inExpr.values.value], inExpr.negated);
1178
- } else {
1179
- values = [];
1180
- }
1181
- const result = {
1182
- kind: "in",
1183
- field,
1184
- values
1185
- };
1186
- if (inExpr.negated) {
1187
- return { kind: "not", condition: result };
1778
+ const cteQueryIntent = {
1779
+ kind: "cteQuery",
1780
+ ctes,
1781
+ query: outerResult
1782
+ };
1783
+ return { cteQuery: cteQueryIntent };
1784
+ } finally {
1785
+ ctx.validator?.clearKnownCteTables();
1188
1786
  }
1189
- return result;
1190
1787
  }
1191
- function compileBetween(expr, ctx, _fns, aliasContext, outerAliases) {
1192
- const between = expr;
1193
- const field = expressionToField(between.expression, aliasContext);
1194
- if (!field) {
1195
- throw new NqlSemanticException(
1196
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1197
- "BETWEEN expression must reference a field"
1198
- );
1199
- }
1200
- validateWhereField(ctx, field, aliasContext, between.expression);
1201
- const lower = resolveFilterValue(
1202
- between.low,
1203
- ctx,
1204
- aliasContext,
1205
- outerAliases
1206
- );
1207
- const upper = resolveFilterValue(
1208
- between.high,
1209
- ctx,
1210
- aliasContext,
1211
- outerAliases
1212
- );
1213
- const lowerValue = paramValue(lower);
1214
- const upperValue = paramValue(upper);
1215
- assertBetweenBoundValueAllowed("lower", between.low, lowerValue);
1216
- assertBetweenBoundValueAllowed("upper", between.high, upperValue);
1217
- const value = { lower, upper };
1218
- return {
1219
- kind: "range",
1220
- field,
1221
- operator: "between",
1222
- value
1223
- };
1788
+
1789
+ // src/compiler/date-range-patterns.ts
1790
+ var DATE_RANGE_YEAR = /^(\d{4})$/;
1791
+ var DATE_RANGE_QUARTER = /^(\d{4})-Q([1-4])$/;
1792
+ var DATE_RANGE_MONTH = /^(\d{4})-(\d{2})$/;
1793
+ var DATE_RANGE_WEEK = /^(\d{4})-W(\d{2})$/;
1794
+ function isDateRangePattern(value) {
1795
+ return DATE_RANGE_YEAR.test(value) || DATE_RANGE_QUARTER.test(value) || DATE_RANGE_MONTH.test(value) || DATE_RANGE_WEEK.test(value);
1224
1796
  }
1225
- function assertBetweenBoundValueAllowed(position, expr, value) {
1226
- if (value === null || typeof value === "number" || typeof value === "string" || typeof value === "bigint" || value instanceof Date) {
1227
- return;
1797
+ var InvalidDateRangeError = class extends Error {
1798
+ constructor(message) {
1799
+ super(message);
1800
+ this.name = "InvalidDateRangeError";
1228
1801
  }
1229
- const paramSuffix = expr.type === "namedParam" ? `; param :${expr.name}` : "";
1230
- throw new NqlSemanticException(
1231
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1232
- `BETWEEN ${position} bound must be a literal number, string, or date, or a bigint param; got type ${typeof value}${paramSuffix}.`
1233
- );
1234
- }
1235
- function compileNull(expr, ctx, aliasContext) {
1236
- const isNull = expr;
1237
- const field = expressionToField(isNull.expression, aliasContext);
1238
- if (!field) {
1239
- throw new NqlSemanticException(
1240
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1241
- "IS NULL expression must reference a field"
1242
- );
1802
+ };
1803
+ function expandDateRange(pattern) {
1804
+ let match;
1805
+ match = pattern.match(DATE_RANGE_YEAR);
1806
+ if (match) {
1807
+ const year = Number(match[1]);
1808
+ return {
1809
+ start: `${year}-01-01`,
1810
+ end: `${year + 1}-01-01`
1811
+ };
1812
+ }
1813
+ match = pattern.match(DATE_RANGE_QUARTER);
1814
+ if (match) {
1815
+ const year = Number(match[1]);
1816
+ const quarter = Number(match[2]);
1817
+ const startMonth = (quarter - 1) * 3 + 1;
1818
+ const endMonth = startMonth + 3;
1819
+ if (endMonth > 12) {
1820
+ return {
1821
+ start: `${year}-${pad(startMonth)}-01`,
1822
+ end: `${year + 1}-01-01`
1823
+ };
1824
+ }
1825
+ return {
1826
+ start: `${year}-${pad(startMonth)}-01`,
1827
+ end: `${year}-${pad(endMonth)}-01`
1828
+ };
1243
1829
  }
1244
- validateWhereField(ctx, field, aliasContext, isNull.expression);
1245
- return {
1246
- kind: "null",
1247
- field,
1248
- operator: isNull.negated ? "isNotNull" : "isNull"
1249
- };
1250
- }
1251
- function compileJson(expr, ctx, _fns, aliasContext, outerAliases) {
1252
- if (expr.type === "function") {
1253
- const fn = expr.name.toLowerCase();
1254
- if (fn === "json_contains" || fn === "json_contained_by") {
1255
- if (expr.args.length < 2) {
1256
- throw new NqlSemanticException(
1257
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1258
- `${fn}() requires 2 arguments: field and value`
1259
- );
1260
- }
1261
- const jsonField2 = expressionToField(expr.args[0], aliasContext);
1262
- if (!jsonField2) {
1263
- throw new NqlSemanticException(
1264
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1265
- `${fn}() first argument must be a field reference`
1266
- );
1267
- }
1268
- const jsonValue2 = resolveFilterValue(
1269
- expr.args[1],
1270
- ctx,
1271
- aliasContext,
1272
- outerAliases
1830
+ match = pattern.match(DATE_RANGE_MONTH);
1831
+ if (match) {
1832
+ const year = Number(match[1]);
1833
+ const month = Number(match[2]);
1834
+ if (month < 1 || month > 12) {
1835
+ throw new InvalidDateRangeError(
1836
+ `Invalid month '${match[2]}' in date range '${pattern}' \u2014 must be 01-12`
1273
1837
  );
1274
- const intent2 = {
1275
- kind: "jsonContains",
1276
- field: jsonField2,
1277
- value: jsonValue2,
1278
- reversed: fn === "json_contained_by"
1279
- };
1280
- return intent2;
1281
1838
  }
1282
- if (fn === "json_exists") {
1283
- if (expr.args.length < 2) {
1284
- throw new NqlSemanticException(
1285
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1286
- `${fn}() requires 2 arguments: field and key`
1287
- );
1288
- }
1289
- const jsonField2 = expressionToField(expr.args[0], aliasContext);
1290
- if (!jsonField2) {
1291
- throw new NqlSemanticException(
1292
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1293
- `${fn}() first argument must be a field reference`
1294
- );
1295
- }
1296
- const key = coerceToStringKey(expr.args[1], `${fn}() key`, ctx);
1839
+ if (month === 12) {
1297
1840
  return {
1298
- kind: "jsonExists",
1299
- field: jsonField2,
1300
- key
1841
+ start: `${year}-12-01`,
1842
+ end: `${year + 1}-01-01`
1301
1843
  };
1302
1844
  }
1303
- throw new NqlSemanticException(
1304
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1305
- `Unsupported function in WHERE context: ${fn}()`
1306
- );
1307
- }
1308
- const jsonComp = expr;
1309
- const jsonField = expressionToField(jsonComp.left, aliasContext);
1310
- if (!jsonField) {
1311
- throw new NqlSemanticException(
1312
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1313
- "Left side of JSON comparison must be a field reference"
1314
- );
1845
+ return {
1846
+ start: `${year}-${pad(month)}-01`,
1847
+ end: `${year}-${pad(month + 1)}-01`
1848
+ };
1315
1849
  }
1316
- if (jsonComp.operator === "?") {
1317
- const key = coerceToStringKey(jsonComp.right, "? operator key", ctx);
1850
+ match = pattern.match(DATE_RANGE_WEEK);
1851
+ if (match) {
1852
+ const year = Number(match[1]);
1853
+ const week = Number(match[2]);
1854
+ const maxWeeks = getISOWeekCount(year);
1855
+ if (week < 1 || week > maxWeeks) {
1856
+ throw new InvalidDateRangeError(
1857
+ `Invalid week 'W${match[2]}' in date range '${pattern}' \u2014 year ${year} has ${maxWeeks} ISO weeks`
1858
+ );
1859
+ }
1860
+ const start = isoWeekToDate(year, week);
1861
+ const end = addDays(start, 7);
1318
1862
  return {
1319
- kind: "jsonExists",
1320
- field: jsonField,
1321
- key
1863
+ start: formatDate(start),
1864
+ end: formatDate(end)
1322
1865
  };
1323
1866
  }
1324
- const jsonValue = resolveFilterValue(
1325
- jsonComp.right,
1326
- ctx,
1327
- aliasContext,
1328
- outerAliases
1867
+ throw new InvalidDateRangeError(
1868
+ `Invalid date range '${pattern}' \u2014 expected YYYY, YYYY-QN, YYYY-MM, or YYYY-WNN`
1329
1869
  );
1330
- const intent = {
1331
- kind: "jsonContains",
1332
- field: jsonField,
1333
- value: jsonValue,
1334
- reversed: jsonComp.operator === "<@"
1335
- };
1336
- return intent;
1337
1870
  }
1338
- function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
1339
- const relFilter = expr;
1340
- const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
1341
- const prevRelationTarget = ctx.currentRelationTarget;
1342
- if (ctx.currentFromTable && ctx.validator && relFilter.relation[0]) {
1343
- ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
1344
- ctx.currentFromTable,
1345
- relFilter.relation[0]
1346
- );
1347
- }
1348
- const where = compileExpression(
1349
- relFilter.condition,
1350
- ctx,
1351
- fns,
1352
- relFilter.alias,
1353
- nestedOuterAliases
1354
- );
1355
- ctx.currentRelationTarget = prevRelationTarget;
1356
- return {
1357
- kind: "relationFilter",
1358
- relation: relFilter.relation,
1359
- where,
1360
- mode: relFilter.mode,
1361
- ...relFilter.alias !== void 0 && { alias: relFilter.alias }
1362
- };
1871
+ function pad(n) {
1872
+ return n < 10 ? `0${n}` : `${n}`;
1363
1873
  }
1364
- function expandDateRangeList(field, patterns, negated) {
1365
- const conditions = patterns.map((pattern) => {
1366
- const { start, end } = expandDateRange(pattern);
1367
- return {
1368
- kind: "and",
1369
- conditions: [
1370
- {
1371
- kind: "comparison",
1372
- field,
1373
- operator: "gte",
1374
- value: start
1375
- },
1376
- {
1377
- kind: "comparison",
1378
- field,
1379
- operator: "lt",
1380
- value: end
1381
- }
1382
- ]
1383
- };
1384
- });
1385
- const result = conditions.length === 1 ? conditions[0] : { kind: "or", conditions };
1386
- if (negated) {
1387
- return { kind: "not", condition: result };
1388
- }
1874
+ function formatDate(d) {
1875
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
1876
+ }
1877
+ function addDays(d, days) {
1878
+ const result = new Date(d.getTime());
1879
+ result.setUTCDate(result.getUTCDate() + days);
1389
1880
  return result;
1390
1881
  }
1391
- function compileExpression(expr, ctx, fns, aliasContext, outerAliases) {
1392
- switch (expr.type) {
1393
- case "binary":
1394
- case "unary":
1395
- return compileLogical(expr, ctx, fns, aliasContext, outerAliases);
1396
- case "comparison":
1397
- return compileComparison(expr, ctx, fns, aliasContext, outerAliases);
1398
- case "rangeOp":
1399
- return compileRange(expr, ctx, fns, aliasContext, outerAliases);
1400
- case "in":
1401
- case "any":
1402
- return compileMembership(expr, ctx, fns, aliasContext, outerAliases);
1403
- case "between":
1404
- return compileBetween(expr, ctx, fns, aliasContext, outerAliases);
1405
- case "isNull":
1406
- return compileNull(expr, ctx, aliasContext);
1407
- case "jsonComparison":
1408
- case "function":
1409
- return compileJson(expr, ctx, fns, aliasContext, outerAliases);
1410
- case "relationFilter":
1411
- return compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases);
1412
- case "case":
1413
- throw new NqlSemanticException(
1414
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1415
- "CASE in WHERE not supported. Use a computed column in SELECT or a relation filter instead."
1416
- );
1417
- case "exists":
1418
- throw new NqlSemanticException(
1419
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1420
- "EXISTS (subquery) is not supported in NQL. Use relation filters instead:\n orders | with customer | where customer.active = true\n orders | where exists(customer, active = true)\nThese compile to efficient EXISTS subqueries automatically."
1421
- );
1422
- /* v8 ignore next — defensive: all parser-produced expression types are handled above -- @preserve */
1423
- default:
1424
- throw new NqlSemanticException(
1425
- NqlErrorCodes.SEM_INVALID_SYNTAX,
1426
- `Unsupported expression type in WHERE: ${expr.type}`
1427
- );
1428
- }
1882
+ function isoWeekToDate(year, week) {
1883
+ const jan4 = new Date(Date.UTC(year, 0, 4));
1884
+ const jan4Dow = jan4.getUTCDay() || 7;
1885
+ const mondayW1 = new Date(jan4.getTime());
1886
+ mondayW1.setUTCDate(jan4.getUTCDate() - (jan4Dow - 1));
1887
+ const result = new Date(mondayW1.getTime());
1888
+ result.setUTCDate(mondayW1.getUTCDate() + (week - 1) * 7);
1889
+ return result;
1429
1890
  }
1430
- function assignMutationValue(target, column, value, ctx) {
1431
- target[column] = expressionToValue(value, ctx);
1891
+ function getISOWeekCount(year) {
1892
+ const jan1 = new Date(Date.UTC(year, 0, 1));
1893
+ const dec31 = new Date(Date.UTC(year, 11, 31));
1894
+ const jan1Dow = jan1.getUTCDay();
1895
+ const dec31Dow = dec31.getUTCDay();
1896
+ return jan1Dow === 4 || dec31Dow === 4 ? 53 : 52;
1432
1897
  }
1433
- function compileMutationPipeline(pipeline, ctx, fns, bindings) {
1434
- ctx.currentFromTable = pipeline.mutation.table;
1435
- const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
1436
- let returning;
1437
- for (const clause of pipeline.clauses) {
1438
- if (clause.type === "select") {
1439
- returning = extractReturningColumns(clause, ctx);
1898
+
1899
+ // src/compiler/compile-expression.ts
1900
+ function paramValue(value) {
1901
+ return value !== null && typeof value === "object" && value.kind === "param" ? value.value : value;
1902
+ }
1903
+ var MAX_ANY_ITEMS = 1e4;
1904
+ function compileLogical(expr, ctx, fns, aliasContext, outerAliases) {
1905
+ if (expr.type === "binary") {
1906
+ const binary = expr;
1907
+ if (binary.operator === "and") {
1908
+ return {
1909
+ kind: "and",
1910
+ conditions: [
1911
+ compileExpression(binary.left, ctx, fns, aliasContext, outerAliases),
1912
+ compileExpression(binary.right, ctx, fns, aliasContext, outerAliases)
1913
+ ]
1914
+ };
1915
+ }
1916
+ if (binary.operator === "or") {
1917
+ return {
1918
+ kind: "or",
1919
+ conditions: [
1920
+ compileExpression(binary.left, ctx, fns, aliasContext, outerAliases),
1921
+ compileExpression(binary.right, ctx, fns, aliasContext, outerAliases)
1922
+ ]
1923
+ };
1440
1924
  }
1925
+ throw new NqlSemanticException(
1926
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1927
+ `Unsupported binary operator in WHERE: ${binary.operator}`
1928
+ );
1441
1929
  }
1442
- if (returning) {
1930
+ const unary = expr;
1931
+ if (unary.operator === "not") {
1443
1932
  return {
1444
- mutation: { ...mutation, returning }
1933
+ kind: "not",
1934
+ condition: compileExpression(
1935
+ unary.operand,
1936
+ ctx,
1937
+ fns,
1938
+ aliasContext,
1939
+ outerAliases
1940
+ )
1445
1941
  };
1446
1942
  }
1447
- return { mutation };
1448
- }
1449
- function compileMutation(mutation, ctx, fns, bindings) {
1450
- switch (mutation.type) {
1451
- case "insert":
1452
- return compileInsert(mutation, ctx);
1453
- case "insert_from":
1454
- return compileInsertFrom(mutation, ctx, fns, bindings);
1455
- case "update":
1456
- return compileUpdate(mutation, ctx, fns, bindings);
1457
- case "delete":
1458
- return compileDelete(mutation, ctx, fns, bindings);
1459
- case "upsert":
1460
- return compileUpsert(mutation, ctx);
1461
- case "upsert_from":
1462
- return compileUpsertFrom(mutation, ctx, fns, bindings);
1463
- }
1943
+ throw new NqlSemanticException(
1944
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1945
+ `Unsupported unary operator: ${unary.operator}`
1946
+ );
1464
1947
  }
1465
- function compileInsert(insert, ctx) {
1466
- ctx.validator?.validateTable(insert.table);
1467
- const allColumns = /* @__PURE__ */ new Set();
1468
- for (const row of insert.rows) {
1469
- for (const assignment of row) {
1470
- ctx.validator?.validateColumn(insert.table, assignment.column);
1471
- allColumns.add(assignment.column);
1948
+ function compileComparison(expr, ctx, _fns, aliasContext, outerAliases) {
1949
+ const comp = expr;
1950
+ if (comp.left.type === "jsonAccess") {
1951
+ const jsonLeft = comp.left;
1952
+ const baseField = expressionToField(jsonLeft.base, aliasContext);
1953
+ if (!baseField) {
1954
+ throw new NqlSemanticException(
1955
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1956
+ "JSON access base must be a field reference"
1957
+ );
1472
1958
  }
1959
+ const operator2 = mapComparisonOperator(comp.operator);
1960
+ const value2 = resolveFilterValue(
1961
+ comp.right,
1962
+ ctx,
1963
+ aliasContext,
1964
+ outerAliases
1965
+ );
1966
+ const intent2 = {
1967
+ kind: "comparison",
1968
+ field: baseField,
1969
+ operator: operator2,
1970
+ value: value2,
1971
+ jsonPath: jsonLeft.path,
1972
+ jsonMode: jsonLeft.mode
1973
+ };
1974
+ return intent2;
1473
1975
  }
1474
- const values = [];
1475
- for (const row of insert.rows) {
1476
- const rowValues = {};
1477
- const rowColumns = /* @__PURE__ */ new Set();
1478
- for (const assignment of row) {
1479
- rowColumns.add(assignment.column);
1480
- assignMutationValue(rowValues, assignment.column, assignment.value, ctx);
1481
- }
1482
- for (const col of allColumns) {
1483
- if (!rowColumns.has(col)) {
1484
- rowValues[col] = void 0;
1976
+ if (comp.left.type === "function") {
1977
+ const fn = comp.left.name.toLowerCase();
1978
+ if (fn === "json_extract" || fn === "json_extract_text") {
1979
+ if (comp.left.args.length < 2) {
1980
+ throw new NqlSemanticException(
1981
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1982
+ `${fn}() requires at least 2 arguments`
1983
+ );
1984
+ }
1985
+ const baseField = expressionToField(comp.left.args[0], aliasContext);
1986
+ if (!baseField) {
1987
+ throw new NqlSemanticException(
1988
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
1989
+ `${fn}() first argument must be a field reference`
1990
+ );
1485
1991
  }
1992
+ const keys = comp.left.args.slice(1).map((a) => coerceToStringKey(a, `${fn}() path argument`, ctx));
1993
+ const operator2 = mapComparisonOperator(comp.operator);
1994
+ const value2 = resolveFilterValue(
1995
+ comp.right,
1996
+ ctx,
1997
+ aliasContext,
1998
+ outerAliases
1999
+ );
2000
+ const intent2 = {
2001
+ kind: "comparison",
2002
+ field: baseField,
2003
+ operator: operator2,
2004
+ value: value2,
2005
+ jsonPath: keys,
2006
+ jsonMode: fn === "json_extract" ? "json" : "text"
2007
+ };
2008
+ return intent2;
1486
2009
  }
1487
- values.push(rowValues);
1488
2010
  }
1489
- return {
1490
- type: "insert",
1491
- table: insert.table,
1492
- values
2011
+ const havingAggregateComparison = compileHavingAggregateComparison(
2012
+ comp,
2013
+ ctx,
2014
+ aliasContext,
2015
+ outerAliases
2016
+ );
2017
+ if (havingAggregateComparison) return havingAggregateComparison;
2018
+ const field = expressionToField(comp.left, aliasContext);
2019
+ if (!field) {
2020
+ throw new NqlSemanticException(
2021
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2022
+ "Left side of comparison must be a field reference"
2023
+ );
2024
+ }
2025
+ validateWhereField(ctx, field, aliasContext, comp.left);
2026
+ if (comp.operator === "like") {
2027
+ const pattern = coerceToStringKey(comp.right, "LIKE pattern", ctx);
2028
+ return {
2029
+ kind: "like",
2030
+ field,
2031
+ pattern
2032
+ };
2033
+ }
2034
+ const operator = mapComparisonOperator(comp.operator);
2035
+ const value = resolveFilterValue(comp.right, ctx, aliasContext, outerAliases);
2036
+ const intent = {
2037
+ kind: "comparison",
2038
+ field,
2039
+ operator,
2040
+ value
1493
2041
  };
2042
+ return intent;
1494
2043
  }
1495
- function compileInsertFrom(insertFrom, ctx, fns, bindings) {
1496
- ctx.validator?.validateTable(insertFrom.table);
1497
- const sourceQuery = bindings?.get(insertFrom.source);
1498
- if (!sourceQuery) {
1499
- ctx.validator?.validateTable(insertFrom.source);
2044
+ function compileHavingAggregateComparison(comp, ctx, aliasContext, outerAliases) {
2045
+ if (ctx.currentHavingAliases === void 0 || aliasContext || comp.left.type !== "function") {
2046
+ return null;
1500
2047
  }
1501
- ctx.currentFromTable = insertFrom.source;
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
+ };
1502
2057
  return {
1503
- type: "insert_from",
1504
- table: insertFrom.table,
1505
- source: insertFrom.source,
1506
- ...sourceQuery !== void 0 && { sourceQuery },
1507
- /* v8 ignore next — NQL grammar does not produce explicit column lists for INSERT FROM -- @preserve */
1508
- ...insertFrom.columns !== void 0 && { columns: insertFrom.columns },
1509
- ...insertFrom.where !== void 0 && {
1510
- where: fns.compileExpression(insertFrom.where, ctx, fns)
1511
- },
1512
- ...insertFrom.limit !== void 0 && {
1513
- limit: resolveIntegerCount(insertFrom.limit, ctx, "insert-from limit")
1514
- }
2058
+ kind: "expression",
2059
+ expr: exprIntent,
2060
+ operator: mapComparisonOperator(comp.operator),
2061
+ value: resolveFilterValue(comp.right, ctx, aliasContext, outerAliases)
1515
2062
  };
1516
2063
  }
1517
- function compileUpdate(update, ctx, fns, bindings) {
1518
- ctx.currentFromTable = update.table;
1519
- ctx.validator?.validateTable(update.table);
1520
- const set = {};
1521
- for (const assignment of update.assignments) {
1522
- ctx.validator?.validateColumn(update.table, assignment.column);
1523
- assignMutationValue(set, assignment.column, assignment.value, ctx);
1524
- }
1525
- if (update.where) {
1526
- return {
1527
- type: "update",
1528
- table: update.table,
1529
- set,
1530
- where: resolveBindingsInWhere(
1531
- fns.compileExpression(update.where, ctx, fns),
1532
- bindings
1533
- )
1534
- };
2064
+ function compileRange(expr, ctx, _fns, aliasContext, outerAliases) {
2065
+ const rangeExpr = expr;
2066
+ const field = expressionToField(rangeExpr.left, aliasContext);
2067
+ if (!field) {
2068
+ throw new NqlSemanticException(
2069
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2070
+ "Left side of range operator must be a field reference"
2071
+ );
1535
2072
  }
1536
- if (!ctx.allowUnfilteredMutations) {
1537
- throw new Error(
1538
- "update without a where clause would affect all rows; pass { allowUnfilteredMutations: true } to the compiler to allow an unfiltered update"
2073
+ validateWhereField(ctx, field, aliasContext, rangeExpr.left);
2074
+ let rangeValue;
2075
+ if (rangeExpr.range) {
2076
+ rangeValue = expressionToRangeValue(rangeExpr.range);
2077
+ } else if (rangeExpr.scalar) {
2078
+ rangeValue = resolveFilterValue(
2079
+ rangeExpr.scalar,
2080
+ ctx,
2081
+ aliasContext,
2082
+ outerAliases
2083
+ );
2084
+ } else {
2085
+ throw new NqlSemanticException(
2086
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2087
+ "Range operator requires either a range literal or scalar value"
1539
2088
  );
1540
2089
  }
1541
- return {
1542
- type: "update",
1543
- table: update.table,
1544
- set,
1545
- allowAll: true
2090
+ const result = {
2091
+ kind: "range",
2092
+ field,
2093
+ operator: rangeExpr.operator,
2094
+ value: rangeValue
1546
2095
  };
2096
+ return result;
1547
2097
  }
1548
- function compileDelete(del, ctx, fns, bindings) {
1549
- ctx.currentFromTable = del.table;
1550
- ctx.validator?.validateTable(del.table);
1551
- if (del.where) {
1552
- return {
1553
- type: "delete",
1554
- table: del.table,
1555
- where: resolveBindingsInWhere(
1556
- fns.compileExpression(del.where, ctx, fns),
1557
- bindings
1558
- )
2098
+ function compileMembership(expr, ctx, fns, aliasContext, outerAliases) {
2099
+ if (expr.type === "any") {
2100
+ const anyExpr = expr;
2101
+ const field2 = expressionToField(anyExpr.column, aliasContext);
2102
+ if (!field2) {
2103
+ throw new NqlSemanticException(
2104
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2105
+ "ANY expression must reference a field"
2106
+ );
2107
+ }
2108
+ validateWhereField(ctx, field2, aliasContext, anyExpr.column);
2109
+ const valuesParam = resolveNamedParam(ctx, anyExpr.paramName);
2110
+ const rawValues = valuesParam.value;
2111
+ if (!Array.isArray(rawValues)) {
2112
+ throw new NqlSemanticException(
2113
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2114
+ `ANY(:${anyExpr.paramName}) requires an array argument`
2115
+ );
2116
+ }
2117
+ if (rawValues.length > ctx.maxAnyItems) {
2118
+ throw new NqlSemanticException(
2119
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2120
+ `ANY(:${anyExpr.paramName}) array length ${rawValues.length} exceeds maximum of ${ctx.maxAnyItems}`
2121
+ );
2122
+ }
2123
+ const result2 = {
2124
+ kind: "any",
2125
+ field: field2,
2126
+ values: valuesParam
1559
2127
  };
2128
+ return result2;
1560
2129
  }
1561
- if (!ctx.allowUnfilteredMutations) {
1562
- throw new Error(
1563
- "delete without a where clause would affect all rows; pass { allowUnfilteredMutations: true } to the compiler to allow an unfiltered delete"
2130
+ const inExpr = expr;
2131
+ const field = expressionToField(inExpr.expression, aliasContext);
2132
+ if (!field) {
2133
+ throw new NqlSemanticException(
2134
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2135
+ "IN expression must reference a field"
1564
2136
  );
1565
2137
  }
1566
- return {
1567
- type: "delete",
1568
- table: del.table,
1569
- allowAll: true
1570
- };
1571
- }
1572
- function compileUpsert(upsert, ctx) {
1573
- ctx.validator?.validateTable(upsert.table);
1574
- const values = {};
1575
- for (const assignment of upsert.assignments) {
1576
- ctx.validator?.validateColumn(upsert.table, assignment.column);
1577
- assignMutationValue(values, assignment.column, assignment.value, ctx);
2138
+ validateWhereField(ctx, field, aliasContext, inExpr.expression);
2139
+ let values;
2140
+ if (Array.isArray(inExpr.values)) {
2141
+ values = inExpr.values.map(
2142
+ (v) => resolveFilterValue(v, ctx, aliasContext, outerAliases)
2143
+ );
2144
+ const dateRangeValues = values.filter(
2145
+ (v) => typeof v === "string" && isDateRangePattern(v)
2146
+ );
2147
+ if (dateRangeValues.length > 0) {
2148
+ if (dateRangeValues.length !== values.length) {
2149
+ throw new NqlSemanticException(
2150
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2151
+ "Cannot mix date range patterns with regular values in IN list. Use all date ranges or all literals."
2152
+ );
2153
+ }
2154
+ return expandDateRangeList(field, dateRangeValues, inExpr.negated);
2155
+ }
2156
+ } else if ("type" in inExpr.values && inExpr.values.type === "subquery") {
2157
+ const subquery = compileNestedQuery(
2158
+ inExpr.values.query,
2159
+ ctx,
2160
+ fns
2161
+ );
2162
+ const result2 = {
2163
+ kind: "in",
2164
+ field,
2165
+ subquery
2166
+ };
2167
+ if (inExpr.negated) {
2168
+ return { kind: "not", condition: result2 };
2169
+ }
2170
+ return result2;
2171
+ } else if ("type" in inExpr.values && inExpr.values.type === "dateRange") {
2172
+ return expandDateRangeList(field, [inExpr.values.value], inExpr.negated);
2173
+ } else {
2174
+ values = [];
1578
2175
  }
1579
- for (const col of upsert.conflictColumns) {
1580
- ctx.validator?.validateColumn(upsert.table, col);
2176
+ const result = {
2177
+ kind: "in",
2178
+ field,
2179
+ values
2180
+ };
2181
+ if (inExpr.negated) {
2182
+ return { kind: "not", condition: result };
1581
2183
  }
1582
- if (upsert.where) {
1583
- throw new Error(
1584
- "conditional upsert is not yet supported: a WHERE clause on `upsert` (ON CONFLICT DO UPDATE ... WHERE) is parsed but cannot be honored by the SQL generator yet. Remove the WHERE, or use a plain conditional update. Tracked for a future release."
2184
+ return result;
2185
+ }
2186
+ function compileBetween(expr, ctx, _fns, aliasContext, outerAliases) {
2187
+ const between = expr;
2188
+ const field = expressionToField(between.expression, aliasContext);
2189
+ if (!field) {
2190
+ throw new NqlSemanticException(
2191
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2192
+ "BETWEEN expression must reference a field"
1585
2193
  );
1586
2194
  }
2195
+ validateWhereField(ctx, field, aliasContext, between.expression);
2196
+ const lower = resolveFilterValue(
2197
+ between.low,
2198
+ ctx,
2199
+ aliasContext,
2200
+ outerAliases
2201
+ );
2202
+ const upper = resolveFilterValue(
2203
+ between.high,
2204
+ ctx,
2205
+ aliasContext,
2206
+ outerAliases
2207
+ );
2208
+ const lowerValue = paramValue(lower);
2209
+ const upperValue = paramValue(upper);
2210
+ assertBetweenBoundValueAllowed("lower", between.low, lowerValue);
2211
+ assertBetweenBoundValueAllowed("upper", between.high, upperValue);
2212
+ const value = { lower, upper };
1587
2213
  return {
1588
- type: "upsert",
1589
- table: upsert.table,
1590
- values: [values],
1591
- onConflict: { columns: upsert.conflictColumns },
1592
- action: { type: "doUpdate", set: values }
2214
+ kind: "range",
2215
+ field,
2216
+ operator: "between",
2217
+ value
1593
2218
  };
1594
2219
  }
1595
- function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
1596
- ctx.validator?.validateTable(upsertFrom.table);
1597
- const sourceQuery = bindings?.get(upsertFrom.source);
1598
- if (!sourceQuery) {
1599
- ctx.validator?.validateTable(upsertFrom.source);
2220
+ function assertBetweenBoundValueAllowed(position, expr, value) {
2221
+ if (value === null || typeof value === "number" || typeof value === "string" || typeof value === "bigint" || value instanceof Date) {
2222
+ return;
1600
2223
  }
1601
- for (const col of upsertFrom.conflictColumns) {
1602
- ctx.validator?.validateColumn(upsertFrom.table, col);
2224
+ const paramSuffix = expr.type === "namedParam" ? `; param :${expr.name}` : "";
2225
+ throw new NqlSemanticException(
2226
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2227
+ `BETWEEN ${position} bound must be a literal number, string, or date, or a bigint param; got type ${typeof value}${paramSuffix}.`
2228
+ );
2229
+ }
2230
+ function compileNull(expr, ctx, aliasContext) {
2231
+ const isNull = expr;
2232
+ const field = expressionToField(isNull.expression, aliasContext);
2233
+ if (!field) {
2234
+ throw new NqlSemanticException(
2235
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2236
+ "IS NULL expression must reference a field"
2237
+ );
1603
2238
  }
1604
- ctx.currentFromTable = upsertFrom.source;
2239
+ validateWhereField(ctx, field, aliasContext, isNull.expression);
1605
2240
  return {
1606
- type: "upsert_from",
1607
- table: upsertFrom.table,
1608
- source: upsertFrom.source,
1609
- conflictColumns: upsertFrom.conflictColumns,
1610
- ...sourceQuery !== void 0 && { sourceQuery },
1611
- /* v8 ignore start — NQL grammar does not produce explicit column lists for UPSERT FROM -- @preserve */
1612
- ...upsertFrom.columns !== void 0 && {
1613
- columns: upsertFrom.columns
1614
- },
1615
- /* v8 ignore stop -- @preserve */
1616
- ...upsertFrom.where !== void 0 && {
1617
- where: fns.compileExpression(upsertFrom.where, ctx, fns)
1618
- },
1619
- ...upsertFrom.limit !== void 0 && {
1620
- limit: resolveIntegerCount(upsertFrom.limit, ctx, "upsert-from limit")
1621
- }
2241
+ kind: "null",
2242
+ field,
2243
+ operator: isNull.negated ? "isNotNull" : "isNull"
1622
2244
  };
1623
2245
  }
1624
- function extractReturningColumns(clause, ctx) {
1625
- const columns = [];
1626
- for (const item of clause.items) {
1627
- if (item.type === "star") {
1628
- return ["*"];
1629
- }
1630
- if (item.type === "expression") {
1631
- const field = expressionToField(item.expression);
1632
- if (field) {
1633
- if (ctx.currentFromTable && !field.includes(".")) {
1634
- ctx.validator?.validateColumn(ctx.currentFromTable, field);
1635
- }
1636
- columns.push(item.alias ?? field);
2246
+ function compileJson(expr, ctx, _fns, aliasContext, outerAliases) {
2247
+ if (expr.type === "function") {
2248
+ const fn = expr.name.toLowerCase();
2249
+ if (fn === "json_contains" || fn === "json_contained_by") {
2250
+ if (expr.args.length < 2) {
2251
+ throw new NqlSemanticException(
2252
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2253
+ `${fn}() requires 2 arguments: field and value`
2254
+ );
2255
+ }
2256
+ const jsonField2 = expressionToField(expr.args[0], aliasContext);
2257
+ if (!jsonField2) {
2258
+ throw new NqlSemanticException(
2259
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2260
+ `${fn}() first argument must be a field reference`
2261
+ );
1637
2262
  }
2263
+ const jsonValue2 = resolveFilterValue(
2264
+ expr.args[1],
2265
+ ctx,
2266
+ aliasContext,
2267
+ outerAliases
2268
+ );
2269
+ const intent2 = {
2270
+ kind: "jsonContains",
2271
+ field: jsonField2,
2272
+ value: jsonValue2,
2273
+ reversed: fn === "json_contained_by"
2274
+ };
2275
+ return intent2;
1638
2276
  }
1639
- }
1640
- return columns;
1641
- }
1642
- function resolveBindingsInWhere(where, bindings) {
1643
- if (!bindings || bindings.size === 0) return where;
1644
- if (where.kind === "in") {
1645
- const inWhere = where;
1646
- const inValues = inWhere.subquery ? void 0 : inWhere.values;
1647
- if (inValues && inValues.length === 1) {
1648
- const val = inValues[0];
1649
- if (!isParamIntent(val) && val && typeof val === "object" && "$ref" in val) {
1650
- const ref = val.$ref;
1651
- if (bindings.has(ref)) {
1652
- const boundQuery = bindings.get(ref);
1653
- const boundSelect = boundQuery.select;
1654
- const selectFields = boundSelect && "fields" in boundSelect ? boundSelect.fields : void 0;
1655
- const cteRef = {
1656
- type: "select",
1657
- from: ref,
1658
- ...selectFields && {
1659
- select: {
1660
- type: "fields",
1661
- fields: selectFields
1662
- }
1663
- }
1664
- };
1665
- return {
1666
- kind: "in",
1667
- field: inWhere.field,
1668
- subquery: cteRef
1669
- };
1670
- }
2277
+ if (fn === "json_exists") {
2278
+ if (expr.args.length < 2) {
2279
+ throw new NqlSemanticException(
2280
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2281
+ `${fn}() requires 2 arguments: field and key`
2282
+ );
2283
+ }
2284
+ const jsonField2 = expressionToField(expr.args[0], aliasContext);
2285
+ if (!jsonField2) {
2286
+ throw new NqlSemanticException(
2287
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2288
+ `${fn}() first argument must be a field reference`
2289
+ );
1671
2290
  }
2291
+ const key = coerceToStringKey(expr.args[1], `${fn}() key`, ctx);
2292
+ return {
2293
+ kind: "jsonExists",
2294
+ field: jsonField2,
2295
+ key
2296
+ };
1672
2297
  }
1673
- return where;
1674
- }
1675
- if (where.kind === "not") {
1676
- const notWhere = where;
1677
- const resolved = resolveBindingsInWhere(notWhere.condition, bindings);
1678
- return resolved === notWhere.condition ? where : { kind: "not", condition: resolved };
2298
+ throw new NqlSemanticException(
2299
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2300
+ `Unsupported function in WHERE context: ${fn}()`
2301
+ );
1679
2302
  }
1680
- if (where.kind === "and" || where.kind === "or") {
1681
- const compound = where;
1682
- const resolved = compound.conditions.map(
1683
- (c) => resolveBindingsInWhere(c, bindings)
2303
+ const jsonComp = expr;
2304
+ const jsonField = expressionToField(jsonComp.left, aliasContext);
2305
+ if (!jsonField) {
2306
+ throw new NqlSemanticException(
2307
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2308
+ "Left side of JSON comparison must be a field reference"
1684
2309
  );
1685
- const changed = resolved.some((r, i) => r !== compound.conditions[i]);
1686
- return changed ? { kind: compound.kind, conditions: resolved } : where;
1687
2310
  }
1688
- return where;
2311
+ if (jsonComp.operator === "?") {
2312
+ const key = coerceToStringKey(jsonComp.right, "? operator key", ctx);
2313
+ return {
2314
+ kind: "jsonExists",
2315
+ field: jsonField,
2316
+ key
2317
+ };
2318
+ }
2319
+ const jsonValue = resolveFilterValue(
2320
+ jsonComp.right,
2321
+ ctx,
2322
+ aliasContext,
2323
+ outerAliases
2324
+ );
2325
+ const intent = {
2326
+ kind: "jsonContains",
2327
+ field: jsonField,
2328
+ value: jsonValue,
2329
+ reversed: jsonComp.operator === "<@"
2330
+ };
2331
+ return intent;
1689
2332
  }
1690
- function extractBindName(stmt) {
1691
- if (stmt.type === "query") {
1692
- for (const clause of stmt.clauses) {
1693
- if (clause.type === "bind") return clause.name;
1694
- }
1695
- } else if (stmt.type === "mutationPipeline") {
1696
- for (const clause of stmt.clauses) {
1697
- if (clause.type === "bind") return clause.name;
2333
+ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2334
+ const relFilter = expr;
2335
+ const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
2336
+ const prevRelationTarget = ctx.currentRelationTarget;
2337
+ if (ctx.currentFromTable && relFilter.relation[0]) {
2338
+ assertNoBindingRelationConstruct(
2339
+ ctx,
2340
+ ctx.currentFromTable,
2341
+ "use relation filters",
2342
+ relFilter.relation.join(".")
2343
+ );
2344
+ if (ctx.validator) {
2345
+ ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2346
+ ctx.currentFromTable,
2347
+ relFilter.relation[0]
2348
+ );
1698
2349
  }
1699
2350
  }
1700
- return void 0;
2351
+ const where = compileExpression(
2352
+ relFilter.condition,
2353
+ ctx,
2354
+ fns,
2355
+ relFilter.alias,
2356
+ nestedOuterAliases
2357
+ );
2358
+ ctx.currentRelationTarget = prevRelationTarget;
2359
+ return {
2360
+ kind: "relationFilter",
2361
+ relation: relFilter.relation,
2362
+ where,
2363
+ mode: relFilter.mode,
2364
+ ...relFilter.alias !== void 0 && { alias: relFilter.alias }
2365
+ };
2366
+ }
2367
+ function expandDateRangeList(field, patterns, negated) {
2368
+ const conditions = patterns.map((pattern) => {
2369
+ const { start, end } = expandDateRange(pattern);
2370
+ return {
2371
+ kind: "and",
2372
+ conditions: [
2373
+ {
2374
+ kind: "comparison",
2375
+ field,
2376
+ operator: "gte",
2377
+ value: start
2378
+ },
2379
+ {
2380
+ kind: "comparison",
2381
+ field,
2382
+ operator: "lt",
2383
+ value: end
2384
+ }
2385
+ ]
2386
+ };
2387
+ });
2388
+ const result = conditions.length === 1 ? conditions[0] : { kind: "or", conditions };
2389
+ if (negated) {
2390
+ return { kind: "not", condition: result };
2391
+ }
2392
+ return result;
2393
+ }
2394
+ function compileExpression(expr, ctx, fns, aliasContext, outerAliases) {
2395
+ switch (expr.type) {
2396
+ case "binary":
2397
+ case "unary":
2398
+ return compileLogical(expr, ctx, fns, aliasContext, outerAliases);
2399
+ case "comparison":
2400
+ return compileComparison(expr, ctx, fns, aliasContext, outerAliases);
2401
+ case "rangeOp":
2402
+ return compileRange(expr, ctx, fns, aliasContext, outerAliases);
2403
+ case "in":
2404
+ case "any":
2405
+ return compileMembership(expr, ctx, fns, aliasContext, outerAliases);
2406
+ case "between":
2407
+ return compileBetween(expr, ctx, fns, aliasContext, outerAliases);
2408
+ case "isNull":
2409
+ return compileNull(expr, ctx, aliasContext);
2410
+ case "jsonComparison":
2411
+ case "function":
2412
+ return compileJson(expr, ctx, fns, aliasContext, outerAliases);
2413
+ case "relationFilter":
2414
+ return compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases);
2415
+ case "case":
2416
+ throw new NqlSemanticException(
2417
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2418
+ "CASE in WHERE not supported. Use a computed column in SELECT or a relation filter instead."
2419
+ );
2420
+ case "exists":
2421
+ throw new NqlSemanticException(
2422
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2423
+ "EXISTS (subquery) is not supported in NQL. Use relation filters instead:\n orders | with customer | where customer.active = true\n orders | where exists(customer, active = true)\nThese compile to efficient EXISTS subqueries automatically."
2424
+ );
2425
+ /* v8 ignore next — defensive: all parser-produced expression types are handled above -- @preserve */
2426
+ default:
2427
+ throw new NqlSemanticException(
2428
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
2429
+ `Unsupported expression type in WHERE: ${expr.type}`
2430
+ );
2431
+ }
1701
2432
  }
1702
2433
  function paramExpressionIntent(param, alias) {
1703
2434
  const intent = { ...param };
@@ -1738,23 +2469,31 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
1738
2469
  ...NQL_SELECT_SCALAR_FUNCTIONS
1739
2470
  ]);
1740
2471
  function validateSelectPathExpression(expr, ctx) {
1741
- if (!ctx.currentFromTable || !ctx.validator) return;
2472
+ if (!ctx.currentFromTable) return;
1742
2473
  const { segments } = expr;
1743
2474
  if (segments.length === 1) {
1744
- ctx.validator.validateColumn(ctx.currentFromTable, segments[0]);
2475
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[0]);
1745
2476
  return;
1746
2477
  }
1747
2478
  const firstSegmentLower = segments[0].toLowerCase();
1748
2479
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
2480
+ assertNoBindingRelationConstruct(
2481
+ ctx,
2482
+ ctx.currentFromTable,
2483
+ "use pseudo-column traversals",
2484
+ segments[0]
2485
+ );
1749
2486
  let i = 1;
1750
2487
  while (i < segments.length && ctx.pseudoColumnKeywords.has(segments[i].toLowerCase())) {
1751
2488
  i++;
1752
2489
  }
1753
2490
  if (i < segments.length) {
1754
- ctx.validator.validateColumn(ctx.currentFromTable, segments[i]);
2491
+ validateColumnForTable(ctx, ctx.currentFromTable, segments[i]);
1755
2492
  }
1756
2493
  return;
1757
2494
  }
2495
+ assertNoBindingRelationPath(ctx, ctx.currentFromTable, segments.join("."));
2496
+ if (!ctx.validator) return;
1758
2497
  const targetTable = ctx.validator.resolveRelationTarget(
1759
2498
  ctx.currentFromTable,
1760
2499
  segments[0]
@@ -1784,6 +2523,12 @@ function compileSelectClause(clause, ctx, fns) {
1784
2523
  } else if (item.type === "relationStar") {
1785
2524
  hasExpressions = true;
1786
2525
  const relation = item.relation.join(".");
2526
+ assertNoBindingRelationConstruct(
2527
+ ctx,
2528
+ ctx.currentFromTable,
2529
+ "select relation columns",
2530
+ relation
2531
+ );
1787
2532
  expressions.push({
1788
2533
  kind: "relationColumn",
1789
2534
  relation,
@@ -1929,14 +2674,14 @@ function compileSelectExpression(item, ctx, fns) {
1929
2674
  const partitionBy = windowExpr.partitionBy.length > 0 ? windowExpr.partitionBy.map((e) => {
1930
2675
  const f = expressionToValidatedField(e, ctx) ?? expressionToSql(e);
1931
2676
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1932
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
2677
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1933
2678
  }
1934
2679
  return f;
1935
2680
  }) : void 0;
1936
2681
  const orderBy = windowExpr.orderBy.length > 0 ? windowExpr.orderBy.map((o) => {
1937
2682
  const f = expressionToValidatedField(o.expression, ctx) ?? expressionToSql(o.expression);
1938
2683
  if (ctx.currentFromTable && !f.includes(".") && !f.includes("(")) {
1939
- ctx.validator?.validateColumn(ctx.currentFromTable, f);
2684
+ validateColumnForTable(ctx, ctx.currentFromTable, f);
1940
2685
  }
1941
2686
  return { field: f, direction: o.direction };
1942
2687
  }) : void 0;
@@ -2054,6 +2799,12 @@ function compileMultiSegmentPath(expr, item, ctx) {
2054
2799
  const segments = expr.segments;
2055
2800
  const firstSegmentLower = segments[0].toLowerCase();
2056
2801
  if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
2802
+ assertNoBindingRelationConstruct(
2803
+ ctx,
2804
+ ctx.currentFromTable,
2805
+ "use pseudo-column traversals",
2806
+ segments[0]
2807
+ );
2057
2808
  const firstSegment = firstSegmentLower;
2058
2809
  const depthHint = expr.depthHint;
2059
2810
  if (depthHint !== void 0) {
@@ -2081,7 +2832,7 @@ function compileMultiSegmentPath(expr, item, ctx) {
2081
2832
  }
2082
2833
  const targetColumn = segments[i];
2083
2834
  if (ctx.currentFromTable) {
2084
- ctx.validator?.validateColumn(ctx.currentFromTable, targetColumn);
2835
+ validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
2085
2836
  }
2086
2837
  const defaultAlias = segments.map((s) => s.toLowerCase()).join(".");
2087
2838
  if (traversals.length === 1) {
@@ -2103,6 +2854,12 @@ function compileMultiSegmentPath(expr, item, ctx) {
2103
2854
  }
2104
2855
  const column = segments[segments.length - 1];
2105
2856
  const relation = segments.slice(0, -1).join(".");
2857
+ assertNoBindingRelationConstruct(
2858
+ ctx,
2859
+ ctx.currentFromTable,
2860
+ "select relation columns",
2861
+ relation
2862
+ );
2106
2863
  if (ctx.currentFromTable && ctx.validator) {
2107
2864
  const targetTable = ctx.validator.resolveRelationTarget(
2108
2865
  ctx.currentFromTable,
@@ -2263,6 +3020,220 @@ function allowsInternalParams(options) {
2263
3020
  const internalOptions = options?.[NQL_INTERNAL_COMPILER_OPTIONS];
2264
3021
  return internalOptions?.allowInternalParams === true;
2265
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
+ }
2266
3237
  var NqlCompiler = class {
2267
3238
  ctx;
2268
3239
  fns;
@@ -2286,9 +3257,11 @@ var NqlCompiler = class {
2286
3257
  this.ctx = {
2287
3258
  currentFromTable: void 0,
2288
3259
  currentRelationTarget: void 0,
3260
+ currentHavingAliases: void 0,
2289
3261
  pseudoColumnKeywords,
2290
3262
  recursiveKeywords,
2291
3263
  validator,
3264
+ bindingOutputColumns: /* @__PURE__ */ new Map(),
2292
3265
  params,
2293
3266
  maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
2294
3267
  allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
@@ -2304,6 +3277,14 @@ var NqlCompiler = class {
2304
3277
  * Compile an NQL program to IntentAST.
2305
3278
  */
2306
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) {
2307
3288
  if (program.statements.length === 0) {
2308
3289
  return {};
2309
3290
  }
@@ -2320,30 +3301,121 @@ var NqlCompiler = class {
2320
3301
  }
2321
3302
  }
2322
3303
  const bindings = /* @__PURE__ */ new Map();
3304
+ const bindingOutputSchemas = /* @__PURE__ */ new Map();
2323
3305
  const mutationBindings = /* @__PURE__ */ new Map();
2324
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;
2325
3313
  let lastResult = {};
2326
3314
  for (let i = 0; i < program.statements.length; i++) {
2327
3315
  const stmt = program.statements[i];
2328
- lastResult = this.compileSingleStatement(stmt, bindings);
2329
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
+ }
2330
3362
  if (bindName) {
2331
3363
  if (lastResult.query) {
3364
+ const outputSchema = this.registerQueryBindingOutputSchema(
3365
+ bindName,
3366
+ lastResult.query,
3367
+ bindingOutputSchemas
3368
+ );
2332
3369
  bindings.set(bindName, lastResult.query);
3370
+ if (outputSchema) {
3371
+ this.ctx.validator?.addVirtualBindingTable(
3372
+ bindName,
3373
+ outputSchema.columns
3374
+ );
3375
+ }
2333
3376
  materializedBindStatements.add(i);
3377
+ readBindingDefinitions.set(bindName, i);
3378
+ definedBindingNames.add(bindName);
3379
+ bindingDependencies.set(bindName, statementBindingDependencies);
2334
3380
  } else if (lastResult.mutation?.returning?.length) {
2335
- 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);
2336
3390
  bindings.set(bindName, {
2337
3391
  type: "select",
2338
- from: lastResult.mutation.table,
3392
+ from: canonicalBinding.mutation.table,
2339
3393
  select: {
2340
3394
  type: "fields",
2341
- fields: [...lastResult.mutation.returning]
3395
+ fields: [...bindingFields]
2342
3396
  }
2343
3397
  });
3398
+ if (outputSchema) {
3399
+ bindingOutputSchemas.set(bindName, outputSchema);
3400
+ this.ctx.validator?.addVirtualBindingTable(
3401
+ bindName,
3402
+ outputSchema.columns
3403
+ );
3404
+ }
2344
3405
  materializedBindStatements.add(i);
3406
+ definedBindingNames.add(bindName);
3407
+ bindingDependencies.set(bindName, []);
2345
3408
  }
2346
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
+ }
2347
3419
  }
2348
3420
  for (let i = 0; i < program.statements.length - 1; i++) {
2349
3421
  const bindName = extractBindName(program.statements[i]);
@@ -2354,15 +3426,68 @@ var NqlCompiler = class {
2354
3426
  }
2355
3427
  }
2356
3428
  const hasMutationBindings = mutationBindings.size > 0;
3429
+ const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
3430
+ const hasNqlProgramSequence = nqlProgramSequence.length === program.statements.length;
2357
3431
  if (bindings.size > 0) {
2358
3432
  return {
2359
3433
  ...lastResult,
2360
3434
  bindings,
2361
- ...hasMutationBindings && { mutationBindings }
3435
+ ...hasBindingOutputSchemas && { bindingOutputSchemas },
3436
+ ...hasMutationBindings && { mutationBindings },
3437
+ ...hasNqlProgramSequence && { nqlProgramSequence }
2362
3438
  };
2363
3439
  }
2364
3440
  return lastResult;
2365
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
+ }
2366
3491
  compileSingleStatement(stmt, bindings) {
2367
3492
  if (stmt.type === "withQuery") {
2368
3493
  return compileWithQuery(stmt, this.ctx, this.fns);
@@ -5463,6 +6588,7 @@ function hasColumnValidatorShape(obj) {
5463
6588
  }
5464
6589
  /* v8 ignore next — '*' columns are validated at call-site before reaching here -- @preserve */
5465
6590
  /* v8 ignore next — graceful degradation: unknown tables skip validation -- @preserve */
6591
+ /* v8 ignore next — defensive: bound queries from NQL always have select.fields -- @preserve */
5466
6592
  /* v8 ignore next — defensive: callers always pass valid relation paths -- @preserve */
5467
6593
  /* v8 ignore next — not yet reachable: flat + pre-existing includes requires WITH clause -- @preserve */
5468
6594
  /* v8 ignore stop -- @preserve */
@@ -5478,7 +6604,6 @@ function hasColumnValidatorShape(obj) {
5478
6604
  /* v8 ignore start — defensive: parser guarantees IS NULL LHS is a path -- @preserve */
5479
6605
  /* v8 ignore start — defensive: parser guarantees at least 2 args -- @preserve */
5480
6606
  /* v8 ignore next — defensive: only json_* functions reach WHERE context -- @preserve */
5481
- /* v8 ignore next — defensive: bound queries from NQL always have select.fields -- @preserve */
5482
6607
  /* v8 ignore start — defensive: star items are handled in compileSelectClause before reaching here -- @preserve */
5483
6608
  /* v8 ignore next — defensive: relationStar items are handled in compileSelectClause -- @preserve */
5484
6609
  /* v8 ignore start — defensive: all parser-produced SELECT expression types are handled above -- @preserve */