@dbsp/nql 1.4.0 → 1.5.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NQL_INTERNAL_COMPILER_OPTIONS, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
1
+ import { NQL_INTERNAL_COMPILER_OPTIONS, markNqlTrustedRelationFilter, createNqlBindingRef, isNqlBindingRef, getNqlBindingRefName } from '@dbsp/types/internal';
2
2
  import { NQL_SELECT_AGGREGATE_FUNCTIONS, NQL_SELECT_VALUE_FUNCTIONS, NQL_SELECT_JSON_FUNCTIONS, NQL_SELECT_SCALAR_FUNCTIONS, isNqlSelectWindowFunctionAllowed, isRankingWindowFunction, isParamIntent } from '@dbsp/types';
3
3
  import { createToken, Lexer, CstParser } from 'chevrotain';
4
4
 
@@ -52,6 +52,7 @@ var ColumnValidator = class _ColumnValidator {
52
52
  schema;
53
53
  knownCteTables = /* @__PURE__ */ new Set();
54
54
  virtualBindingTables = /* @__PURE__ */ new Map();
55
+ virtualBindingRelationFilters = /* @__PURE__ */ new Map();
55
56
  /**
56
57
  * Register CTE names so validateTable() skips validation for them.
57
58
  * CTE names are not physical tables in the schema, but are valid FROM targets.
@@ -69,12 +70,16 @@ var ColumnValidator = class _ColumnValidator {
69
70
  * Register an NQL binding as a virtual table with a concrete output schema.
70
71
  * Binding columns are logical output names, so matching is exact.
71
72
  */
72
- addVirtualBindingTable(name, columns) {
73
+ addVirtualBindingTable(name, columns, relationFilters) {
73
74
  this.virtualBindingTables.set(name, columns);
75
+ if (relationFilters) {
76
+ this.virtualBindingRelationFilters.set(name, relationFilters);
77
+ }
74
78
  }
75
79
  /** Clear all registered virtual binding tables (used between compilations). */
76
80
  clearVirtualBindingTables() {
77
81
  this.virtualBindingTables.clear();
82
+ this.virtualBindingRelationFilters.clear();
78
83
  }
79
84
  isVirtualBindingTable(name) {
80
85
  return name !== void 0 && this.virtualBindingTables.has(name);
@@ -87,6 +92,98 @@ var ColumnValidator = class _ColumnValidator {
87
92
  if (virtualColumns) return virtualColumns;
88
93
  return this.schema.getTable(name)?.columns.map((column) => column.name);
89
94
  }
95
+ hasPhysicalTable(name) {
96
+ return !this.virtualBindingTables.has(name) && !!this.schema.getTable(name);
97
+ }
98
+ hasQualifiedRelationLookup() {
99
+ return typeof this.schema.getRelation === "function";
100
+ }
101
+ getRelation(sourceTable, relationName) {
102
+ return this.schema.getRelation?.(`${sourceTable}.${relationName}`) ?? this.schema.getRelationsFrom(sourceTable).find((relation) => relation.name === relationName);
103
+ }
104
+ getRelationsFrom(sourceTable) {
105
+ return this.schema.getRelationsFrom(sourceTable);
106
+ }
107
+ getVirtualBindingRelation(bindingName, relationName) {
108
+ return this.virtualBindingRelationFilters.get(bindingName)?.relations.find((relation) => relation.relation === relationName);
109
+ }
110
+ getVirtualBindingScalarRelation(bindingName, relationName) {
111
+ return this.virtualBindingRelationFilters.get(bindingName)?.scalarRelations?.find((relation) => relation.relation === relationName);
112
+ }
113
+ explainVirtualBindingRelationRejection(bindingName, relationName) {
114
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
115
+ if (!metadata) {
116
+ return "no binding relation-filter metadata is available";
117
+ }
118
+ if (metadata.unsafeReason) return metadata.unsafeReason;
119
+ const sourceTable = metadata.sourceTable;
120
+ if (!sourceTable) {
121
+ return "the binding source table could not be proven";
122
+ }
123
+ const relation = this.getRelation(sourceTable, relationName);
124
+ if (!relation) {
125
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
126
+ }
127
+ const fk = relation.foreignKey;
128
+ const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
129
+ if (relation.type !== "belongsTo") {
130
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; A-lite only supports relations whose FK column is on the binding source table`;
131
+ }
132
+ if (fkColumns.length !== 1) {
133
+ return `relation '${relationName}' must have exactly one FK column for A-lite binding relation filters`;
134
+ }
135
+ const fkColumn = fkColumns[0];
136
+ if (fkColumn === void 0) {
137
+ return `relation '${relationName}' must have exactly one FK column for A-lite binding relation filters`;
138
+ }
139
+ const directProjection = metadata.directProjectionLineage?.find(
140
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, fkColumn)
141
+ );
142
+ if (!directProjection) {
143
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
144
+ return `relation '${relationName}' FK column '${fkColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
145
+ }
146
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
147
+ return `relation '${relationName}' FK column '${fkColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
148
+ }
149
+ explainVirtualBindingScalarRelationRejection(bindingName, relationName) {
150
+ const metadata = this.virtualBindingRelationFilters.get(bindingName);
151
+ if (!metadata) {
152
+ return "no binding relation metadata is available";
153
+ }
154
+ if (metadata.unsafeReason) return metadata.unsafeReason;
155
+ const sourceTable = metadata.sourceTable;
156
+ if (!sourceTable) {
157
+ return "the binding source table could not be proven";
158
+ }
159
+ const relation = this.getRelation(sourceTable, relationName);
160
+ if (!relation) {
161
+ return `relation '${relationName}' is not declared on source table '${sourceTable}'`;
162
+ }
163
+ const fk = relation.foreignKey;
164
+ const fkColumns = typeof fk === "string" ? [fk] : Array.isArray(fk) ? [...fk] : [];
165
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
166
+ return `relation '${relationName}' is '${relation.type ?? "unknown"}'; binding relation columns require a scalar belongsTo/hasOne relation`;
167
+ }
168
+ if (fkColumns.length !== 1) {
169
+ return `relation '${relationName}' must have exactly one FK column for binding relation columns`;
170
+ }
171
+ const fkColumn = fkColumns[0];
172
+ if (fkColumn === void 0) {
173
+ return `relation '${relationName}' must have exactly one FK column for binding relation columns`;
174
+ }
175
+ const sourceColumn = relation.type === "belongsTo" ? fkColumn : relation.sourceKey ?? "id";
176
+ const directProjection = metadata.directProjectionLineage?.find(
177
+ (projection) => projection.sourceTable === sourceTable && _ColumnValidator.columnsMatch(projection.sourceColumn, sourceColumn)
178
+ );
179
+ if (!directProjection) {
180
+ const available2 = this.virtualBindingTables.get(bindingName)?.join(", ");
181
+ const sourceColumnLabel = relation.type === "belongsTo" ? "FK column" : "source key column";
182
+ return `relation '${relationName}' ${sourceColumnLabel} '${sourceColumn}' is not projected as a direct source-column projection by binding '${bindingName}'${available2 ? ` (available columns: ${available2})` : ""}`;
183
+ }
184
+ const available = this.virtualBindingTables.get(bindingName)?.join(", ");
185
+ return `relation '${relationName}' source column '${sourceColumn}' is not available through binding '${bindingName}'${available ? ` (available columns: ${available})` : ""}`;
186
+ }
90
187
  /**
91
188
  * Convert camelCase to snake_case for column name matching.
92
189
  * NQL queries may use either form (e.g., viewCount or view_count).
@@ -156,13 +253,17 @@ var ColumnValidator = class _ColumnValidator {
156
253
  }
157
254
  resolveRelationTarget(sourceTable, relationName) {
158
255
  if (this.virtualBindingTables.has(sourceTable)) {
256
+ const virtualRelation = this.getVirtualBindingRelation(
257
+ sourceTable,
258
+ relationName
259
+ );
260
+ if (virtualRelation) return virtualRelation.targetTable;
159
261
  throw new NqlSemanticException(
160
262
  NqlErrorCodes.SEM_INVALID_SYNTAX,
161
- `Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}). Relation constructs require a physical model table, not a CTE binding.`
263
+ `Query '${sourceTable}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${this.explainVirtualBindingRelationRejection(sourceTable, relationName)}.`
162
264
  );
163
265
  }
