@dbsp/nql 1.5.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 +7 -2
- package/dist/index.js +348 -102
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -427,8 +427,13 @@ interface ColumnValidatorRelation {
|
|
|
427
427
|
readonly target: string;
|
|
428
428
|
readonly type?: 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany';
|
|
429
429
|
readonly foreignKey?: string | readonly string[] | undefined;
|
|
430
|
-
readonly
|
|
431
|
-
readonly
|
|
430
|
+
readonly through?: string | undefined;
|
|
431
|
+
readonly otherKey?: string | undefined;
|
|
432
|
+
readonly throughSourceKey?: string | undefined;
|
|
433
|
+
readonly throughTargetKey?: string | undefined;
|
|
434
|
+
readonly recursive?: unknown;
|
|
435
|
+
readonly sourceKey?: string | readonly string[] | undefined;
|
|
436
|
+
readonly targetKey?: string | readonly string[] | undefined;
|
|
432
437
|
}
|
|
433
438
|
/**
|
|
434
439
|
* Options for the NQL compiler.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { NQL_INTERNAL_COMPILER_OPTIONS, markNqlTrustedRelationFilter, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
|
|
2
|
-
import { NQL_SELECT_AGGREGATE_FUNCTIONS, NQL_SELECT_VALUE_FUNCTIONS, NQL_SELECT_JSON_FUNCTIONS, NQL_SELECT_SCALAR_FUNCTIONS, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction, isParamIntent } from '@dbsp/types';
|
|
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;
|
|
@@ -110,6 +108,88 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
110
108
|
getVirtualBindingScalarRelation(bindingName, relationName) {
|
|
111
109
|
return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
|
|
112
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
|
+
}
|
|
113
193
|
explainVirtualBindingRelationRejection(bindingName, relationName) {
|
|
114
194
|
const metadata = this.virtualBindingRelationFilters.get(bindingName);
|
|
115
195
|
if (!metadata) {
|
|
@@ -124,27 +204,24 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
124
204
|
if (!relation) {
|
|
125
205
|
return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
|
|
126
206
|
}
|
|
127
|
-
const
|
|
128
|
-
const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
|
|
207
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
129
208
|
if (relation.type !== "belongsTo") {
|
|
130
209
|
return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
|
|
131
210
|
}
|
|
132
|
-
if (fkColumns.length
|
|
133
|
-
return `relation '${relationName}' must have
|
|
211
|
+
if (fkColumns.length === 0) {
|
|
212
|
+
return `relation '${relationName}' must have at least one FK column for A-lite binding relation filters`;
|
|
134
213
|
}
|
|
135
|
-
const fkColumn
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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})` : ""}`;
|
|
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
|
+
}
|
|
145
222
|
}
|
|
146
223
|
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
147
|
-
return `relation '${relationName}' FK
|
|
224
|
+
return `relation '${relationName}' FK columns '${fkColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
|
|
148
225
|
}
|
|
149
226
|
explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
|
|
150
227
|
const metadata = this.virtualBindingRelationFilters.get(bindingName);
|
|
@@ -160,29 +237,25 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
160
237
|
if (!relation) {
|
|
161
238
|
return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
|
|
162
239
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
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)
|
|
240
|
+
const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
|
|
241
|
+
relationName,
|
|
242
|
+
relation
|
|
178
243
|
);
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
256
|
}
|
|
184
257
|
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
185
|
-
return `relation '${relationName}' source
|
|
258
|
+
return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
|
|
186
259
|
}
|
|
187
260
|
/**
|
|
188
261
|
* Convert camelCase to snake_case for column name matching.
|
|
@@ -281,6 +354,28 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
281
354
|
);
|
|
282
355
|
}
|
|
283
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
|
+
}
|
|
284
379
|
var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
|
|
285
380
|
"__proto__",
|
|
286
381
|
"constructor",
|
|
@@ -401,6 +496,17 @@ function throwBindingRelationColumn182(bindingName, relationName, reason) {
|
|
|
401
496
|
`Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
|
|
402
497
|
);
|
|
403
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
|
+
}
|
|
404
510
|
function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
|
|
405
511
|
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
406
512
|
const actualBindingName = bindingName;
|
|
@@ -435,19 +541,19 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
|
|
|
435
541
|
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
436
542
|
const actualBindingName = bindingName;
|
|
437
543
|
const relationName = relationPath.join(".");
|
|
438
|
-
|
|
544
|
+
const relation = relationPath[0];
|
|
545
|
+
if (!relation) {
|
|
439
546
|
throwBindingRelationColumn182(
|
|
440
547
|
actualBindingName,
|
|
441
548
|
relationName,
|
|
442
|
-
"
|
|
549
|
+
"the relation path must name a source-table relation"
|
|
443
550
|
);
|
|
444
551
|
}
|
|
445
|
-
|
|
446
|
-
if (!relation) {
|
|
552
|
+
if (!ctx.validator) {
|
|
447
553
|
throwBindingRelationColumn182(
|
|
448
554
|
actualBindingName,
|
|
449
555
|
relationName,
|
|
450
|
-
"
|
|
556
|
+
"model metadata is not available"
|
|
451
557
|
);
|
|
452
558
|
}
|
|
453
559
|
const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
|
|
@@ -459,10 +565,92 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
|
|
|
459
565
|
actualBindingName,
|
|
460
566
|
relation
|
|
461
567
|
) ?? "model metadata is not available";
|
|
462
|
-
throwBindingRelationColumn182(actualBindingName,
|
|
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
|
+
);
|
|
463
584
|
}
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
};
|
|
466
654
|
}
|
|
467
655
|
function assertNoBindingRelationPath(ctx, bindingName, path) {
|
|
468
656
|
if (!isBindingTable(ctx, bindingName)) return;
|
|
@@ -1099,7 +1287,6 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
|
1099
1287
|
...NQL_SELECT_AGGREGATE_FUNCTIONS,
|
|
1100
1288
|
...NQL_SELECT_VALUE_FUNCTIONS
|
|
1101
1289
|
]);
|
|
1102
|
-
var DEFAULT_RELATION_TARGET_COLUMN = "id";
|
|
1103
1290
|
function compileNestedQuery(query, ctx, fns, bindings) {
|
|
1104
1291
|
const savedContext = {
|
|
1105
1292
|
currentFromTable: ctx.currentFromTable,
|
|
@@ -1182,19 +1369,9 @@ function selectContainsAggregate(select) {
|
|
|
1182
1369
|
);
|
|
1183
1370
|
}
|
|
1184
1371
|
function relationForeignKeysOnSource(relation) {
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
1187
|
-
|
|
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";
|
|
1372
|
+
if (relation?.type !== "belongsTo") return void 0;
|
|
1373
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
1374
|
+
return fkColumns.length > 0 ? fkColumns : void 0;
|
|
1198
1375
|
}
|
|
1199
1376
|
function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
|
|
1200
1377
|
if (!ctx.validator) return "model metadata is not available";
|
|
@@ -1229,48 +1406,70 @@ function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionL
|
|
|
1229
1406
|
(projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
|
|
1230
1407
|
);
|
|
1231
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
|
+
}
|
|
1232
1422
|
function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1233
1423
|
const fkColumns = relationForeignKeysOnSource(relation);
|
|
1234
|
-
if (!relation || !fkColumns
|
|
1235
|
-
const
|
|
1236
|
-
const sourceProjection = findDirectSourceProjection(
|
|
1424
|
+
if (!relation || !fkColumns) return void 0;
|
|
1425
|
+
const sourceProjections = findDirectSourceProjections(
|
|
1237
1426
|
sourceTable,
|
|
1238
|
-
|
|
1427
|
+
fkColumns,
|
|
1239
1428
|
directProjectionLineage
|
|
1240
1429
|
);
|
|
1241
|
-
if (!
|
|
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;
|
|
1242
1434
|
return {
|
|
1243
1435
|
relation: relation.name,
|
|
1244
1436
|
sourceTable,
|
|
1245
1437
|
targetTable: relation.target,
|
|
1246
|
-
sourceColumn:
|
|
1247
|
-
|
|
1438
|
+
sourceColumn: sourceProjections.map(
|
|
1439
|
+
(projection) => projection.outputColumn
|
|
1440
|
+
),
|
|
1441
|
+
targetColumn: targetColumns,
|
|
1442
|
+
hops: [],
|
|
1248
1443
|
cardinality: "one"
|
|
1249
1444
|
};
|
|
1250
1445
|
}
|
|
1251
1446
|
function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1252
1447
|
if (!relation) return void 0;
|
|
1253
|
-
if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
|
|
1448
|
+
if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
|
|
1254
1449
|
return void 0;
|
|
1255
1450
|
}
|
|
1256
|
-
const
|
|
1257
|
-
if (!
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const
|
|
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(
|
|
1262
1457
|
sourceTable,
|
|
1263
|
-
sourceJoinColumn,
|
|
1458
|
+
joinColumns.sourceJoinColumn,
|
|
1264
1459
|
directProjectionLineage
|
|
1265
1460
|
);
|
|
1266
|
-
if (!
|
|
1461
|
+
if (!sourceProjections) return void 0;
|
|
1267
1462
|
return {
|
|
1268
1463
|
relation: relation.name,
|
|
1269
1464
|
sourceTable,
|
|
1270
1465
|
targetTable: relation.target,
|
|
1271
|
-
sourceColumn:
|
|
1272
|
-
|
|
1273
|
-
|
|
1466
|
+
sourceColumn: sourceProjections.map(
|
|
1467
|
+
(projection) => projection.outputColumn
|
|
1468
|
+
),
|
|
1469
|
+
targetColumn: joinColumns.targetJoinColumn,
|
|
1470
|
+
hops: [],
|
|
1471
|
+
cardinality: relationCardinality(relation),
|
|
1472
|
+
relationType: relation.type
|
|
1274
1473
|
};
|
|
1275
1474
|
}
|
|
1276
1475
|
function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
|
|
@@ -1338,6 +1537,19 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
|
1338
1537
|
}
|
|
1339
1538
|
assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
|
|
1340
1539
|
}
|
|
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
|
+
}
|
|
1341
1553
|
function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
|
|
1342
1554
|
const fn = expr.name.toLowerCase();
|
|
1343
1555
|
if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
|
|
@@ -1538,12 +1750,7 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
|
|
|
1538
1750
|
case "star":
|
|
1539
1751
|
break;
|
|
1540
1752
|
case "relationStar":
|
|
1541
|
-
|
|
1542
|
-
ctx,
|
|
1543
|
-
bindingName,
|
|
1544
|
-
"select relation columns",
|
|
1545
|
-
item.relation.join(".")
|
|
1546
|
-
);
|
|
1753
|
+
resolveBindingRelationInclude(ctx, bindingName, item.relation);
|
|
1547
1754
|
break;
|
|
1548
1755
|
case "expression":
|
|
1549
1756
|
if (item.expression.type === "path" && item.expression.segments.length > 1) {
|
|
@@ -1779,21 +1986,38 @@ function compileQueryInternal(query, ctx, fns, bindings) {
|
|
|
1779
1986
|
}
|
|
1780
1987
|
}
|
|
1781
1988
|
}
|
|
1782
|
-
if (select && select.type === "expressions"
|
|
1989
|
+
if (select && select.type === "expressions") {
|
|
1783
1990
|
const relationPaths = /* @__PURE__ */ new Set();
|
|
1784
1991
|
for (const expr of select.columns) {
|
|
1785
1992
|
if (expr.kind === "relationColumn") {
|
|
1786
|
-
|
|
1993
|
+
if (!isBindingSource || expr.column === "*") {
|
|
1994
|
+
relationPaths.add(expr.relation);
|
|
1995
|
+
}
|
|
1787
1996
|
}
|
|
1788
1997
|
}
|
|
1789
1998
|
if (relationPaths.size > 0) {
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
);
|
|
1795
|
-
|
|
1796
|
-
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
|
+
}
|
|
1797
2021
|
}
|
|
1798
2022
|
}
|
|
1799
2023
|
}
|
|
@@ -2858,6 +3082,7 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
|
|
|
2858
3082
|
targetTable: bindingRelation.targetTable,
|
|
2859
3083
|
sourceColumn: bindingRelation.sourceColumn,
|
|
2860
3084
|
targetColumn: bindingRelation.targetColumn,
|
|
3085
|
+
hops: bindingRelation.hops,
|
|
2861
3086
|
...bindingRelation.cardinality !== void 0 && {
|
|
2862
3087
|
cardinality: bindingRelation.cardinality
|
|
2863
3088
|
}
|
|
@@ -2967,6 +3192,20 @@ var SELECT_SCALAR_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
|
2967
3192
|
...NQL_SELECT_JSON_FUNCTIONS,
|
|
2968
3193
|
...NQL_SELECT_SCALAR_FUNCTIONS
|
|
2969
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
|
+
}
|
|
2970
3209
|
function validateSelectPathExpression(expr, ctx) {
|
|
2971
3210
|
if (!ctx.currentFromTable) return;
|
|
2972
3211
|
const { segments } = expr;
|
|
@@ -3022,12 +3261,14 @@ function compileSelectClause(clause, ctx, fns) {
|
|
|
3022
3261
|
} else if (item.type === "relationStar") {
|
|
3023
3262
|
hasExpressions = true;
|
|
3024
3263
|
const relation = item.relation.join(".");
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3264
|
+
if (!resolveBindingRelationInclude2(ctx, ctx.currentFromTable, item.relation)) {
|
|
3265
|
+
assertNoBindingRelationConstruct(
|
|
3266
|
+
ctx,
|
|
3267
|
+
ctx.currentFromTable,
|
|
3268
|
+
"select relation columns",
|
|
3269
|
+
relation
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3031
3272
|
expressions.push({
|
|
3032
3273
|
kind: "relationColumn",
|
|
3033
3274
|
relation,
|
|
@@ -3368,7 +3609,8 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
3368
3609
|
);
|
|
3369
3610
|
}
|
|
3370
3611
|
if (ctx.currentFromTable && ctx.validator) {
|
|
3371
|
-
const
|
|
3612
|
+
const lastBindingHop = bindingRelation?.hops[bindingRelation.hops.length - 1];
|
|
3613
|
+
const targetTable = lastBindingHop?.target ?? bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
|
|
3372
3614
|
if (targetTable) {
|
|
3373
3615
|
ctx.validator.validateColumn(targetTable, column);
|
|
3374
3616
|
}
|
|
@@ -3384,9 +3626,13 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
3384
3626
|
targetTable: bindingRelation.targetTable,
|
|
3385
3627
|
sourceColumn: bindingRelation.sourceColumn,
|
|
3386
3628
|
targetColumn: bindingRelation.targetColumn,
|
|
3629
|
+
hops: bindingRelation.hops,
|
|
3387
3630
|
selectedColumn: column,
|
|
3388
3631
|
...bindingRelation.cardinality !== void 0 && {
|
|
3389
3632
|
cardinality: bindingRelation.cardinality
|
|
3633
|
+
},
|
|
3634
|
+
...bindingRelation.relationType !== void 0 && {
|
|
3635
|
+
relationType: bindingRelation.relationType
|
|
3390
3636
|
}
|
|
3391
3637
|
}) : relationColumnIntent;
|
|
3392
3638
|
}
|