@dbsp/adapter-pgsql 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.d.ts CHANGED
@@ -289,6 +289,8 @@ declare class PlanCompiler {
289
289
  private rawJoins;
290
290
  /** CTE nodes from include handlers (e.g., CTE strategy) */
291
291
  private pendingCtes;
292
+ /** Local aliases for binding relation-column scalar subqueries. */
293
+ private bindingRelationColumnSubqueryIndex;
292
294
  /**
293
295
  * Maps relation-dotted include path → JOIN alias for multi-hop FK resolution.
294
296
  * Populated as join decisions are compiled so later hops can find the
@@ -347,6 +349,8 @@ declare class PlanCompiler {
347
349
  /** Build a fresh HandlerCompilerState sharing the current parameter array. */
348
350
  private createHandlerState;
349
351
  private schemaForRangeVar;
352
+ private isNqlBindingRoot;
353
+ private compileBindingRelationColumnSubquery;
350
354
  private compileExpressionSubquery;
351
355
  private compileGenericNqlFunction;
352
356
  private compileNqlFunctionArg;
package/dist/index.js CHANGED
@@ -1457,6 +1457,10 @@ var init_custom = __esm({
1457
1457
  });
1458
1458
 
1459
1459
  // src/select-expression-handlers.ts
1460
+ import {
1461
+ getTrustedNqlRelationFilterFields,
1462
+ markNqlTrustedRelationFilter
1463
+ } from "@dbsp/types/internal";
1460
1464
  function handleColumnExpression(expr, rootTable, decisions) {
1461
1465
  const decision = {
1462
1466
  type: "select",
@@ -1585,14 +1589,18 @@ function handleCaseExpression(expr, rootTable, decisions, _applyFilter, convertC
1585
1589
  decisions.push(decision);
1586
1590
  }
1587
1591
  function handleRelationColumnExpression(expr, rootTable, decisions) {
1592
+ const trusted = getTrustedNqlRelationFilterFields(expr);
1593
+ const trustedRelation = typeof trusted?.relation === "string" ? trusted.relation : trusted?.relation.join(".");
1588
1594
  const decision = {
1589
1595
  type: "selectRelationColumn",
1590
- relation: expr.relation,
1591
- column: expr.column ?? "*",
1596
+ relation: trustedRelation ?? expr.relation,
1597
+ column: trusted?.selectedColumn ?? (expr.column ?? "*"),
1592
1598
  table: rootTable
1593
1599
  };
1594
1600
  if (expr.as) decision.alias = expr.as;
1595
- decisions.push(decision);
1601
+ decisions.push(
1602
+ trusted?.selectedColumn !== void 0 ? markNqlTrustedRelationFilter(decision, trusted) : decision
1603
+ );
1596
1604
  }
1597
1605
  function handleArithmeticExpression(expr, rootTable, decisions) {
1598
1606
  const decision = {
@@ -1690,6 +1698,9 @@ var init_select_expression_handlers = __esm({
1690
1698
 
1691
1699
  // src/intent-to-decisions.ts
1692
1700
  import { isParamIntent } from "@dbsp/types";
1701
+ import {
1702
+ getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields2
1703
+ } from "@dbsp/types/internal";
1693
1704
  function intentToDecisions(intent, rootTable) {
1694
1705
  const decisions = [];
1695
1706
  if (intent.select) {
@@ -2075,10 +2086,29 @@ function convertNot(cond, rootTable) {
2075
2086
  if (!subDecision) return null;
2076
2087
  return { type: "whereNot", conditions: [subDecision] };
2077
2088
  }
2089
+ function formatRelationName(relation) {
2090
+ return typeof relation === "string" ? relation : relation.join(".");
2091
+ }
2078
2092
  function convertExistsLike(cond, operator) {
2079
- const targetTable = cond.relation;
2093
+ const rawRelation = cond.relation;
2094
+ const preResolved = getTrustedNqlRelationFilterFields2(cond);
2095
+ const trustedRelation = preResolved?.relation;
2096
+ const relationName = trustedRelation !== void 0 ? formatRelationName(trustedRelation) : formatRelationName(rawRelation);
2097
+ const targetTable = preResolved?.targetTable ?? rawRelation;
2080
2098
  const subDecisions = cond.where ? convertWhere(cond.where, targetTable) : [];
2081
- const base = { type: "where", operator, targetTable };
2099
+ const isPreResolved = preResolved !== void 0;
2100
+ const base = {
2101
+ type: "where",
2102
+ operator,
2103
+ targetTable,
2104
+ ...preResolved !== void 0 && {
2105
+ sourceColumn: preResolved.sourceColumn
2106
+ },
2107
+ ...preResolved !== void 0 && {
2108
+ targetColumn: preResolved.targetColumn
2109
+ },
2110
+ ...isPreResolved && { relationName }
2111
+ };
2082
2112
  return subDecisions.length > 0 ? { ...base, conditions: subDecisions } : base;
2083
2113
  }
2084
2114
  function convertSubquery(cond) {
@@ -3744,6 +3774,7 @@ var init_range = __esm({
3744
3774
  });
3745
3775
 
3746
3776
  // src/compile-where.ts
3777
+ import { getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields3 } from "@dbsp/types/internal";
3747
3778
  function toHandlerContext(ctx) {
3748
3779
  return {
3749
3780
  naming: ctx.naming,
@@ -3937,11 +3968,16 @@ function handleSubqueryIntent(intent, ctx, _handlerCtx) {
3937
3968
  }
3938
3969
  function handleRelationFilterIntent(intent, ctx) {
3939
3970
  const rf = intent;
3940
- const hops = Array.isArray(rf.relation) ? rf.relation : [rf.relation];
3971
+ const preResolved = getTrustedNqlRelationFilterFields3(rf);
3972
+ const relationPath = preResolved?.relation ?? rf.relation;
3973
+ const hops = Array.isArray(relationPath) ? [...relationPath] : [relationPath];
3941
3974
  const existsKind = rf.mode === "none" || rf.mode === "every" ? "notExists" : "exists";
3942
3975
  const rfWhere = rf.where;
3943
3976
  const isVacuousEvery = rf.mode === "every" && (!rfWhere || typeof rfWhere === "object" && rfWhere.kind === "and" && Array.isArray(rfWhere.conditions) && rfWhere.conditions.length === 0);
3944
3977
  if (isVacuousEvery) {
3978
+ if (preResolved) {
3979
+ return { A_Const: { boolval: { boolval: true } } };
3980
+ }
3945
3981
  if (hops.length <= 1) {
3946
3982
  const relation = hops[0] ?? rf.relation;
3947
3983
  if (!ctx.model) {
@@ -3977,6 +4013,19 @@ function handleRelationFilterIntent(intent, ctx) {
3977
4013
  const innermostWhere = rf.mode === "every" ? { kind: "not", condition: rf.where } : rf.where;
3978
4014
  if (hops.length <= 1) {
3979
4015
  const relation = hops[0] ?? rf.relation;
4016
+ if (preResolved) {
4017
+ return compileWhereIntent(
4018
+ {
4019
+ kind: existsKind,
4020
+ relation,
4021
+ targetTable: preResolved.targetTable,
4022
+ sourceColumn: preResolved.sourceColumn,
4023
+ targetColumn: preResolved.targetColumn,
4024
+ where: innermostWhere
4025
+ },
4026
+ ctx
4027
+ );
4028
+ }
3980
4029
  const resolvedRelation = ctx.model?.getRelation(
3981
4030
  `${ctx.rootTable}.${relation}`
3982
4031
  );
@@ -7182,7 +7231,11 @@ import {
7182
7231
  NQL_SELECT_SCALAR_FUNCTION_ALLOWLIST,
7183
7232
  NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST
7184
7233
  } from "@dbsp/types";
7185
- import { getNqlBindingRefName, isNqlBindingRef } from "@dbsp/types/internal";
7234
+ import {
7235
+ getNqlBindingRefName,
7236
+ getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields4,
7237
+ isNqlBindingRef
7238
+ } from "@dbsp/types/internal";
7186
7239
 
7187
7240
  // src/pgsql-deparser.ts
7188
7241
  function deparse(node) {
@@ -8284,6 +8337,12 @@ function assertNqlSelectWindowFunctionAllowed(functionName) {
8284
8337
  }
8285
8338
  }
8286
8339
  function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8340
+ const derivedFkColumns = deriveFkColumns(
8341
+ pd,
8342
+ pd.sourceTable ?? rootTable,
8343
+ defaultPk,
8344
+ deriveFk
8345
+ );
8287
8346
  return {
8288
8347
  type: pd.type,
8289
8348
  table: pd.table,
@@ -8294,7 +8353,8 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8294
8353
  paramIndex: pd.paramIndex,
8295
8354
  direction: pd.direction,
8296
8355
  joinType: pd.joinType,
8297
- ...deriveFkColumns(pd, pd.sourceTable ?? rootTable, defaultPk, deriveFk),
8356
+ sourceColumn: pd.sourceColumn ?? derivedFkColumns.sourceColumn,
8357
+ targetColumn: pd.targetColumn ?? derivedFkColumns.targetColumn,
8298
8358
  targetTable: pd.targetTable,
8299
8359
  function: pd.function,
8300
8360
  args: pd.args,
@@ -8488,6 +8548,8 @@ var PlanCompiler = class _PlanCompiler {
8488
8548
  rawJoins = [];
8489
8549
  /** CTE nodes from include handlers (e.g., CTE strategy) */
8490
8550
  pendingCtes = [];
8551
+ /** Local aliases for binding relation-column scalar subqueries. */
8552
+ bindingRelationColumnSubqueryIndex = 0;
8491
8553
  /**
8492
8554
  * Maps relation-dotted include path → JOIN alias for multi-hop FK resolution.
8493
8555
  * Populated as join decisions are compiled so later hops can find the
@@ -8875,6 +8937,56 @@ var PlanCompiler = class _PlanCompiler {
8875
8937
  this.naming
8876
8938
  );
8877
8939
  }
8940
+ isNqlBindingRoot(plan) {
8941
+ return hasBindingName(this.bindingNames, plan.rootTable, this.naming);
8942
+ }
8943
+ compileBindingRelationColumnSubquery(fields, plan) {
8944
+ if (fields.selectedColumn === void 0) {
8945
+ throw new Error(
8946
+ `NQL binding relation-column proof for '${plan.rootTable}' is missing selectedColumn.`
8947
+ );
8948
+ }
8949
+ if (fields.cardinality !== "one") {
8950
+ throw new Error(
8951
+ `NQL binding relation-column proof for '${plan.rootTable}' is not scalar (cardinality: ${fields.cardinality ?? "unknown"}).`
8952
+ );
8953
+ }
8954
+ const relatedAlias = `rc_${this.bindingRelationColumnSubqueryIndex++}`;
8955
+ const relatedTable = rangeVar(
8956
+ fields.targetTable,
8957
+ relatedAlias,
8958
+ this.schemaForRangeVar(plan, fields.targetTable),
8959
+ this.naming
8960
+ );
8961
+ const relatedColumn = columnRef(
8962
+ fields.selectedColumn,
8963
+ relatedAlias,
8964
+ void 0,
8965
+ this.naming
8966
+ );
8967
+ const relatedJoinColumn = columnRef(
8968
+ fields.targetColumn,
8969
+ relatedAlias,
8970
+ void 0,
8971
+ this.naming
8972
+ );
8973
+ const bindingJoinColumn = columnRef(
8974
+ fields.sourceColumn,
8975
+ plan.rootTable,
8976
+ void 0,
8977
+ this.naming
8978
+ );
8979
+ return {
8980
+ SubLink: {
8981
+ subLinkType: "EXPR_SUBLINK",
8982
+ subselect: selectStmt({
8983
+ targetList: [{ ResTarget: { val: relatedColumn } }],
8984
+ from: [relatedTable],
8985
+ where: eqExpr(relatedJoinColumn, bindingJoinColumn)
8986
+ })
8987
+ }
8988
+ };
8989
+ }
8878
8990
  compileExpressionSubquery(query, paramOffset) {
8879
8991
  const innerCompiler = new _PlanCompiler(this.childCompilerOptions());
8880
8992
  const innerPlan = {
@@ -9252,6 +9364,27 @@ var PlanCompiler = class _PlanCompiler {
9252
9364
  case "selectRelationColumn":
9253
9365
  case "selectPseudoColumn":
9254
9366
  case "selectArithmetic": {
9367
+ if (decision.type === "selectRelationColumn") {
9368
+ const trusted = getTrustedNqlRelationFilterFields4(decision);
9369
+ if (trusted?.selectedColumn !== void 0) {
9370
+ const node2 = this.compileBindingRelationColumnSubquery(
9371
+ trusted,
9372
+ plan
9373
+ );
9374
+ targetList.push({
9375
+ ResTarget: {
9376
+ val: node2,
9377
+ ...decision.alias ? { name: this.naming.toDatabase(decision.alias) } : {}
9378
+ }
9379
+ });
9380
+ break;
9381
+ }
9382
+ if (this.isNqlBindingRoot(plan)) {
9383
+ throw new Error(
9384
+ `NQL binding-final query '${plan.rootTable}' cannot select relation column '${decision.relation ?? "unknown"}.${decision.column ?? "unknown"}' without a trusted compiler proof.`
9385
+ );
9386
+ }
9387
+ }
9255
9388
  ensureExpressionHandlersRegistered();
9256
9389
  const exprType = decision.type === "selectRelationColumn" ? "relationColumn" : decision.type === "selectPseudoColumn" ? "pseudoColumn" : "arithmetic";
9257
9390
  const handler = getExpressionHandler(exprType);