164
- const relations = this.schema.getRelationsFrom(sourceTable);
165
- const rel = relations.find((r) => r.name === relationName);
266
+ const rel = this.getRelation(sourceTable, relationName);
166
267
  return rel?.target;
167
268
  }
168
269
  assertNoBindingRelationConstruct(bindingName, construct, detail) {
@@ -288,6 +389,81 @@ function assertNoBindingRelationConstruct(ctx, bindingName, construct, detail) {
288
389
  `Query '${bindingName}' reads from an NQL binding and cannot ${construct} (${detail}). Relation constructs require a physical model table, not a CTE binding.`
289
390
  );
290
391
  }
392
+ function throwBindingRelationFilter182(bindingName, relationName, reason) {
393
+ throw new NqlSemanticException(
394
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
395
+ `Query '${bindingName}' reads from an NQL binding and cannot use relation filters (${relationName}) under A-lite (ref-#182): ${reason}.`
396
+ );
397
+ }
398
+ function throwBindingRelationColumn182(bindingName, relationName, reason) {
399
+ throw new NqlSemanticException(
400
+ NqlErrorCodes.SEM_INVALID_SYNTAX,
401
+ `Query '${bindingName}' reads from an NQL binding and cannot select relation column '${relationName}' under A-full (ref-#182): ${reason}.`
402
+ );
403
+ }
404
+ function resolveBindingRelationFilter(ctx, bindingName, relationPath) {
405
+ if (!isBindingTable(ctx, bindingName)) return void 0;
406
+ const actualBindingName = bindingName;
407
+ const relationName = relationPath.join(".");
408
+ if (relationPath.length !== 1) {
409
+ throwBindingRelationFilter182(
410
+ actualBindingName,
411
+ relationName,
412
+ "multi-hop binding relation filters are not supported; the relation path must be a single source-table relation"
413
+ );
414
+ }
415
+ const relation = relationPath[0];
416
+ if (!relation) {
417
+ throwBindingRelationFilter182(
418
+ actualBindingName,
419
+ relationName,
420
+ "the relation path must name a source-table relation"
421
+ );
422
+ }
423
+ const virtualRelation = ctx.validator?.getVirtualBindingRelation(
424
+ actualBindingName,
425
+ relation
426
+ );
427
+ if (virtualRelation) return virtualRelation;
428
+ const reason = ctx.validator?.explainVirtualBindingRelationRejection(
429
+ actualBindingName,
430
+ relation
431
+ ) ?? "model metadata is not available";
432
+ throwBindingRelationFilter182(actualBindingName, relation, reason);
433
+ }
434
+ function resolveBindingRelationColumn(ctx, bindingName, relationPath, selectedColumn) {
435
+ if (!isBindingTable(ctx, bindingName)) return void 0;
436
+ const actualBindingName = bindingName;
437
+ const relationName = relationPath.join(".");
438
+ if (relationPath.length !== 1) {
439
+ throwBindingRelationColumn182(
440
+ actualBindingName,
441
+ relationName,
442
+ "multi-hop binding relation columns are not supported; the relation path must be a single source-table relation"
443
+ );
444
+ }
445
+ const relation = relationPath[0];
446
+ if (!relation) {
447
+ throwBindingRelationColumn182(
448
+ actualBindingName,
449
+ relationName,
450
+ "the relation path must name a source-table relation"
451
+ );
452
+ }
453
+ const virtualRelation = ctx.validator?.getVirtualBindingScalarRelation(
454
+ actualBindingName,
455
+ relation
456
+ );
457
+ if (!virtualRelation) {
458
+ const reason = ctx.validator?.explainVirtualBindingScalarRelationRejection(
459
+ actualBindingName,
460
+ relation
461
+ ) ?? "model metadata is not available";
462
+ throwBindingRelationColumn182(actualBindingName, relation, reason);
463
+ }
464
+ ctx.validator?.validateColumn(virtualRelation.targetTable, selectedColumn);
465
+ return virtualRelation;
466
+ }
291
467
  function assertNoBindingRelationPath(ctx, bindingName, path) {
292
468
  if (!isBindingTable(ctx, bindingName)) return;
293
469
  throw new NqlSemanticException(
@@ -518,6 +694,10 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
518
694
  `HAVING cannot reference SELECT alias '${field}'. PostgreSQL does not allow SELECT aliases in HAVING; repeat the aggregate expression or filter the result in an outer query.`
519
695
  );
520
696
  }
