@dbsp/nql 1.4.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 +11 -4
- package/dist/index.js +593 -63
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NQL_INTERNAL_COMPILER_OPTIONS, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
|
|
1
|
+
import { NQL_INTERNAL_COMPILER_OPTIONS, markNqlTrustedRelationFilter, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
|
|
2
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
|
|
|
@@ -52,6 +52,7 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
52
52
|
schema;
|
|
53
53
|
knownCteTables = /* @__PURE__ */ new Set();
|
|
54
54
|
virtualBindingTables = /* @__PURE__ */ new Map();
|
|
55
|
+
virtualBindingRelationFilters = /* @__PURE__ */ new Map();
|
|
55
56
|
/**
|
|
56
57
|
* Register CTE names so validateTable() skips validation for them.
|
|
57
58
|
* CTE names are not physical tables in the schema, but are valid FROM targets.
|
|
@@ -69,12 +70,16 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
69
70
|
* Register an NQL binding as a virtual table with a concrete output schema.
|
|
70
71
|
* Binding columns are logical output names, so matching is exact.
|
|
71
72
|
*/
|
|
72
|
-
addVirtualBindingTable(name, columns) {
|
|
73
|
+
addVirtualBindingTable(name, columns, relationFilters) {
|
|
73
74
|
this.virtualBindingTables.set(name, columns);
|
|
75
|
+
if (relationFilters) {
|
|
76
|
+
this.virtualBindingRelationFilters.set(name, relationFilters);
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
/** Clear all registered virtual binding tables (used between compilations). */
|
|
76
80
|
clearVirtualBindingTables() {
|
|
77
81
|
this.virtualBindingTables.clear();
|
|
82
|
+
this.virtualBindingRelationFilters.clear();
|
|
78
83
|
}
|
|
79
84
|
isVirtualBindingTable(name) {
|
|
80
85
|
return name !== void 0 && this.virtualBindingTables.has(name);
|
|
@@ -87,6 +92,98 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
87
92
|
if (virtualColumns) return virtualColumns;
|
|
88
93
|
return this.schema.getTable(name)?.columns.map((column) => column.name);
|
|
89
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
|
+
}
|
|
90
187
|
/**
|
|
91
188
|
* Convert camelCase to snake_case for column name matching.
|
|
92
189
|
* NQL queries may use either form (e.g., viewCount or view_count).
|
|
@@ -156,13 +253,17 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
156
253
|
}
|
|
157
254
|
resolveRelationTarget(sourceTable, relationName) {
|
|
158
255
|
if (this.virtualBindingTables.has(sourceTable)) {
|
|
256
|
+
const virtualRelation = this.getVirtualBindingRelation(
|
|
257
|
+
sourceTable,
|
|
258
|
+
relationName
|
|
259
|
+
);
|
|
260
|
+
if (virtualRelation) return virtualRelation.targetTable;
|
|
159
261
|
throw new NqlSemanticException(
|
|
160
262
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
161
|
-
`Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName})
|
|
263
|
+
`Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${this.explainVirtualBindingRelationRejection(sourceTable, relationName)}.`
|
|
162
264
|
);
|
|
163
265
|
}
|
|
164
|
-
const
|
|
165
|
-
const rel = relations.find((r) => r.name === relationName);
|
|
266
|
+
const rel = this.getRelation(sourceTable, relationName);
|
|
166
267
|
return rel?.target;
|
|
167
268
|
}
|
|
168
269
|
assertNoBindingRelationConstruct(bindingName, construct, detail) {
|
|
@@ -288,6 +389,81 @@ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
|
|
|
288
389
|
`Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
|
|
289
390
|
);
|
|
290
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
|
+
}
|
|
291
467
|
function assertNoBindingRelationPath(ctx, bindingName, path) {
|
|
292
468
|
if (!isBindingTable(ctx, bindingName)) return;
|
|
293
469
|
throw new NqlSemanticException(
|
|
@@ -518,6 +694,10 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
|
|
|
518
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.`
|
|
519
695
|
);
|
|
520
696
|
}
|
|
697
|
+
if (!aliasContext && ctx.currentRelationTarget && !field.includes(".")) {
|
|
698
|
+
ctx.validator?.validateColumn(ctx.currentRelationTarget, field);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
521
701
|
if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
|
|
522
702
|
if (field.includes(".")) {
|
|
523
703
|
assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
|
|
@@ -540,10 +720,6 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
|
|
|
540
720
|
}
|
|
541
721
|
return;
|
|
542
722
|
}
|
|
543
|
-
if (ctx.currentRelationTarget && !field.includes(".")) {
|
|
544
|
-
ctx.validator.validateColumn(ctx.currentRelationTarget, field);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
723
|
if (ctx.currentFromTable && field.includes(".")) {
|
|
548
724
|
if (isBindingTable(ctx, ctx.currentFromTable)) {
|
|
549
725
|
assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
|
|
@@ -923,6 +1099,7 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
|
923
1099
|
...NQL_SELECT_AGGREGATE_FUNCTIONS,
|
|
924
1100
|
...NQL_SELECT_VALUE_FUNCTIONS
|
|
925
1101
|
]);
|
|
1102
|
+
var DEFAULT_RELATION_TARGET_COLUMN = "id";
|
|
926
1103
|
function compileNestedQuery(query, ctx, fns, bindings) {
|
|
927
1104
|
const savedContext = {
|
|
928
1105
|
currentFromTable: ctx.currentFromTable,
|
|
@@ -975,6 +1152,168 @@ function throwUnsupportedBindingFinal(bindingName, feature) {
|
|
|
975
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.`
|
|
976
1153
|
);
|
|
977
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
|
+
}
|
|
978
1317
|
function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
979
1318
|
const { segments } = expr;
|
|
980
1319
|
if (segments.length === 1) {
|
|
@@ -999,63 +1338,151 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
|
999
1338
|
}
|
|
1000
1339
|
assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
|
|
1001
1340
|
}
|
|
1002
|
-
function validateBindingFinalFunction(expr, ctx, bindingName) {
|
|
1341
|
+
function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
|
|
1003
1342
|
const fn = expr.name.toLowerCase();
|
|
1004
1343
|
if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
|
|
1005
1344
|
throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
|
|
1006
1345
|
}
|
|
1007
1346
|
for (const arg of expr.args) {
|
|
1008
|
-
validateBindingFinalExpression(arg, ctx, bindingName);
|
|
1347
|
+
validateBindingFinalExpression(arg, ctx, bindingName, allowRelationFilters);
|
|
1009
1348
|
}
|
|
1010
1349
|
}
|
|
1011
|
-
function validateBindingFinalExpression(expr, ctx, bindingName) {
|
|
1350
|
+
function validateBindingFinalExpression(expr, ctx, bindingName, allowRelationFilters = false) {
|
|
1012
1351
|
switch (expr.type) {
|
|
1013
1352
|
case "path":
|
|
1014
1353
|
validateBindingFinalPath(expr, ctx, bindingName);
|
|
1015
1354
|
break;
|
|
1016
1355
|
case "binary":
|
|
1017
|
-
validateBindingFinalExpression(
|
|
1018
|
-
|
|
1356
|
+
validateBindingFinalExpression(
|
|
1357
|
+
expr.left,
|
|
1358
|
+
ctx,
|
|
1359
|
+
bindingName,
|
|
1360
|
+
allowRelationFilters
|
|
1361
|
+
);
|
|
1362
|
+
validateBindingFinalExpression(
|
|
1363
|
+
expr.right,
|
|
1364
|
+
ctx,
|
|
1365
|
+
bindingName,
|
|
1366
|
+
allowRelationFilters
|
|
1367
|
+
);
|
|
1019
1368
|
break;
|
|
1020
1369
|
case "unary":
|
|
1021
|
-
validateBindingFinalExpression(
|
|
1370
|
+
validateBindingFinalExpression(
|
|
1371
|
+
expr.operand,
|
|
1372
|
+
ctx,
|
|
1373
|
+
bindingName,
|
|
1374
|
+
allowRelationFilters
|
|
1375
|
+
);
|
|
1022
1376
|
break;
|
|
1023
1377
|
case "comparison":
|
|
1024
|
-
validateBindingFinalExpression(
|
|
1025
|
-
|
|
1378
|
+
validateBindingFinalExpression(
|
|
1379
|
+
expr.left,
|
|
1380
|
+
ctx,
|
|
1381
|
+
bindingName,
|
|
1382
|
+
allowRelationFilters
|
|
1383
|
+
);
|
|
1384
|
+
validateBindingFinalExpression(
|
|
1385
|
+
expr.right,
|
|
1386
|
+
ctx,
|
|
1387
|
+
bindingName,
|
|
1388
|
+
allowRelationFilters
|
|
1389
|
+
);
|
|
1026
1390
|
break;
|
|
1027
1391
|
case "in":
|
|
1028
|
-
validateBindingFinalExpression(
|
|
1392
|
+
validateBindingFinalExpression(
|
|
1393
|
+
expr.expression,
|
|
1394
|
+
ctx,
|
|
1395
|
+
bindingName,
|
|
1396
|
+
allowRelationFilters
|
|
1397
|
+
);
|
|
1029
1398
|
if (Array.isArray(expr.values)) {
|
|
1030
1399
|
for (const value of expr.values) {
|
|
1031
|
-
validateBindingFinalExpression(
|
|
1400
|
+
validateBindingFinalExpression(
|
|
1401
|
+
value,
|
|
1402
|
+
ctx,
|
|
1403
|
+
bindingName,
|
|
1404
|
+
allowRelationFilters
|
|
1405
|
+
);
|
|
1032
1406
|
}
|
|
1033
1407
|
} else if (expr.values.type === "dateRange") ; else ;
|
|
1034
1408
|
break;
|
|
1035
1409
|
case "between":
|
|
1036
|
-
validateBindingFinalExpression(
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
+
);
|
|
1039
1428
|
break;
|
|
1040
1429
|
case "isNull":
|
|
1041
|
-
validateBindingFinalExpression(
|
|
1430
|
+
validateBindingFinalExpression(
|
|
1431
|
+
expr.expression,
|
|
1432
|
+
ctx,
|
|
1433
|
+
bindingName,
|
|
1434
|
+
allowRelationFilters
|
|
1435
|
+
);
|
|
1042
1436
|
break;
|
|
1043
1437
|
case "function":
|
|
1044
|
-
validateBindingFinalFunction(
|
|
1438
|
+
validateBindingFinalFunction(
|
|
1439
|
+
expr,
|
|
1440
|
+
ctx,
|
|
1441
|
+
bindingName,
|
|
1442
|
+
allowRelationFilters
|
|
1443
|
+
);
|
|
1045
1444
|
break;
|
|
1046
1445
|
case "case":
|
|
1047
1446
|
if (expr.subject) {
|
|
1048
|
-
validateBindingFinalExpression(
|
|
1447
|
+
validateBindingFinalExpression(
|
|
1448
|
+
expr.subject,
|
|
1449
|
+
ctx,
|
|
1450
|
+
bindingName,
|
|
1451
|
+
allowRelationFilters
|
|
1452
|
+
);
|
|
1049
1453
|
}
|
|
1050
1454
|
for (const when of expr.whenClauses) {
|
|
1051
|
-
validateBindingFinalExpression(
|
|
1052
|
-
|
|
1455
|
+
validateBindingFinalExpression(
|
|
1456
|
+
when.condition,
|
|
1457
|
+
ctx,
|
|
1458
|
+
bindingName,
|
|
1459
|
+
allowRelationFilters
|
|
1460
|
+
);
|
|
1461
|
+
validateBindingFinalExpression(
|
|
1462
|
+
when.result,
|
|
1463
|
+
ctx,
|
|
1464
|
+
bindingName,
|
|
1465
|
+
allowRelationFilters
|
|
1466
|
+
);
|
|
1053
1467
|
}
|
|
1054
1468
|
if (expr.elseClause) {
|
|
1055
|
-
validateBindingFinalExpression(
|
|
1469
|
+
validateBindingFinalExpression(
|
|
1470
|
+
expr.elseClause,
|
|
1471
|
+
ctx,
|
|
1472
|
+
bindingName,
|
|
1473
|
+
allowRelationFilters
|
|
1474
|
+
);
|
|
1056
1475
|
}
|
|
1057
1476
|
break;
|
|
1058
1477
|
case "relationFilter":
|
|
1478
|
+
if (allowRelationFilters) {
|
|
1479
|
+
resolveBindingRelationFilter(
|
|
1480
|
+
ctx,
|
|
1481
|
+
bindingName,
|
|
1482
|
+
expr.relation
|
|
1483
|
+
);
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1059
1486
|
assertNoBindingRelationConstruct(
|
|
1060
1487
|
ctx,
|
|
1061
1488
|
bindingName,
|
|
@@ -1130,11 +1557,11 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
|
|
|
1130
1557
|
firstSegment
|
|
1131
1558
|
);
|
|
1132
1559
|
} else {
|
|
1133
|
-
|
|
1560
|
+
resolveBindingRelationColumn(
|
|
1134
1561
|
ctx,
|
|
1135
1562
|
bindingName,
|
|
1136
|
-
|
|
1137
|
-
segments.
|
|
1563
|
+
segments.slice(0, -1),
|
|
1564
|
+
segments.at(-1)
|
|
1138
1565
|
);
|
|
1139
1566
|
}
|
|
1140
1567
|
} else {
|
|
@@ -1166,7 +1593,12 @@ function validateBindingFinalQuery(query, ctx) {
|
|
|
1166
1593
|
ctx.currentHavingAliases = previousHavingAliases;
|
|
1167
1594
|
}
|
|
1168
1595
|
} else {
|
|
1169
|
-
validateBindingFinalExpression(
|
|
1596
|
+
validateBindingFinalExpression(
|
|
1597
|
+
clause.condition,
|
|
1598
|
+
ctx,
|
|
1599
|
+
query.table,
|
|
1600
|
+
true
|
|
1601
|
+
);
|
|
1170
1602
|
}
|
|
1171
1603
|
break;
|
|
1172
1604
|
}
|
|
@@ -1347,7 +1779,7 @@ function compileQueryInternal(query, ctx, fns, bindings) {
|
|
|
1347
1779
|
}
|
|
1348
1780
|
}
|
|
1349
1781
|
}
|
|
1350
|
-
if (select && select.type === "expressions") {
|
|
1782
|
+
if (select && select.type === "expressions" && !isBindingSource) {
|
|
1351
1783
|
const relationPaths = /* @__PURE__ */ new Set();
|
|
1352
1784
|
for (const expr of select.columns) {
|
|
1353
1785
|
if (expr.kind === "relationColumn") {
|
|
@@ -1489,9 +1921,10 @@ function expressionOutputColumn(expr) {
|
|
|
1489
1921
|
}
|
|
1490
1922
|
}
|
|
1491
1923
|
function addUnique(columns, seen, column) {
|
|
1492
|
-
if (seen.has(column)) return;
|
|
1924
|
+
if (seen.has(column)) return false;
|
|
1493
1925
|
seen.add(column);
|
|
1494
1926
|
columns.push(column);
|
|
1927
|
+
return true;
|
|
1495
1928
|
}
|
|
1496
1929
|
function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
1497
1930
|
const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
|
|
@@ -1503,33 +1936,62 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
|
1503
1936
|
`Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
|
|
1504
1937
|
);
|
|
1505
1938
|
}
|
|
1506
|
-
function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
1939
|
+
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
|
|
1507
1940
|
const columns = [];
|
|
1508
1941
|
const seen = /* @__PURE__ */ new Set();
|
|
1942
|
+
const directProjectionLineage = [];
|
|
1509
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
|
+
};
|
|
1510
1954
|
const addSourceColumns = () => {
|
|
1511
1955
|
for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
|
|
1512
|
-
|
|
1956
|
+
addDirectProjection(column, column);
|
|
1513
1957
|
}
|
|
1514
1958
|
};
|
|
1515
1959
|
const { select } = intent;
|
|
1516
1960
|
if (!select || select.type === "all") {
|
|
1517
1961
|
addSourceColumns();
|
|
1518
|
-
|
|
1962
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
1963
|
+
return {
|
|
1964
|
+
columns: outputSchema2.columns,
|
|
1965
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
1966
|
+
intent,
|
|
1967
|
+
ctx,
|
|
1968
|
+
outputSchema2,
|
|
1969
|
+
bindingDependencies
|
|
1970
|
+
)
|
|
1971
|
+
};
|
|
1519
1972
|
}
|
|
1520
1973
|
if (select.type === "fields") {
|
|
1521
1974
|
for (const field of select.fields) {
|
|
1522
1975
|
if (field === "*") {
|
|
1523
1976
|
addSourceColumns();
|
|
1524
1977
|
} else {
|
|
1525
|
-
|
|
1978
|
+
addDirectProjection(field, field);
|
|
1526
1979
|
}
|
|
1527
1980
|
}
|
|
1528
|
-
|
|
1981
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
1982
|
+
return {
|
|
1983
|
+
columns: outputSchema2.columns,
|
|
1984
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
1985
|
+
intent,
|
|
1986
|
+
ctx,
|
|
1987
|
+
outputSchema2,
|
|
1988
|
+
bindingDependencies
|
|
1989
|
+
)
|
|
1990
|
+
};
|
|
1529
1991
|
}
|
|
1530
1992
|
if (select.type === "aggregate") {
|
|
1531
1993
|
for (const field of select.fields ?? []) {
|
|
1532
|
-
|
|
1994
|
+
addDirectProjection(field, field);
|
|
1533
1995
|
}
|
|
1534
1996
|
for (const aggregate of select.aggregates) {
|
|
1535
1997
|
if (!aggregate.as) {
|
|
@@ -1540,7 +2002,16 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
|
1540
2002
|
}
|
|
1541
2003
|
addColumn(aggregate.as);
|
|
1542
2004
|
}
|
|
1543
|
-
|
|
2005
|
+
const outputSchema2 = { columns, directProjectionLineage };
|
|
2006
|
+
return {
|
|
2007
|
+
columns: outputSchema2.columns,
|
|
2008
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2009
|
+
intent,
|
|
2010
|
+
ctx,
|
|
2011
|
+
outputSchema2,
|
|
2012
|
+
bindingDependencies
|
|
2013
|
+
)
|
|
2014
|
+
};
|
|
1544
2015
|
}
|
|
1545
2016
|
for (const expr of select.columns) {
|
|
1546
2017
|
if (expr.kind === "column" && expr.column === "*") {
|
|
@@ -1560,9 +2031,24 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
|
|
|
1560
2031
|
`Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
|
|
1561
2032
|
);
|
|
1562
2033
|
}
|
|
1563
|
-
|
|
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
|
+
}
|
|
1564
2041
|
}
|
|
1565
|
-
|
|
2042
|
+
const outputSchema = { columns, directProjectionLineage };
|
|
2043
|
+
return {
|
|
2044
|
+
columns: outputSchema.columns,
|
|
2045
|
+
relationFilters: getBindingRelationFilterMetadata(
|
|
2046
|
+
intent,
|
|
2047
|
+
ctx,
|
|
2048
|
+
outputSchema,
|
|
2049
|
+
bindingDependencies
|
|
2050
|
+
)
|
|
2051
|
+
};
|
|
1566
2052
|
}
|
|
1567
2053
|
function validateNqlExpressionPaths(expr, ctx) {
|
|
1568
2054
|
switch (expr.type) {
|
|
@@ -2334,13 +2820,12 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
|
|
|
2334
2820
|
const relFilter = expr;
|
|
2335
2821
|
const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
|
|
2336
2822
|
const prevRelationTarget = ctx.currentRelationTarget;
|
|
2823
|
+
const bindingRelation = ctx.currentFromTable && relFilter.relation[0] ? resolveBindingRelationFilter(
|
|
2824
|
+
ctx,
|
|
2825
|
+
ctx.currentFromTable,
|
|
2826
|
+
relFilter.relation
|
|
2827
|
+
) : void 0;
|
|
2337
2828
|
if (ctx.currentFromTable && relFilter.relation[0]) {
|
|
2338
|
-
assertNoBindingRelationConstruct(
|
|
2339
|
-
ctx,
|
|
2340
|
-
ctx.currentFromTable,
|
|
2341
|
-
"use relation filters",
|
|
2342
|
-
relFilter.relation.join(".")
|
|
2343
|
-
);
|
|
2344
2829
|
if (ctx.validator) {
|
|
2345
2830
|
ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
|
|
2346
2831
|
ctx.currentFromTable,
|
|
@@ -2356,13 +2841,27 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
|
|
|
2356
2841
|
nestedOuterAliases
|
|
2357
2842
|
);
|
|
2358
2843
|
ctx.currentRelationTarget = prevRelationTarget;
|
|
2359
|
-
|
|
2844
|
+
const relationFilterIntent = {
|
|
2360
2845
|
kind: "relationFilter",
|
|
2361
2846
|
relation: relFilter.relation,
|
|
2362
2847
|
where,
|
|
2363
2848
|
mode: relFilter.mode,
|
|
2364
|
-
...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
|
+
}
|
|
2365
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;
|
|
2366
2865
|
}
|
|
2367
2866
|
function expandDateRangeList(field, patterns, negated) {
|
|
2368
2867
|
const conditions = patterns.map((pattern) => {
|
|
@@ -2854,27 +3353,42 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
2854
3353
|
}
|
|
2855
3354
|
const column = segments[segments.length - 1];
|
|
2856
3355
|
const relation = segments.slice(0, -1).join(".");
|
|
2857
|
-
|
|
3356
|
+
const bindingRelation = resolveBindingRelationColumn(
|
|
2858
3357
|
ctx,
|
|
2859
3358
|
ctx.currentFromTable,
|
|
2860
|
-
|
|
2861
|
-
|
|
3359
|
+
segments.slice(0, -1),
|
|
3360
|
+
column
|
|
2862
3361
|
);
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
3362
|
+
if (!bindingRelation) {
|
|
3363
|
+
assertNoBindingRelationConstruct(
|
|
3364
|
+
ctx,
|
|
2865
3365
|
ctx.currentFromTable,
|
|
2866
|
-
|
|
3366
|
+
"select relation columns",
|
|
3367
|
+
relation
|
|
2867
3368
|
);
|
|
3369
|
+
}
|
|
3370
|
+
if (ctx.currentFromTable && ctx.validator) {
|
|
3371
|
+
const targetTable = bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
|
|
2868
3372
|
if (targetTable) {
|
|
2869
3373
|
ctx.validator.validateColumn(targetTable, column);
|
|
2870
3374
|
}
|
|
2871
3375
|
}
|
|
2872
|
-
|
|
3376
|
+
const relationColumnIntent = {
|
|
2873
3377
|
kind: "relationColumn",
|
|
2874
3378
|
relation,
|
|
2875
3379
|
column,
|
|
2876
3380
|
as: item.alias ?? `${relation}.${column}`
|
|
2877
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;
|
|
2878
3392
|
}
|
|
2879
3393
|
function compileCaseExpression(caseExpr, item, ctx, fns) {
|
|
2880
3394
|
if (caseExpr.subject) {
|
|
@@ -3262,6 +3776,7 @@ var NqlCompiler = class {
|
|
|
3262
3776
|
recursiveKeywords,
|
|
3263
3777
|
validator,
|
|
3264
3778
|
bindingOutputColumns: /* @__PURE__ */ new Map(),
|
|
3779
|
+
bindingRelationFilters: /* @__PURE__ */ new Map(),
|
|
3265
3780
|
params,
|
|
3266
3781
|
maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
|
|
3267
3782
|
allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
|
|
@@ -3281,6 +3796,7 @@ var NqlCompiler = class {
|
|
|
3281
3796
|
return this.compileProgram(program);
|
|
3282
3797
|
} finally {
|
|
3283
3798
|
this.ctx.bindingOutputColumns.clear();
|
|
3799
|
+
this.ctx.bindingRelationFilters.clear();
|
|
3284
3800
|
this.ctx.validator?.clearVirtualBindingTables();
|
|
3285
3801
|
}
|
|
3286
3802
|
}
|
|
@@ -3364,13 +3880,15 @@ var NqlCompiler = class {
|
|
|
3364
3880
|
const outputSchema = this.registerQueryBindingOutputSchema(
|
|
3365
3881
|
bindName,
|
|
3366
3882
|
lastResult.query,
|
|
3367
|
-
bindingOutputSchemas
|
|
3883
|
+
bindingOutputSchemas,
|
|
3884
|
+
statementBindingDependencies
|
|
3368
3885
|
);
|
|
3369
3886
|
bindings.set(bindName, lastResult.query);
|
|
3370
3887
|
if (outputSchema) {
|
|
3371
3888
|
this.ctx.validator?.addVirtualBindingTable(
|
|
3372
3889
|
bindName,
|
|
3373
|
-
outputSchema.columns
|
|
3890
|
+
outputSchema.columns,
|
|
3891
|
+
outputSchema.relationFilters
|
|
3374
3892
|
);
|
|
3375
3893
|
}
|
|
3376
3894
|
materializedBindStatements.add(i);
|
|
@@ -3399,7 +3917,8 @@ var NqlCompiler = class {
|
|
|
3399
3917
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
3400
3918
|
this.ctx.validator?.addVirtualBindingTable(
|
|
3401
3919
|
bindName,
|
|
3402
|
-
outputSchema.columns
|
|
3920
|
+
outputSchema.columns,
|
|
3921
|
+
outputSchema.relationFilters
|
|
3403
3922
|
);
|
|
3404
3923
|
}
|
|
3405
3924
|
materializedBindStatements.add(i);
|
|
@@ -3439,11 +3958,22 @@ var NqlCompiler = class {
|
|
|
3439
3958
|
}
|
|
3440
3959
|
return lastResult;
|
|
3441
3960
|
}
|
|
3442
|
-
registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas) {
|
|
3961
|
+
registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas, bindingDependencies) {
|
|
3443
3962
|
try {
|
|
3444
|
-
const outputSchema = getQueryOutputSchema(
|
|
3963
|
+
const outputSchema = getQueryOutputSchema(
|
|
3964
|
+
query,
|
|
3965
|
+
this.ctx,
|
|
3966
|
+
bindName,
|
|
3967
|
+
bindingDependencies
|
|
3968
|
+
);
|
|
3445
3969
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
3446
3970
|
this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
|
|
3971
|
+
if (outputSchema.relationFilters) {
|
|
3972
|
+
this.ctx.bindingRelationFilters.set(
|
|
3973
|
+
bindName,
|
|
3974
|
+
outputSchema.relationFilters
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3447
3977
|
return outputSchema;
|
|
3448
3978
|
} catch (error) {
|
|
3449
3979
|
if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {
|