@dbsp/nql 1.7.0 → 1.8.1
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 +22 -4
- package/dist/index.js +283 -118
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompiledNqlQuery } from '@dbsp/types';
|
|
1
|
+
import { ColumnType, CompiledNqlQuery } from '@dbsp/types';
|
|
2
2
|
export { CompiledNqlQuery, DeleteIntent, ExpressionIntent, IncludeIntent, InsertIntent, MutationIntent, OrderByIntent, QueryIntent, SelectAllIntent, SelectFieldsIntent, SelectIntent, SelectWithExpressionsIntent, UpdateIntent, UpsertIntent, WhereAndIntent, WhereComparisonIntent, WhereInIntent, WhereIntent, WhereLikeIntent, WhereNotIntent, WhereNullIntent, WhereOrIntent, WhereRangeIntent } from '@dbsp/types';
|
|
3
3
|
import * as chevrotain from 'chevrotain';
|
|
4
4
|
import { Lexer, CstParser, CstNode, IToken } from 'chevrotain';
|
|
@@ -454,11 +454,20 @@ interface ColumnValidatorPseudoColumn {
|
|
|
454
454
|
readonly ascendantKeyword?: string;
|
|
455
455
|
readonly descendantKeyword?: string;
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Duck-type column shape carried by `ColumnValidatorSchema.getTable()`.
|
|
459
|
+
* `type`/`originalDbType` are optional so hand-authored test schemas that
|
|
460
|
+
* only supply `name` remain valid — absence makes a column's type
|
|
461
|
+
* unresolvable (untypeable), never silently mismatched.
|
|
462
|
+
*/
|
|
463
|
+
interface ColumnValidatorTableColumn {
|
|
464
|
+
readonly name: string;
|
|
465
|
+
readonly type?: ColumnType;
|
|
466
|
+
readonly originalDbType?: string;
|
|
467
|
+
}
|
|
457
468
|
interface ColumnValidatorSchema {
|
|
458
469
|
getTable(name: string): {
|
|
459
|
-
readonly columns: readonly
|
|
460
|
-
readonly name: string;
|
|
461
|
-
}[];
|
|
470
|
+
readonly columns: readonly ColumnValidatorTableColumn[];
|
|
462
471
|
readonly pseudoColumns?: readonly ColumnValidatorPseudoColumn[];
|
|
463
472
|
} | undefined;
|
|
464
473
|
getRelationsFrom(sourceTable: string): readonly ColumnValidatorRelation[];
|
|
@@ -518,6 +527,15 @@ declare class NqlCompiler {
|
|
|
518
527
|
private registerQueryBindingOutputSchema;
|
|
519
528
|
private canonicalizeMutationBinding;
|
|
520
529
|
private getMutationBindingOutputSchema;
|
|
530
|
+
/**
|
|
531
|
+
* Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
|
|
532
|
+
* binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
|
|
533
|
+
* (alias-aware, positional) — NEVER the collapsed `columns` names — so an
|
|
534
|
+
* alias colliding with a real column (`returning email as name`) is
|
|
535
|
+
* flagged 'aliased-returning' rather than silently mistyped as the
|
|
536
|
+
* colliding column's type.
|
|
537
|
+
*/
|
|
538
|
+
private buildMutationReturningColumnTypes;
|
|
521
539
|
private compileSingleStatement;
|
|
522
540
|
}
|
|
523
541
|
/**
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,45 @@ 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
|
+
}
|
|
46
85
|
var DEFAULT_RELATION_TARGET_COLUMN = "id";
|
|
47
86
|
function relationForeignKeys(relation) {
|
|
48
87
|
if (!relation) return void 0;
|
|
@@ -117,6 +156,25 @@ var ColumnValidator = class _ColumnValidator {
|
|
|
117
156
|
getPhysicalTableColumns(name) {
|
|
118
157
|
return this.schema.getTable(name)?.columns.map((column) => column.name);
|
|
119
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
|
+
}
|
|
120
178
|
getPseudoColumns(name) {
|
|
121
179
|
return this.schema.getTable(name)?.pseudoColumns ?? [];
|
|
122
180
|
}
|
|
@@ -1118,15 +1176,29 @@ function assignMutationValue(target, column, value, ctx) {
|
|
|
1118
1176
|
function compileMutationPipeline(pipeline, ctx, fns, bindings) {
|
|
1119
1177
|
ctx.currentFromTable = pipeline.mutation.table;
|
|
1120
1178
|
const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
|
|
1179
|
+
ctx.currentFromTable = pipeline.mutation.table;
|
|
1121
1180
|
let returning;
|
|
1181
|
+
let returningItems;
|
|
1122
1182
|
for (const clause of pipeline.clauses) {
|
|
1123
1183
|
if (clause.type === "select") {
|
|
1124
|
-
|
|
1184
|
+
returningItems = extractReturningItems(clause, ctx);
|
|
1185
|
+
returning = returningItems === "star" ? ["*"] : returningItems.map((item) => item.output);
|
|
1125
1186
|
}
|
|
1126
1187
|
}
|
|
1188
|
+
ctx.lastMutationReturningItems = returningItems;
|
|
1127
1189
|
if (returning) {
|
|
1190
|
+
const aliasAwareReturningItems = returningItems !== void 0 && returningItems !== "star" && returningItems.some((item) => item.aliased) ? returningItems.map((item) => ({
|
|
1191
|
+
source: item.source,
|
|
1192
|
+
output: item.output
|
|
1193
|
+
})) : void 0;
|
|
1128
1194
|
return {
|
|
1129
|
-
mutation: {
|
|
1195
|
+
mutation: {
|
|
1196
|
+
...mutation,
|
|
1197
|
+
returning,
|
|
1198
|
+
...aliasAwareReturningItems !== void 0 && {
|
|
1199
|
+
returningItems: aliasAwareReturningItems
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1130
1202
|
};
|
|
1131
1203
|
}
|
|
1132
1204
|
return { mutation };
|
|
@@ -1311,23 +1383,49 @@ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
|
|
|
1311
1383
|
}
|
|
1312
1384
|
};
|
|
1313
1385
|
}
|
|
1314
|
-
function
|
|
1315
|
-
const
|
|
1386
|
+
function extractReturningItems(clause, ctx) {
|
|
1387
|
+
const items = [];
|
|
1388
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
1316
1389
|
for (const item of clause.items) {
|
|
1317
1390
|
if (item.type === "star") {
|
|
1318
|
-
|
|
1391
|
+
if (clause.items.length > 1) {
|
|
1392
|
+
throw new NqlSemanticException(
|
|
1393
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
1394
|
+
"Mutation RETURNING cannot mix `select *` with explicit projection items."
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
return "star";
|
|
1319
1398
|
}
|
|
1320
1399
|
if (item.type === "expression") {
|
|
1321
1400
|
const field = expressionToField(item.expression);
|
|
1322
1401
|
if (field) {
|
|
1402
|
+
const aliased = item.alias !== void 0;
|
|
1403
|
+
if (aliased && field.includes(".")) {
|
|
1404
|
+
throw new NqlSemanticException(
|
|
1405
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
1406
|
+
`Mutation RETURNING alias cannot use dotted source '${field}'.`
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1323
1409
|
if (ctx.currentFromTable && !field.includes(".")) {
|
|
1324
1410
|
ctx.validator?.validateColumn(ctx.currentFromTable, field);
|
|
1325
1411
|
}
|
|
1326
|
-
|
|
1412
|
+
const output = item.alias ?? field;
|
|
1413
|
+
if (outputs.has(output)) {
|
|
1414
|
+
throw new NqlSemanticException(
|
|
1415
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
1416
|
+
`Mutation RETURNING has duplicate output name '${output}'.`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
outputs.add(output);
|
|
1420
|
+
items.push({
|
|
1421
|
+
source: field,
|
|
1422
|
+
output,
|
|
1423
|
+
aliased
|
|
1424
|
+
});
|
|
1327
1425
|
}
|
|
1328
1426
|
}
|
|
1329
1427
|
}
|
|
1330
|
-
return
|
|
1428
|
+
return items;
|
|
1331
1429
|
}
|
|
1332
1430
|
function resolveBindingsInWhere(where, bindings) {
|
|
1333
1431
|
if (!bindings || bindings.size === 0) return where;
|
|
@@ -2470,39 +2568,74 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
|
2470
2568
|
`Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
|
|
2471
2569
|
);
|
|
2472
2570
|
}
|
|
2473
|
-
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
|
|
2571
|
+
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = [], sourceBindingSchemas) {
|
|
2474
2572
|
const columns = [];
|
|
2475
2573
|
const seen = /* @__PURE__ */ new Set();
|
|
2476
2574
|
const directProjectionLineage = [];
|
|
2575
|
+
const typeCandidates = [];
|
|
2576
|
+
const sourceIsPhysicalTable = ctx.validator?.hasPhysicalTable(intent.from) ?? false;
|
|
2577
|
+
const sourceBindingSchema = sourceIsPhysicalTable ? void 0 : sourceBindingSchemas?.get(intent.from);
|
|
2477
2578
|
const addColumn = (column) => addUnique(columns, seen, column);
|
|
2478
2579
|
const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
|
|
2479
2580
|
const addDirectProjection = (outputColumn, sourceColumn) => {
|
|
2581
|
+
const resolvedSourceColumn = resolveSourceColumn(sourceColumn);
|
|
2582
|
+
let typeInfo;
|
|
2583
|
+
let untypeableReason = "unresolvable-source";
|
|
2584
|
+
if (sourceIsPhysicalTable) {
|
|
2585
|
+
typeInfo = ctx.validator?.getTableColumnType(
|
|
2586
|
+
intent.from,
|
|
2587
|
+
resolvedSourceColumn
|
|
2588
|
+
);
|
|
2589
|
+
} else if (sourceBindingSchema?.columnTypes) {
|
|
2590
|
+
typeInfo = sourceBindingSchema.columnTypes[resolvedSourceColumn];
|
|
2591
|
+
} else if (sourceBindingSchema?.columnTypesUnavailable) {
|
|
2592
|
+
untypeableReason = sourceBindingSchema.columnTypesUnavailable.reason;
|
|
2593
|
+
}
|
|
2594
|
+
typeCandidates.push(
|
|
2595
|
+
typeInfo !== void 0 ? { column: outputColumn, typed: typeInfo } : { column: outputColumn, untypeable: untypeableReason }
|
|
2596
|
+
);
|
|
2480
2597
|
if (!addColumn(outputColumn)) return;
|
|
2481
2598
|
directProjectionLineage.push({
|
|
2482
2599
|
kind: "directProjection",
|
|
2483
2600
|
sourceTable: intent.from,
|
|
2484
|
-
sourceColumn:
|
|
2601
|
+
sourceColumn: resolvedSourceColumn,
|
|
2485
2602
|
outputColumn
|
|
2486
2603
|
});
|
|
2487
2604
|
};
|
|
2605
|
+
const addUntypedColumn = (outputColumn, reason) => {
|
|
2606
|
+
typeCandidates.push({ column: outputColumn, untypeable: reason });
|
|
2607
|
+
addColumn(outputColumn);
|
|
2608
|
+
};
|
|
2609
|
+
const addCountAggregateColumn = (outputColumn) => {
|
|
2610
|
+
typeCandidates.push({
|
|
2611
|
+
column: outputColumn,
|
|
2612
|
+
typed: { kind: "aggregate", fn: "count" }
|
|
2613
|
+
});
|
|
2614
|
+
addColumn(outputColumn);
|
|
2615
|
+
};
|
|
2488
2616
|
const addSourceColumns = () => {
|
|
2489
2617
|
for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
|
|
2490
2618
|
addDirectProjection(column, column);
|
|
2491
2619
|
}
|
|
2492
2620
|
};
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const outputSchema2 = { columns, directProjectionLineage };
|
|
2621
|
+
const finalizeOutputSchema = () => {
|
|
2622
|
+
const outputSchema = { columns, directProjectionLineage };
|
|
2623
|
+
const typesResult = buildBindingColumnTypes(columns, typeCandidates);
|
|
2497
2624
|
return {
|
|
2498
|
-
columns:
|
|
2625
|
+
columns: outputSchema.columns,
|
|
2499
2626
|
relationFilters: getBindingRelationFilterMetadata(
|
|
2500
2627
|
intent,
|
|
2501
2628
|
ctx,
|
|
2502
|
-
|
|
2629
|
+
outputSchema,
|
|
2503
2630
|
bindingDependencies
|
|
2504
|
-
)
|
|
2631
|
+
),
|
|
2632
|
+
..."columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable }
|
|
2505
2633
|
};
|
|
2634
|
+
};
|
|
2635
|
+
const { select } = intent;
|
|
2636
|
+
if (!select || select.type === "all") {
|
|
2637
|
+
addSourceColumns();
|
|
2638
|
+
return finalizeOutputSchema();
|
|
2506
2639
|
}
|
|
2507
2640
|
if (select.type === "fields") {
|
|
2508
2641
|
for (const field of select.fields) {
|
|
@@ -2512,16 +2645,7 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2512
2645
|
addDirectProjection(field, field);
|
|
2513
2646
|
}
|
|
2514
2647
|
}
|
|
2515
|
-
|
|
2516
|
-
return {
|
|
2517
|
-
columns: outputSchema2.columns,
|
|
2518
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2519
|
-
intent,
|
|
2520
|
-
ctx,
|
|
2521
|
-
outputSchema2,
|
|
2522
|
-
bindingDependencies
|
|
2523
|
-
)
|
|
2524
|
-
};
|
|
2648
|
+
return finalizeOutputSchema();
|
|
2525
2649
|
}
|
|
2526
2650
|
if (select.type === "aggregate") {
|
|
2527
2651
|
for (const field of select.fields ?? []) {
|
|
@@ -2534,18 +2658,13 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2534
2658
|
`Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
|
|
2535
2659
|
);
|
|
2536
2660
|
}
|
|
2537
|
-
|
|
2661
|
+
if (aggregate.function === "count") {
|
|
2662
|
+
addCountAggregateColumn(aggregate.as);
|
|
2663
|
+
} else {
|
|
2664
|
+
addUntypedColumn(aggregate.as, "unsupported-aggregate");
|
|
2665
|
+
}
|
|
2538
2666
|
}
|
|
2539
|
-
|
|
2540
|
-
return {
|
|
2541
|
-
columns: outputSchema2.columns,
|
|
2542
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2543
|
-
intent,
|
|
2544
|
-
ctx,
|
|
2545
|
-
outputSchema2,
|
|
2546
|
-
bindingDependencies
|
|
2547
|
-
)
|
|
2548
|
-
};
|
|
2667
|
+
return finalizeOutputSchema();
|
|
2549
2668
|
}
|
|
2550
2669
|
for (const expr of select.columns) {
|
|
2551
2670
|
if (expr.kind === "column" && expr.column === "*") {
|
|
@@ -2569,20 +2688,16 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2569
2688
|
addDirectProjection(outputColumn, expr.column);
|
|
2570
2689
|
} else if (expr.kind === "columnAlias") {
|
|
2571
2690
|
addDirectProjection(outputColumn, expr.column);
|
|
2691
|
+
} else if (expr.kind === "aggregate" && expr.function === "count") {
|
|
2692
|
+
addCountAggregateColumn(outputColumn);
|
|
2572
2693
|
} else {
|
|
2573
|
-
|
|
2694
|
+
let reason = "computed-expression";
|
|
2695
|
+
if (expr.kind === "relationColumn") reason = "relation-column";
|
|
2696
|
+
else if (expr.kind === "aggregate") reason = "unsupported-aggregate";
|
|
2697
|
+
addUntypedColumn(outputColumn, reason);
|
|
2574
2698
|
}
|
|
2575
2699
|
}
|
|
2576
|
-
|
|
2577
|
-
return {
|
|
2578
|
-
columns: outputSchema.columns,
|
|
2579
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2580
|
-
intent,
|
|
2581
|
-
ctx,
|
|
2582
|
-
outputSchema,
|
|
2583
|
-
bindingDependencies
|
|
2584
|
-
)
|
|
2585
|
-
};
|
|
2700
|
+
return finalizeOutputSchema();
|
|
2586
2701
|
}
|
|
2587
2702
|
function validateNqlExpressionPaths(expr, ctx) {
|
|
2588
2703
|
switch (expr.type) {
|
|
@@ -4162,81 +4277,39 @@ function programSequenceStepFromResult(result, bindName, final, bindingDependenc
|
|
|
4162
4277
|
function stringArraysEqual(left, right) {
|
|
4163
4278
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
4164
4279
|
}
|
|
4280
|
+
function mutationReturningItemsMatchReturning(returning, returningItems) {
|
|
4281
|
+
return returningItems.length === returning.length && returningItems.every((item, index) => item.output === returning[index]);
|
|
4282
|
+
}
|
|
4165
4283
|
function isNqlAstRecord(value) {
|
|
4166
4284
|
return typeof value === "object" && value !== null;
|
|
4167
4285
|
}
|
|
4168
|
-
function
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
}
|
|
4175
|
-
function hasDirectPhysicalColumnProjection(query, ctx) {
|
|
4176
|
-
const physicalColumns = ctx.validator?.getPhysicalTableColumns(query.from);
|
|
4177
|
-
if (physicalColumns === void 0) return false;
|
|
4178
|
-
const select = query.select;
|
|
4179
|
-
if (!select || select.type === "all") return physicalColumns.length > 0;
|
|
4180
|
-
let projectedColumnCount = 0;
|
|
4181
|
-
const addStarProjection = () => {
|
|
4182
|
-
projectedColumnCount += physicalColumns.length;
|
|
4183
|
-
return physicalColumns.length > 0;
|
|
4184
|
-
};
|
|
4185
|
-
const addDirectProjection = (sourceColumn, outputColumn) => {
|
|
4186
|
-
projectedColumnCount++;
|
|
4187
|
-
return isExactPhysicalProjectionColumn(
|
|
4188
|
-
query,
|
|
4189
|
-
ctx,
|
|
4190
|
-
sourceColumn,
|
|
4191
|
-
outputColumn
|
|
4192
|
-
);
|
|
4193
|
-
};
|
|
4194
|
-
if (select.type === "fields") {
|
|
4195
|
-
for (const field of select.fields) {
|
|
4196
|
-
if (field === "*") {
|
|
4197
|
-
if (!addStarProjection()) return false;
|
|
4198
|
-
} else if (!addDirectProjection(field, field)) {
|
|
4199
|
-
return false;
|
|
4200
|
-
}
|
|
4201
|
-
}
|
|
4202
|
-
return projectedColumnCount > 0;
|
|
4203
|
-
}
|
|
4204
|
-
if (select.type === "aggregate") return false;
|
|
4205
|
-
for (const expr of select.columns) {
|
|
4206
|
-
if (expr.kind === "column" && expr.column === "*") {
|
|
4207
|
-
if (!addStarProjection()) return false;
|
|
4208
|
-
continue;
|
|
4209
|
-
}
|
|
4210
|
-
if (expr.kind === "column") {
|
|
4211
|
-
const outputColumn = expr.as ?? expr.column;
|
|
4212
|
-
if (!addDirectProjection(expr.column, outputColumn)) return false;
|
|
4213
|
-
continue;
|
|
4214
|
-
}
|
|
4215
|
-
if (expr.kind === "columnAlias") {
|
|
4216
|
-
if (!addDirectProjection(expr.column, expr.alias)) return false;
|
|
4217
|
-
continue;
|
|
4218
|
-
}
|
|
4219
|
-
return false;
|
|
4286
|
+
function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding, outputSchema) {
|
|
4287
|
+
if (!sourceWasBinding && ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
|
|
4288
|
+
return {
|
|
4289
|
+
supported: false,
|
|
4290
|
+
reason: `no physical model table source '${query.from}'`
|
|
4291
|
+
};
|
|
4220
4292
|
}
|
|
4221
|
-
|
|
4222
|
-
}
|
|
4223
|
-
function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding) {
|
|
4224
|
-
if (sourceWasBinding) {
|
|
4225
|
-
return { supported: false, reason: "a binding source" };
|
|
4293
|
+
if (outputSchema?.columnTypes) {
|
|
4294
|
+
return { supported: true };
|
|
4226
4295
|
}
|
|
4227
|
-
|
|
4296
|
+
const untypeable = outputSchema?.columnTypesUnavailable;
|
|
4297
|
+
if (untypeable?.reason === "unsupported-aggregate") {
|
|
4228
4298
|
return {
|
|
4229
4299
|
supported: false,
|
|
4230
|
-
reason: `
|
|
4300
|
+
reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
|
|
4231
4301
|
};
|
|
4232
4302
|
}
|
|
4233
|
-
if (
|
|
4303
|
+
if (untypeable) {
|
|
4234
4304
|
return {
|
|
4235
4305
|
supported: false,
|
|
4236
|
-
reason:
|
|
4306
|
+
reason: `${untypeable.reason} column '${untypeable.column}'`
|
|
4237
4307
|
};
|
|
4238
4308
|
}
|
|
4239
|
-
|
|
4309
|
+
if (sourceWasBinding) {
|
|
4310
|
+
return { supported: false, reason: "a binding source" };
|
|
4311
|
+
}
|
|
4312
|
+
return { supported: false, reason: "aliased/computed/aggregate columns" };
|
|
4240
4313
|
}
|
|
4241
4314
|
function addReadBindingReference(references, readBindingNames, name) {
|
|
4242
4315
|
if (typeof name === "string" && readBindingNames.has(name)) {
|
|
@@ -4479,6 +4552,7 @@ var NqlCompiler = class {
|
|
|
4479
4552
|
} finally {
|
|
4480
4553
|
this.ctx.bindingOutputColumns.clear();
|
|
4481
4554
|
this.ctx.bindingRelationFilters.clear();
|
|
4555
|
+
this.ctx.lastMutationReturningItems = void 0;
|
|
4482
4556
|
this.ctx.validator?.clearVirtualBindingTables();
|
|
4483
4557
|
}
|
|
4484
4558
|
}
|
|
@@ -4575,7 +4649,8 @@ var NqlCompiler = class {
|
|
|
4575
4649
|
classifyReadBindingSnapshotShape(
|
|
4576
4650
|
lastResult.query,
|
|
4577
4651
|
this.ctx,
|
|
4578
|
-
sourceWasBinding
|
|
4652
|
+
sourceWasBinding,
|
|
4653
|
+
outputSchema
|
|
4579
4654
|
)
|
|
4580
4655
|
);
|
|
4581
4656
|
bindings.set(bindName, lastResult.query);
|
|
@@ -4665,7 +4740,12 @@ var NqlCompiler = class {
|
|
|
4665
4740
|
query,
|
|
4666
4741
|
this.ctx,
|
|
4667
4742
|
bindName,
|
|
4668
|
-
bindingDependencies
|
|
4743
|
+
bindingDependencies,
|
|
4744
|
+
// #213 B2: earlier bindings are already registered here (this
|
|
4745
|
+
// map is populated in program-statement order), so a
|
|
4746
|
+
// transitive `from` = binding chains through the SOURCE's
|
|
4747
|
+
// typed schema instead of staying untypeable.
|
|
4748
|
+
bindingOutputSchemas
|
|
4669
4749
|
);
|
|
4670
4750
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
4671
4751
|
this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
|
|
@@ -4690,7 +4770,50 @@ var NqlCompiler = class {
|
|
|
4690
4770
|
mutation
|
|
4691
4771
|
);
|
|
4692
4772
|
const returning = mutation.returning;
|
|
4693
|
-
|
|
4773
|
+
const returningItems = mutation.returningItems;
|
|
4774
|
+
if (returning === void 0 || returning.length === 0 || returning.includes("*") || outputSchema === void 0) {
|
|
4775
|
+
return { mutation, outputSchema };
|
|
4776
|
+
}
|
|
4777
|
+
const seenOutputs = /* @__PURE__ */ new Set();
|
|
4778
|
+
for (const column of outputSchema.columns) {
|
|
4779
|
+
if (seenOutputs.has(column)) {
|
|
4780
|
+
throw new NqlSemanticException(
|
|
4781
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
4782
|
+
`Mutation RETURNING resolves to duplicate output name '${column}' after column canonicalization for binding '${bindName}'.`
|
|
4783
|
+
);
|
|
4784
|
+
}
|
|
4785
|
+
seenOutputs.add(column);
|
|
4786
|
+
}
|
|
4787
|
+
if (returningItems !== void 0) {
|
|
4788
|
+
const internalItems = this.ctx.lastMutationReturningItems !== void 0 && this.ctx.lastMutationReturningItems !== "star" ? this.ctx.lastMutationReturningItems : void 0;
|
|
4789
|
+
const canonicalItems = returningItems.map((item, index) => {
|
|
4790
|
+
const source = this.ctx.validator?.resolveColumnName(mutation.table, item.source) ?? item.source;
|
|
4791
|
+
const internalItem = internalItems?.[index];
|
|
4792
|
+
const output = internalItem?.aliased ? item.output : source;
|
|
4793
|
+
return source === item.source && output === item.output ? item : { source, output };
|
|
4794
|
+
});
|
|
4795
|
+
if (!mutationReturningItemsMatchReturning(
|
|
4796
|
+
outputSchema.columns,
|
|
4797
|
+
canonicalItems
|
|
4798
|
+
)) {
|
|
4799
|
+
throw new NqlSemanticException(
|
|
4800
|
+
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
4801
|
+
`Mutation RETURNING item outputs drifted during canonicalization for binding '${bindName}'.`
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
if (stringArraysEqual(returning, outputSchema.columns) && canonicalItems.every((item, index) => item === returningItems[index])) {
|
|
4805
|
+
return { mutation, outputSchema };
|
|
4806
|
+
}
|
|
4807
|
+
return {
|
|
4808
|
+
mutation: {
|
|
4809
|
+
...mutation,
|
|
4810
|
+
returning: outputSchema.columns,
|
|
4811
|
+
returningItems: canonicalItems
|
|
4812
|
+
},
|
|
4813
|
+
outputSchema
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4816
|
+
if (stringArraysEqual(returning, outputSchema.columns)) {
|
|
4694
4817
|
return { mutation, outputSchema };
|
|
4695
4818
|
}
|
|
4696
4819
|
return {
|
|
@@ -4705,20 +4828,62 @@ var NqlCompiler = class {
|
|
|
4705
4828
|
const returning = mutation.returning;
|
|
4706
4829
|
if (!returning || returning.length === 0) return void 0;
|
|
4707
4830
|
if (returning.includes("*")) {
|
|
4708
|
-
const
|
|
4709
|
-
if (
|
|
4831
|
+
const columns2 = this.ctx.validator?.getTableColumns(mutation.table);
|
|
4832
|
+
if (columns2 !== void 0) {
|
|
4833
|
+
return {
|
|
4834
|
+
columns: columns2,
|
|
4835
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns2)
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4710
4838
|
if (!this.ctx.validator) return void 0;
|
|
4711
4839
|
throw new NqlSemanticException(
|
|
4712
4840
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
4713
4841
|
`Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
|
|
4714
4842
|
);
|
|
4715
4843
|
}
|
|
4844
|
+
const columns = returning.map(
|
|
4845
|
+
(column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
|
|
4846
|
+
);
|
|
4847
|
+
const returningItems = mutation.returningItems;
|
|
4848
|
+
if (returningItems !== void 0) {
|
|
4849
|
+
const internalItems = this.ctx.lastMutationReturningItems !== void 0 && this.ctx.lastMutationReturningItems !== "star" ? this.ctx.lastMutationReturningItems : void 0;
|
|
4850
|
+
const columns2 = returningItems.map((item, index) => {
|
|
4851
|
+
const source = this.ctx.validator?.resolveColumnName(mutation.table, item.source) ?? item.source;
|
|
4852
|
+
return internalItems?.[index]?.aliased ? item.output : source;
|
|
4853
|
+
});
|
|
4854
|
+
return {
|
|
4855
|
+
columns: columns2,
|
|
4856
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns2)
|
|
4857
|
+
};
|
|
4858
|
+
}
|
|
4716
4859
|
return {
|
|
4717
|
-
columns
|
|
4718
|
-
|
|
4719
|
-
)
|
|
4860
|
+
columns,
|
|
4861
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns)
|
|
4720
4862
|
};
|
|
4721
4863
|
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Build `columnTypes`/`columnTypesUnavailable` for a mutation-RETURNING
|
|
4866
|
+
* binding. #213 B2: detection consumes `ctx.lastMutationReturningItems`
|
|
4867
|
+
* (alias-aware, positional) — NEVER the collapsed `columns` names — so an
|
|
4868
|
+
* alias colliding with a real column (`returning email as name`) is
|
|
4869
|
+
* flagged 'aliased-returning' rather than silently mistyped as the
|
|
4870
|
+
* colliding column's type.
|
|
4871
|
+
*/
|
|
4872
|
+
buildMutationReturningColumnTypes(table, columns) {
|
|
4873
|
+
const items = this.ctx.lastMutationReturningItems;
|
|
4874
|
+
const candidates = columns.map(
|
|
4875
|
+
(column, index) => {
|
|
4876
|
+
const item = items !== void 0 && items !== "star" ? items[index] : void 0;
|
|
4877
|
+
if (item?.aliased) {
|
|
4878
|
+
return { column, untypeable: "aliased-returning" };
|
|
4879
|
+
}
|
|
4880
|
+
const typeInfo = this.ctx.validator?.getTableColumnType(table, column);
|
|
4881
|
+
return typeInfo !== void 0 ? { column, typed: typeInfo } : { column, untypeable: "unresolvable-source" };
|
|
4882
|
+
}
|
|
4883
|
+
);
|
|
4884
|
+
const typesResult = buildBindingColumnTypes(columns, candidates);
|
|
4885
|
+
return "columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable };
|
|
4886
|
+
}
|
|
4722
4887
|
compileSingleStatement(stmt, bindings) {
|
|
4723
4888
|
if (stmt.type === "withQuery") {
|
|
4724
4889
|
return compileWithQuery(stmt, this.ctx, this.fns);
|