697
+ if (!aliasContext && ctx.currentRelationTarget && !field.includes(".")) {
698
+ ctx.validator?.validateColumn(ctx.currentRelationTarget, field);
699
+ return;
700
+ }
521
701
  if (!aliasContext && ctx.currentFromTable && isBindingTable(ctx, ctx.currentFromTable)) {
522
702
  if (field.includes(".")) {
523
703
  assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
@@ -540,10 +720,6 @@ function validateWhereField(ctx, field, aliasContext, originalExpr) {
540
720
  }
541
721
  return;
542
722
  }
543
- if (ctx.currentRelationTarget && !field.includes(".")) {
544
- ctx.validator.validateColumn(ctx.currentRelationTarget, field);
545
- return;
546
- }
547
723
  if (ctx.currentFromTable && field.includes(".")) {
548
724
  if (isBindingTable(ctx, ctx.currentFromTable)) {
549
725
  assertNoBindingRelationPath(ctx, ctx.currentFromTable, field);
@@ -923,6 +1099,7 @@ var PORTABLE_BINDING_FINAL_FUNCTIONS = /* @__PURE__ */ new Set([
923
1099
  ...NQL_SELECT_AGGREGATE_FUNCTIONS,
924
1100
  ...NQL_SELECT_VALUE_FUNCTIONS
925
1101
  ]);
1102
+ var DEFAULT_RELATION_TARGET_COLUMN = "id";
926
1103
  function compileNestedQuery(query, ctx, fns, bindings) {
927
1104
  const savedContext = {
928
1105
  currentFromTable: ctx.currentFromTable,
@@ -975,6 +1152,168 @@ function throwUnsupportedBindingFinal(bindingName, feature) {
975
1152
  `Query '${bindingName}' reads from an NQL binding and cannot use ${feature} in a binding-final query (#183). Binding-final queries are restricted to portable common-ground SQL over projected binding columns: SELECT (including plain DISTINCT), WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, and UNION/INTERSECT/EXCEPT.`
976
1153
  );
977
1154
  }
1155
+ function toSnakeCase(name) {
1156
+ return name.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
1157
+ }
1158
+ function columnsMatch(left, right) {
1159
+ return left === right || toSnakeCase(left) === toSnakeCase(right);
1160
+ }
1161
+ function expressionIntentContainsAggregate(value) {
1162
+ if (!value || typeof value !== "object") return false;
1163
+ const record = value;
1164
+ if (record.kind === "aggregate") return true;
1165
+ for (const child of Object.values(record)) {
1166
+ if (Array.isArray(child)) {
1167
+ if (child.some((item) => expressionIntentContainsAggregate(item))) {
1168
+ return true;
1169
+ }
1170
+ } else if (expressionIntentContainsAggregate(child)) {
1171
+ return true;
1172
+ }
1173
+ }
1174
+ return false;
1175
+ }
1176
+ function selectContainsAggregate(select) {
1177
+ if (!select) return false;
1178
+ if (select.type === "aggregate") return true;
1179
+ if (select.type !== "expressions") return false;
1180
+ return select.columns.some(
1181
+ (column) => expressionIntentContainsAggregate(column)
1182
+ );
1183
+ }
1184
+ function relationForeignKeysOnSource(relation) {
1185
+ if (!relation || relation.type !== "belongsTo") return void 0;
1186
+ if (typeof relation.foreignKey === "string") return [relation.foreignKey];
1187
+ if (Array.isArray(relation.foreignKey)) return relation.foreignKey;
1188
+ return void 0;
1189
+ }
1190
+ function relationForeignKeys(relation) {
1191
+ if (!relation) return void 0;
1192
+ if (typeof relation.foreignKey === "string") return [relation.foreignKey];
1193
+ if (Array.isArray(relation.foreignKey)) return relation.foreignKey;
1194
+ return void 0;
1195
+ }
1196
+ function relationCardinality(relation) {
1197
+ return relation?.type === "hasMany" || relation?.type === "belongsToMany" ? "many" : "one";
1198
+ }
1199
+ function unsafeBindingRelationReason(intent, ctx, bindingDependencies) {
1200
+ if (!ctx.validator) return "model metadata is not available";
1201
+ if (!ctx.validator.hasQualifiedRelationLookup()) {
1202
+ return "model.getRelation metadata is not available";
1203
+ }
1204
+ if (bindingDependencies.length > 0 || isBindingTable(ctx, intent.from)) {
1205
+ return "the binding body reads from another NQL binding";
1206
+ }
1207
+ if (!ctx.validator.hasPhysicalTable(intent.from)) {
1208
+ return `the binding source '${intent.from}' is not a single real model table`;
1209
+ }
1210
+ if (intent.batchValuesSource) {
1211
+ return "the binding body reads from a batch-values source";
1212
+ }
1213
+ if ((intent.joins?.length ?? 0) > 0) {
1214
+ return "the binding body uses joins";
1215
+ }
1216
+ if ((intent.include?.length ?? 0) > 0) {
1217
+ return "the binding body uses relation includes";
1218
+ }
1219
+ if ((intent.groupBy?.length ?? 0) > 0 || intent.having) {
1220
+ return "the binding body uses GROUP BY or HAVING";
1221
+ }
1222
+ if (selectContainsAggregate(intent.select)) {
1223
+ return "the binding body uses aggregate projections";
1224
+ }
1225
+ return void 0;
1226
+ }
1227
+ function findDirectSourceProjection(sourceTable, sourceColumn, directProjectionLineage) {
1228
+ return directProjectionLineage.find(
1229
+ (projection) => projection.sourceTable === sourceTable && columnsMatch(projection.sourceColumn, sourceColumn)
1230
+ );
1231
+ }
1232
+ function virtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1233
+ const fkColumns = relationForeignKeysOnSource(relation);
1234
+ if (!relation || !fkColumns || fkColumns.length !== 1) return void 0;
1235
+ const fkColumn = fkColumns[0];
1236
+ const sourceProjection = findDirectSourceProjection(
1237
+ sourceTable,
1238
+ fkColumn,
1239
+ directProjectionLineage
1240
+ );
1241
+ if (!sourceProjection) return void 0;
1242
+ return {
1243
+ relation: relation.name,
1244
+ sourceTable,
1245
+ targetTable: relation.target,
1246
+ sourceColumn: sourceProjection.outputColumn,
1247
+ targetColumn: relation.targetKey ?? DEFAULT_RELATION_TARGET_COLUMN,
1248
+ cardinality: "one"
1249
+ };
1250
+ }
1251
+ function scalarVirtualRelationForBinding(relation, sourceTable, directProjectionLineage) {
1252
+ if (!relation) return void 0;
1253
+ if (relation.type !== "belongsTo" && relation.type !== "hasOne") {
1254
+ return void 0;
1255
+ }
1256
+ const fkColumns = relationForeignKeys(relation);
1257
+ if (!fkColumns || fkColumns.length !== 1) return void 0;
1258
+ const fkColumn = fkColumns[0];
1259
+ const sourceJoinColumn = relation.type === "belongsTo" ? fkColumn : relation.sourceKey ?? DEFAULT_RELATION_TARGET_COLUMN;
1260
+ const targetJoinColumn = relation.type === "belongsTo" ? relation.targetKey ?? DEFAULT_RELATION_TARGET_COLUMN : fkColumn;
1261
+ const sourceProjection = findDirectSourceProjection(
1262
+ sourceTable,
1263
+ sourceJoinColumn,
1264
+ directProjectionLineage
1265
+ );
1266
+ if (!sourceProjection) return void 0;
1267
+ return {
1268
+ relation: relation.name,
1269
+ sourceTable,
1270
+ targetTable: relation.target,
1271
+ sourceColumn: sourceProjection.outputColumn,
1272
+ targetColumn: targetJoinColumn,
1273
+ cardinality: relationCardinality(relation)
1274
+ };
1275
+ }
1276
+ function getBindingRelationFilterMetadata(intent, ctx, outputSchema, bindingDependencies) {
1277
+ const unsafeReason = unsafeBindingRelationReason(
1278
+ intent,
1279
+ ctx,
1280
+ bindingDependencies
1281
+ );
1282
+ if (unsafeReason) {
1283
+ return {
1284
+ unsafeReason,
1285
+ directProjectionLineage: outputSchema.directProjectionLineage,
1286
+ relations: []
1287
+ };
1288
+ }
1289
+ const sourceTable = intent.from;
1290
+ const relations = [];
1291
+ const scalarRelations = [];
1292
+ const relationNames = new Set(
1293
+ ctx.validator?.getRelationsFrom(sourceTable).map((relation) => relation.name)
1294
+ );
1295
+ for (const relationName of relationNames ?? []) {
1296
+ const relation = ctx.validator?.getRelation(sourceTable, relationName);
1297
+ const virtualRelation = virtualRelationForBinding(
1298
+ relation,
1299
+ sourceTable,
1300
+ outputSchema.directProjectionLineage ?? []
1301
+ );
1302
+ if (virtualRelation) relations.push(virtualRelation);
1303
+ const scalarRelation = scalarVirtualRelationForBinding(
1304
+ relation,
1305
+ sourceTable,
1306
+ outputSchema.directProjectionLineage ?? []
1307
+ );
1308
+ if (scalarRelation) scalarRelations.push(scalarRelation);
1309
+ }
1310
+ return {
1311
+ sourceTable,
1312
+ directProjectionLineage: outputSchema.directProjectionLineage,
1313
+ relations,
1314
+ scalarRelations
1315
+ };
1316
+ }
978
1317
  function validateBindingFinalPath(expr, ctx, bindingName) {
979
1318
  const { segments } = expr;
980
1319
  if (segments.length === 1) {
@@ -999,63 +1338,151 @@ function validateBindingFinalPath(expr, ctx, bindingName) {
999
1338
  }
1000
1339
  assertNoBindingRelationPath(ctx, bindingName, segments.join("."));
1001
1340
  }
1002
- function validateBindingFinalFunction(expr, ctx, bindingName) {
1341
+ function validateBindingFinalFunction(expr, ctx, bindingName, allowRelationFilters) {
1003
1342
  const fn = expr.name.toLowerCase();
1004
1343
  if (!PORTABLE_BINDING_FINAL_FUNCTIONS.has(fn)) {
1005
1344
  throwUnsupportedBindingFinal(bindingName, `function ${expr.name}()`);
1006
1345
  }
1007
1346
  for (const arg of expr.args) {
1008
- validateBindingFinalExpression(arg, ctx, bindingName);
1347
+ validateBindingFinalExpression(arg, ctx, bindingName, allowRelationFilters);
1009
1348
  }
1010
1349
  }
1011
- function validateBindingFinalExpression(expr, ctx, bindingName) {
1350
+ function validateBindingFinalExpression(expr, ctx, bindingName, allowRelationFilters = false) {
1012
1351
  switch (expr.type) {
1013
1352
  case "path":
1014
1353
  validateBindingFinalPath(expr, ctx, bindingName);
1015
1354
  break;
1016
1355
  case "binary":
1017
- validateBindingFinalExpression(expr.left, ctx, bindingName);
1018
- validateBindingFinalExpression(expr.right, ctx, bindingName);
1356
+ validateBindingFinalExpression(
1357
+ expr.left,
1358
+ ctx,
1359
+ bindingName,
1360
+ allowRelationFilters
1361
+ );
1362
+ validateBindingFinalExpression(
1363
+ expr.right,
1364
+ ctx,
1365
+ bindingName,
1366
+ allowRelationFilters
1367
+ );
1019
1368
  break;
1020
1369
  case "unary":
1021
- validateBindingFinalExpression(expr.operand, ctx, bindingName);
1370
+ validateBindingFinalExpression(
1371
+ expr.operand,
1372
+ ctx,
1373
+ bindingName,
1374
+ allowRelationFilters
1375
+ );
1022
1376
  break;
1023
1377
  case "comparison":
1024
- validateBindingFinalExpression(expr.left, ctx, bindingName);
1025
- validateBindingFinalExpression(expr.right, ctx, bindingName);
1378
+ validateBindingFinalExpression(
1379
+ expr.left,
1380
+ ctx,
1381
+ bindingName,
1382
+ allowRelationFilters
1383
+ );
1384
+ validateBindingFinalExpression(
1385
+ expr.right,
1386
+ ctx,
1387
+ bindingName,
1388
+ allowRelationFilters
1389
+ );
1026
1390
  break;
1027
1391
  case "in":
1028
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1392
+ validateBindingFinalExpression(
1393
+ expr.expression,
1394
+ ctx,
1395
+ bindingName,
1396
+ allowRelationFilters
1397
+ );
1029
1398
  if (Array.isArray(expr.values)) {
1030
1399
  for (const value of expr.values) {
1031
- validateBindingFinalExpression(value, ctx, bindingName);
1400
+ validateBindingFinalExpression(
1401
+ value,
1402
+ ctx,
1403
+ bindingName,
1404
+ allowRelationFilters
1405
+ );
1032
1406
  }
1033
1407
  } else if (expr.values.type === "dateRange") ; else ;
1034
1408
  break;
1035
1409
  case "between":
1036
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1037
- validateBindingFinalExpression(expr.low, ctx, bindingName);
1038
- validateBindingFinalExpression(expr.high, ctx, bindingName);
1410
+ validateBindingFinalExpression(
1411
+ expr.expression,
1412
+ ctx,
1413
+ bindingName,
1414
+ allowRelationFilters
1415
+ );
1416
+ validateBindingFinalExpression(
1417
+ expr.low,
1418
+ ctx,
1419
+ bindingName,
1420
+ allowRelationFilters
1421
+ );
1422
+ validateBindingFinalExpression(
1423
+ expr.high,
1424
+ ctx,
1425
+ bindingName,
1426
+ allowRelationFilters
1427
+ );
1039
1428
  break;
1040
1429
  case "isNull":
1041
- validateBindingFinalExpression(expr.expression, ctx, bindingName);
1430
+ validateBindingFinalExpression(
1431
+ expr.expression,
1432
+ ctx,
1433
+ bindingName,
1434
+ allowRelationFilters
1435
+ );
1042
1436
  break;
1043
1437
  case "function":
1044
- validateBindingFinalFunction(expr, ctx, bindingName);
1438
+ validateBindingFinalFunction(
1439
+ expr,
1440
+ ctx,
1441
+ bindingName,
1442
+ allowRelationFilters
1443
+ );
1045
1444
  break;
1046
1445
  case "case":
1047
1446
  if (expr.subject) {
1048
- validateBindingFinalExpression(expr.subject, ctx, bindingName);
1447
+ validateBindingFinalExpression(
1448
+ expr.subject,
1449
+ ctx,
1450
+ bindingName,
1451
+ allowRelationFilters
1452
+ );
1049
1453
  }
1050
1454
  for (const when of expr.whenClauses) {
1051
- validateBindingFinalExpression(when.condition, ctx, bindingName);
1052
- validateBindingFinalExpression(when.result, ctx, bindingName);
1455
+ validateBindingFinalExpression(
1456
+ when.condition,
1457
+ ctx,
1458
+ bindingName,
1459
+ allowRelationFilters
1460
+ );
1461
+ validateBindingFinalExpression(
1462
+ when.result,
1463
+ ctx,
1464
+ bindingName,
1465
+ allowRelationFilters
1466
+ );
1053
1467
  }
1054
1468
  if (expr.elseClause) {
1055
- validateBindingFinalExpression(expr.elseClause, ctx, bindingName);
1469
+ validateBindingFinalExpression(
1470
+ expr.elseClause,
1471
+ ctx,
1472
+ bindingName,
1473
+ allowRelationFilters
1474
+ );
1056
1475
  }
1057
1476
  break;
1058
1477
  case "relationFilter":
1478
+ if (allowRelationFilters) {
1479
+ resolveBindingRelationFilter(
1480
+ ctx,
1481
+ bindingName,
1482
+ expr.relation
1483
+ );
1484
+ break;
1485
+ }
1059
1486
  assertNoBindingRelationConstruct(
1060
1487
  ctx,
1061
1488
  bindingName,
@@ -1130,11 +1557,11 @@ function validateBindingFinalSelectClause(clause, ctx, bindingName) {
1130
1557
  firstSegment
1131
1558
  );
1132
1559
  } else {
1133
- assertNoBindingRelationConstruct(
1560
+ resolveBindingRelationColumn(
1134
1561
  ctx,
1135
1562
  bindingName,
1136
- "select relation columns",
1137
- segments.slice(0, -1).join(".")
1563
+ segments.slice(0, -1),
1564
+ segments.at(-1)
1138
1565
  );
1139
1566
  }
1140
1567
  } else {
@@ -1166,7 +1593,12 @@ function validateBindingFinalQuery(query, ctx) {
1166
1593
  ctx.currentHavingAliases = previousHavingAliases;
1167
1594
  }
1168
1595
  } else {
1169
- validateBindingFinalExpression(clause.condition, ctx, query.table);
1596
+ validateBindingFinalExpression(
1597
+ clause.condition,
1598
+ ctx,
1599
+ query.table,
1600
+ true
1601
+ );
1170
1602
  }
1171
1603
  break;
1172
1604
  }
@@ -1347,7 +1779,7 @@ function compileQueryInternal(query, ctx, fns, bindings) {
1347
1779
  }
1348
1780
  }
1349
1781
  }
1350
- if (select && select.type === "expressions") {
1782
+ if (select && select.type === "expressions" && !isBindingSource) {
1351
1783
  const relationPaths = /* @__PURE__ */ new Set();
1352
1784
  for (const expr of select.columns) {
1353
1785
  if (expr.kind === "relationColumn") {
@@ -1489,9 +1921,10 @@ function expressionOutputColumn(expr) {
1489
1921
  }
1490
1922
  }
1491
1923
  function addUnique(columns, seen, column) {
1492
- if (seen.has(column)) return;
1924
+ if (seen.has(column)) return false;
1493
1925
  seen.add(column);
1494
1926
  columns.push(column);
1927
+ return true;
1495
1928
  }
1496
1929
  function resolveSourceOutputColumns(intent, ctx, bindingName) {
1497
1930
  const bindingColumns2 = getKnownBindingColumns(ctx, intent.from);
@@ -1503,33 +1936,62 @@ function resolveSourceOutputColumns(intent, ctx, bindingName) {
1503
1936
  `Cannot compute output schema for NQL binding '${bindingName}' from SELECT * on '${intent.from}' without a concrete table schema.`
1504
1937
  );
1505
1938
  }
1506
- function getQueryOutputSchema(intent, ctx, bindingName) {
1939
+ function getQueryOutputSchema(intent, ctx, bindingName, bindingDependencies = []) {
1507
1940
  const columns = [];
1508
1941
  const seen = /* @__PURE__ */ new Set();
1942
+ const directProjectionLineage = [];
1509
1943
  const addColumn = (column) => addUnique(columns, seen, column);
1944
+ const resolveSourceColumn = (column) => ctx.validator?.resolveColumnName(intent.from, column) ?? column;
1945
+ const addDirectProjection = (outputColumn, sourceColumn) => {
1946
+ if (!addColumn(outputColumn)) return;
1947
+ directProjectionLineage.push({
1948
+ kind: "directProjection",
1949
+ sourceTable: intent.from,
1950
+ sourceColumn: resolveSourceColumn(sourceColumn),
1951
+ outputColumn
1952
+ });
1953
+ };
1510
1954
  const addSourceColumns = () => {
1511
1955
  for (const column of resolveSourceOutputColumns(intent, ctx, bindingName)) {
1512
- addColumn(column);
1956
+ addDirectProjection(column, column);
1513
1957
  }
1514
1958
  };
1515
1959
  const { select } = intent;
1516
1960
  if (!select || select.type === "all") {
1517
1961
  addSourceColumns();
1518
- return { columns };
1962
+ const outputSchema2 = { columns, directProjectionLineage };
1963
+ return {
1964
+ columns: outputSchema2.columns,
1965
+ relationFilters: getBindingRelationFilterMetadata(
1966
+ intent,
1967
+ ctx,
1968
+ outputSchema2,
1969
+ bindingDependencies
1970
+ )
1971
+ };
1519
1972
  }
1520
1973
  if (select.type === "fields") {
1521
1974
  for (const field of select.fields) {
1522
1975
  if (field === "*") {
1523
1976
  addSourceColumns();
1524
1977
  } else {
1525
- addColumn(field);
1978
+ addDirectProjection(field, field);
1526
1979
  }
1527
1980
  }
1528
- return { columns };
1981
+ const outputSchema2 = { columns, directProjectionLineage };
1982
+ return {
1983
+ columns: outputSchema2.columns,
1984
+ relationFilters: getBindingRelationFilterMetadata(
1985
+ intent,
1986
+ ctx,
1987
+ outputSchema2,
1988
+ bindingDependencies
1989
+ )
1990
+ };
1529
1991
  }
1530
1992
  if (select.type === "aggregate") {
1531
1993
  for (const field of select.fields ?? []) {
1532
- addColumn(field);
1994
+ addDirectProjection(field, field);
1533
1995
  }
1534
1996
  for (const aggregate of select.aggregates) {
1535
1997
  if (!aggregate.as) {
@@ -1540,7 +2002,16 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
1540
2002
  }
1541
2003
  addColumn(aggregate.as);
1542
2004
  }
1543
- return { columns };
2005
+ const outputSchema2 = { columns, directProjectionLineage };
2006
+ return {
2007
+ columns: outputSchema2.columns,
2008
+ relationFilters: getBindingRelationFilterMetadata(
2009
+ intent,
2010
+ ctx,
2011
+ outputSchema2,
2012
+ bindingDependencies
2013
+ )
2014
+ };
1544
2015
  }
1545
2016
  for (const expr of select.columns) {
1546
2017
  if (expr.kind === "column" && expr.column === "*") {
@@ -1560,9 +2031,24 @@ function getQueryOutputSchema(intent, ctx, bindingName) {
1560
2031
  `Cannot compute output schema for NQL binding '${bindingName}': selected expression must use an alias.`
1561
2032
  );
1562
2033
  }
1563
- addColumn(outputColumn);
2034
+ if (expr.kind === "column") {
2035
+ addDirectProjection(outputColumn, expr.column);
2036
+ } else if (expr.kind === "columnAlias") {
2037
+ addDirectProjection(outputColumn, expr.column);
2038
+ } else {
2039
+ addColumn(outputColumn);
2040
+ }
1564
2041
  }
1565
- return { columns };
2042
+ const outputSchema = { columns, directProjectionLineage };
2043
+ return {
2044
+ columns: outputSchema.columns,
2045
+ relationFilters: getBindingRelationFilterMetadata(
2046
+ intent,
2047
+ ctx,
2048
+ outputSchema,
2049
+ bindingDependencies
2050
+ )
2051
+ };
1566
2052
  }
1567
2053
  function validateNqlExpressionPaths(expr, ctx) {
1568
2054
  switch (expr.type) {
@@ -2334,13 +2820,12 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2334
2820
  const relFilter = expr;
2335
2821
  const nestedOuterAliases = aliasContext ? [...outerAliases ?? [], aliasContext] : outerAliases ?? [];
2336
2822
  const prevRelationTarget = ctx.currentRelationTarget;
2823
+ const bindingRelation = ctx.currentFromTable && relFilter.relation[0] ? resolveBindingRelationFilter(
2824
+ ctx,
2825
+ ctx.currentFromTable,
2826
+ relFilter.relation
2827
+ ) : void 0;
2337
2828
  if (ctx.currentFromTable && relFilter.relation[0]) {
2338
- assertNoBindingRelationConstruct(
2339
- ctx,
2340
- ctx.currentFromTable,
2341
- "use relation filters",
2342
- relFilter.relation.join(".")
2343
- );
2344
2829
  if (ctx.validator) {
2345
2830
  ctx.currentRelationTarget = ctx.validator.resolveRelationTarget(
2346
2831
  ctx.currentFromTable,
@@ -2356,13 +2841,27 @@ function compileRelationFilter(expr, ctx, fns, aliasContext, outerAliases) {
2356
2841
  nestedOuterAliases
2357
2842
  );
2358
2843
  ctx.currentRelationTarget = prevRelationTarget;
2359
- return {
2844
+ const relationFilterIntent = {
2360
2845
  kind: "relationFilter",
2361
2846
  relation: relFilter.relation,
2362
2847
  where,
2363
2848
  mode: relFilter.mode,
2364
- ...relFilter.alias !== void 0 && { alias: relFilter.alias }
2849
+ ...relFilter.alias !== void 0 && { alias: relFilter.alias },
2850
+ ...bindingRelation !== void 0 && {
2851
+ targetTable: bindingRelation.targetTable,
2852
+ sourceColumn: bindingRelation.sourceColumn,
2853
+ targetColumn: bindingRelation.targetColumn
2854
+ }
2365
2855
  };
2856
+ return bindingRelation ? markNqlTrustedRelationFilter(relationFilterIntent, {
2857
+ relation: bindingRelation.relation,
2858
+ targetTable: bindingRelation.targetTable,
2859
+ sourceColumn: bindingRelation.sourceColumn,
2860
+ targetColumn: bindingRelation.targetColumn,
2861
+ ...bindingRelation.cardinality !== void 0 && {
2862
+ cardinality: bindingRelation.cardinality
2863
+ }
2864
+ }) : relationFilterIntent;
2366
2865
  }
2367
2866
  function expandDateRangeList(field, patterns, negated) {
2368
2867
  const conditions = patterns.map((pattern) => {
@@ -2854,27 +3353,42 @@ function compileMultiSegmentPath(expr, item, ctx) {
2854
3353
  }
2855
3354
  const column = segments[segments.length - 1];
2856
3355
  const relation = segments.slice(0, -1).join(".");
2857
- assertNoBindingRelationConstruct(
3356
+ const bindingRelation = resolveBindingRelationColumn(
2858
3357
  ctx,
2859
3358
  ctx.currentFromTable,
2860
- "select relation columns",
2861
- relation
3359
+ segments.slice(0, -1),
3360
+ column
2862
3361
  );
2863
- if (ctx.currentFromTable && ctx.validator) {
2864
- const targetTable = ctx.validator.resolveRelationTarget(
3362
+ if (!bindingRelation) {
3363
+ assertNoBindingRelationConstruct(
3364
+ ctx,
2865
3365
  ctx.currentFromTable,
2866
- segments[0]
3366
+ "select relation columns",
3367
+ relation
2867
3368
  );
3369
+ }
3370
+ if (ctx.currentFromTable && ctx.validator) {
3371
+ const targetTable = bindingRelation?.targetTable ?? ctx.validator.resolveRelationTarget(ctx.currentFromTable, segments[0]);
2868
3372
  if (targetTable) {
2869
3373
  ctx.validator.validateColumn(targetTable, column);
2870
3374
  }
2871
3375
  }
2872
- return {
3376
+ const relationColumnIntent = {
2873
3377
  kind: "relationColumn",
2874
3378
  relation,
2875
3379
  column,
2876
3380
  as: item.alias ?? `${relation}.${column}`
2877
3381
  };
3382
+ return bindingRelation ? markNqlTrustedRelationFilter(relationColumnIntent, {
3383
+ relation: bindingRelation.relation,
3384
+ targetTable: bindingRelation.targetTable,
3385
+ sourceColumn: bindingRelation.sourceColumn,
3386
+ targetColumn: bindingRelation.targetColumn,
3387
+ selectedColumn: column,
3388
+ ...bindingRelation.cardinality !== void 0 && {
3389
+ cardinality: bindingRelation.cardinality
3390
+ }
3391
+ }) : relationColumnIntent;
2878
3392
  }
2879
3393
  function compileCaseExpression(caseExpr, item, ctx, fns) {
2880
3394
  if (caseExpr.subject) {
@@ -3262,6 +3776,7 @@ var NqlCompiler = class {
3262
3776
  recursiveKeywords,
3263
3777
  validator,
3264
3778
  bindingOutputColumns: /* @__PURE__ */ new Map(),
3779
+ bindingRelationFilters: /* @__PURE__ */ new Map(),
3265
3780
  params,
3266
3781
  maxAnyItems: maxAnyItemsRaw ?? MAX_ANY_ITEMS,
3267
3782
  allowUnfilteredMutations: options?.allowUnfilteredMutations ?? false,
@@ -3281,6 +3796,7 @@ var NqlCompiler = class {
3281
3796
  return this.compileProgram(program);
3282
3797
  } finally {
3283
3798
  this.ctx.bindingOutputColumns.clear();
3799
+ this.ctx.bindingRelationFilters.clear();
3284
3800
  this.ctx.validator?.clearVirtualBindingTables();
3285
3801
  }
3286
3802
  }
@@ -3364,13 +3880,15 @@ var NqlCompiler = class {
3364
3880
  const outputSchema = this.registerQueryBindingOutputSchema(
3365
3881
  bindName,
3366
3882
  lastResult.query,
3367
- bindingOutputSchemas
3883
+ bindingOutputSchemas,
3884
+ statementBindingDependencies
3368
3885
  );
3369
3886
  bindings.set(bindName, lastResult.query);
3370
3887
  if (outputSchema) {
3371
3888
  this.ctx.validator?.addVirtualBindingTable(
3372
3889
  bindName,
3373
- outputSchema.columns
3890
+ outputSchema.columns,
3891
+ outputSchema.relationFilters
3374
3892
  );
3375
3893
  }
3376
3894
  materializedBindStatements.add(i);
@@ -3399,7 +3917,8 @@ var NqlCompiler = class {
3399
3917
  bindingOutputSchemas.set(bindName, outputSchema);
3400
3918
  this.ctx.validator?.addVirtualBindingTable(
3401
3919
  bindName,
3402
- outputSchema.columns
3920
+ outputSchema.columns,
3921
+ outputSchema.relationFilters
3403
3922
  );
3404
3923
  }
3405
3924
  materializedBindStatements.add(i);
@@ -3439,11 +3958,22 @@ var NqlCompiler = class {
3439
3958
  }
3440
3959
  return lastResult;
3441
3960
  }
3442
- registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas) {
3961
+ registerQueryBindingOutputSchema(bindName, query, bindingOutputSchemas, bindingDependencies) {
3443
3962
  try {
3444
- const outputSchema = getQueryOutputSchema(query, this.ctx, bindName);
3963
+ const outputSchema = getQueryOutputSchema(
3964
+ query,
3965
+ this.ctx,
3966
+ bindName,
3967
+ bindingDependencies
3968
+ );
3445
3969
  bindingOutputSchemas.set(bindName, outputSchema);
3446
3970
  this.ctx.bindingOutputColumns.set(bindName, outputSchema.columns);
3971
+ if (outputSchema.relationFilters) {
3972
+ this.ctx.bindingRelationFilters.set(
3973
+ bindName,
3974
+ outputSchema.relationFilters
3975
+ );
3976
+ }
3447
3977
  return outputSchema;
3448
3978
  } catch (error) {
3449
3979
  if (!this.ctx.validator && isUnresolvedSelectAllOutputSchemaError(error)) {