@dbsp/nql 1.6.0 → 1.8.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 +70 -46
- package/dist/index.js +635 -107
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -43,6 +43,69 @@ var NqlSemanticException = class extends Error {
|
|
|
43
43
|
this.relatedSymbol = relatedSymbol;
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
+
|
|
47
|
+
// src/compiler/binding-column-types.ts
|
|
48
|
+
function freezeColumnTypes(types) {
|
|
49
|
+
for (const info of Object.values(types)) {
|
|
50
|
+
Object.freeze(info);
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(types);
|
|
53
|
+
}
|
|
54
|
+
function buildBindingColumnTypes(columns, candidates) {
|
|
55
|
+
const attemptsByColumn = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
const bucket = attemptsByColumn.get(candidate.column);
|
|
58
|
+
if (bucket) {
|
|
59
|
+
bucket.push(candidate);
|
|
60
|
+
} else {
|
|
61
|
+
attemptsByColumn.set(candidate.column, [candidate]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const types = /* @__PURE__ */ Object.create(
|
|
65
|
+
null
|
|
66
|
+
);
|
|
67
|
+
for (const column of columns) {
|
|
68
|
+
const attempts = attemptsByColumn.get(column) ?? [];
|
|
69
|
+
if (attempts.length > 1) {
|
|
70
|
+
return { untypeable: { column, reason: "duplicate-output-name" } };
|
|
71
|
+
}
|
|
72
|
+
const candidate = attempts[0];
|
|
73
|
+
if (candidate?.typed === void 0) {
|
|
74
|
+
return {
|
|
75
|
+
untypeable: {
|
|
76
|
+
column,
|
|
77
|
+
reason: candidate?.untypeable ?? "unresolvable-source"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
types[column] = candidate.typed;
|
|
82
|
+
}
|
|
83
|
+
return { columnTypes: freezeColumnTypes(types) };
|
|
84
|
+
}
|
|
85
|
+
var DEFAULT_RELATION_TARGET_COLUMN = "id";
|
|
86
|
+
function relationForeignKeys(relation) {
|
|
87
|
+
if (!relation) return void 0;
|
|
88
|
+
const columns = toColumnList(relation.foreignKey);
|
|
89
|
+
return columns.length > 0 ? columns : void 0;
|
|
90
|
+
}
|
|
91
|
+
function relationCardinality(relation) {
|
|
92
|
+
return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
|
|
93
|
+
}
|
|
94
|
+
function scalarRelationJoinColumns(relation) {
|
|
95
|
+
if (!relation) return void 0;
|
|
96
|
+
if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
const fkColumns = relationForeignKeys(relation);
|
|
100
|
+
if (!fkColumns) return void 0;
|
|
101
|
+
const sourceKeys = toColumnList(relation.sourceKey);
|
|
102
|
+
const targetKeys = toColumnList(relation.targetKey);
|
|
103
|
+
const sourceJoinColumn = relation.type === "belongsTo" ? fkColumns : sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN];
|
|
104
|
+
const targetJoinColumn = relation.type === "belongsTo" ? targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN] : fkColumns;
|
|
105
|
+
return { sourceJoinColumn, targetJoinColumn };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/compiler/column-validator.ts
|
|
46
109
|
var ColumnValidator = class _ColumnValidator {
|
|
47
110
|
constructor(schema) {
|
|
48
111
|
this.schema = schema;
|
|
@@ -90,6 +153,31 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
90
153
|
if (virtualColumns) return virtualColumns;
|
|
91
154
|
return this.schema.getTable(name)?.columns.map((column) => column.name);
|
|
92
155
|
}
|
|
156
|
+
getPhysicalTableColumns(name) {
|
|
157
|
+
return this.schema.getTable(name)?.columns.map((column) => column.name);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a physical column's neutral type info for binding-snapshot
|
|
161
|
+
* type propagation (#213). Returns undefined when the table/column is
|
|
162
|
+
* unknown OR the schema did not supply a `type` for it — the caller
|
|
163
|
+
* treats undefined as untypeable, never falls back to a guess.
|
|
164
|
+
*/
|
|
165
|
+
getTableColumnType(table, column) {
|
|
166
|
+
const resolvedName = this.resolvePhysicalColumnName(table, column);
|
|
167
|
+
if (resolvedName === void 0) return void 0;
|
|
168
|
+
const columnInfo = this.schema.getTable(table)?.columns.find((candidate) => candidate.name === resolvedName);
|
|
169
|
+
if (columnInfo?.type === void 0) return void 0;
|
|
170
|
+
return {
|
|
171
|
+
kind: "column",
|
|
172
|
+
type: columnInfo.type,
|
|
173
|
+
...columnInfo.originalDbType !== void 0 && {
|
|
174
|
+
originalDbType: columnInfo.originalDbType
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
getPseudoColumns(name) {
|
|
179
|
+
return this.schema.getTable(name)?.pseudoColumns ?? [];
|
|
180
|
+
}
|
|
93
181
|
hasPhysicalTable(name) {
|
|
94
182
|
return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
|
|
95
183
|
}
|
|
@@ -233,10 +321,54 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
233
321
|
if (!sourceTable) {
|
|
234
322
|
return "the binding source table could not be proven";
|
|
235
323
|
}
|
|
324
|
+
const recursiveReason = this.explainVirtualBindingRecursiveScalarRelationRejection(
|
|
325
|
+
bindingName,
|
|
326
|
+
relationName,
|
|
327
|
+
sourceTable,
|
|
328
|
+
metadata
|
|
329
|
+
);
|
|
330
|
+
if (recursiveReason) return recursiveReason;
|
|
236
331
|
const relation = this.getRelation(sourceTable, relationName);
|
|
237
332
|
if (!relation) {
|
|
238
333
|
return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
|
|
239
334
|
}
|
|
335
|
+
if (relation.type === "belongsToMany") {
|
|
336
|
+
if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
|
|
337
|
+
return `relation '${relationName}' is recursive/self-referential; binding relation columns for recursive relations require recursive CTE handling and are not supported (ref-#193)`;
|
|
338
|
+
}
|
|
339
|
+
if (typeof relation.through !== "string" || relation.through.length === 0) {
|
|
340
|
+
return `relation '${relationName}' is many-to-many but does not declare a resolvable junction table (ref-#192)`;
|
|
341
|
+
}
|
|
342
|
+
const sourceColumns2 = toColumnList(relation.sourceKey).length > 0 ? toColumnList(relation.sourceKey) : ["id"];
|
|
343
|
+
const throughSourceColumns = toColumnList(relation.foreignKey);
|
|
344
|
+
const throughTargetColumns = toColumnList(relation.otherKey);
|
|
345
|
+
const targetColumns = toColumnList(relation.targetKey).length > 0 ? toColumnList(relation.targetKey) : ["id"];
|
|
346
|
+
if (sourceColumns2.length !== 1) {
|
|
347
|
+
return `relation '${relationName}' source key is composite; binding many-to-many relation columns require a single source key column (ref-#179)`;
|
|
348
|
+
}
|
|
349
|
+
if (throughSourceColumns.length !== 1) {
|
|
350
|
+
return `relation '${relationName}' junction source FK is composite or missing; binding many-to-many relation columns require a single junction source FK column (ref-#179)`;
|
|
351
|
+
}
|
|
352
|
+
if (throughTargetColumns.length !== 1) {
|
|
353
|
+
return `relation '${relationName}' junction target FK is composite or missing; binding many-to-many relation columns require a single junction target FK column (ref-#179)`;
|
|
354
|
+
}
|
|
355
|
+
if (targetColumns.length !== 1) {
|
|
356
|
+
return `relation '${relationName}' target key is composite; binding many-to-many relation columns require a single target key column (ref-#179)`;
|
|
357
|
+
}
|
|
358
|
+
const [sourceColumn] = sourceColumns2;
|
|
359
|
+
if (sourceColumn === void 0) {
|
|
360
|
+
return `relation '${relationName}' source key is missing; binding many-to-many relation columns require a single source key column (ref-#179)`;
|
|
361
|
+
}
|
|
362
|
+
const directProjection = metadata.directProjectionLineage?.find(
|
|
363
|
+
(projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
|
|
364
|
+
);
|
|
365
|
+
if (!directProjection) {
|
|
366
|
+
const available3 = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
367
|
+
return `relation '${relationName}' source key column '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available3 ? ` (available columns: ${available3})` : ""}`;
|
|
368
|
+
}
|
|
369
|
+
const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
370
|
+
return `relation '${relationName}' many-to-many source column '${sourceColumn}' is not available through binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
|
|
371
|
+
}
|
|
240
372
|
const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
|
|
241
373
|
relationName,
|
|
242
374
|
relation
|
|
@@ -257,6 +389,64 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
257
389
|
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
258
390
|
return `relation '${relationName}' source columns '${sourceColumns.join(", ")}' are not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
|
|
259
391
|
}
|
|
392
|
+
static recursiveDirection(value) {
|
|
393
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
394
|
+
const direction = value.direction;
|
|
395
|
+
if (direction === "up" || direction === "ancestors") return "up";
|
|
396
|
+
if (direction === "down" || direction === "descendants") return "down";
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
explainVirtualBindingRecursiveScalarRelationRejection(bindingName, relationName, sourceTable, metadata) {
|
|
400
|
+
const lowerRelationName = relationName.toLowerCase();
|
|
401
|
+
const pseudoColumn = this.getPseudoColumns(sourceTable).find(
|
|
402
|
+
(candidate) => candidate.ascendantKeyword?.toLowerCase() === lowerRelationName || candidate.descendantKeyword?.toLowerCase() === lowerRelationName
|
|
403
|
+
);
|
|
404
|
+
if (!pseudoColumn) return void 0;
|
|
405
|
+
const direction = pseudoColumn.ascendantKeyword?.toLowerCase() === lowerRelationName ? "up" : "down";
|
|
406
|
+
const recursiveRelations = this.getRelationsFrom(sourceTable).filter(
|
|
407
|
+
(relation) => relation.recursive !== void 0 && relation.source === relation.target && relation.source === sourceTable && _ColumnValidator.recursiveDirection(relation.recursive) === direction
|
|
408
|
+
);
|
|
409
|
+
if (recursiveRelations.length === 0) {
|
|
410
|
+
return `recursive traversal '${relationName}' is missing recursive relation metadata for direction '${direction}' (ref-#193)`;
|
|
411
|
+
}
|
|
412
|
+
for (const relation of recursiveRelations) {
|
|
413
|
+
const relationFkColumns = toColumnList(relation.foreignKey);
|
|
414
|
+
const fkColumns = relationFkColumns.length > 0 ? relationFkColumns : toColumnList(pseudoColumn.foreignKeyColumn);
|
|
415
|
+
if (fkColumns.length !== 1) {
|
|
416
|
+
return `relation '${relation.name}' self-ref FK is composite or missing; binding recursive relation columns require a single self-ref FK column (ref-#193)`;
|
|
417
|
+
}
|
|
418
|
+
const relationTargetColumns = toColumnList(relation.targetKey);
|
|
419
|
+
const targetColumns = relationTargetColumns.length > 0 ? relationTargetColumns : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
|
|
420
|
+
if (targetColumns.length !== 1) {
|
|
421
|
+
return `relation '${relation.name}' target key is composite; binding recursive relation columns require a single target key column (ref-#193)`;
|
|
422
|
+
}
|
|
423
|
+
const selfRefColumn = fkColumns[0];
|
|
424
|
+
const targetKeyColumn = targetColumns[0];
|
|
425
|
+
if (selfRefColumn === void 0 || targetKeyColumn === void 0) {
|
|
426
|
+
return `relation '${relation.name}' recursive metadata is missing a single-column seed; binding recursive relation columns require a single self-ref FK and target key column (ref-#193)`;
|
|
427
|
+
}
|
|
428
|
+
if (pseudoColumn.foreignKeyColumn !== void 0 && !_ColumnValidator.columnsMatch(
|
|
429
|
+
pseudoColumn.foreignKeyColumn,
|
|
430
|
+
selfRefColumn
|
|
431
|
+
) || pseudoColumn.targetColumn !== void 0 && !_ColumnValidator.columnsMatch(
|
|
432
|
+
pseudoColumn.targetColumn,
|
|
433
|
+
targetKeyColumn
|
|
434
|
+
)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
|
|
438
|
+
const directProjection = metadata.directProjectionLineage?.find(
|
|
439
|
+
(projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, seedColumn)
|
|
440
|
+
);
|
|
441
|
+
if (!directProjection) {
|
|
442
|
+
const available = this.virtualBindingTables.get(bindingName)?.join(", ");
|
|
443
|
+
const seedLabel = direction === "up" ? "self-ref FK column" : "target key column";
|
|
444
|
+
return `recursive traversal '${relationName}' ${seedLabel} '${seedColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available ? ` (available columns: ${available})` : ""} (ref-#193)`;
|
|
445
|
+
}
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
return `recursive traversal '${relationName}' pseudo metadata does not match a single-column recursive self-reference (ref-#193)`;
|
|
449
|
+
}
|
|
260
450
|
/**
|
|
261
451
|
* Convert camelCase to snake_case for column name matching.
|
|
262
452
|
* NQL queries may use either form (e.g., viewCount or view_count).
|
|
@@ -288,6 +478,14 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
288
478
|
(c) => _ColumnValidator.columnsMatch(column, c.name)
|
|
289
479
|
)?.name;
|
|
290
480
|
}
|
|
481
|
+
resolvePhysicalColumnName(table, column) {
|
|
482
|
+
if (column === "*") return column;
|
|
483
|
+
const tableInfo = this.schema.getTable(table);
|
|
484
|
+
if (!tableInfo) return void 0;
|
|
485
|
+
return tableInfo.columns.find(
|
|
486
|
+
(c) => _ColumnValidator.columnsMatch(column, c.name)
|
|
487
|
+
)?.name;
|
|
488
|
+
}
|
|
291
489
|
validateColumn(table, column) {
|
|
292
490
|
if (column === "*") return;
|
|
293
491
|
const virtualColumns = this.virtualBindingTables.get(table);
|
|
@@ -354,28 +552,6 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
354
552
|
);
|
|
355
553
|
}
|
|
356
554
|
};
|
|
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
|
-
}
|
|
379
555
|
var FORBIDDEN_PARAM_NAMES = /* @__PURE__ */ new Set([
|
|
380
556
|
"__proto__",
|
|
381
557
|
"constructor",
|
|
@@ -652,6 +828,43 @@ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedCo
|
|
|
652
828
|
hops
|
|
653
829
|
};
|
|
654
830
|
}
|
|
831
|
+
function resolveBindingRecursiveRelationColumn(ctx, bindingName, traversal, selectedColumn, maxDepth) {
|
|
832
|
+
if (!isBindingTable(ctx, bindingName)) return void 0;
|
|
833
|
+
const traversalName = traversal.toLowerCase();
|
|
834
|
+
const isRecursiveTraversal = ctx.recursiveKeywords.has(traversalName) || traversalName === "ascendant" || traversalName === "descendant";
|
|
835
|
+
const actualBindingName = bindingName;
|
|
836
|
+
if (!ctx.validator) {
|
|
837
|
+
if (!isRecursiveTraversal) return void 0;
|
|
838
|
+
throwBindingRelationColumn182(
|
|
839
|
+
actualBindingName,
|
|
840
|
+
traversalName,
|
|
841
|
+
"model metadata is not available"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const virtualRelation = ctx.validator.getVirtualBindingScalarRelation(
|
|
845
|
+
actualBindingName,
|
|
846
|
+
traversalName
|
|
847
|
+
);
|
|
848
|
+
if (!isRecursiveTraversal && !virtualRelation?.recursive) {
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
if (!virtualRelation?.recursive) {
|
|
852
|
+
const reason = ctx.validator.explainVirtualBindingScalarRelationRejection(
|
|
853
|
+
actualBindingName,
|
|
854
|
+
traversalName
|
|
855
|
+
);
|
|
856
|
+
throwBindingRelationColumn182(actualBindingName, traversalName, reason);
|
|
857
|
+
}
|
|
858
|
+
ctx.validator.validateColumn(virtualRelation.targetTable, selectedColumn);
|
|
859
|
+
if (maxDepth === void 0) return virtualRelation;
|
|
860
|
+
return {
|
|
861
|
+
...virtualRelation,
|
|
862
|
+
recursive: {
|
|
863
|
+
...virtualRelation.recursive,
|
|
864
|
+
maxDepth
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
655
868
|
function assertNoBindingRelationPath(ctx, bindingName, path) {
|
|
656
869
|
if (!isBindingTable(ctx, bindingName)) return;
|
|
657
870
|
throw new NqlSemanticException(
|
|
@@ -964,11 +1177,14 @@ function compileMutationPipeline(pipeline, ctx, fns, bindings) {
|
|
|
964
1177
|
ctx.currentFromTable = pipeline.mutation.table;
|
|
965
1178
|
const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
|
|
966
1179
|
let returning;
|
|
1180
|
+
let returningItems;
|
|
967
1181
|
for (const clause of pipeline.clauses) {
|
|
968
1182
|
if (clause.type === "select") {
|
|
969
|
-
|
|
1183
|
+
returningItems = extractReturningItems(clause, ctx);
|
|
1184
|
+
returning = returningItems === "star" ? ["*"] : returningItems.map((item) => item.output);
|
|
970
1185
|
}
|
|
971
1186
|
}
|
|
1187
|
+
ctx.lastMutationReturningItems = returningItems;
|
|
972
1188
|
if (returning) {
|
|
973
1189
|
return {
|
|
974
1190
|
mutation: { ...mutation, returning }
|
|
@@ -1156,11 +1372,11 @@ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
|
|
|
1156
1372
|
}
|
|
1157
1373
|
};
|
|
1158
1374
|
}
|
|
1159
|
-
function
|
|
1160
|
-
const
|
|
1375
|
+
function extractReturningItems(clause, ctx) {
|
|
1376
|
+
const items = [];
|
|
1161
1377
|
for (const item of clause.items) {
|
|
1162
1378
|
if (item.type === "star") {
|
|
1163
|
-
return
|
|
1379
|
+
return "star";
|
|
1164
1380
|
}
|
|
1165
1381
|
if (item.type === "expression") {
|
|
1166
1382
|
const field = expressionToField(item.expression);
|
|
@@ -1168,11 +1384,14 @@ function extractReturningColumns(clause, ctx) {
|
|
|
1168
1384
|
if (ctx.currentFromTable && !field.includes(".")) {
|
|
1169
1385
|
ctx.validator?.validateColumn(ctx.currentFromTable, field);
|
|
1170
1386
|
}
|
|
1171
|
-
|
|
1387
|
+
items.push({
|
|
1388
|
+
output: item.alias ?? field,
|
|
1389
|
+
aliased: item.alias !== void 0
|
|
1390
|
+
});
|
|
1172
1391
|
}
|
|
1173
1392
|
}
|
|
1174
1393
|
}
|
|
1175
|
-
return
|
|
1394
|
+
return items;
|
|
1176
1395
|
}
|
|
1177
1396
|
function resolveBindingsInWhere(where, bindings) {
|
|
1178
1397
|
if (!bindings || bindings.size === 0) return where;
|
|
@@ -1287,6 +1506,12 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
|
1287
1506
|
...NQL_SELECT_AGGREGATE_FUNCTIONS,
|
|
1288
1507
|
...NQL_SELECT_VALUE_FUNCTIONS
|
|
1289
1508
|
]);
|
|
1509
|
+
var UnresolvedSelectAllOutputSchemaError = class extends NqlSemanticException {
|
|
1510
|
+
constructor(message) {
|
|
1511
|
+
super(NqlErrorCodes.SEM_INVALID_SYNTAX, message);
|
|
1512
|
+
this.name = "UnresolvedSelectAllOutputSchemaError";
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1290
1515
|
function compileNestedQuery(query, ctx, fns, bindings) {
|
|
1291
1516
|
const savedContext = {
|
|
1292
1517
|
currentFromTable: ctx.currentFromTable,
|
|
@@ -1373,6 +1598,21 @@ function relationForeignKeysOnSource(relation) {
|
|
|
1373
1598
|
const fkColumns = toColumnList(relation.foreignKey);
|
|
1374
1599
|
return fkColumns.length > 0 ? fkColumns : void 0;
|
|
1375
1600
|
}
|
|
1601
|
+
function recursiveDirection(recursive) {
|
|
1602
|
+
if (recursive === null || typeof recursive !== "object") return void 0;
|
|
1603
|
+
const direction = recursive.direction;
|
|
1604
|
+
if (direction === "up" || direction === "ancestors") return "up";
|
|
1605
|
+
if (direction === "down" || direction === "descendants") return "down";
|
|
1606
|
+
return void 0;
|
|
1607
|
+
}
|
|
1608
|
+
function recursiveMaxDepth(recursive) {
|
|
1609
|
+
if (recursive === null || typeof recursive !== "object") return void 0;
|
|
1610
|
+
const maxDepth = recursive.maxDepth;
|
|
1611
|
+
return typeof maxDepth === "number" && Number.isSafeInteger(maxDepth) && maxDepth > 0 ? maxDepth : void 0;
|
|
1612
|
+
}
|
|
1613
|
+
function pseudoColumnMatchesSelfRef(pseudoColumn, sourceTable, selfRefColumn, targetKeyColumn) {
|
|
1614
|
+
return (pseudoColumn.table === void 0 || pseudoColumn.table === sourceTable) && (pseudoColumn.foreignKeyColumn === void 0 || columnsMatch(pseudoColumn.foreignKeyColumn, selfRefColumn)) && (pseudoColumn.targetColumn === void 0 || columnsMatch(pseudoColumn.targetColumn, targetKeyColumn));
|
|
1615
|
+
}
|
|
1376
1616
|
function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
|
|
1377
1617
|
if (!ctx.validator) return "model metadata is not available";
|
|
1378
1618
|
if (!ctx.validator.hasQualifiedRelationLookup()) {
|
|
@@ -1443,8 +1683,119 @@ function virtualRelationForBinding(relation, sourceTable, directProjectionLineag
|
|
|
1443
1683
|
cardinality: "one"
|
|
1444
1684
|
};
|
|
1445
1685
|
}
|
|
1686
|
+
function singleResolvedColumn(columns) {
|
|
1687
|
+
return columns.length === 1 ? columns[0] : void 0;
|
|
1688
|
+
}
|
|
1689
|
+
function manyToManyVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1690
|
+
if (relation?.type !== "belongsToMany") return void 0;
|
|
1691
|
+
if (typeof relation.through !== "string" || relation.through.length === 0 || relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
|
|
1692
|
+
return void 0;
|
|
1693
|
+
}
|
|
1694
|
+
const sourceKeys = toColumnList(relation.sourceKey);
|
|
1695
|
+
const sourceColumn = singleResolvedColumn(
|
|
1696
|
+
sourceKeys.length > 0 ? sourceKeys : [DEFAULT_RELATION_TARGET_COLUMN]
|
|
1697
|
+
);
|
|
1698
|
+
const targetKeys = toColumnList(relation.targetKey);
|
|
1699
|
+
const targetColumn = singleResolvedColumn(
|
|
1700
|
+
targetKeys.length > 0 ? targetKeys : [DEFAULT_RELATION_TARGET_COLUMN]
|
|
1701
|
+
);
|
|
1702
|
+
const throughSourceColumn = singleResolvedColumn(
|
|
1703
|
+
toColumnList(relation.foreignKey)
|
|
1704
|
+
);
|
|
1705
|
+
const throughTargetColumn = singleResolvedColumn(
|
|
1706
|
+
toColumnList(relation.otherKey)
|
|
1707
|
+
);
|
|
1708
|
+
if (!sourceColumn || !targetColumn || !throughSourceColumn || !throughTargetColumn) {
|
|
1709
|
+
return void 0;
|
|
1710
|
+
}
|
|
1711
|
+
const sourceProjection = findDirectSourceProjection(
|
|
1712
|
+
sourceTable,
|
|
1713
|
+
sourceColumn,
|
|
1714
|
+
directProjectionLineage
|
|
1715
|
+
);
|
|
1716
|
+
if (!sourceProjection) return void 0;
|
|
1717
|
+
return {
|
|
1718
|
+
relation: relation.name,
|
|
1719
|
+
sourceTable,
|
|
1720
|
+
targetTable: relation.target,
|
|
1721
|
+
sourceColumn: [sourceProjection.outputColumn],
|
|
1722
|
+
targetColumn: [targetColumn],
|
|
1723
|
+
hops: [],
|
|
1724
|
+
through: relation.through,
|
|
1725
|
+
throughSourceColumn,
|
|
1726
|
+
throughTargetColumn,
|
|
1727
|
+
cardinality: "many",
|
|
1728
|
+
relationType: "manyToMany"
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function recursiveVirtualRelationForBinding(relation, sourceTable, directProjectionLineage, pseudoColumns) {
|
|
1732
|
+
const direction = recursiveDirection(relation?.recursive);
|
|
1733
|
+
if (!relation || !direction) return void 0;
|
|
1734
|
+
if (relation.source !== relation.target || relation.source !== sourceTable) {
|
|
1735
|
+
return void 0;
|
|
1736
|
+
}
|
|
1737
|
+
const fkColumns = toColumnList(relation.foreignKey);
|
|
1738
|
+
const targetKeys = toColumnList(relation.targetKey);
|
|
1739
|
+
const pseudoColumn = pseudoColumns.find((candidate) => {
|
|
1740
|
+
const candidateFkColumns = fkColumns.length > 0 ? fkColumns : toColumnList(candidate.foreignKeyColumn);
|
|
1741
|
+
const candidateTargetColumns = targetKeys.length > 0 ? targetKeys : candidate.targetColumn !== void 0 ? [candidate.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN];
|
|
1742
|
+
const candidateSelfRefColumn = singleResolvedColumn(candidateFkColumns);
|
|
1743
|
+
const candidateTargetKeyColumn = singleResolvedColumn(
|
|
1744
|
+
candidateTargetColumns
|
|
1745
|
+
);
|
|
1746
|
+
return candidateSelfRefColumn !== void 0 && candidateTargetKeyColumn !== void 0 && pseudoColumnMatchesSelfRef(
|
|
1747
|
+
candidate,
|
|
1748
|
+
sourceTable,
|
|
1749
|
+
candidateSelfRefColumn,
|
|
1750
|
+
candidateTargetKeyColumn
|
|
1751
|
+
);
|
|
1752
|
+
});
|
|
1753
|
+
if (!pseudoColumn) return void 0;
|
|
1754
|
+
const selfRefColumn = singleResolvedColumn(
|
|
1755
|
+
fkColumns.length > 0 ? fkColumns : toColumnList(pseudoColumn.foreignKeyColumn)
|
|
1756
|
+
);
|
|
1757
|
+
const targetKeyColumn = singleResolvedColumn(
|
|
1758
|
+
targetKeys.length > 0 ? targetKeys : pseudoColumn.targetColumn !== void 0 ? [pseudoColumn.targetColumn] : [DEFAULT_RELATION_TARGET_COLUMN]
|
|
1759
|
+
);
|
|
1760
|
+
if (!selfRefColumn || !targetKeyColumn) return void 0;
|
|
1761
|
+
const relationName = direction === "up" ? pseudoColumn.ascendantKeyword : pseudoColumn.descendantKeyword;
|
|
1762
|
+
if (!relationName) return void 0;
|
|
1763
|
+
const seedColumn = direction === "up" ? selfRefColumn : targetKeyColumn;
|
|
1764
|
+
const sourceProjection = findDirectSourceProjection(
|
|
1765
|
+
sourceTable,
|
|
1766
|
+
seedColumn,
|
|
1767
|
+
directProjectionLineage
|
|
1768
|
+
);
|
|
1769
|
+
if (!sourceProjection) return void 0;
|
|
1770
|
+
return {
|
|
1771
|
+
relation: relationName.toLowerCase(),
|
|
1772
|
+
sourceTable,
|
|
1773
|
+
targetTable: relation.target,
|
|
1774
|
+
sourceColumn: [sourceProjection.outputColumn],
|
|
1775
|
+
targetColumn: [direction === "up" ? targetKeyColumn : selfRefColumn],
|
|
1776
|
+
hops: [],
|
|
1777
|
+
cardinality: "many",
|
|
1778
|
+
relationType: relation.type,
|
|
1779
|
+
recursive: {
|
|
1780
|
+
direction,
|
|
1781
|
+
maxDepth: recursiveMaxDepth(relation.recursive) ?? 10,
|
|
1782
|
+
selfRefColumn,
|
|
1783
|
+
targetKeyColumn
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1446
1787
|
function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
|
|
1447
1788
|
if (!relation) return void 0;
|
|
1789
|
+
if (relation.type === "belongsToMany") {
|
|
1790
|
+
return manyToManyVirtualRelationForBinding(
|
|
1791
|
+
relation,
|
|
1792
|
+
sourceTable,
|
|
1793
|
+
directProjectionLineage
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
if (relation.recursive !== void 0 || relation.source !== void 0 && relation.source === relation.target) {
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1448
1799
|
if (relation.type !== "belongsTo" && relation.type !== "hasOne" && relation.type !== "hasMany") {
|
|
1449
1800
|
return void 0;
|
|
1450
1801
|
}
|
|
@@ -1493,6 +1844,13 @@ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDepe
|
|
|
1493
1844
|
);
|
|
1494
1845
|
for (const relationName of relationNames ?? []) {
|
|
1495
1846
|
const relation = ctx.validator?.getRelation(sourceTable, relationName);
|
|
1847
|
+
const recursiveRelation = recursiveVirtualRelationForBinding(
|
|
1848
|
+
relation,
|
|
1849
|
+
sourceTable,
|
|
1850
|
+
outputSchema.directProjectionLineage ?? [],
|
|
1851
|
+
ctx.validator?.getPseudoColumns(sourceTable) ?? []
|
|
1852
|
+
);
|
|
1853
|
+
if (recursiveRelation) scalarRelations.push(recursiveRelation);
|
|
1496
1854
|
const virtualRelation = virtualRelationForBinding(
|
|
1497
1855
|
relation,
|
|
1498
1856
|
sourceTable,
|
|
@@ -1527,6 +1885,15 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
|
|
|
1527
1885
|
}
|
|
1528
1886
|
const firstSegment = segments[0];
|
|
1529
1887
|
if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
|
|
1888
|
+
if (segments.length === 2 && resolveBindingRecursiveRelationColumn(
|
|
1889
|
+
ctx,
|
|
1890
|
+
bindingName,
|
|
1891
|
+
firstSegment,
|
|
1892
|
+
segments[1],
|
|
1893
|
+
expr.depthHint
|
|
1894
|
+
)) {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1530
1897
|
assertNoBindingRelationConstruct(
|
|
1531
1898
|
ctx,
|
|
1532
1899
|
bindingName,
|
|
@@ -1757,12 +2124,20 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
|
|
|
1757
2124
|
const segments = item.expression.segments;
|
|
1758
2125
|
const firstSegment = segments[0];
|
|
1759
2126
|
if (ctx.pseudoColumnKeywords.has(firstSegment.toLowerCase())) {
|
|
1760
|
-
|
|
2127
|
+
if (segments.length !== 2 || !resolveBindingRecursiveRelationColumn(
|
|
1761
2128
|
ctx,
|
|
1762
2129
|
bindingName,
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2130
|
+
firstSegment,
|
|
2131
|
+
segments[1],
|
|
2132
|
+
item.expression.depthHint
|
|
2133
|
+
)) {
|
|
2134
|
+
assertNoBindingRelationConstruct(
|
|
2135
|
+
ctx,
|
|
2136
|
+
bindingName,
|
|
2137
|
+
"use pseudo-column traversals",
|
|
2138
|
+
firstSegment
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
1766
2141
|
} else {
|
|
1767
2142
|
resolveBindingRelationColumn(
|
|
1768
2143
|
ctx,
|
|
@@ -2155,44 +2530,78 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
|
2155
2530
|
if (bindingColumns2 !== void 0) return bindingColumns2;
|
|
2156
2531
|
const columns = ctx.validator?.getTableColumns(intent.from);
|
|
2157
2532
|
if (columns !== void 0) return columns;
|
|
2158
|
-
throw new
|
|
2159
|
-
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
2533
|
+
throw new UnresolvedSelectAllOutputSchemaError(
|
|
2160
2534
|
`Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
|
|
2161
2535
|
);
|
|
2162
2536
|
}
|
|
2163
|
-
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
|
|
2537
|
+
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = [], sourceBindingSchemas) {
|
|
2164
2538
|
const columns = [];
|
|
2165
2539
|
const seen = /* @__PURE__ */ new Set();
|
|
2166
2540
|
const directProjectionLineage = [];
|
|
2541
|
+
const typeCandidates = [];
|
|
2542
|
+
const sourceIsPhysicalTable = ctx.validator?.hasPhysicalTable(intent.from) ?? false;
|
|
2543
|
+
const sourceBindingSchema = sourceIsPhysicalTable ? void 0 : sourceBindingSchemas?.get(intent.from);
|
|
2167
2544
|
const addColumn = (column) => addUnique(columns, seen, column);
|
|
2168
2545
|
const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
|
|
2169
2546
|
const addDirectProjection = (outputColumn, sourceColumn) => {
|
|
2547
|
+
const resolvedSourceColumn = resolveSourceColumn(sourceColumn);
|
|
2548
|
+
let typeInfo;
|
|
2549
|
+
let untypeableReason = "unresolvable-source";
|
|
2550
|
+
if (sourceIsPhysicalTable) {
|
|
2551
|
+
typeInfo = ctx.validator?.getTableColumnType(
|
|
2552
|
+
intent.from,
|
|
2553
|
+
resolvedSourceColumn
|
|
2554
|
+
);
|
|
2555
|
+
} else if (sourceBindingSchema?.columnTypes) {
|
|
2556
|
+
typeInfo = sourceBindingSchema.columnTypes[resolvedSourceColumn];
|
|
2557
|
+
} else if (sourceBindingSchema?.columnTypesUnavailable) {
|
|
2558
|
+
untypeableReason = sourceBindingSchema.columnTypesUnavailable.reason;
|
|
2559
|
+
}
|
|
2560
|
+
typeCandidates.push(
|
|
2561
|
+
typeInfo !== void 0 ? { column: outputColumn, typed: typeInfo } : { column: outputColumn, untypeable: untypeableReason }
|
|
2562
|
+
);
|
|
2170
2563
|
if (!addColumn(outputColumn)) return;
|
|
2171
2564
|
directProjectionLineage.push({
|
|
2172
2565
|
kind: "directProjection",
|
|
2173
2566
|
sourceTable: intent.from,
|
|
2174
|
-
sourceColumn:
|
|
2567
|
+
sourceColumn: resolvedSourceColumn,
|
|
2175
2568
|
outputColumn
|
|
2176
2569
|
});
|
|
2177
2570
|
};
|
|
2571
|
+
const addUntypedColumn = (outputColumn, reason) => {
|
|
2572
|
+
typeCandidates.push({ column: outputColumn, untypeable: reason });
|
|
2573
|
+
addColumn(outputColumn);
|
|
2574
|
+
};
|
|
2575
|
+
const addCountAggregateColumn = (outputColumn) => {
|
|
2576
|
+
typeCandidates.push({
|
|
2577
|
+
column: outputColumn,
|
|
2578
|
+
typed: { kind: "aggregate", fn: "count" }
|
|
2579
|
+
});
|
|
2580
|
+
addColumn(outputColumn);
|
|
2581
|
+
};
|
|
2178
2582
|
const addSourceColumns = () => {
|
|
2179
2583
|
for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
|
|
2180
2584
|
addDirectProjection(column, column);
|
|
2181
2585
|
}
|
|
2182
2586
|
};
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
const outputSchema2 = { columns, directProjectionLineage };
|
|
2587
|
+
const finalizeOutputSchema = () => {
|
|
2588
|
+
const outputSchema = { columns, directProjectionLineage };
|
|
2589
|
+
const typesResult = buildBindingColumnTypes(columns, typeCandidates);
|
|
2187
2590
|
return {
|
|
2188
|
-
columns:
|
|
2591
|
+
columns: outputSchema.columns,
|
|
2189
2592
|
relationFilters: getBindingRelationFilterMetadata(
|
|
2190
2593
|
intent,
|
|
2191
2594
|
ctx,
|
|
2192
|
-
|
|
2595
|
+
outputSchema,
|
|
2193
2596
|
bindingDependencies
|
|
2194
|
-
)
|
|
2597
|
+
),
|
|
2598
|
+
..."columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable }
|
|
2195
2599
|
};
|
|
2600
|
+
};
|
|
2601
|
+
const { select } = intent;
|
|
2602
|
+
if (!select || select.type === "all") {
|
|
2603
|
+
addSourceColumns();
|
|
2604
|
+
return finalizeOutputSchema();
|
|
2196
2605
|
}
|
|
2197
2606
|
if (select.type === "fields") {
|
|
2198
2607
|
for (const field of select.fields) {
|
|
@@ -2202,16 +2611,7 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2202
2611
|
addDirectProjection(field, field);
|
|
2203
2612
|
}
|
|
2204
2613
|
}
|
|
2205
|
-
|
|
2206
|
-
return {
|
|
2207
|
-
columns: outputSchema2.columns,
|
|
2208
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2209
|
-
intent,
|
|
2210
|
-
ctx,
|
|
2211
|
-
outputSchema2,
|
|
2212
|
-
bindingDependencies
|
|
2213
|
-
)
|
|
2214
|
-
};
|
|
2614
|
+
return finalizeOutputSchema();
|
|
2215
2615
|
}
|
|
2216
2616
|
if (select.type === "aggregate") {
|
|
2217
2617
|
for (const field of select.fields ?? []) {
|
|
@@ -2224,18 +2624,13 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2224
2624
|
`Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
|
|
2225
2625
|
);
|
|
2226
2626
|
}
|
|
2227
|
-
|
|
2627
|
+
if (aggregate.function === "count") {
|
|
2628
|
+
addCountAggregateColumn(aggregate.as);
|
|
2629
|
+
} else {
|
|
2630
|
+
addUntypedColumn(aggregate.as, "unsupported-aggregate");
|
|
2631
|
+
}
|
|
2228
2632
|
}
|
|
2229
|
-
|
|
2230
|
-
return {
|
|
2231
|
-
columns: outputSchema2.columns,
|
|
2232
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2233
|
-
intent,
|
|
2234
|
-
ctx,
|
|
2235
|
-
outputSchema2,
|
|
2236
|
-
bindingDependencies
|
|
2237
|
-
)
|
|
2238
|
-
};
|
|
2633
|
+
return finalizeOutputSchema();
|
|
2239
2634
|
}
|
|
2240
2635
|
for (const expr of select.columns) {
|
|
2241
2636
|
if (expr.kind === "column" && expr.column === "*") {
|
|
@@ -2259,20 +2654,16 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2259
2654
|
addDirectProjection(outputColumn, expr.column);
|
|
2260
2655
|
} else if (expr.kind === "columnAlias") {
|
|
2261
2656
|
addDirectProjection(outputColumn, expr.column);
|
|
2657
|
+
} else if (expr.kind === "aggregate" && expr.function === "count") {
|
|
2658
|
+
addCountAggregateColumn(outputColumn);
|
|
2262
2659
|
} else {
|
|
2263
|
-
|
|
2660
|
+
let reason = "computed-expression";
|
|
2661
|
+
if (expr.kind === "relationColumn") reason = "relation-column";
|
|
2662
|
+
else if (expr.kind === "aggregate") reason = "unsupported-aggregate";
|
|
2663
|
+
addUntypedColumn(outputColumn, reason);
|
|
2264
2664
|
}
|
|
2265
2665
|
}
|
|
2266
|
-
|
|
2267
|
-
return {
|
|
2268
|
-
columns: outputSchema.columns,
|
|
2269
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2270
|
-
intent,
|
|
2271
|
-
ctx,
|
|
2272
|
-
outputSchema,
|
|
2273
|
-
bindingDependencies
|
|
2274
|
-
)
|
|
2275
|
-
};
|
|
2666
|
+
return finalizeOutputSchema();
|
|
2276
2667
|
}
|
|
2277
2668
|
function validateNqlExpressionPaths(expr, ctx) {
|
|
2278
2669
|
switch (expr.type) {
|
|
@@ -3539,12 +3930,6 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
3539
3930
|
const segments = expr.segments;
|
|
3540
3931
|
const firstSegmentLower = segments[0].toLowerCase();
|
|
3541
3932
|
if (ctx.pseudoColumnKeywords.has(firstSegmentLower)) {
|
|
3542
|
-
assertNoBindingRelationConstruct(
|
|
3543
|
-
ctx,
|
|
3544
|
-
ctx.currentFromTable,
|
|
3545
|
-
"use pseudo-column traversals",
|
|
3546
|
-
segments[0]
|
|
3547
|
-
);
|
|
3548
3933
|
const firstSegment = firstSegmentLower;
|
|
3549
3934
|
const depthHint = expr.depthHint;
|
|
3550
3935
|
if (depthHint !== void 0) {
|
|
@@ -3571,6 +3956,44 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
3571
3956
|
);
|
|
3572
3957
|
}
|
|
3573
3958
|
const targetColumn = segments[i];
|
|
3959
|
+
const bindingRelation2 = traversals.length === 1 ? resolveBindingRecursiveRelationColumn(
|
|
3960
|
+
ctx,
|
|
3961
|
+
ctx.currentFromTable,
|
|
3962
|
+
firstSegment,
|
|
3963
|
+
targetColumn,
|
|
3964
|
+
depthHint
|
|
3965
|
+
) : void 0;
|
|
3966
|
+
if (bindingRelation2) {
|
|
3967
|
+
const relationColumnIntent2 = {
|
|
3968
|
+
kind: "relationColumn",
|
|
3969
|
+
relation: bindingRelation2.relation,
|
|
3970
|
+
column: targetColumn,
|
|
3971
|
+
as: item.alias ?? `${bindingRelation2.relation}.${targetColumn}`
|
|
3972
|
+
};
|
|
3973
|
+
return markNqlTrustedRelationFilter(relationColumnIntent2, {
|
|
3974
|
+
relation: bindingRelation2.relation,
|
|
3975
|
+
targetTable: bindingRelation2.targetTable,
|
|
3976
|
+
sourceColumn: bindingRelation2.sourceColumn,
|
|
3977
|
+
targetColumn: bindingRelation2.targetColumn,
|
|
3978
|
+
hops: bindingRelation2.hops,
|
|
3979
|
+
selectedColumn: targetColumn,
|
|
3980
|
+
...bindingRelation2.cardinality !== void 0 && {
|
|
3981
|
+
cardinality: bindingRelation2.cardinality
|
|
3982
|
+
},
|
|
3983
|
+
...bindingRelation2.relationType !== void 0 && {
|
|
3984
|
+
relationType: bindingRelation2.relationType
|
|
3985
|
+
},
|
|
3986
|
+
...bindingRelation2.recursive !== void 0 && {
|
|
3987
|
+
recursive: bindingRelation2.recursive
|
|
3988
|
+
}
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
assertNoBindingRelationConstruct(
|
|
3992
|
+
ctx,
|
|
3993
|
+
ctx.currentFromTable,
|
|
3994
|
+
"use pseudo-column traversals",
|
|
3995
|
+
segments[0]
|
|
3996
|
+
);
|
|
3574
3997
|
if (ctx.currentFromTable) {
|
|
3575
3998
|
validateColumnForTable(ctx, ctx.currentFromTable, targetColumn);
|
|
3576
3999
|
}
|
|
@@ -3627,12 +4050,24 @@ function compileMultiSegmentPath(expr, item, ctx) {
|
|
|
3627
4050
|
sourceColumn: bindingRelation.sourceColumn,
|
|
3628
4051
|
targetColumn: bindingRelation.targetColumn,
|
|
3629
4052
|
hops: bindingRelation.hops,
|
|
4053
|
+
...bindingRelation.through !== void 0 && {
|
|
4054
|
+
through: bindingRelation.through
|
|
4055
|
+
},
|
|
4056
|
+
...bindingRelation.throughSourceColumn !== void 0 && {
|
|
4057
|
+
throughSourceColumn: bindingRelation.throughSourceColumn
|
|
4058
|
+
},
|
|
4059
|
+
...bindingRelation.throughTargetColumn !== void 0 && {
|
|
4060
|
+
throughTargetColumn: bindingRelation.throughTargetColumn
|
|
4061
|
+
},
|
|
3630
4062
|
selectedColumn: column,
|
|
3631
4063
|
...bindingRelation.cardinality !== void 0 && {
|
|
3632
4064
|
cardinality: bindingRelation.cardinality
|
|
3633
4065
|
},
|
|
3634
4066
|
...bindingRelation.relationType !== void 0 && {
|
|
3635
4067
|
relationType: bindingRelation.relationType
|
|
4068
|
+
},
|
|
4069
|
+
...bindingRelation.recursive !== void 0 && {
|
|
4070
|
+
recursive: bindingRelation.recursive
|
|
3636
4071
|
}
|
|
3637
4072
|
}) : relationColumnIntent;
|
|
3638
4073
|
}
|
|
@@ -3781,16 +4216,17 @@ function allowsInternalParams(options) {
|
|
|
3781
4216
|
return internalOptions?.allowInternalParams === true;
|
|
3782
4217
|
}
|
|
3783
4218
|
function isUnresolvedSelectAllOutputSchemaError(error) {
|
|
3784
|
-
return error instanceof
|
|
4219
|
+
return error instanceof UnresolvedSelectAllOutputSchemaError;
|
|
3785
4220
|
}
|
|
3786
|
-
function programSequenceStepFromResult(result, bindName, final, bindingDependencies) {
|
|
4221
|
+
function programSequenceStepFromResult(result, bindName, final, bindingDependencies, snapshotReadBindings) {
|
|
3787
4222
|
if (result.query) {
|
|
3788
4223
|
return {
|
|
3789
4224
|
kind: "query",
|
|
3790
4225
|
query: result.query,
|
|
3791
4226
|
...bindName !== void 0 && { bindName },
|
|
3792
4227
|
final,
|
|
3793
|
-
bindingDependencies
|
|
4228
|
+
bindingDependencies,
|
|
4229
|
+
...bindName !== void 0 && snapshotReadBindings.has(bindName) && { snapshot: true }
|
|
3794
4230
|
};
|
|
3795
4231
|
}
|
|
3796
4232
|
if (result.mutation) {
|
|
@@ -3810,6 +4246,34 @@ function stringArraysEqual(left, right) {
|
|
|
3810
4246
|
function isNqlAstRecord(value) {
|
|
3811
4247
|
return typeof value === "object" && value !== null;
|
|
3812
4248
|
}
|
|
4249
|
+
function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding, outputSchema) {
|
|
4250
|
+
if (!sourceWasBinding && ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
|
|
4251
|
+
return {
|
|
4252
|
+
supported: false,
|
|
4253
|
+
reason: `no physical model table source '${query.from}'`
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
if (outputSchema?.columnTypes) {
|
|
4257
|
+
return { supported: true };
|
|
4258
|
+
}
|
|
4259
|
+
const untypeable = outputSchema?.columnTypesUnavailable;
|
|
4260
|
+
if (untypeable?.reason === "unsupported-aggregate") {
|
|
4261
|
+
return {
|
|
4262
|
+
supported: false,
|
|
4263
|
+
reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
if (untypeable) {
|
|
4267
|
+
return {
|
|
4268
|
+
supported: false,
|
|
4269
|
+
reason: `${untypeable.reason} column '${untypeable.column}'`
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
if (sourceWasBinding) {
|
|
4273
|
+
return { supported: false, reason: "a binding source" };
|
|
4274
|
+
}
|
|
4275
|
+
return { supported: false, reason: "aliased/computed/aggregate columns" };
|
|
4276
|
+
}
|
|
3813
4277
|
function addReadBindingReference(references, readBindingNames, name) {
|
|
3814
4278
|
if (typeof name === "string" && readBindingNames.has(name)) {
|
|
3815
4279
|
references.add(name);
|
|
@@ -3973,23 +4437,31 @@ function collectTransitiveReadBindingDependencies(directReferences, bindingDepen
|
|
|
3973
4437
|
}
|
|
3974
4438
|
return ordered;
|
|
3975
4439
|
}
|
|
3976
|
-
function rejectReadBindingReferenceAcrossMutation(bindName,
|
|
4440
|
+
function rejectReadBindingReferenceAcrossMutation(bindName, reason) {
|
|
3977
4441
|
throw new NqlSemanticException(
|
|
3978
4442
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
3979
|
-
`read binding referenced across a mutation (#186):
|
|
4443
|
+
`read binding referenced across a mutation: unsupported snapshot shape (#186): snapshotting currently supports only a direct single-table column projection; binding '${bindName}' has ${reason}, so it cannot be materialized - move the reference before the mutation, or bind the mutation RETURNING result.`
|
|
3980
4444
|
);
|
|
3981
4445
|
}
|
|
3982
|
-
function
|
|
4446
|
+
function markReadBindingReferenceAcrossMutation(bindName, snapshotReadBindings, readBindingSnapshotShapes) {
|
|
4447
|
+
const snapshotShape = readBindingSnapshotShapes.get(bindName);
|
|
4448
|
+
if (snapshotShape?.supported !== true) {
|
|
4449
|
+
rejectReadBindingReferenceAcrossMutation(
|
|
4450
|
+
bindName,
|
|
4451
|
+
snapshotShape?.reason ?? "an unavailable output shape"
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
4454
|
+
snapshotReadBindings.add(bindName);
|
|
4455
|
+
}
|
|
4456
|
+
function markReadBindingDependenciesRequiringSnapshot(statementBindingDependencies, readBindingDefinitions, lastMutationStatement, snapshotReadBindings, readBindingSnapshotShapes) {
|
|
3983
4457
|
for (const referencedBindName of statementBindingDependencies) {
|
|
3984
4458
|
const definitionIndex = readBindingDefinitions.get(referencedBindName);
|
|
3985
4459
|
if (definitionIndex === void 0) continue;
|
|
3986
4460
|
if (lastMutationStatement > definitionIndex) {
|
|
3987
|
-
|
|
4461
|
+
markReadBindingReferenceAcrossMutation(
|
|
3988
4462
|
referencedBindName,
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
referenceIndex,
|
|
3992
|
-
statementCount
|
|
4463
|
+
snapshotReadBindings,
|
|
4464
|
+
readBindingSnapshotShapes
|
|
3993
4465
|
);
|
|
3994
4466
|
}
|
|
3995
4467
|
}
|
|
@@ -4043,6 +4515,7 @@ var NqlCompiler = class {
|
|
|
4043
4515
|
} finally {
|
|
4044
4516
|
this.ctx.bindingOutputColumns.clear();
|
|
4045
4517
|
this.ctx.bindingRelationFilters.clear();
|
|
4518
|
+
this.ctx.lastMutationReturningItems = void 0;
|
|
4046
4519
|
this.ctx.validator?.clearVirtualBindingTables();
|
|
4047
4520
|
}
|
|
4048
4521
|
}
|
|
@@ -4069,6 +4542,8 @@ var NqlCompiler = class {
|
|
|
4069
4542
|
const seenBindNames = /* @__PURE__ */ new Map();
|
|
4070
4543
|
const nqlProgramSequence = [];
|
|
4071
4544
|
const readBindingDefinitions = /* @__PURE__ */ new Map();
|
|
4545
|
+
const readBindingSnapshotShapes = /* @__PURE__ */ new Map();
|
|
4546
|
+
const snapshotReadBindings = /* @__PURE__ */ new Set();
|
|
4072
4547
|
const definedBindingNames = /* @__PURE__ */ new Set();
|
|
4073
4548
|
const bindingDependencies = /* @__PURE__ */ new Map();
|
|
4074
4549
|
let lastMutationStatement = -1;
|
|
@@ -4094,12 +4569,12 @@ var NqlCompiler = class {
|
|
|
4094
4569
|
readBindingReferences,
|
|
4095
4570
|
bindingDependencies
|
|
4096
4571
|
);
|
|
4097
|
-
|
|
4572
|
+
markReadBindingDependenciesRequiringSnapshot(
|
|
4098
4573
|
statementBindingDependencies,
|
|
4099
4574
|
readBindingDefinitions,
|
|
4100
4575
|
lastMutationStatement,
|
|
4101
|
-
|
|
4102
|
-
|
|
4576
|
+
snapshotReadBindings,
|
|
4577
|
+
readBindingSnapshotShapes
|
|
4103
4578
|
);
|
|
4104
4579
|
lastResult = this.compileSingleStatement(stmt, bindings);
|
|
4105
4580
|
collectCompileResultReadBindingReferences(
|
|
@@ -4111,24 +4586,36 @@ var NqlCompiler = class {
|
|
|
4111
4586
|
readBindingReferences,
|
|
4112
4587
|
bindingDependencies
|
|
4113
4588
|
);
|
|
4114
|
-
|
|
4589
|
+
markReadBindingDependenciesRequiringSnapshot(
|
|
4115
4590
|
statementBindingDependencies,
|
|
4116
4591
|
readBindingDefinitions,
|
|
4117
4592
|
lastMutationStatement,
|
|
4118
|
-
|
|
4119
|
-
|
|
4593
|
+
snapshotReadBindings,
|
|
4594
|
+
readBindingSnapshotShapes
|
|
4120
4595
|
);
|
|
4121
4596
|
if (stmt.type === "mutationPipeline") {
|
|
4122
4597
|
lastMutationStatement = i;
|
|
4123
4598
|
}
|
|
4124
4599
|
if (bindName) {
|
|
4125
4600
|
if (lastResult.query) {
|
|
4601
|
+
const sourceWasBinding = definedBindingNames.has(
|
|
4602
|
+
lastResult.query.from
|
|
4603
|
+
);
|
|
4126
4604
|
const outputSchema = this.registerQueryBindingOutputSchema(
|
|
4127
4605
|
bindName,
|
|
4128
4606
|
lastResult.query,
|
|
4129
4607
|
bindingOutputSchemas,
|
|
4130
4608
|
statementBindingDependencies
|
|
4131
4609
|
);
|
|
4610
|
+
readBindingSnapshotShapes.set(
|
|
4611
|
+
bindName,
|
|
4612
|
+
classifyReadBindingSnapshotShape(
|
|
4613
|
+
lastResult.query,
|
|
4614
|
+
this.ctx,
|
|
4615
|
+
sourceWasBinding,
|
|
4616
|
+
outputSchema
|
|
4617
|
+
)
|
|
4618
|
+
);
|
|
4132
4619
|
bindings.set(bindName, lastResult.query);
|
|
4133
4620
|
if (outputSchema) {
|
|
4134
4621
|
this.ctx.validator?.addVirtualBindingTable(
|
|
@@ -4176,7 +4663,8 @@ var NqlCompiler = class {
|
|
|
4176
4663
|
lastResult,
|
|
4177
4664
|
bindName,
|
|
4178
4665
|
i === program.statements.length - 1,
|
|
4179
|
-
statementBindingDependencies
|
|
4666
|
+
statementBindingDependencies,
|
|
4667
|
+
snapshotReadBindings
|
|
4180
4668
|
);
|
|
4181
4669
|
if (sequenceStep !== void 0) {
|
|
4182
4670
|
nqlProgramSequence.push(sequenceStep);
|
|
@@ -4192,14 +4680,19 @@ var NqlCompiler = class {
|
|
|
4192
4680
|
}
|
|
4193
4681
|
const hasMutationBindings = mutationBindings.size > 0;
|
|
4194
4682
|
const hasBindingOutputSchemas = bindingOutputSchemas.size > 0;
|
|
4195
|
-
const
|
|
4683
|
+
const taggedNqlProgramSequence = nqlProgramSequence.map(
|
|
4684
|
+
(step) => step.kind === "query" && step.bindName !== void 0 && snapshotReadBindings.has(step.bindName) ? { ...step, snapshot: true } : step
|
|
4685
|
+
);
|
|
4686
|
+
const hasNqlProgramSequence = taggedNqlProgramSequence.length === program.statements.length;
|
|
4196
4687
|
if (bindings.size > 0) {
|
|
4197
4688
|
return {
|
|
4198
4689
|
...lastResult,
|
|
4199
4690
|
bindings,
|
|
4200
4691
|
...hasBindingOutputSchemas && { bindingOutputSchemas },
|
|
4201
4692
|
...hasMutationBindings && { mutationBindings },
|
|
4202
|
-
...hasNqlProgramSequence && {
|
|
4693
|
+
...hasNqlProgramSequence && {
|
|
4694
|
+
nqlProgramSequence: taggedNqlProgramSequence
|
|
4695
|
+
}
|
|
4203
4696
|
};
|
|
4204
4697
|
}
|
|
4205
4698
|
return lastResult;
|
|
@@ -4210,7 +4703,12 @@ var NqlCompiler = class {
|
|
|
4210
4703
|
query,
|
|
4211
4704
|
this.ctx,
|
|
4212
4705
|
bindName,
|
|
4213
|
-
bindingDependencies
|
|
4706
|
+
bindingDependencies,
|
|
4707
|
+
// #213 B2: earlier bindings are already registered here (this
|
|
4708
|
+
// map is populated in program-statement order), so a
|
|
4709
|
+
// transitive `from` = binding chains through the SOURCE's
|
|
4710
|
+
// typed schema instead of staying untypeable.
|
|
4711
|
+
bindingOutputSchemas
|
|
4214
4712
|
);
|
|
4215
4713
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
4216
4714
|
this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
|
|
@@ -4250,20 +4748,50 @@ var NqlCompiler = class {
|
|
|
4250
4748
|
const returning = mutation.returning;
|
|
4251
4749
|
if (!returning || returning.length === 0) return void 0;
|
|
4252
4750
|
if (returning.includes("*")) {
|
|
4253
|
-
const
|
|
4254
|
-
if (
|
|
4751
|
+
const columns2 = this.ctx.validator?.getTableColumns(mutation.table);
|
|
4752
|
+
if (columns2 !== void 0) {
|
|
4753
|
+
return {
|
|
4754
|
+
columns: columns2,
|
|
4755
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns2)
|
|
4756
|
+
};
|
|
4757
|
+
}
|
|
4255
4758
|
if (!this.ctx.validator) return void 0;
|
|
4256
4759
|
throw new NqlSemanticException(
|
|
4257
4760
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
4258
4761
|
`Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
|
|
4259
4762
|
);
|
|
4260
4763
|
}
|
|
4764
|
+
const columns = returning.map(
|
|
4765
|
+
(column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
|
|
4766
|
+
);
|
|
4261
4767
|
return {
|
|
4262
|
-
columns
|
|
4263
|
-
|
|
4264
|
-
)
|
|
4768
|
+
columns,
|
|
4769
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns)
|
|
4265
4770
|
};
|
|
4266
4771
|
}
|
|
4772
|
+
/**
|
|
4773
|
+
* Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
|
|
4774
|
+
* binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
|
|
4775
|
+
* (alias-aware, positional) — NEVER the collapsed `columns` names — so an
|
|
4776
|
+
* alias colliding with a real column (`returning email as name`) is
|
|
4777
|
+
* flagged 'aliased-returning' rather than silently mistyped as the
|
|
4778
|
+
* colliding column's type.
|
|
4779
|
+
*/
|
|
4780
|
+
buildMutationReturningColumnTypes(table, columns) {
|
|
4781
|
+
const items = this.ctx.lastMutationReturningItems;
|
|
4782
|
+
const candidates = columns.map(
|
|
4783
|
+
(column, index) => {
|
|
4784
|
+
const item = items !== void 0 && items !== "star" ? items[index] : void 0;
|
|
4785
|
+
if (item?.aliased) {
|
|
4786
|
+
return { column, untypeable: "aliased-returning" };
|
|
4787
|
+
}
|
|
4788
|
+
const typeInfo = this.ctx.validator?.getTableColumnType(table, column);
|
|
4789
|
+
return typeInfo !== void 0 ? { column, typed: typeInfo } : { column, untypeable: "unresolvable-source" };
|
|
4790
|
+
}
|
|
4791
|
+
);
|
|
4792
|
+
const typesResult = buildBindingColumnTypes(columns, candidates);
|
|
4793
|
+
return "columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable };
|
|
4794
|
+
}
|
|
4267
4795
|
compileSingleStatement(stmt, bindings) {
|
|
4268
4796
|
if (stmt.type === "withQuery") {
|
|
4269
4797
|
return compileWithQuery(stmt, this.ctx, this.fns);
|