@dbsp/nql 1.7.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 +22 -4
- package/dist/index.js +189 -116
- 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
|
}
|
|
@@ -1119,11 +1177,14 @@ function compileMutationPipeline(pipeline, ctx, fns, bindings) {
|
|
|
1119
1177
|
ctx.currentFromTable = pipeline.mutation.table;
|
|
1120
1178
|
const mutation = compileMutation(pipeline.mutation, ctx, fns, bindings);
|
|
1121
1179
|
let returning;
|
|
1180
|
+
let returningItems;
|
|
1122
1181
|
for (const clause of pipeline.clauses) {
|
|
1123
1182
|
if (clause.type === "select") {
|
|
1124
|
-
|
|
1183
|
+
returningItems = extractReturningItems(clause, ctx);
|
|
1184
|
+
returning = returningItems === "star" ? ["*"] : returningItems.map((item) => item.output);
|
|
1125
1185
|
}
|
|
1126
1186
|
}
|
|
1187
|
+
ctx.lastMutationReturningItems = returningItems;
|
|
1127
1188
|
if (returning) {
|
|
1128
1189
|
return {
|
|
1129
1190
|
mutation: { ...mutation, returning }
|
|
@@ -1311,11 +1372,11 @@ function compileUpsertFrom(upsertFrom, ctx, fns, bindings) {
|
|
|
1311
1372
|
}
|
|
1312
1373
|
};
|
|
1313
1374
|
}
|
|
1314
|
-
function
|
|
1315
|
-
const
|
|
1375
|
+
function extractReturningItems(clause, ctx) {
|
|
1376
|
+
const items = [];
|
|
1316
1377
|
for (const item of clause.items) {
|
|
1317
1378
|
if (item.type === "star") {
|
|
1318
|
-
return
|
|
1379
|
+
return "star";
|
|
1319
1380
|
}
|
|
1320
1381
|
if (item.type === "expression") {
|
|
1321
1382
|
const field = expressionToField(item.expression);
|
|
@@ -1323,11 +1384,14 @@ function extractReturningColumns(clause, ctx) {
|
|
|
1323
1384
|
if (ctx.currentFromTable && !field.includes(".")) {
|
|
1324
1385
|
ctx.validator?.validateColumn(ctx.currentFromTable, field);
|
|
1325
1386
|
}
|
|
1326
|
-
|
|
1387
|
+
items.push({
|
|
1388
|
+
output: item.alias ?? field,
|
|
1389
|
+
aliased: item.alias !== void 0
|
|
1390
|
+
});
|
|
1327
1391
|
}
|
|
1328
1392
|
}
|
|
1329
1393
|
}
|
|
1330
|
-
return
|
|
1394
|
+
return items;
|
|
1331
1395
|
}
|
|
1332
1396
|
function resolveBindingsInWhere(where, bindings) {
|
|
1333
1397
|
if (!bindings || bindings.size === 0) return where;
|
|
@@ -2470,39 +2534,74 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
|
|
|
2470
2534
|
`Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
|
|
2471
2535
|
);
|
|
2472
2536
|
}
|
|
2473
|
-
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
|
|
2537
|
+
function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = [], sourceBindingSchemas) {
|
|
2474
2538
|
const columns = [];
|
|
2475
2539
|
const seen = /* @__PURE__ */ new Set();
|
|
2476
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);
|
|
2477
2544
|
const addColumn = (column) => addUnique(columns, seen, column);
|
|
2478
2545
|
const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
|
|
2479
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
|
+
);
|
|
2480
2563
|
if (!addColumn(outputColumn)) return;
|
|
2481
2564
|
directProjectionLineage.push({
|
|
2482
2565
|
kind: "directProjection",
|
|
2483
2566
|
sourceTable: intent.from,
|
|
2484
|
-
sourceColumn:
|
|
2567
|
+
sourceColumn: resolvedSourceColumn,
|
|
2485
2568
|
outputColumn
|
|
2486
2569
|
});
|
|
2487
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
|
+
};
|
|
2488
2582
|
const addSourceColumns = () => {
|
|
2489
2583
|
for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
|
|
2490
2584
|
addDirectProjection(column, column);
|
|
2491
2585
|
}
|
|
2492
2586
|
};
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const outputSchema2 = { columns, directProjectionLineage };
|
|
2587
|
+
const finalizeOutputSchema = () => {
|
|
2588
|
+
const outputSchema = { columns, directProjectionLineage };
|
|
2589
|
+
const typesResult = buildBindingColumnTypes(columns, typeCandidates);
|
|
2497
2590
|
return {
|
|
2498
|
-
columns:
|
|
2591
|
+
columns: outputSchema.columns,
|
|
2499
2592
|
relationFilters: getBindingRelationFilterMetadata(
|
|
2500
2593
|
intent,
|
|
2501
2594
|
ctx,
|
|
2502
|
-
|
|
2595
|
+
outputSchema,
|
|
2503
2596
|
bindingDependencies
|
|
2504
|
-
)
|
|
2597
|
+
),
|
|
2598
|
+
..."columnTypes" in typesResult ? { columnTypes: typesResult.columnTypes } : { columnTypesUnavailable: typesResult.untypeable }
|
|
2505
2599
|
};
|
|
2600
|
+
};
|
|
2601
|
+
const { select } = intent;
|
|
2602
|
+
if (!select || select.type === "all") {
|
|
2603
|
+
addSourceColumns();
|
|
2604
|
+
return finalizeOutputSchema();
|
|
2506
2605
|
}
|
|
2507
2606
|
if (select.type === "fields") {
|
|
2508
2607
|
for (const field of select.fields) {
|
|
@@ -2512,16 +2611,7 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2512
2611
|
addDirectProjection(field, field);
|
|
2513
2612
|
}
|
|
2514
2613
|
}
|
|
2515
|
-
|
|
2516
|
-
return {
|
|
2517
|
-
columns: outputSchema2.columns,
|
|
2518
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2519
|
-
intent,
|
|
2520
|
-
ctx,
|
|
2521
|
-
outputSchema2,
|
|
2522
|
-
bindingDependencies
|
|
2523
|
-
)
|
|
2524
|
-
};
|
|
2614
|
+
return finalizeOutputSchema();
|
|
2525
2615
|
}
|
|
2526
2616
|
if (select.type === "aggregate") {
|
|
2527
2617
|
for (const field of select.fields ?? []) {
|
|
@@ -2534,18 +2624,13 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2534
2624
|
`Cannot compute output schema for NQL binding '${bindingName}': aggregate '${aggregate.function}' must use an alias.`
|
|
2535
2625
|
);
|
|
2536
2626
|
}
|
|
2537
|
-
|
|
2627
|
+
if (aggregate.function === "count") {
|
|
2628
|
+
addCountAggregateColumn(aggregate.as);
|
|
2629
|
+
} else {
|
|
2630
|
+
addUntypedColumn(aggregate.as, "unsupported-aggregate");
|
|
2631
|
+
}
|
|
2538
2632
|
}
|
|
2539
|
-
|
|
2540
|
-
return {
|
|
2541
|
-
columns: outputSchema2.columns,
|
|
2542
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2543
|
-
intent,
|
|
2544
|
-
ctx,
|
|
2545
|
-
outputSchema2,
|
|
2546
|
-
bindingDependencies
|
|
2547
|
-
)
|
|
2548
|
-
};
|
|
2633
|
+
return finalizeOutputSchema();
|
|
2549
2634
|
}
|
|
2550
2635
|
for (const expr of select.columns) {
|
|
2551
2636
|
if (expr.kind === "column" && expr.column === "*") {
|
|
@@ -2569,20 +2654,16 @@ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []
|
|
|
2569
2654
|
addDirectProjection(outputColumn, expr.column);
|
|
2570
2655
|
} else if (expr.kind === "columnAlias") {
|
|
2571
2656
|
addDirectProjection(outputColumn, expr.column);
|
|
2657
|
+
} else if (expr.kind === "aggregate" && expr.function === "count") {
|
|
2658
|
+
addCountAggregateColumn(outputColumn);
|
|
2572
2659
|
} else {
|
|
2573
|
-
|
|
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);
|
|
2574
2664
|
}
|
|
2575
2665
|
}
|
|
2576
|
-
|
|
2577
|
-
return {
|
|
2578
|
-
columns: outputSchema.columns,
|
|
2579
|
-
relationFilters: getBindingRelationFilterMetadata(
|
|
2580
|
-
intent,
|
|
2581
|
-
ctx,
|
|
2582
|
-
outputSchema,
|
|
2583
|
-
bindingDependencies
|
|
2584
|
-
)
|
|
2585
|
-
};
|
|
2666
|
+
return finalizeOutputSchema();
|
|
2586
2667
|
}
|
|
2587
2668
|
function validateNqlExpressionPaths(expr, ctx) {
|
|
2588
2669
|
switch (expr.type) {
|
|
@@ -4165,78 +4246,33 @@ function stringArraysEqual(left, right) {
|
|
|
4165
4246
|
function isNqlAstRecord(value) {
|
|
4166
4247
|
return typeof value === "object" && value !== null;
|
|
4167
4248
|
}
|
|
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;
|
|
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
|
+
};
|
|
4220
4255
|
}
|
|
4221
|
-
|
|
4222
|
-
}
|
|
4223
|
-
function classifyReadBindingSnapshotShape(query, ctx, sourceWasBinding) {
|
|
4224
|
-
if (sourceWasBinding) {
|
|
4225
|
-
return { supported: false, reason: "a binding source" };
|
|
4256
|
+
if (outputSchema?.columnTypes) {
|
|
4257
|
+
return { supported: true };
|
|
4226
4258
|
}
|
|
4227
|
-
|
|
4259
|
+
const untypeable = outputSchema?.columnTypesUnavailable;
|
|
4260
|
+
if (untypeable?.reason === "unsupported-aggregate") {
|
|
4228
4261
|
return {
|
|
4229
4262
|
supported: false,
|
|
4230
|
-
reason: `
|
|
4263
|
+
reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
|
|
4231
4264
|
};
|
|
4232
4265
|
}
|
|
4233
|
-
if (
|
|
4266
|
+
if (untypeable) {
|
|
4234
4267
|
return {
|
|
4235
4268
|
supported: false,
|
|
4236
|
-
reason:
|
|
4269
|
+
reason: `${untypeable.reason} column '${untypeable.column}'`
|
|
4237
4270
|
};
|
|
4238
4271
|
}
|
|
4239
|
-
|
|
4272
|
+
if (sourceWasBinding) {
|
|
4273
|
+
return { supported: false, reason: "a binding source" };
|
|
4274
|
+
}
|
|
4275
|
+
return { supported: false, reason: "aliased/computed/aggregate columns" };
|
|
4240
4276
|
}
|
|
4241
4277
|
function addReadBindingReference(references, readBindingNames, name) {
|
|
4242
4278
|
if (typeof name === "string" && readBindingNames.has(name)) {
|
|
@@ -4479,6 +4515,7 @@ var NqlCompiler = class {
|
|
|
4479
4515
|
} finally {
|
|
4480
4516
|
this.ctx.bindingOutputColumns.clear();
|
|
4481
4517
|
this.ctx.bindingRelationFilters.clear();
|
|
4518
|
+
this.ctx.lastMutationReturningItems = void 0;
|
|
4482
4519
|
this.ctx.validator?.clearVirtualBindingTables();
|
|
4483
4520
|
}
|
|
4484
4521
|
}
|
|
@@ -4575,7 +4612,8 @@ var NqlCompiler = class {
|
|
|
4575
4612
|
classifyReadBindingSnapshotShape(
|
|
4576
4613
|
lastResult.query,
|
|
4577
4614
|
this.ctx,
|
|
4578
|
-
sourceWasBinding
|
|
4615
|
+
sourceWasBinding,
|
|
4616
|
+
outputSchema
|
|
4579
4617
|
)
|
|
4580
4618
|
);
|
|
4581
4619
|
bindings.set(bindName, lastResult.query);
|
|
@@ -4665,7 +4703,12 @@ var NqlCompiler = class {
|
|
|
4665
4703
|
query,
|
|
4666
4704
|
this.ctx,
|
|
4667
4705
|
bindName,
|
|
4668
|
-
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
|
|
4669
4712
|
);
|
|
4670
4713
|
bindingOutputSchemas.set(bindName, outputSchema);
|
|
4671
4714
|
this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
|
|
@@ -4705,20 +4748,50 @@ var NqlCompiler = class {
|
|
|
4705
4748
|
const returning = mutation.returning;
|
|
4706
4749
|
if (!returning || returning.length === 0) return void 0;
|
|
4707
4750
|
if (returning.includes("*")) {
|
|
4708
|
-
const
|
|
4709
|
-
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
|
+
}
|
|
4710
4758
|
if (!this.ctx.validator) return void 0;
|
|
4711
4759
|
throw new NqlSemanticException(
|
|
4712
4760
|
NqlErrorCodes.SEM_INVALID_SYNTAX,
|
|
4713
4761
|
`Cannot compute output schema for NQL binding '${bindName}' from mutation RETURNING * on '${mutation.table}' without a concrete table schema.`
|
|
4714
4762
|
);
|
|
4715
4763
|
}
|
|
4764
|
+
const columns = returning.map(
|
|
4765
|
+
(column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
|
|
4766
|
+
);
|
|
4716
4767
|
return {
|
|
4717
|
-
columns
|
|
4718
|
-
|
|
4719
|
-
)
|
|
4768
|
+
columns,
|
|
4769
|
+
...this.buildMutationReturningColumnTypes(mutation.table, columns)
|
|
4720
4770
|
};
|
|
4721
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
|
+
}
|
|
4722
4795
|
compileSingleStatement(stmt, bindings) {
|
|
4723
4796
|
if (stmt.type === "withQuery") {
|
|
4724
4797
|
return compileWithQuery(stmt, this.ctx, this.fns);
|