@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/README.md +36 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2235 -1110
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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: "
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
...
|
|
665
|
-
|
|
666
|
-
...
|
|
667
|
-
...
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
...limit !== void 0 && {
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
707
|
+
return {
|
|
708
|
+
type: "update",
|
|
709
|
+
table: update.table,
|
|
710
|
+
set,
|
|
711
|
+
allowAll: true
|
|
708
712
|
};
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
left,
|
|
736
|
-
right
|
|
733
|
+
type: "delete",
|
|
734
|
+
table: del.table,
|
|
735
|
+
allowAll: true
|
|
737
736
|
};
|
|
738
737
|
}
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
function compileOrderByClause(clause, ctx) {
|
|
752
|
-
return clause.items.map((item) => compileOrderItem(item, ctx));
|
|
764
|
+
};
|
|
753
765
|
}
|
|
754
|
-
function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
763
|
-
|
|
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
|
-
|
|
771
|
-
return {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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/
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
859
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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
|
|
904
|
-
|
|
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
|
|
907
|
-
|
|
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
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
kind
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
`
|
|
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
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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 (
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1538
|
+
`Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
|
|
1015
1539
|
);
|
|
1016
1540
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1653
|
+
ctx.currentFromTable,
|
|
1654
|
+
"use relation filters",
|
|
1655
|
+
expr.relation.join(".")
|
|
1031
1656
|
);
|
|
1032
|
-
|
|
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
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
throw new
|
|
1074
|
-
|
|
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
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
1096
|
-
kind: "
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1129
|
-
kind: "any",
|
|
1130
|
-
field: field2,
|
|
1131
|
-
values: valuesParam
|
|
1132
|
-
};
|
|
1133
|
-
return result2;
|
|
1728
|
+
return { field, direction: item.direction };
|
|
1134
1729
|
}
|
|
1135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
)
|
|
1152
|
-
|
|
1153
|
-
|
|
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.
|
|
1156
|
-
|
|
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
|
-
|
|
1765
|
+
ctes.push({
|
|
1766
|
+
kind: "simpleCte",
|
|
1767
|
+
name: cte.name,
|
|
1768
|
+
query: bodyResult
|
|
1769
|
+
});
|
|
1160
1770
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1797
|
+
var InvalidDateRangeError = class extends Error {
|
|
1798
|
+
constructor(message) {
|
|
1799
|
+
super(message);
|
|
1800
|
+
this.name = "InvalidDateRangeError";
|
|
1228
1801
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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 (
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
key
|
|
1841
|
+
start: `${year}-12-01`,
|
|
1842
|
+
end: `${year + 1}-01-01`
|
|
1301
1843
|
};
|
|
1302
1844
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
key
|
|
1863
|
+
start: formatDate(start),
|
|
1864
|
+
end: formatDate(end)
|
|
1322
1865
|
};
|
|
1323
1866
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
|
1339
|
-
|
|
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
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
-
|
|
1930
|
+
const unary = expr;
|
|
1931
|
+
if (unary.operator === "not") {
|
|
1443
1932
|
return {
|
|
1444
|
-
|
|
1933
|
+
kind: "not",
|
|
1934
|
+
condition: compileExpression(
|
|
1935
|
+
unary.operand,
|
|
1936
|
+
ctx,
|
|
1937
|
+
fns,
|
|
1938
|
+
aliasContext,
|
|
1939
|
+
outerAliases
|
|
1940
|
+
)
|
|
1445
1941
|
};
|
|
1446
1942
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1496
|
-
ctx.
|
|
1497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2090
|
+
const result = {
|
|
2091
|
+
kind: "range",
|
|
2092
|
+
field,
|
|
2093
|
+
operator: rangeExpr.operator,
|
|
2094
|
+
value: rangeValue
|
|
1546
2095
|
};
|
|
2096
|
+
return result;
|
|
1547
2097
|
}
|
|
1548
|
-
function
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
2176
|
+
const result = {
|
|
2177
|
+
kind: "in",
|
|
2178
|
+
field,
|
|
2179
|
+
values
|
|
2180
|
+
};
|
|
2181
|
+
if (inExpr.negated) {
|
|
2182
|
+
return { kind: "not", condition: result };
|
|
1581
2183
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
action: { type: "doUpdate", set: values }
|
|
2214
|
+
kind: "range",
|
|
2215
|
+
field,
|
|
2216
|
+
operator: "between",
|
|
2217
|
+
value
|
|
1593
2218
|
};
|
|
1594
2219
|
}
|
|
1595
|
-
function
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
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
|
|
2239
|
+
validateWhereField(ctx, field, aliasContext, isNull.expression);
|
|
1605
2240
|
return {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
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
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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
|
-
|
|
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
|
|
2472
|
+
if (!ctx.currentFromTable) return;
|
|
1742
2473
|
const { segments } = expr;
|
|
1743
2474
|
if (segments.length === 1) {
|
|
1744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3392
|
+
from: canonicalBinding.mutation.table,
|
|
2339
3393
|
select: {
|
|
2340
3394
|
type: "fields",
|
|
2341
|
-
fields: [...
|
|
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
|
-
...
|
|
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 */
|