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