@dbsp/nql 1.4.0 → 1.6.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 +16 -4
- package/dist/index.js +861 -85
- 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, 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';
|
|
1
|
+
import { NQL_INTERNAL_COMPILER_OPTIONS, explainUnsupportedNqlBindingIncludeHop, 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, toColumnList, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction, isParamIntent } from '@dbsp/types';
|
|
3
3
|
import { createToken, Lexer, CstParser } from 'chevrotain';
|
|
4
4
|
|
|
5
5
|
// src/compiler/index.ts
|
|
@@ -43,8 +43,6 @@ var NqlSemanticException = class extends Error {
|
|
|
43
43
|
this.relatedSymbol = relatedSymbol;
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
// src/compiler/column-validator.ts
|
|
48
46
|
var ColumnValidator = class _ColumnValidator {
|
|
49
47
|
constructor(schema) {
|
|
50
48
|
this.schema = schema;
|
|
@@ -52,6 +50,7 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
52
50
|
schema;
|
|
53
51
|
knownCteTables = /* @__PURE__ */ new Set();
|
|
54
52
|
virtualBindingTables = /* @__PURE__ */ new Map();
|
|
53
|
+
virtualBindingRelationFilters = /* @__PURE__ */ new Map();
|
|
55
54
|
/**
|
|
56
55
|
* Register CTE names so validateTable() skips validation for them.
|
|
57
56
|
* CTE names are not physical tables in the schema, but are valid FROM targets.
|
|
@@ -69,12 +68,16 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
69
68
|
* Register an NQL binding as a virtual table with a concrete output schema.
|
|
70
69
|
* Binding columns are logical output names, so matching is exact.
|
|
71
70
|
*/
|
|
72
|
-
addVirtualBindingTable(name, columns) {
|
|
71
|
+
addVirtualBindingTable(name, columns, relationFilters) {
|
|
73
72
|
this.virtualBindingTables.set(name, columns);
|
|
73
|
+
if (relationFilters) {
|
|
74
|
+
this.virtualBindingRelationFilters.set(name, relationFilters);
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
77
|
/** Clear all registered virtual binding tables (used between compilations). */
|
|
76
78
|
clearVirtualBindingTables() {
|
|
77
79
|
this.virtualBindingTables.clear();
|
|
80
|
+
this.virtualBindingRelationFilters.clear();
|
|
78
81
|
}
|
|
79
82
|
isVirtualBindingTable(name) {
|
|
80
83
|
return name !== void 0 && this.virtualBindingTables.has(name);
|
|
@@ -87,6 +90,173 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
87
90
|
if (virtualColumns) return virtualColumns;
|
|
88
91
|
return this.schema.getTable(name)?.columns.map((column) => column.name);
|
|
89
92
|
}
|
|
93
|
+
hasPhysicalTable(name) {
|
|
94
|
+
return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
|
|
95
|
+
}
|
|
96
|
+
hasQualifiedRelationLookup() {
|
|
97
|
+
return typeof this.schema.getRelation === "function";
|
|
98
|
+
}
|
|
99
|
+
getRelation(sourceTable, relationName) {
|
|
100
|
+
return this.schema.getRelation?.(`${sourceTable}.${relationName}`) ?? this.schema.getRelationsFrom(sourceTable).find((relation) => relation.name === relationName);
|
|
101
|
+
}
|
|
102
|
+
getRelationsFrom(sourceTable) {
|
|
103
|
+
return this.schema.getRelationsFrom(sourceTable);
|
|
104
|
+
}
|
|
105
|
+
getVirtualBindingRelation(bindingName, relationName) {
|
|
106
|
+
return this.virtualBindingRelationFilters.get(bindingName)?.relations.find((relation) => relation.relation === relationName);
|
|
107
|
+
}
|
|
108
|
+
getVirtualBindingScalarRelation(bindingName, relationName) {
|
|
109
|
+
return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
|
|
110
|
+
}
|
|
111
|
+
static virtualRelationIncludeShape(relation) {
|
|
112
|
+
const relationType = relation.relationType;
|
|
113
|
+
const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relation.targetColumn;
|
|
114
|
+
return {
|
|
115
|
+
type: relationType,
|
|
116
|
+
foreignKey,
|
|
117
|
+
source: relation.sourceTable,
|
|
118
|
+
target: relation.targetTable
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
resolveVirtualBindingScalarRelationForInclude(bindingName, relationPath) {
|
|
122
|
+
const relationName = relationPath.join(".");
|
|
123
|
+
if (relationPath.length < 1) {
|
|
124
|
+
throw new NqlSemanticException(
|
|
125
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
126
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const relation = relationPath[0];
|
|
130
|
+
if (!relation) {
|
|
131
|
+
throw new NqlSemanticException(
|
|
132
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
133
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): the relation path must name a source-table relation.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const virtualRelation = this.getVirtualBindingScalarRelation(
|
|
137
|
+
bindingName,
|
|
138
|
+
relation
|
|
139
|
+
);
|
|
140
|
+
if (virtualRelation) {
|
|
141
|
+
const resolvedFirstHop = this.getRelation(virtualRelation.sourceTable, relation) ?? _ColumnValidator.virtualRelationIncludeShape(virtualRelation);
|
|
142
|
+
const unsupportedFirstHopReason = explainUnsupportedNqlBindingIncludeHop(
|
|
143
|
+
relation,
|
|
144
|
+
resolvedFirstHop,
|
|
145
|
+
{ relation }
|
|
146
|
+
);
|
|
147
|
+
if (unsupportedFirstHopReason) {
|
|
148
|
+
throw new NqlSemanticException(
|
|
149
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
150
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): ${unsupportedFirstHopReason}.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
let sourceTable = virtualRelation.targetTable;
|
|
154
|
+
for (let i = 1; i < relationPath.length; i++) {
|
|
155
|
+
const tailRelation = relationPath[i];
|
|
156
|
+
if (!tailRelation) {
|
|
157
|
+
throw new NqlSemanticException(
|
|
158
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
159
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): relation path segment ${i + 1} is empty.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const resolvedTail = this.getRelation(sourceTable, tailRelation);
|
|
163
|
+
if (!resolvedTail) {
|
|
164
|
+
throw new NqlSemanticException(
|
|
165
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
166
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail relation '${tailRelation}' is not declared on table '${sourceTable}'.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
|
|
170
|
+
tailRelation,
|
|
171
|
+
resolvedTail,
|
|
172
|
+
{ relation: tailRelation }
|
|
173
|
+
);
|
|
174
|
+
if (unsupportedReason) {
|
|
175
|
+
throw new NqlSemanticException(
|
|
176
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
177
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relationName}' (ref-#192): tail ${unsupportedReason}.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
sourceTable = resolvedTail.target;
|
|
181
|
+
}
|
|
182
|
+
return virtualRelation;
|
|
183
|
+
}
|
|
184
|
+
const reason = this.explainVirtualBindingScalarRelationRejection(
|
|
185
|
+
bindingName,
|
|
186
|
+
relation
|
|
187
|
+
);
|
|
188
|
+
throw new NqlSemanticException(
|
|
189
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
190
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation include '${relation}' (ref-#192): ${reason}.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
explainVirtualBindingRelationRejection(bindingName, relationName) {
|
|
194
|
+
const metadata = this.virtualBindingRelationFilters.get(bindingName);
|
|
195
|
+
if (!metadata) {
|
|
196
|
+
return "no binding relation-filter metadata is available";
|
|
197
|
+
}
|
|
198
|
+
if (metadata.unsafeReason) return metadata.unsafeReason;
|
|
199
|
+
const sourceTable = metadata.sourceTable;
|
|
200
|
+
if (!sourceTable) {
|
|
201
|
+
return "the binding source table could not be proven";
|
|
202
|
+
}
|
|
203
|
+
const relation = this.getRelation(sourceTable, relationName);
|
|
204
|
+
if (!relation) {
|
|
205
|
+
return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
|
|
206
|
+
}
|
|
207
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
208
|
+
if (relation.type !== "belongsTo") {
|
|
209
|
+
return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
|
|
210
|
+
}
|
|
211
|
+
if (fkColumns.length === 0) {
|
|
212
|
+
return `relation '${relationName}' must have at least one FK column for A-lite binding relation filters`;
|
|
213
|
+
}
|
|
214
|
+
for (const fkColumn of fkColumns) {
|
|
215
|
+
const directProjection = metadata.directProjectionLineage?.find(
|
|
216
|
+
(projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, fkColumn)
|
|
217
|
+
);
|
|
218
|
+
if (!directProjection) {
|
|
219
|
+
const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
220
|
+
return `relation '${relationName}' FK column '${fkColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
224
|
+
return `relation '${relationName}' FK columns '${fkColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
|
|
225
|
+
}
|
|
226
|
+
explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
|
|
227
|
+
const metadata = this.virtualBindingRelationFilters.get(bindingName);
|
|
228
|
+
if (!metadata) {
|
|
229
|
+
return "no binding relation metadata is available";
|
|
230
|
+
}
|
|
231
|
+
if (metadata.unsafeReason) return metadata.unsafeReason;
|
|
232
|
+
const sourceTable = metadata.sourceTable;
|
|
233
|
+
if (!sourceTable) {
|
|
234
|
+
return "the binding source table could not be proven";
|
|
235
|
+
}
|
|
236
|
+
const relation = this.getRelation(sourceTable, relationName);
|
|
237
|
+
if (!relation) {
|
|
238
|
+
return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
|
|
239
|
+
}
|
|
240
|
+
const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
|
|
241
|
+
relationName,
|
|
242
|
+
relation
|
|
243
|
+
);
|
|
244
|
+
if (unsupportedReason) return unsupportedReason;
|
|
245
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
246
|
+
const sourceColumns = relation.type === "belongsTo" ? fkColumns : toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
|
|
247
|
+
for (const sourceColumn of sourceColumns) {
|
|
248
|
+
const directProjection = metadata.directProjectionLineage?.find(
|
|
249
|
+
(projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
|
|
250
|
+
);
|
|
251
|
+
if (!directProjection) {
|
|
252
|
+
const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
253
|
+
const sourceColumnLabel = relation.type === "belongsTo" ? "FK column" : "source key column";
|
|
254
|
+
return `relation '${relationName}' ${sourceColumnLabel} '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
258
|
+
return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
|
|
259
|
+
}
|
|
90
260
|
/**
|
|
91
261
|
* Convert camelCase to snake_case for column name matching.
|
|
92
262
|
* NQL queries may use either form (e.g., viewCount or view_count).
|
|
@@ -156,13 +326,17 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
156
326
|
}
|
|
157
327
|
resolveRelationTarget(sourceTable, relationName) {
|
|
158
328
|
if (this.virtualBindingTables.has(sourceTable)) {
|
|
329
|
+
const virtualRelation = this.getVirtualBindingRelation(
|
|
330
|
+
sourceTable,
|
|
331
|
+
relationName
|
|
332
|
+
);
|
|
333
|
+
if (virtualRelation) return virtualRelation.targetTable;
|
|
159
334
|
throw new NqlSemanticException(
|
|
160
335
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
161
|
-
`Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName})
|
|
336
|
+
`Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${this.explainVirtualBindingRelationRejection(sourceTable, relationName)}.`
|
|
162
337
|
);
|
|
163
338
|
}
|
|
164
|
-
const
|
|
165
|
-
const rel = relations.find((r) => r.name === relationName);
|
|
339
|
+
const rel = this.getRelation(sourceTable, relationName);
|
|
166
340
|
return rel?.target;
|
|
167
341
|
}
|
|
168
342
|
assertNoBindingRelationConstruct(bindingName, construct, detail) {
|
|
@@ -180,6 +354,28 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
180
354
|
);
|
|
181
355
|
}
|
|
182
356
|
};
|
|
357
|
+
var DEFAULT_RELATION_TARGET_COLUMN = "id";
|
|
358
|
+
function relationForeignKeys(relation) {
|
|
359
|
+
if (!relation) return void 0;
|
|
360
|
+
const columns = toColumnList(relation.foreignKey);
|
|
361
|
+
return columns.length > 0 ? columns : void 0;
|
|
362
|
+
}
|
|
363
|
+
function relationCardinality(relation) {
|
|
364
|
+
return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
|
|
365
|
+
}
|
|
366
|
+
function scalarRelationJoinColumns(relation) {
|
|
367
|
+
if (!relation) return void 0;
|
|
368
|
+
if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
|
|
369
|
+
return void 0;
|
|
370
|
+
}
|
|
371
|
+
const fkColumns = relationForeignKeys(relation);
|
|
372
|
+
if (!fkColumns) return void 0;
|
|
373
|
+
const sourceKeys = toColumnList(relation.sourceKey);
|
|
374
|
+
const targetKeys = toColumnList(relation.targetKey);
|
|
375
|
+
const sourceJoinColumn = relation.type === "belongsTo" ? fkColumns : sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN];
|
|
376
|
+
const targetJoinColumn = relation.type === "belongsTo" ? targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN] : fkColumns;
|
|
377
|
+
return { sourceJoinColumn, targetJoinColumn };
|
|
378
|
+
}
|
|
183
379
|
var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
|
|
184
380
|
"__proto__",
|
|
185
381
|
"constructor",
|
|
@@ -288,6 +484,174 @@ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
|
|
|
288
484
|
`Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
|
|
289
485
|
);
|
|
290
486
|
}
|
|
487
|
+
function throwBindingRelationFilter182(bindingName, relationName, reason) {
|
|
488
|
+
throw new NqlSemanticException(
|
|
489
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
490
|
+
`Query '${bindingName}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${reason}.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function throwBindingRelationColumn182(bindingName, relationName, reason) {
|
|
494
|
+
throw new NqlSemanticException(
|
|
495
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
496
|
+
`Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function bindingRelationColumnUnsupportedReason(relationName, relation) {
|
|
500
|
+
const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
|
|
501
|
+
relationName,
|
|
502
|
+
relation
|
|
503
|
+
);
|
|
504
|
+
if (unsupportedReason) return unsupportedReason;
|
|
505
|
+
if (relationCardinality(relation) !== "one") {
|
|
506
|
+
return `relation '${relationName}' is '${relation.type ?? "unknown"}'; scalar binding relation columns require every hop to be to-one (belongsTo/hasOne) (ref-#192)`;
|
|
507
|
+
}
|
|
508
|
+
return void 0;
|
|
509
|
+
}
|
|
510
|
+
function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
|
|
511
|
+
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
512
|
+
const actualBindingName = bindingName;
|
|
513
|
+
const relationName = relationPath.join(".");
|
|
514
|
+
if (relationPath.length !== 1) {
|
|
515
|
+
throwBindingRelationFilter182(
|
|
516
|
+
actualBindingName,
|
|
517
|
+
relationName,
|
|
518
|
+
"multi-hop binding relation filters are not supported; the relation path must be a single source-table relation"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const relation = relationPath[0];
|
|
522
|
+
if (!relation) {
|
|
523
|
+
throwBindingRelationFilter182(
|
|
524
|
+
actualBindingName,
|
|
525
|
+
relationName,
|
|
526
|
+
"the relation path must name a source-table relation"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const virtualRelation = ctx.validator?.getVirtualBindingRelation(
|
|
530
|
+
actualBindingName,
|
|
531
|
+
relation
|
|
532
|
+
);
|
|
533
|
+
if (virtualRelation) return virtualRelation;
|
|
534
|
+
const reason = ctx.validator?.explainVirtualBindingRelationRejection(
|
|
535
|
+
actualBindingName,
|
|
536
|
+
relation
|
|
537
|
+
) ?? "model metadata is not available";
|
|
538
|
+
throwBindingRelationFilter182(actualBindingName, relation, reason);
|
|
539
|
+
}
|
|
540
|
+
function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedColumn) {
|
|
541
|
+
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
542
|
+
const actualBindingName = bindingName;
|
|
543
|
+
const relationName = relationPath.join(".");
|
|
544
|
+
const relation = relationPath[0];
|
|
545
|
+
if (!relation) {
|
|
546
|
+
throwBindingRelationColumn182(
|
|
547
|
+
actualBindingName,
|
|
548
|
+
relationName,
|
|
549
|
+
"the relation path must name a source-table relation"
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (!ctx.validator) {
|
|
553
|
+
throwBindingRelationColumn182(
|
|
554
|
+
actualBindingName,
|
|
555
|
+
relationName,
|
|
556
|
+
"model metadata is not available"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
|
|
560
|
+
actualBindingName,
|
|
561
|
+
relation
|
|
562
|
+
);
|
|
563
|
+
if (!virtualRelation) {
|
|
564
|
+
const reason = ctx.validator?.explainVirtualBindingScalarRelationRejection(
|
|
565
|
+
actualBindingName,
|
|
566
|
+
relation
|
|
567
|
+
) ?? "model metadata is not available";
|
|
568
|
+
throwBindingRelationColumn182(actualBindingName, relationName, reason);
|
|
569
|
+
}
|
|
570
|
+
if (relationPath.length === 1) {
|
|
571
|
+
ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
|
|
572
|
+
return virtualRelation;
|
|
573
|
+
}
|
|
574
|
+
const firstHop = ctx.validator.getRelation(
|
|
575
|
+
virtualRelation.sourceTable,
|
|
576
|
+
relation
|
|
577
|
+
);
|
|
578
|
+
if (!firstHop) {
|
|
579
|
+
throwBindingRelationColumn182(
|
|
580
|
+
actualBindingName,
|
|
581
|
+
relationName,
|
|
582
|
+
`relation '${relation}' is not declared on source table '${virtualRelation.sourceTable}'`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const firstHopReason = bindingRelationColumnUnsupportedReason(
|
|
586
|
+
relation,
|
|
587
|
+
firstHop
|
|
588
|
+
);
|
|
589
|
+
if (firstHopReason) {
|
|
590
|
+
throwBindingRelationColumn182(
|
|
591
|
+
actualBindingName,
|
|
592
|
+
relationName,
|
|
593
|
+
firstHopReason
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
let sourceTable = virtualRelation.targetTable;
|
|
597
|
+
const hops = [];
|
|
598
|
+
for (let i = 1; i < relationPath.length; i++) {
|
|
599
|
+
const tailRelation = relationPath[i];
|
|
600
|
+
if (!tailRelation) {
|
|
601
|
+
throwBindingRelationColumn182(
|
|
602
|
+
actualBindingName,
|
|
603
|
+
relationName,
|
|
604
|
+
`relation path segment ${i + 1} is empty (ref-#192)`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const resolvedTail = ctx.validator.getRelation(sourceTable, tailRelation);
|
|
608
|
+
if (!resolvedTail) {
|
|
609
|
+
throwBindingRelationColumn182(
|
|
610
|
+
actualBindingName,
|
|
611
|
+
relationName,
|
|
612
|
+
`tail relation '${tailRelation}' is not declared on table '${sourceTable}' (ref-#192)`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const tailReason = bindingRelationColumnUnsupportedReason(
|
|
616
|
+
tailRelation,
|
|
617
|
+
resolvedTail
|
|
618
|
+
);
|
|
619
|
+
if (tailReason) {
|
|
620
|
+
throwBindingRelationColumn182(
|
|
621
|
+
actualBindingName,
|
|
622
|
+
relationName,
|
|
623
|
+
`tail ${tailReason}`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const joinColumns = scalarRelationJoinColumns(resolvedTail);
|
|
627
|
+
if (!joinColumns) {
|
|
628
|
+
throwBindingRelationColumn182(
|
|
629
|
+
actualBindingName,
|
|
630
|
+
relationName,
|
|
631
|
+
`tail relation '${tailRelation}' cannot be resolved to a scalar join (ref-#192)`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
|
|
635
|
+
throwBindingRelationColumn182(
|
|
636
|
+
actualBindingName,
|
|
637
|
+
relationName,
|
|
638
|
+
`tail relation '${tailRelation}' has mismatched join column counts (${joinColumns.sourceJoinColumn.length} source, ${joinColumns.targetJoinColumn.length} target) (ref-#179)`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
hops.push({
|
|
642
|
+
target: resolvedTail.target,
|
|
643
|
+
fkColumn: joinColumns.sourceJoinColumn,
|
|
644
|
+
joinColumn: joinColumns.targetJoinColumn
|
|
645
|
+
});
|
|
646
|
+
sourceTable = resolvedTail.target;
|
|
647
|
+
}
|
|
648
|
+
ctx.validator.validateColumn(sourceTable, selectedColumn);
|
|
649
|
+
return {
|
|
650
|
+
...virtualRelation,
|
|
651
|
+
relation: relationName,
|
|
652
|
+
hops
|
|
653
|
+
};
|
|
654
|
+
}
|
|
291
655
|
function assertNoBindingRelationPath(ctx, bindingName, path) {
|
|
292
656
|
if (!isBindingTable(ctx, bindingName)) return;
|
|
293
657
|
throw new NqlSemanticException(
|
|
@@ -518,6 +882,10 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
|
|
|
518
882
|
`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
883
|
);
|
|
520
884
|
}
|
|
885
|
+
if (!aliasContext && ctx.currentRelationTarget && !field.includes(".")) {
|
|
886
|
+
ctx.validator?.validateColumn(ctx.currentRelationTarget, field);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
521
889
|
if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
|
|
522
890
|
if (field.includes(".")) {
|
|
523
891
|
assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
|
|
@@ -540,10 +908,6 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
|
|
|
540
908
|
}
|
|
541
909
|
return;
|
|
542
910
|
}
|
|
543
|
-
if (ctx.currentRelationTarget && !field.includes(".")) {
|
|
544
|
-
ctx.validator.validateColumn(ctx.currentRelationTarget, field);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
911
|
if (ctx.currentFromTable && field.includes(".")) {
|
|
548
912
|
if (isBindingTable(ctx, ctx.currentFromTable)) {
|
|
549
913
|
assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
|
|
@@ -975,6 +1339,180 @@ function throwUnsupportedBindingFinal(bindingName, feature) {
|
|
|
975
1339
|
`Query '${bindingName}' reads from an NQL binding and cannot use ${feature} in a binding-final query (#183). Binding-final queries are restricted to portable common-ground SQL over projected binding columns: SELECT (including plain DISTINCT), WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, and UNION/INTERSECT/EXCEPT.`
|
|
976
1340
|
);
|
|
977
1341
|
}
|
|
1342
|
+
function toSnakeCase(name) {
|
|
1343
|
+
return name.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
1344
|
+
}
|
|
1345
|
+
function columnsMatch(left, right) {
|
|
1346
|
+
return left === right || toSnakeCase(left) === toSnakeCase(right);
|
|
1347
|
+
}
|
|
1348
|
+
function expressionIntentContainsAggregate(value) {
|
|
1349
|
+
if (!value || typeof value !== "object") return false;
|
|
1350
|
+
const record = value;
|
|
1351
|
+
if (record.kind === "aggregate") return true;
|
|
1352
|
+
for (const child of Object.values(record)) {
|
|
1353
|
+
if (Array.isArray(child)) {
|
|
1354
|
+
if (child.some((item) => expressionIntentContainsAggregate(item))) {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
} else if (expressionIntentContainsAggregate(child)) {
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
function selectContainsAggregate(select) {
|
|
1364
|
+
if (!select) return false;
|
|
1365
|
+
if (select.type === "aggregate") return true;
|
|
1366
|
+
if (select.type !== "expressions") return false;
|
|
1367
|
+
return select.columns.some(
|
|
1368
|
+
(column) => expressionIntentContainsAggregate(column)
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
function relationForeignKeysOnSource(relation) {
|
|
1372
|
+
if (relation?.type !== "belongsTo") return void 0;
|
|
1373
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
1374
|
+
return fkColumns.length > 0 ? fkColumns : void 0;
|
|
1375
|
+
}
|
|
1376
|
+
function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
|
|
1377
|
+
if (!ctx.validator) return "model metadata is not available";
|
|
1378
|
+
if (!ctx.validator.hasQualifiedRelationLookup()) {
|
|
1379
|
+
return "model.getRelation metadata is not available";
|
|
1380
|
+
}
|
|
1381
|
+
if (bindingDependencies.length > 0 || isBindingTable(ctx, intent.from)) {
|
|
1382
|
+
return "the binding body reads from another NQL binding";
|
|
1383
|
+
}
|
|
1384
|
+
if (!ctx.validator.hasPhysicalTable(intent.from)) {
|
|
1385
|
+
return `the binding source '${intent.from}' is not a single real model table`;
|
|
1386
|
+
}
|
|
1387
|
+
if (intent.batchValuesSource) {
|
|
1388
|
+
return "the binding body reads from a batch-values source";
|
|
1389
|
+
}
|
|
1390
|
+
if ((intent.joins?.length ?? 0) > 0) {
|
|
1391
|
+
return "the binding body uses joins";
|
|
1392
|
+
}
|
|
1393
|
+
if ((intent.include?.length ?? 0) > 0) {
|
|
1394
|
+
return "the binding body uses relation includes";
|
|
1395
|
+
}
|
|
1396
|
+
if ((intent.groupBy?.length ?? 0) > 0 || intent.having) {
|
|
1397
|
+
return "the binding body uses GROUP BY or HAVING";
|
|
1398
|
+
}
|
|
1399
|
+
if (selectContainsAggregate(intent.select)) {
|
|
1400
|
+
return "the binding body uses aggregate projections";
|
|
1401
|
+
}
|
|
1402
|
+
return void 0;
|
|
1403
|
+
}
|
|
1404
|
+
function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionLineage) {
|
|
1405
|
+
return directProjectionLineage.find(
|
|
1406
|
+
(projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
function findDirectSourceProjections(sourceTable, sourceColumns, directProjectionLineage) {
|
|
1410
|
+
const projections = [];
|
|
1411
|
+
for (const sourceColumn of sourceColumns) {
|
|
1412
|
+
const projection = findDirectSourceProjection(
|
|
1413
|
+
sourceTable,
|
|
1414
|
+
sourceColumn,
|
|
1415
|
+
directProjectionLineage
|
|
1416
|
+
);
|
|
1417
|
+
if (!projection) return void 0;
|
|
1418
|
+
projections.push(projection);
|
|
1419
|
+
}
|
|
1420
|
+
return projections;
|
|
1421
|
+
}
|
|
1422
|
+
function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1423
|
+
const fkColumns = relationForeignKeysOnSource(relation);
|
|
1424
|
+
if (!relation || !fkColumns) return void 0;
|
|
1425
|
+
const sourceProjections = findDirectSourceProjections(
|
|
1426
|
+
sourceTable,
|
|
1427
|
+
fkColumns,
|
|
1428
|
+
directProjectionLineage
|
|
1429
|
+
);
|
|
1430
|
+
if (!sourceProjections) return void 0;
|
|
1431
|
+
const targetKey = toColumnList(relation.targetKey);
|
|
1432
|
+
const targetColumns = targetKey.length > 0 ? targetKey : [DEFAULT_RELATION_TARGET_COLUMN];
|
|
1433
|
+
if (targetColumns.length !== fkColumns.length) return void 0;
|
|
1434
|
+
return {
|
|
1435
|
+
relation: relation.name,
|
|
1436
|
+
sourceTable,
|
|
1437
|
+
targetTable: relation.target,
|
|
1438
|
+
sourceColumn: sourceProjections.map(
|
|
1439
|
+
(projection) => projection.outputColumn
|
|
1440
|
+
),
|
|
1441
|
+
targetColumn: targetColumns,
|
|
1442
|
+
hops: [],
|
|
1443
|
+
cardinality: "one"
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1447
|
+
if (!relation) return void 0;
|
|
1448
|
+
if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
|
|
1449
|
+
return void 0;
|
|
1450
|
+
}
|
|
1451
|
+
const joinColumns = scalarRelationJoinColumns(relation);
|
|
1452
|
+
if (!joinColumns) return void 0;
|
|
1453
|
+
if (joinColumns.sourceJoinColumn.length !== joinColumns.targetJoinColumn.length) {
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
const sourceProjections = findDirectSourceProjections(
|
|
1457
|
+
sourceTable,
|
|
1458
|
+
joinColumns.sourceJoinColumn,
|
|
1459
|
+
directProjectionLineage
|
|
1460
|
+
);
|
|
1461
|
+
if (!sourceProjections) return void 0;
|
|
1462
|
+
return {
|
|
1463
|
+
relation: relation.name,
|
|
1464
|
+
sourceTable,
|
|
1465
|
+
targetTable: relation.target,
|
|
1466
|
+
sourceColumn: sourceProjections.map(
|
|
1467
|
+
(projection) => projection.outputColumn
|
|
1468
|
+
),
|
|
1469
|
+
targetColumn: joinColumns.targetJoinColumn,
|
|
1470
|
+
hops: [],
|
|
1471
|
+
cardinality: relationCardinality(relation),
|
|
1472
|
+
relationType: relation.type
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
|
|
1476
|
+
const unsafeReason = unsafeBindingRelationReason(
|
|
1477
|
+
intent,
|
|
1478
|
+
ctx,
|
|
1479
|
+
bindingDependencies
|
|
1480
|
+
);
|
|
1481
|
+
if (unsafeReason) {
|
|
1482
|
+
return {
|
|
1483
|
+
unsafeReason,
|
|
1484
|
+
directProjectionLineage: outputSchema.directProjectionLineage,
|
|
1485
|
+
relations: []
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const sourceTable = intent.from;
|
|
1489
|
+
const relations = [];
|
|
1490
|
+
const scalarRelations = [];
|
|
1491
|
+
const relationNames = new Set(
|
|
1492
|
+
ctx.validator?.getRelationsFrom(sourceTable).map((relation) => relation.name)
|
|
1493
|
+
);
|
|
1494
|
+
for (const relationName of relationNames ?? []) {
|
|
1495
|
+
const relation = ctx.validator?.getRelation(sourceTable, relationName);
|
|
1496
|
+
const virtualRelation = virtualRelationForBinding(
|
|
1497
|
+
relation,
|
|
1498
|
+
sourceTable,
|
|
1499
|
+
outputSchema.directProjectionLineage ?? []
|
|
1500
|
+
);
|
|
1501
|
+
if (virtualRelation) relations.push(virtualRelation);
|
|
1502
|
+
const scalarRelation = scalarVirtualRelationForBinding(
|
|
1503
|
+
relation,
|
|
1504
|
+
sourceTable,
|
|
1505
|
+
outputSchema.directProjectionLineage ?? []
|
|
1506
|
+
);
|
|
1507
|
+
if (scalarRelation) scalarRelations.push(scalarRelation);
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
sourceTable,
|
|
1511
|
+
directProjectionLineage: outputSchema.directProjectionLineage,
|
|
1512
|
+
relations,
|
|
1513
|
+
scalarRelations
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
978
1516
|
function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
979
1517
|
const { segments } = expr;
|
|
980
1518
|
if (segments.length === 1) {
|
|
@@ -999,63 +1537,164 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
|
999
1537
|
}
|
|
1000
1538
|
assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
|
|
1001
1539
|
}
|
|
1002
|
-
function
|
|
1540
|
+
function resolveBindingRelationInclude(ctx, bindingName, relationPath) {
|
|
1541
|
+
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
1542
|
+
if (!bindingName || !ctx.validator) {
|
|
1543
|
+
throw new NqlSemanticException(
|
|
1544
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
1545
|
+
`Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
return ctx.validator.resolveVirtualBindingScalarRelationForInclude(
|
|
1549
|
+
bindingName,
|
|
1550
|
+
relationPath
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
|
|
1003
1554
|
const fn = expr.name.toLowerCase();
|
|
1004
1555
|
if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
|
|
1005
1556
|
throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
|
|
1006
1557
|
}
|
|
1007
1558
|
for (const arg of expr.args) {
|
|
1008
|
-
validateBindingFinalExpression(arg, ctx, bindingName);
|
|
1559
|
+
validateBindingFinalExpression(arg, ctx, bindingName, allowRelationFilters);
|
|
1009
1560
|
}
|
|
1010
1561
|
}
|
|
1011
|
-
function validateBindingFinalExpression(expr, ctx, bindingName) {
|
|
1562
|
+
function validateBindingFinalExpression(expr, ctx, bindingName, allowRelationFilters = false) {
|
|
1012
1563
|
switch (expr.type) {
|
|
1013
1564
|
case "path":
|
|
1014
1565
|
validateBindingFinalPath(expr, ctx, bindingName);
|
|
1015
1566
|
break;
|
|
1016
1567
|
case "binary":
|
|
1017
|
-
validateBindingFinalExpression(
|
|
1018
|
-
|
|
1568
|
+
validateBindingFinalExpression(
|
|
1569
|
+
expr.left,
|
|
1570
|
+
ctx,
|
|
1571
|
+
bindingName,
|
|
1572
|
+
allowRelationFilters
|
|
1573
|
+
);
|
|
1574
|
+
validateBindingFinalExpression(
|
|
1575
|
+
expr.right,
|
|
1576
|
+
ctx,
|
|
1577
|
+
bindingName,
|
|
1578
|
+
allowRelationFilters
|
|
1579
|
+
);
|
|
1019
1580
|
break;
|
|
1020
1581
|
case "unary":
|
|
1021
|
-
validateBindingFinalExpression(
|
|
1582
|
+
validateBindingFinalExpression(
|
|
1583
|
+
expr.operand,
|
|
1584
|
+
ctx,
|
|
1585
|
+
bindingName,
|
|
1586
|
+
allowRelationFilters
|
|
1587
|
+
);
|
|
1022
1588
|
break;
|
|
1023
1589
|
case "comparison":
|
|
1024
|
-
validateBindingFinalExpression(
|
|
1025
|
-
|
|
1590
|
+
validateBindingFinalExpression(
|
|
1591
|
+
expr.left,
|
|
1592
|
+
ctx,
|
|
1593
|
+
bindingName,
|
|
1594
|
+
allowRelationFilters
|
|
1595
|
+
);
|
|
1596
|
+
validateBindingFinalExpression(
|
|
1597
|
+
expr.right,
|
|
1598
|
+
ctx,
|
|
1599
|
+
bindingName,
|
|
1600
|
+
allowRelationFilters
|
|
1601
|
+
);
|
|
1026
1602
|
break;
|
|
1027
1603
|
case "in":
|
|
1028
|
-
validateBindingFinalExpression(
|
|
1604
|
+
validateBindingFinalExpression(
|
|
1605
|
+
expr.expression,
|
|
1606
|
+
ctx,
|
|
1607
|
+
bindingName,
|
|
1608
|
+
allowRelationFilters
|
|
1609
|
+
);
|
|
1029
1610
|
if (Array.isArray(expr.values)) {
|
|
1030
1611
|
for (const value of expr.values) {
|
|
1031
|
-
validateBindingFinalExpression(
|
|
1612
|
+
validateBindingFinalExpression(
|
|
1613
|
+
value,
|
|
1614
|
+
ctx,
|
|
1615
|
+
bindingName,
|
|
1616
|
+
allowRelationFilters
|
|
1617
|
+
);
|
|
1032
1618
|
}
|
|
1033
1619
|
} else if (expr.values.type === "dateRange") ; else ;
|
|
1034
1620
|
break;
|
|
1035
1621
|
case "between":
|
|
1036
|
-
validateBindingFinalExpression(
|
|
1037
|
-
|
|
1038
|
-
|
|
1622
|
+
validateBindingFinalExpression(
|
|
1623
|
+
expr.expression,
|
|
1624
|
+
ctx,
|
|
1625
|
+
bindingName,
|
|
1626
|
+
allowRelationFilters
|
|
1627
|
+
);
|
|
1628
|
+
validateBindingFinalExpression(
|
|
1629
|
+
expr.low,
|
|
1630
|
+
ctx,
|
|
1631
|
+
bindingName,
|
|
1632
|
+
allowRelationFilters
|
|
1633
|
+
);
|
|
1634
|
+
validateBindingFinalExpression(
|
|
1635
|
+
expr.high,
|
|
1636
|
+
ctx,
|
|
1637
|
+
bindingName,
|
|
1638
|
+
allowRelationFilters
|
|
1639
|
+
);
|
|
1039
1640
|
break;
|
|
1040
1641
|
case "isNull":
|
|
1041
|
-
validateBindingFinalExpression(
|
|
1642
|
+
validateBindingFinalExpression(
|
|
1643
|
+
expr.expression,
|
|
1644
|
+
ctx,
|
|
1645
|
+
bindingName,
|
|
1646
|
+
allowRelationFilters
|
|
1647
|
+
);
|
|
1042
1648
|
break;
|
|
1043
1649
|
case "function":
|
|
1044
|
-
validateBindingFinalFunction(
|
|
1650
|
+
validateBindingFinalFunction(
|
|
1651
|
+
expr,
|
|
1652
|
+
ctx,
|
|
1653
|
+
bindingName,
|
|
1654
|
+
allowRelationFilters
|
|
1655
|
+
);
|
|
1045
1656
|
break;
|
|
1046
1657
|
case "case":
|
|
1047
1658
|
if (expr.subject) {
|
|
1048
|
-
validateBindingFinalExpression(
|
|
1659
|
+
validateBindingFinalExpression(
|
|
1660
|
+
expr.subject,
|
|
1661
|
+
ctx,
|
|
1662
|
+
bindingName,
|
|
1663
|
+
allowRelationFilters
|
|
1664
|
+
);
|
|
1049
1665
|
}
|
|
1050
1666
|
for (const when of expr.whenClauses) {
|
|
1051
|
-
validateBindingFinalExpression(
|
|
1052
|
-
|
|
1667
|
+
validateBindingFinalExpression(
|
|
1668
|
+
when.condition,
|
|
1669
|
+
ctx,
|
|
1670
|
+
bindingName,
|
|
1671
|
+
allowRelationFilters
|
|
1672
|
+
);
|
|
1673
|
+
validateBindingFinalExpression(
|
|
1674
|
+
when.result,
|
|
1675
|
+
ctx,
|
|
1676
|
+
bindingName,
|
|
1677
|
+
allowRelationFilters
|
|
1678
|
+
);
|
|
1053
1679
|
}
|
|
1054
1680
|
if (expr.elseClause) {
|
|
1055
|
-
validateBindingFinalExpression(
|
|
1681
|
+
validateBindingFinalExpression(
|
|
1682
|
+
expr.elseClause,
|
|
1683
|
+
ctx,
|
|
1684
|
+
bindingName,
|
|
1685
|
+
allowRelationFilters
|
|
1686
|
+
);
|
|
1056
1687
|
}
|
|
1057
1688
|
break;
|
|
1058
1689
|
case "relationFilter":
|
|
1690
|
+
if (allowRelationFilters) {
|
|
1691
|
+
resolveBindingRelationFilter(
|
|
1692
|
+
ctx,
|
|
1693
|
+
bindingName,
|
|
1694
|
+
expr.relation
|
|
1695
|
+
);
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1059
1698
|
assertNoBindingRelationConstruct(
|
|
1060
1699
|
ctx,
|
|
1061
1700
|
bindingName,
|
|
@@ -1111,12 +1750,7 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
|
|
|
1111
1750
|
case "star":
|
|
1112
1751
|
break;
|
|
1113
1752
|
case "relationStar":
|
|
1114
|
-
|
|
1115
|
-
ctx,
|
|
1116
|
-
bindingName,
|
|
1117
|
-
"select relation columns",
|
|
1118
|
-
item.relation.join(".")
|
|
1119
|
-
);
|
|
1753
|
+
resolveBindingRelationInclude(ctx, bindingName, item.relation);
|
|
1120
1754
|
break;
|
|
1121
1755
|
case "expression":
|
|
1122
1756
|
if (item.expression.type === "path" && item.expression.segments.length > 1) {
|
|
@@ -1130,11 +1764,11 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
|
|
|
1130
1764
|
firstSegment
|
|
1131
1765
|
);
|
|
1132
1766
|
} else {
|
|
1133
|
-
|
|
1767
|
+
resolveBindingRelationColumn(
|
|
1134
1768
|
ctx,
|
|
1135
1769
|
bindingName,
|
|
1136
|
-
|
|
1137
|
-
segments.
|
|
1770
|
+
segments.slice(0, -1),
|
|
1771
|
+
segments.at(-1)
|
|
1138
1772
|
);
|
|
1139
1773
|
}
|
|
1140
1774
|
} else {
|
|
@@ -1166,7 +1800,12 @@ function validateBindingFinalQuery(query, ctx) {
|
|
|
1166
1800
|
ctx.currentHavingAliases = previousHavingAliases;
|
|
1167
1801
|
}
|
|
1168
1802
|
} else {
|
|
1169
|
-
validateBindingFinalExpression(
|
|
1803
|
+
validateBindingFinalExpression(
|
|
1804
|
+
clause.condition,
|
|
1805
|
+
ctx,
|
|
1806
|
+
query.table,
|
|
1807
|
+
true
|
|
1808
|
+
);
|
|
1170
1809
|
}
|
|
1171
1810
|
break;
|
|
1172
1811
|
}
|
|
@@ -1351,17 +1990,34 @@ function compileQueryInternal(query, ctx, fns, bindings) {
|
|
|
1351
1990
|
const relationPaths = /* @__PURE__ */ new Set();
|
|
1352
1991
|
for (const expr of select.columns) {
|
|
1353
1992
|
if (expr.kind === "relationColumn") {
|
|
1354
|
-
|
|
1993
|
+
if (!isBindingSource || expr.column === "*") {
|
|
1994
|
+
relationPaths.add(expr.relation);
|
|
1995
|
+
}
|
|
1355
1996
|
}
|
|
1356
1997
|
}
|
|
1357
1998
|
if (relationPaths.size > 0) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
);
|
|
1363
|
-
|
|
1364
|
-
allIncludes.
|
|
1999
|
+
if (isBindingSource) {
|
|
2000
|
+
for (const relation of relationPaths) {
|
|
2001
|
+
resolveBindingRelationInclude(ctx, query.table, relation.split("."));
|
|
2002
|
+
}
|
|
2003
|
+
const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
|
|
2004
|
+
for (const inc of nestedIncludes) {
|
|
2005
|
+
const exists = allIncludes.some(
|
|
2006
|
+
(existing) => existing.relation === inc.relation
|
|
2007
|
+
);
|
|
2008
|
+
if (!exists) {
|
|
2009
|
+
allIncludes.push(inc);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} else {
|
|
2013
|
+
const nestedIncludes = buildNestedIncludes(relationPaths, flatMode);
|
|
2014
|
+
for (const inc of nestedIncludes) {
|
|
2015
|
+
const exists = allIncludes.some(
|
|
2016
|
+
(existing) => existing.relation === inc.relation
|
|
2017
|
+
);
|
|
2018
|
+
if (!exists) {
|
|
2019
|
+
allIncludes.push(inc);
|
|
2020
|
+
}
|
|
1365
2021
|
}
|
|
1366
2022
|
}
|
|
1367
2023
|
}
|
|
@@ -1489,9 +2145,10 @@ function expressionOutputColumn(expr) {
|
|
|
1489
2145
|
}
|
|
1490
2146
|
}
|
|
1491
2147
|
function addUnique(columns, seen, column) {
|
|
1492
|
-
if (seen.has(column)) return;
|
|
2148
|
+
if (seen.has(column)) return false;
|
|
1493
2149
|
seen.add(column);
|
|
1494
2150
|
columns.push(column);
|
|
2151
|
+
return true;
|
|
1495
2152
|
}
|
|
1496
2153
|
function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
1497
2154
|
const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
|
|
@@ -1503,33 +2160,62 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
|
1503
2160
|
`Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
|
|
1504
2161
|
);
|
|
1505
2162
|
}
|
|
1506
|
-
function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
2163
|
+
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
|
|
1507
2164
|
const columns = [];
|
|
1508
2165
|
const seen = /* @__PURE__ */ new Set();
|
|
2166
|
+
const directProjectionLineage = [];
|
|
1509
2167
|
const addColumn = (column) => addUnique(columns, seen, column);
|
|
2168
|
+
const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
|
|
2169
|
+
const addDirectProjection = (outputColumn, sourceColumn) => {
|
|
2170
|
+
if (!addColumn(outputColumn)) return;
|
|
2171
|
+
directProjectionLineage.push({
|
|
2172
|
+
kind: "directProjection",
|
|
2173
|
+
sourceTable: intent.from,
|
|
2174
|
+
sourceColumn: resolveSourceColumn(sourceColumn),
|
|
2175
|
+
outputColumn
|
|
2176
|
+
});
|
|
2177
|
+
};
|
|
1510
2178
|
const addSourceColumns = () => {
|
|
1511
2179
|
for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
|
|
1512
|
-
|
|
2180
|
+
addDirectProjection(column, column);
|
|
1513
2181
|
}
|
|
1514
2182
|
};
|
|
1515
2183
|
const { select } = intent;
|
|
1516
2184
|
if (!select || select.type === "all") {
|
|
1517
2185
|
addSourceColumns();
|
|
1518
|
-
|
|
2186
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
2187
|
+
return {
|
|
2188
|
+
columns: outputSchema2.columns,
|
|
2189
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2190
|
+
intent,
|
|
2191
|
+
ctx,
|
|
2192
|
+
outputSchema2,
|
|
2193
|
+
bindingDependencies
|
|
2194
|
+
)
|
|
2195
|
+
};
|
|
1519
2196
|
}
|
|
1520
2197
|
if (select.type === "fields") {
|
|
1521
2198
|
for (const field of select.fields) {
|
|
1522
2199
|
if (field === "*") {
|
|
1523
2200
|
addSourceColumns();
|
|
1524
2201
|
} else {
|
|
1525
|
-
|
|
2202
|
+
addDirectProjection(field, field);
|
|
1526
2203
|
}
|
|
1527
2204
|
}
|
|
1528
|
-
|
|
2205
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
2206
|
+
return {
|
|
2207
|
+
columns: outputSchema2.columns,
|
|
2208
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2209
|
+
intent,
|
|
2210
|
+
ctx,
|
|
2211
|
+
outputSchema2,
|
|
2212
|
+
bindingDependencies
|
|
2213
|
+
)
|
|
2214
|
+
};
|
|
1529
2215
|
}
|
|
1530
2216
|
if (select.type === "aggregate") {
|
|
1531
2217
|
for (const field of select.fields ?? []) {
|
|
1532
|
-
|
|
2218
|
+
addDirectProjection(field, field);
|
|
1533
2219
|
}
|
|
1534
2220
|
for (const aggregate of select.aggregates) {
|
|
1535
2221
|
if (!aggregate.as) {
|
|
@@ -1540,7 +2226,16 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
|
1540
2226
|
}
|
|
1541
2227
|
addColumn(aggregate.as);
|
|
1542
2228
|
}
|
|
1543
|
-
|
|
2229
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
2230
|
+
return {
|
|
2231
|
+
columns: outputSchema2.columns,
|
|
2232
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2233
|
+
intent,
|
|
2234
|
+
ctx,
|
|
2235
|
+
outputSchema2,
|
|
2236
|
+
bindingDependencies
|
|
2237
|
+
)
|
|
2238
|
+
};
|
|
1544
2239
|
}
|
|
1545
2240
|
for (const expr of select.columns) {
|
|
1546
2241
|
if (expr.kind === "column" && expr.column === "*") {
|
|
@@ -1560,9 +2255,24 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
|
1560
2255
|
`Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
|
|
1561
2256
|
);
|
|
1562
2257
|
}
|
|
1563
|
-
|
|
2258
|
+
if (expr.kind === "column") {
|
|
2259
|
+
addDirectProjection(outputColumn, expr.column);
|
|
2260
|
+
} else if (expr.kind === "columnAlias") {
|
|
2261
|
+
addDirectProjection(outputColumn, expr.column);
|
|
2262
|
+
} else {
|
|
2263
|
+
addColumn(outputColumn);
|
|
2264
|
+
}
|
|
1564
2265
|
}
|
|
1565
|
-
|
|
2266
|
+
const outputSchema = { columns, directProjectionLineage };
|
|
2267
|
+
return {
|
|
2268
|
+
columns: outputSchema.columns,
|
|
2269
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2270
|
+
intent,
|
|
2271
|
+
ctx,
|
|
2272
|
+
outputSchema,
|
|
2273
|
+
bindingDependencies
|
|
2274
|
+
)
|
|
2275
|
+
};
|
|
1566
2276
|
}
|
|
1567
2277
|
function validateNqlExpressionPaths(expr, ctx) {
|
|
1568
2278
|
switch (expr.type) {
|
|
@@ -2334,13 +3044,12 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
|
|
|
2334
3044
|
const relFilter = expr;
|
|
2335
3045
|
const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
|
|
2336
3046
|
const prevRelationTarget = ctx.currentRelationTarget;
|
|
3047
|
+
const bindingRelation = ctx.currentFromTable && relFilter.relation[0] ? resolveBindingRelationFilter(
|
|
3048
|
+
ctx,
|
|
3049
|
+
ctx.currentFromTable,
|
|
3050
|
+
relFilter.relation
|
|
3051
|
+
) : void 0;
|
|
2337
3052
|
if (ctx.currentFromTable && relFilter.relation[0]) {
|
|
2338
|
-
assertNoBindingRelationConstruct(
|
|
2339
|
-
ctx,
|
|
2340
|
-
ctx.currentFromTable,
|
|
2341
|
-
"use relation filters",
|
|
2342
|
-
relFilter.relation.join(".")
|
|
2343
|
-
);
|
|
2344
3053
|
if (ctx.validator) {
|
|
2345
3054
|
ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
|
|
2346
3055
|
ctx.currentFromTable,
|
|
@@ -2356,13 +3065,28 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
|
|
|
2356
3065
|
nestedOuterAliases
|
|
2357
3066
|
);
|
|
2358
3067
|
ctx.currentRelationTarget = prevRelationTarget;
|
|
2359
|
-
|
|
3068
|
+
const relationFilterIntent = {
|
|
2360
3069
|
kind: "relationFilter",
|
|
2361
3070
|
relation: relFilter.relation,
|
|
2362
3071
|
where,
|
|
2363
3072
|
mode: relFilter.mode,
|
|
2364
|
-
...relFilter.alias !== void 0 && { alias: relFilter.alias }
|
|
3073
|
+
...relFilter.alias !== void 0 && { alias: relFilter.alias },
|
|
3074
|
+
...bindingRelation !== void 0 && {
|
|
3075
|
+
targetTable: bindingRelation.targetTable,
|
|
3076
|
+
sourceColumn: bindingRelation.sourceColumn,
|
|
3077
|
+
targetColumn: bindingRelation.targetColumn
|
|
3078
|
+
}
|
|
2365
3079
|
};
|
|
3080
|
+
return bindingRelation ? markNqlTrustedRelationFilter(relationFilterIntent, {
|
|
3081
|
+
relation: bindingRelation.relation,
|
|
3082
|
+
targetTable: bindingRelation.targetTable,
|
|
3083
|
+
sourceColumn: bindingRelation.sourceColumn,
|
|
3084
|
+
targetColumn: bindingRelation.targetColumn,
|
|
3085
|
+
hops: bindingRelation.hops,
|
|
3086
|
+
...bindingRelation.cardinality !== void 0 && {
|
|
3087
|
+
cardinality: bindingRelation.cardinality
|
|
3088
|
+
}
|
|
3089
|
+
}) : relationFilterIntent;
|
|
2366
3090
|
}
|
|
2367
3091
|
function expandDateRangeList(field, patterns, negated) {
|
|
2368
3092
|
const conditions = patterns.map((pattern) => {
|
|
@@ -2468,6 +3192,20 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
|
2468
3192
|
...NQL_SELECT_JSON_FUNCTIONS,
|
|
2469
3193
|
...NQL_SELECT_SCALAR_FUNCTIONS
|
|
2470
3194
|
]);
|
|
3195
|
+
function resolveBindingRelationInclude2(ctx, bindingName, relationPath) {
|
|
3196
|
+
if (!isBindingTable(ctx, bindingName)) return false;
|
|
3197
|
+
if (!bindingName || !ctx.validator) {
|
|
3198
|
+
throw new NqlSemanticException(
|
|
3199
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
3200
|
+
`Query '${bindingName ?? "<unknown>"}' reads from an NQL binding and cannot use relation include '${relationPath.join(".")}' (ref-#192): model metadata is not available.`
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
ctx.validator.resolveVirtualBindingScalarRelationForInclude(
|
|
3204
|
+
bindingName,
|
|
3205
|
+
relationPath
|
|
3206
|
+
);
|
|
3207
|
+
return true;
|
|
3208
|
+
}
|
|
2471
3209
|
function validateSelectPathExpression(expr, ctx) {
|
|
2472
3210
|
if (!ctx.currentFromTable) return;
|
|
2473
3211
|
const { segments } = expr;
|
|
@@ -2523,12 +3261,14 @@ function compileSelectClause(clause, ctx, fns) {
|
|
|
2523
3261
|
} else if (item.type === "relationStar") {
|
|
2524
3262
|
hasExpressions = true;
|
|
2525
3263
|
const relation = item.relation.join(".");
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3264
|
+
if (!resolveBindingRelationInclude2(ctx, ctx.currentFromTable, item.relation)) {
|
|
3265
|
+
assertNoBindingRelationConstruct(
|
|
3266
|
+
ctx,
|
|
3267
|
+
ctx.currentFromTable,
|
|
3268
|
+
"select relation columns",
|
|
3269
|
+
relation
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
2532
3272
|
expressions.push({
|
|
2533
3273
|
kind: "relationColumn",
|
|
2534
3274
|
relation,
|
|
@@ -2854,27 +3594,47 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
2854
3594
|
}
|
|
2855
3595
|
const column = segments[segments.length - 1];
|
|
2856
3596
|
const relation = segments.slice(0, -1).join(".");
|
|
2857
|
-
|
|
3597
|
+
const bindingRelation = resolveBindingRelationColumn(
|
|
2858
3598
|
ctx,
|
|
2859
3599
|
ctx.currentFromTable,
|
|
2860
|
-
|
|
2861
|
-
|
|
3600
|
+
segments.slice(0, -1),
|
|
3601
|
+
column
|
|
2862
3602
|
);
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
3603
|
+
if (!bindingRelation) {
|
|
3604
|
+
assertNoBindingRelationConstruct(
|
|
3605
|
+
ctx,
|
|
2865
3606
|
ctx.currentFromTable,
|
|
2866
|
-
|
|
3607
|
+
"select relation columns",
|
|
3608
|
+
relation
|
|
2867
3609
|
);
|
|
3610
|
+
}
|
|
3611
|
+
if (ctx.currentFromTable && ctx.validator) {
|
|
3612
|
+
const lastBindingHop = bindingRelation?.hops[bindingRelation.hops.length - 1];
|
|
3613
|
+
const targetTable = lastBindingHop?.target ?? bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
|
|
2868
3614
|
if (targetTable) {
|
|
2869
3615
|
ctx.validator.validateColumn(targetTable, column);
|
|
2870
3616
|
}
|
|
2871
3617
|
}
|
|
2872
|
-
|
|
3618
|
+
const relationColumnIntent = {
|
|
2873
3619
|
kind: "relationColumn",
|
|
2874
3620
|
relation,
|
|
2875
3621
|
column,
|
|
2876
3622
|
as: item.alias ?? `${relation}.${column}`
|
|
2877
3623
|
};
|
|
3624
|
+
return bindingRelation ? markNqlTrustedRelationFilter(relationColumnIntent, {
|
|
3625
|
+
relation: bindingRelation.relation,
|
|
3626
|
+
targetTable: bindingRelation.targetTable,
|
|
3627
|
+
sourceColumn: bindingRelation.sourceColumn,
|
|
3628
|
+
targetColumn: bindingRelation.targetColumn,
|
|
3629
|
+
hops: bindingRelation.hops,
|
|
3630
|
+
selectedColumn: column,
|
|
3631
|
+
...bindingRelation.cardinality !== void 0 && {
|
|
3632
|
+
cardinality: bindingRelation.cardinality
|
|
3633
|
+
},
|
|
3634
|
+
...bindingRelation.relationType !== void 0 && {
|
|
3635
|
+
relationType: bindingRelation.relationType
|
|
3636
|
+
}
|
|
3637
|
+
}) : relationColumnIntent;
|
|
2878
3638
|
}
|
|
2879
3639
|
function compileCaseExpression(caseExpr, item, ctx, fns) {
|
|
2880
3640
|
if (caseExpr.subject) {
|
|
@@ -3262,6 +4022,7 @@ var NqlCompiler = class {
|
|
|
3262
4022
|
recursiveKeywords,
|
|
3263
4023
|
validator,
|
|
3264
4024
|
bindingOutputColumns: /* @__PURE__ */ new Map(),
|
|
4025
|
+
bindingRelationFilters: /* @__PURE__ */ new Map(),
|
|
3265
4026
|
params,
|
|
3266
4027
|
maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
|
|
3267
4028
|
allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
|
|
@@ -3281,6 +4042,7 @@ var NqlCompiler = class {
|
|
|
3281
4042
|
return this.compileProgram(program);
|
|
3282
4043
|
} finally {
|
|
3283
4044
|
this.ctx.bindingOutputColumns.clear();
|
|
4045
|
+
this.ctx.bindingRelationFilters.clear();
|
|
3284
4046
|
this.ctx.validator?.clearVirtualBindingTables();
|
|
3285
4047
|
}
|
|
3286
4048
|
}
|
|
@@ -3364,13 +4126,15 @@ var NqlCompiler = class {
|
|
|
3364
4126
|
const outputSchema = this.registerQueryBindingOutputSchema(
|
|
3365
4127
|
bindName,
|
|
3366
4128
|
lastResult.query,
|
|
3367
|
-
bindingOutputSchemas
|
|
4129
|
+
bindingOutputSchemas,
|
|
4130
|
+
statementBindingDependencies
|
|
3368
4131
|
);
|
|
3369
4132
|
bindings.set(bindName, lastResult.query);
|
|
3370
4133
|
if (outputSchema) {
|
|
3371
4134
|
this.ctx.validator?.addVirtualBindingTable(
|
|
3372
4135
|
bindName,
|
|
3373
|
-
outputSchema.columns
|
|
4136
|
+
outputSchema.columns,
|
|
4137
|
+
outputSchema.relationFilters
|
|
3374
4138
|
);
|
|
3375
4139
|
}
|
|
3376
4140
|
materializedBindStatements.add(i);
|
|
@@ -3399,7 +4163,8 @@ var NqlCompiler = class {
|
|
|
3399
4163
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
3400
4164
|
this.ctx.validator?.addVirtualBindingTable(
|
|
3401
4165
|
bindName,
|
|
3402
|
-
outputSchema.columns
|
|
4166
|
+
outputSchema.columns,
|
|
4167
|
+
outputSchema.relationFilters
|
|
3403
4168
|
);
|
|
3404
4169
|
}
|
|
3405
4170
|
materializedBindStatements.add(i);
|
|
@@ -3439,11 +4204,22 @@ var NqlCompiler = class {
|
|
|
3439
4204
|
}
|
|
3440
4205
|
return lastResult;
|
|
3441
4206
|
}
|
|
3442
|
-
registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas) {
|
|
4207
|
+
registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas, bindingDependencies) {
|
|
3443
4208
|
try {
|
|
3444
|
-
const outputSchema = getQueryOutputSchema(
|
|
4209
|
+
const outputSchema = getQueryOutputSchema(
|
|
4210
|
+
query,
|
|
4211
|
+
this.ctx,
|
|
4212
|
+
bindName,
|
|
4213
|
+
bindingDependencies
|
|
4214
|
+
);
|
|
3445
4215
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
3446
4216
|
this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
|
|
4217
|
+
if (outputSchema.relationFilters) {
|
|
4218
|
+
this.ctx.bindingRelationFilters.set(
|
|
4219
|
+
bindName,
|
|
4220
|
+
outputSchema.relationFilters
|
|
4221
|
+
);
|
|
4222
|
+
}
|
|
3447
4223
|
return outputSchema;
|
|
3448
4224
|
} catch (error) {
|
|
3449
4225
|
if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {
|