@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 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
- returning = extractReturningColumns(clause, ctx);
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 extractReturningColumns(clause, ctx) {
1315
- const columns = [];
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
- columns.push(item.alias ?? field);
1387
+ items.push({
1388
+ output: item.alias ?? field,
1389
+ aliased: item.alias !== void 0
1390
+ });
1327
1391
  }
1328
1392
  }
1329
1393
  }
1330
- return columns;
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: resolveSourceColumn(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 { select } = intent;
2494
- if (!select || select.type === "all") {
2495
- addSourceColumns();
2496
- const outputSchema2 = { columns, directProjectionLineage };
2587
+ const finalizeOutputSchema = () => {
2588
+ const outputSchema = { columns, directProjectionLineage };
2589
+ const typesResult = buildBindingColumnTypes(columns, typeCandidates);
2497
2590
  return {
2498
- columns: outputSchema2.columns,
2591
+ columns: outputSchema.columns,
2499
2592
  relationFilters: getBindingRelationFilterMetadata(
2500
2593
  intent,
2501
2594
  ctx,
2502
- outputSchema2,
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
- const outputSchema2 = { columns, directProjectionLineage };
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
- addColumn(aggregate.as);
2627
+ if (aggregate.function === "count") {
2628
+ addCountAggregateColumn(aggregate.as);
2629
+ } else {
2630
+ addUntypedColumn(aggregate.as, "unsupported-aggregate");
2631
+ }
2538
2632
  }
2539
- const outputSchema2 = { columns, directProjectionLineage };
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
- addColumn(outputColumn);
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
- const outputSchema = { columns, directProjectionLineage };
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 isExactPhysicalProjectionColumn(query, ctx, sourceColumn, outputColumn) {
4169
- const resolvedColumn = ctx.validator?.resolvePhysicalColumnName(
4170
- query.from,
4171
- sourceColumn
4172
- );
4173
- return resolvedColumn !== void 0 && sourceColumn === resolvedColumn && outputColumn === resolvedColumn;
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
- return projectedColumnCount > 0;
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
- if (ctx.validator?.getPhysicalTableColumns(query.from) === void 0) {
4259
+ const untypeable = outputSchema?.columnTypesUnavailable;
4260
+ if (untypeable?.reason === "unsupported-aggregate") {
4228
4261
  return {
4229
4262
  supported: false,
4230
- reason: `no physical model table source '${query.from}'`
4263
+ reason: `aliased/computed/aggregate columns (unsupported aggregate column '${untypeable.column}')`
4231
4264
  };
4232
4265
  }
4233
- if (!hasDirectPhysicalColumnProjection(query, ctx)) {
4266
+ if (untypeable) {
4234
4267
  return {
4235
4268
  supported: false,
4236
- reason: "aliased/computed/aggregate columns"
4269
+ reason: `${untypeable.reason} column '${untypeable.column}'`
4237
4270
  };
4238
4271
  }
4239
- return { supported: true };
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 columns = this.ctx.validator?.getTableColumns(mutation.table);
4709
- if (columns !== void 0) return { columns };
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: returning.map(
4718
- (column) => this.ctx.validator?.resolveColumnName(mutation.table, column) ?? column
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);