@dbsp/adapter-pgsql 1.4.0 → 1.6.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
@@ -588,12 +588,6 @@ function binaryExpr(operator, left, right, kind = "AEXPR_OP") {
588
588
  function eqExpr(left, right) {
589
589
  return binaryExpr("=", left, right);
590
590
  }
591
- function fkCorrelation(col1, alias1, col2, alias2, naming) {
592
- return eqExpr(
593
- columnRef(col1, alias1, void 0, naming),
594
- columnRef(col2, alias2, void 0, naming)
595
- );
596
- }
597
591
  function neExpr(left, right) {
598
592
  return binaryExpr("<>", left, right);
599
593
  }
@@ -885,10 +879,23 @@ function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = ide
885
879
  }
886
880
  };
887
881
  }
882
+ const aggOrder = options?.orderBy && options.orderBy.length > 0 ? options.orderBy.map(
883
+ (col) => sortBy(
884
+ jsonAggOrderByExpression(
885
+ col,
886
+ targetAlias,
887
+ naming,
888
+ options.orderByFallback === true
889
+ ),
890
+ "ASC",
891
+ "LAST"
892
+ )
893
+ ) : void 0;
888
894
  const jsonAggCall = {
889
895
  FuncCall: {
890
896
  funcname: [stringNode("json_agg")],
891
- args: [toJsonbCall]
897
+ args: [toJsonbCall],
898
+ ...aggOrder && { agg_order: aggOrder }
892
899
  }
893
900
  };
894
901
  const fromTable = rangeVar(targetTable, targetAlias, schemaName, naming);
@@ -923,11 +930,9 @@ function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = ide
923
930
  }
924
931
  };
925
932
  }
926
- function jsonAggCorrelation(parentAlias, parentColumn, targetAlias, targetColumn, naming = identityNaming) {
927
- return eqExpr(
928
- columnRef(targetColumn, targetAlias, void 0, naming),
929
- columnRef(parentColumn, parentAlias, void 0, naming)
930
- );
933
+ function jsonAggOrderByExpression(entry, targetAlias, naming, castToText) {
934
+ const ref = columnRef(entry, targetAlias, void 0, naming);
935
+ return castToText ? typeCast(ref, "text") : ref;
931
936
  }
932
937
  var STRENGTH_MAP, POLICY_MAP;
933
938
  var init_ast_helpers = __esm({
@@ -1457,6 +1462,10 @@ var init_custom = __esm({
1457
1462
  });
1458
1463
 
1459
1464
  // src/select-expression-handlers.ts
1465
+ import {
1466
+ getTrustedNqlRelationFilterFields,
1467
+ markNqlTrustedRelationFilter
1468
+ } from "@dbsp/types/internal";
1460
1469
  function handleColumnExpression(expr, rootTable, decisions) {
1461
1470
  const decision = {
1462
1471
  type: "select",
@@ -1585,14 +1594,18 @@ function handleCaseExpression(expr, rootTable, decisions, _applyFilter, convertC
1585
1594
  decisions.push(decision);
1586
1595
  }
1587
1596
  function handleRelationColumnExpression(expr, rootTable, decisions) {
1597
+ const trusted = getTrustedNqlRelationFilterFields(expr);
1598
+ const trustedRelation = typeof trusted?.relation === "string" ? trusted.relation : trusted?.relation.join(".");
1588
1599
  const decision = {
1589
1600
  type: "selectRelationColumn",
1590
- relation: expr.relation,
1591
- column: expr.column ?? "*",
1601
+ relation: trustedRelation ?? expr.relation,
1602
+ column: trusted?.selectedColumn ?? (expr.column ?? "*"),
1592
1603
  table: rootTable
1593
1604
  };
1594
1605
  if (expr.as) decision.alias = expr.as;
1595
- decisions.push(decision);
1606
+ decisions.push(
1607
+ trusted?.selectedColumn !== void 0 ? markNqlTrustedRelationFilter(decision, trusted) : decision
1608
+ );
1596
1609
  }
1597
1610
  function handleArithmeticExpression(expr, rootTable, decisions) {
1598
1611
  const decision = {
@@ -1690,6 +1703,9 @@ var init_select_expression_handlers = __esm({
1690
1703
 
1691
1704
  // src/intent-to-decisions.ts
1692
1705
  import { isParamIntent } from "@dbsp/types";
1706
+ import {
1707
+ getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields2
1708
+ } from "@dbsp/types/internal";
1693
1709
  function intentToDecisions(intent, rootTable) {
1694
1710
  const decisions = [];
1695
1711
  if (intent.select) {
@@ -2075,10 +2091,29 @@ function convertNot(cond, rootTable) {
2075
2091
  if (!subDecision) return null;
2076
2092
  return { type: "whereNot", conditions: [subDecision] };
2077
2093
  }
2094
+ function formatRelationName(relation) {
2095
+ return typeof relation === "string" ? relation : relation.join(".");
2096
+ }
2078
2097
  function convertExistsLike(cond, operator) {
2079
- const targetTable = cond.relation;
2098
+ const rawRelation = cond.relation;
2099
+ const preResolved = getTrustedNqlRelationFilterFields2(cond);
2100
+ const trustedRelation = preResolved?.relation;
2101
+ const relationName = trustedRelation !== void 0 ? formatRelationName(trustedRelation) : formatRelationName(rawRelation);
2102
+ const targetTable = preResolved?.targetTable ?? rawRelation;
2080
2103
  const subDecisions = cond.where ? convertWhere(cond.where, targetTable) : [];
2081
- const base = { type: "where", operator, targetTable };
2104
+ const isPreResolved = preResolved !== void 0;
2105
+ const base = {
2106
+ type: "where",
2107
+ operator,
2108
+ targetTable,
2109
+ ...preResolved !== void 0 && {
2110
+ sourceColumn: preResolved.sourceColumn
2111
+ },
2112
+ ...preResolved !== void 0 && {
2113
+ targetColumn: preResolved.targetColumn
2114
+ },
2115
+ ...isPreResolved && { relationName }
2116
+ };
2082
2117
  return subDecisions.length > 0 ? { ...base, conditions: subDecisions } : base;
2083
2118
  }
2084
2119
  function convertSubquery(cond) {
@@ -3142,6 +3177,7 @@ var init_custom_expression = __esm({
3142
3177
  });
3143
3178
 
3144
3179
  // src/handlers/where/exists.ts
3180
+ import { toColumnList } from "@dbsp/types";
3145
3181
  function createSubLinkExists(subquery, negated) {
3146
3182
  const subLink = {
3147
3183
  subLinkType: "EXISTS_SUBLINK",
@@ -3158,10 +3194,27 @@ function createSubLinkExists(subquery, negated) {
3158
3194
  }
3159
3195
  return existsNode;
3160
3196
  }
3161
- function buildCorrelation(sourceAlias, sourceColumn, targetAlias, targetColumn, ctx) {
3162
- const left = columnRef(sourceColumn, sourceAlias, void 0, ctx.naming);
3163
- const right = columnRef(targetColumn, targetAlias, void 0, ctx.naming);
3164
- return eqExpr(left, right);
3197
+ function buildKeyCorrelation(sourceAlias, sourceCols, targetAlias, targetCols, ctx) {
3198
+ const normalizedSourceCols = toColumnList(sourceCols);
3199
+ const normalizedTargetCols = toColumnList(targetCols);
3200
+ if (normalizedSourceCols.length === 0 || normalizedTargetCols.length === 0 || normalizedSourceCols.length !== normalizedTargetCols.length || normalizedSourceCols.some((column) => column.length === 0) || normalizedTargetCols.some((column) => column.length === 0)) {
3201
+ throw new Error(
3202
+ `Invalid relation correlation: source columns (${normalizedSourceCols.length}) must match target columns (${normalizedTargetCols.length}) and both must be non-empty.`
3203
+ );
3204
+ }
3205
+ const comparisons = normalizedSourceCols.map(
3206
+ (sourceColumn, index) => eqExpr(
3207
+ columnRef(sourceColumn, sourceAlias, void 0, ctx.naming),
3208
+ columnRef(
3209
+ normalizedTargetCols[index],
3210
+ targetAlias,
3211
+ void 0,
3212
+ ctx.naming
3213
+ )
3214
+ )
3215
+ );
3216
+ if (comparisons.length === 1) return comparisons[0];
3217
+ return andExpr(...comparisons);
3165
3218
  }
3166
3219
  function schemaForExistsFromName(ctx, fromName) {
3167
3220
  return schemaForFromName(ctx.schema, fromName, ctx.bindingNames, ctx.naming);
@@ -3169,11 +3222,15 @@ function schemaForExistsFromName(ctx, fromName) {
3169
3222
  function buildExistsSubquery(decision, ctx, state, dispatch) {
3170
3223
  const relation = decision.relation;
3171
3224
  const targetTable = decision.targetTable ?? relation;
3172
- const sourceColumn = decision.sourceColumn ?? ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3173
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3174
- ctx.rootTable,
3225
+ const sourceColumn = decision.sourceColumn ?? [
3175
3226
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3176
- );
3227
+ ];
3228
+ const targetColumn = decision.targetColumn ?? [
3229
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3230
+ ctx.rootTable,
3231
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3232
+ )
3233
+ ];
3177
3234
  if (!targetTable) {
3178
3235
  throw new Error("EXISTS handler requires targetTable or relation");
3179
3236
  }
@@ -3181,7 +3238,7 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3181
3238
  const targetAlias = `${targetTable}_exists_${existingAliases}`;
3182
3239
  state.aliases.set(`exists_${targetTable}`, targetAlias);
3183
3240
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
3184
- const correlation = buildCorrelation(
3241
+ const correlation = buildKeyCorrelation(
3185
3242
  sourceAlias,
3186
3243
  sourceColumn,
3187
3244
  targetAlias,
@@ -3219,8 +3276,8 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3219
3276
  const joinRelation = inc.relation;
3220
3277
  if (!joinRelation) continue;
3221
3278
  let joinTargetTable = joinRelation;
3222
- let joinSourceCol;
3223
- let joinTargetCol;
3279
+ let joinSourceCols;
3280
+ let joinTargetCols;
3224
3281
  let sourceAliasForJoin = targetAlias;
3225
3282
  const model = ctx.model;
3226
3283
  if (model) {
@@ -3238,31 +3295,41 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3238
3295
  if (rel) {
3239
3296
  joinTargetTable = rel.target;
3240
3297
  if (rel.type === "belongsTo") {
3241
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
3242
- joinSourceCol = fk;
3243
- joinTargetCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3298
+ const fk = toColumnList(rel.foreignKey);
3299
+ joinSourceCols = fk.length > 0 ? fk : void 0;
3300
+ const targetKey = toColumnList(rel.targetKey);
3301
+ joinTargetCols = targetKey.length > 0 ? targetKey : [
3302
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3303
+ ];
3244
3304
  } else {
3245
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
3246
- joinSourceCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3247
- joinTargetCol = fk;
3305
+ const fk = toColumnList(rel.foreignKey);
3306
+ const sourceKey = toColumnList(rel.sourceKey);
3307
+ joinSourceCols = sourceKey.length > 0 ? sourceKey : [
3308
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3309
+ ];
3310
+ joinTargetCols = fk.length > 0 ? fk : void 0;
3248
3311
  }
3249
3312
  }
3250
3313
  }
3251
- if (!joinSourceCol) {
3252
- joinSourceCol = (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3253
- joinRelation,
3314
+ if (!joinSourceCols || joinSourceCols.length === 0) {
3315
+ joinSourceCols = [
3316
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3317
+ joinRelation,
3318
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3319
+ )
3320
+ ];
3321
+ joinTargetCols = [
3254
3322
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3255
- );
3256
- joinTargetCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3323
+ ];
3257
3324
  }
3258
3325
  const joinAlias = joinRelation;
3259
- const joinQuals = fkCorrelation(
3260
- joinSourceCol,
3326
+ const joinQuals = buildKeyCorrelation(
3261
3327
  sourceAliasForJoin,
3262
3328
  // resolved source alias (root or intermediate)
3263
- joinTargetCol ?? DEFAULT_PK_COLUMN,
3329
+ joinSourceCols,
3264
3330
  joinAlias,
3265
- ctx.naming
3331
+ joinTargetCols,
3332
+ ctx
3266
3333
  );
3267
3334
  const joinType = inc.joinType === "left" ? "JOIN_LEFT" : "JOIN_INNER";
3268
3335
  const joinRangeVar = rangeVar(
@@ -3744,6 +3811,8 @@ var init_range = __esm({
3744
3811
  });
3745
3812
 
3746
3813
  // src/compile-where.ts
3814
+ import { toColumnList as toColumnList2 } from "@dbsp/types";
3815
+ import { getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields3 } from "@dbsp/types/internal";
3747
3816
  function toHandlerContext(ctx) {
3748
3817
  return {
3749
3818
  naming: ctx.naming,
@@ -3937,11 +4006,16 @@ function handleSubqueryIntent(intent, ctx, _handlerCtx) {
3937
4006
  }
3938
4007
  function handleRelationFilterIntent(intent, ctx) {
3939
4008
  const rf = intent;
3940
- const hops = Array.isArray(rf.relation) ? rf.relation : [rf.relation];
4009
+ const preResolved = getTrustedNqlRelationFilterFields3(rf);
4010
+ const relationPath = preResolved?.relation ?? rf.relation;
4011
+ const hops = Array.isArray(relationPath) ? [...relationPath] : [relationPath];
3941
4012
  const existsKind = rf.mode === "none" || rf.mode === "every" ? "notExists" : "exists";
3942
4013
  const rfWhere = rf.where;
3943
4014
  const isVacuousEvery = rf.mode === "every" && (!rfWhere || typeof rfWhere === "object" && rfWhere.kind === "and" && Array.isArray(rfWhere.conditions) && rfWhere.conditions.length === 0);
3944
4015
  if (isVacuousEvery) {
4016
+ if (preResolved) {
4017
+ return { A_Const: { boolval: { boolval: true } } };
4018
+ }
3945
4019
  if (hops.length <= 1) {
3946
4020
  const relation = hops[0] ?? rf.relation;
3947
4021
  if (!ctx.model) {
@@ -3977,6 +4051,19 @@ function handleRelationFilterIntent(intent, ctx) {
3977
4051
  const innermostWhere = rf.mode === "every" ? { kind: "not", condition: rf.where } : rf.where;
3978
4052
  if (hops.length <= 1) {
3979
4053
  const relation = hops[0] ?? rf.relation;
4054
+ if (preResolved) {
4055
+ return compileWhereIntent(
4056
+ {
4057
+ kind: existsKind,
4058
+ relation,
4059
+ targetTable: preResolved.targetTable,
4060
+ sourceColumn: preResolved.sourceColumn,
4061
+ targetColumn: preResolved.targetColumn,
4062
+ where: innermostWhere
4063
+ },
4064
+ ctx
4065
+ );
4066
+ }
3980
4067
  const resolvedRelation = ctx.model?.getRelation(
3981
4068
  `${ctx.rootTable}.${relation}`
3982
4069
  );
@@ -3990,14 +4077,16 @@ function handleRelationFilterIntent(intent, ctx) {
3990
4077
  let singleHopTargetColumn;
3991
4078
  if (resolvedRelation) {
3992
4079
  const rel = resolvedRelation;
3993
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4080
+ const fk = toColumnList2(rel.foreignKey);
3994
4081
  const defaultPk = DEFAULT_PK_COLUMN;
3995
4082
  if (rel.type === "belongsTo") {
3996
- singleHopSourceColumn = fk ?? defaultFkDerivation(rel.target, defaultPk);
3997
- singleHopTargetColumn = rel.targetKey ?? defaultPk;
4083
+ singleHopSourceColumn = fk.length > 0 ? fk : [defaultFkDerivation(rel.target, defaultPk)];
4084
+ const targetKey = toColumnList2(rel.targetKey);
4085
+ singleHopTargetColumn = targetKey.length > 0 ? targetKey : [defaultPk];
3998
4086
  } else {
3999
- singleHopSourceColumn = rel.sourceKey ?? defaultPk;
4000
- singleHopTargetColumn = fk ?? defaultFkDerivation(ctx.rootTable, defaultPk);
4087
+ const sourceKey = toColumnList2(rel.sourceKey);
4088
+ singleHopSourceColumn = sourceKey.length > 0 ? sourceKey : [defaultPk];
4089
+ singleHopTargetColumn = fk.length > 0 ? fk : [defaultFkDerivation(ctx.rootTable, defaultPk)];
4001
4090
  }
4002
4091
  }
4003
4092
  return compileWhereIntent(
@@ -4034,15 +4123,19 @@ function handleRelationFilterIntent(intent, ctx) {
4034
4123
  );
4035
4124
  }
4036
4125
  hopTargets.push(rel.target);
4037
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4126
+ const fk = toColumnList2(rel.foreignKey);
4038
4127
  const defaultPk = DEFAULT_PK_COLUMN;
4039
4128
  if (rel.type === "belongsTo") {
4040
- hopSourceColumns.push(fk ?? defaultFkDerivation(rel.target, defaultPk));
4041
- hopTargetColumns.push(rel.targetKey ?? defaultPk);
4129
+ hopSourceColumns.push(
4130
+ fk.length > 0 ? fk : [defaultFkDerivation(rel.target, defaultPk)]
4131
+ );
4132
+ const targetKey = toColumnList2(rel.targetKey);
4133
+ hopTargetColumns.push(targetKey.length > 0 ? targetKey : [defaultPk]);
4042
4134
  } else {
4043
- hopSourceColumns.push(rel.sourceKey ?? defaultPk);
4135
+ const sourceKey = toColumnList2(rel.sourceKey);
4136
+ hopSourceColumns.push(sourceKey.length > 0 ? sourceKey : [defaultPk]);
4044
4137
  hopTargetColumns.push(
4045
- fk ?? defaultFkDerivation(currentSource, defaultPk)
4138
+ fk.length > 0 ? fk : [defaultFkDerivation(currentSource, defaultPk)]
4046
4139
  );
4047
4140
  }
4048
4141
  currentSource = rel.target;
@@ -4270,6 +4363,7 @@ var init_raw_exists = __esm({
4270
4363
  });
4271
4364
 
4272
4365
  // src/handlers/where/relation-filter.ts
4366
+ import { toColumnList as toColumnList3 } from "@dbsp/types";
4273
4367
  function getFilterMode(decision) {
4274
4368
  const operator = decision.operator;
4275
4369
  if (operator === "some" || operator === "exists") return "some";
@@ -4281,15 +4375,18 @@ function getFilterMode(decision) {
4281
4375
  function buildJoinFilter(decision, ctx, state, dispatch) {
4282
4376
  const relation = decision.relation;
4283
4377
  const targetTable = decision.targetTable ?? relation;
4284
- const sourceColumn = requiredColumn(
4285
- decision.sourceColumn,
4286
- "sourceColumn",
4287
- "relation filter"
4288
- );
4289
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4290
- ctx.rootTable,
4291
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4292
- );
4378
+ const sourceColumn = toColumnList3(decision.sourceColumn);
4379
+ if (sourceColumn.length === 0) {
4380
+ throw new Error(
4381
+ "Missing required column 'sourceColumn' in relation filter"
4382
+ );
4383
+ }
4384
+ const targetColumn = decision.targetColumn ?? [
4385
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4386
+ ctx.rootTable,
4387
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4388
+ )
4389
+ ];
4293
4390
  if (!targetTable) {
4294
4391
  throw new Error("Relation filter requires targetTable");
4295
4392
  }
@@ -4297,9 +4394,12 @@ function buildJoinFilter(decision, ctx, state, dispatch) {
4297
4394
  const targetAlias = `${targetTable}_rel_${existingAliases}`;
4298
4395
  state.aliases.set(`rel_${targetTable}`, targetAlias);
4299
4396
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
4300
- const joinCondition = eqExpr(
4301
- columnRef(sourceColumn, sourceAlias, ctx.schema, ctx.naming),
4302
- columnRef(targetColumn, targetAlias, ctx.schema, ctx.naming)
4397
+ const joinCondition = buildKeyCorrelation(
4398
+ sourceAlias,
4399
+ sourceColumn,
4400
+ targetAlias,
4401
+ targetColumn,
4402
+ ctx
4303
4403
  );
4304
4404
  const joinExpr2 = {
4305
4405
  jointype: "JOIN_INNER",
@@ -4334,6 +4434,7 @@ var init_relation_filter = __esm({
4334
4434
  "use strict";
4335
4435
  init_assert_field();
4336
4436
  init_ast_helpers();
4437
+ init_exists();
4337
4438
  relationFilterHandler = {
4338
4439
  operators: ["relationFilter", "relation", "is", "isNot"],
4339
4440
  compile(decision, ctx, state, dispatch) {
@@ -4763,6 +4864,7 @@ var init_where = __esm({
4763
4864
  });
4764
4865
 
4765
4866
  // src/handlers/include/cte.ts
4867
+ import { toColumnList as toColumnList4 } from "@dbsp/types";
4766
4868
  function buildCteTargets(columns, alias, ctx) {
4767
4869
  if (columns && columns.length > 0 && !columns.every((c) => c === "*")) {
4768
4870
  return columns.filter((col) => col !== "*").map((col) => ({
@@ -4813,12 +4915,12 @@ function buildCTE(cteName, cteSelect, ctx) {
4813
4915
  return { CommonTableExpr: cte };
4814
4916
  }
4815
4917
  function buildCteJoin(cteName, cteAlias, sourceAlias, sourceColumn, targetColumn, ctx) {
4816
- const joinCondition = fkCorrelation(
4817
- sourceColumn,
4918
+ const joinCondition = buildKeyCorrelation(
4818
4919
  sourceAlias,
4819
- targetColumn,
4920
+ sourceColumn,
4820
4921
  cteAlias,
4821
- ctx.naming
4922
+ targetColumn,
4923
+ ctx
4822
4924
  );
4823
4925
  const cteRef = {
4824
4926
  RangeVar: {
@@ -4842,20 +4944,22 @@ var init_cte = __esm({
4842
4944
  init_assert_field();
4843
4945
  init_ast_helpers();
4844
4946
  init_handlers();
4947
+ init_exists();
4845
4948
  cteIncludeHandler = {
4846
4949
  strategy: "cte",
4847
4950
  compile(decision, ctx, state) {
4848
4951
  const relation = decision.relation;
4849
4952
  const targetTable = decision.targetTable ?? relation;
4850
- const sourceColumn = requiredColumn(
4851
- decision.sourceColumn,
4852
- "sourceColumn",
4853
- "CTE include"
4854
- );
4855
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4856
- ctx.rootTable,
4857
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4858
- );
4953
+ const sourceColumn = toColumnList4(decision.sourceColumn);
4954
+ if (sourceColumn.length === 0 || sourceColumn.some((col) => col === "")) {
4955
+ throw new Error("Missing required column 'sourceColumn' in CTE include");
4956
+ }
4957
+ const targetColumn = decision.targetColumn ?? [
4958
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4959
+ ctx.rootTable,
4960
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4961
+ )
4962
+ ];
4859
4963
  const columns = decision.columns;
4860
4964
  const conditions = decision.conditions;
4861
4965
  if (!targetTable) {
@@ -4898,13 +5002,14 @@ var init_cte = __esm({
4898
5002
  });
4899
5003
 
4900
5004
  // src/handlers/include/join.ts
5005
+ import { toColumnList as toColumnList5 } from "@dbsp/types";
4901
5006
  function buildJoin(targetTable, targetAlias, sourceAlias, sourceColumn, targetColumn, ctx, joinType = "left") {
4902
- const joinCondition = fkCorrelation(
4903
- sourceColumn,
5007
+ const joinCondition = buildKeyCorrelation(
4904
5008
  sourceAlias,
4905
- targetColumn,
5009
+ sourceColumn,
4906
5010
  targetAlias,
4907
- ctx.naming
5011
+ targetColumn,
5012
+ ctx
4908
5013
  );
4909
5014
  const joinExpr2 = {
4910
5015
  jointype: joinType === "inner" ? "JOIN_INNER" : "JOIN_LEFT",
@@ -4919,6 +5024,7 @@ var init_join = __esm({
4919
5024
  "use strict";
4920
5025
  init_assert_field();
4921
5026
  init_ast_helpers();
5027
+ init_exists();
4922
5028
  joinIncludeHandler = {
4923
5029
  strategy: "join",
4924
5030
  compile(decision, ctx, _state) {
@@ -4929,15 +5035,16 @@ var init_join = __esm({
4929
5035
  }
4930
5036
  const targetAlias = relation ?? targetTable;
4931
5037
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
4932
- const sourceColumn = requiredColumn(
4933
- decision.sourceColumn,
4934
- "sourceColumn",
4935
- "JOIN include"
4936
- );
4937
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4938
- ctx.rootTable,
4939
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4940
- );
5038
+ const sourceColumn = toColumnList5(decision.sourceColumn);
5039
+ if (sourceColumn.length === 0) {
5040
+ throw new Error("Missing required column 'sourceColumn' in JOIN include");
5041
+ }
5042
+ const targetColumn = decision.targetColumn ?? [
5043
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5044
+ ctx.rootTable,
5045
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5046
+ )
5047
+ ];
4941
5048
  const join = buildJoin(
4942
5049
  targetTable,
4943
5050
  targetAlias,
@@ -4991,6 +5098,22 @@ var init_shared = __esm({
4991
5098
  });
4992
5099
 
4993
5100
  // src/handlers/include/json-agg.ts
5101
+ import { toColumnList as toColumnList6 } from "@dbsp/types";
5102
+ function resolveJsonAggOrderBy(decision, targetTable, ctx) {
5103
+ const table = ctx.model?.getTable(targetTable);
5104
+ if (table) {
5105
+ const pkColumns = toColumnList6(table.primaryKey);
5106
+ const orderIntent = pkColumns.length > 0 ? { columns: pkColumns, fallback: false } : {
5107
+ columns: table.columns.map((col) => col.name),
5108
+ fallback: true
5109
+ };
5110
+ return orderIntent.columns.length > 0 ? orderIntent : void 0;
5111
+ }
5112
+ return decision.targetPrimaryKey && decision.targetPrimaryKey.length > 0 ? {
5113
+ columns: decision.targetPrimaryKey,
5114
+ fallback: decision.orderByFallback === true
5115
+ } : void 0;
5116
+ }
4994
5117
  function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
4995
5118
  const innerAlias = depth === 0 ? "__t__" : `__t${depth}__`;
4996
5119
  const relation = decision.relation ?? decision.relationName;
@@ -5007,12 +5130,12 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5007
5130
  ctx.defaultPkColumnName,
5008
5131
  ctx.deriveFkColumnName
5009
5132
  );
5010
- let whereExpr = jsonAggCorrelation(
5011
- parentAlias,
5012
- sourceColumn,
5133
+ let whereExpr = buildKeyCorrelation(
5013
5134
  innerAlias,
5014
5135
  targetColumn,
5015
- ctx.naming
5136
+ parentAlias,
5137
+ sourceColumn,
5138
+ ctx
5016
5139
  );
5017
5140
  const compiledFilter = decision._compiledFilterWhere;
5018
5141
  if (compiledFilter) {
@@ -5043,6 +5166,7 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5043
5166
  if (childNodes.length === 0) childNodes = void 0;
5044
5167
  }
5045
5168
  const limit = typeof decision.limit === "number" ? decision.limit : void 0;
5169
+ const orderBy = resolveJsonAggOrderBy(decision, targetTable, ctx);
5046
5170
  return jsonAggSubquery(
5047
5171
  targetTable,
5048
5172
  whereExpr,
@@ -5053,7 +5177,9 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5053
5177
  ...childNodes && { childNodes },
5054
5178
  innerAlias,
5055
5179
  ...limit !== void 0 && { limit },
5056
- ...decision.columns && { columns: decision.columns }
5180
+ ...decision.columns && { columns: decision.columns },
5181
+ ...orderBy && { orderBy: orderBy.columns },
5182
+ ...orderBy?.fallback && { orderByFallback: true }
5057
5183
  }
5058
5184
  );
5059
5185
  }
@@ -5062,6 +5188,7 @@ var init_json_agg = __esm({
5062
5188
  "src/handlers/include/json-agg.ts"() {
5063
5189
  "use strict";
5064
5190
  init_ast_helpers();
5191
+ init_exists();
5065
5192
  init_shared();
5066
5193
  jsonAggIncludeHandler = {
5067
5194
  strategy: "json_agg",
@@ -5083,6 +5210,7 @@ var init_json_agg = __esm({
5083
5210
  });
5084
5211
 
5085
5212
  // src/handlers/include/lateral.ts
5213
+ import { toColumnList as toColumnList7 } from "@dbsp/types";
5086
5214
  function buildLateralTargets(columns, alias, ctx) {
5087
5215
  if (columns && columns.length > 0 && !(columns.length === 1 && columns[0] === "*")) {
5088
5216
  return columns.map((col) => ({
@@ -5094,12 +5222,12 @@ function buildLateralTargets(columns, alias, ctx) {
5094
5222
  return [starTarget(alias, ctx.naming)];
5095
5223
  }
5096
5224
  function buildLateralSubquery(targetTable, innerAlias, outerAlias, sourceColumn, targetColumn, columns, limit, ctx) {
5097
- const whereClause = fkCorrelation(
5098
- targetColumn,
5225
+ const whereClause = buildKeyCorrelation(
5099
5226
  innerAlias,
5100
- sourceColumn,
5227
+ targetColumn,
5101
5228
  outerAlias,
5102
- ctx.naming
5229
+ sourceColumn,
5230
+ ctx
5103
5231
  );
5104
5232
  const targetList = buildLateralTargets(columns, innerAlias, ctx);
5105
5233
  const stmt = {
@@ -5179,19 +5307,23 @@ var init_lateral = __esm({
5179
5307
  "use strict";
5180
5308
  init_assert_field();
5181
5309
  init_ast_helpers();
5310
+ init_exists();
5182
5311
  init_shared();
5183
5312
  lateralIncludeHandler = {
5184
5313
  strategy: "lateral",
5185
5314
  compile(decision, ctx, state) {
5186
- const sourceColumn = requiredColumn(
5187
- decision.sourceColumn,
5188
- "sourceColumn",
5189
- "lateral include"
5190
- );
5191
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5192
- ctx.rootTable,
5193
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5194
- );
5315
+ const sourceColumn = toColumnList7(decision.sourceColumn);
5316
+ if (sourceColumn.length === 0) {
5317
+ throw new Error(
5318
+ "Missing required column 'sourceColumn' in lateral include"
5319
+ );
5320
+ }
5321
+ const targetColumn = decision.targetColumn ?? [
5322
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5323
+ ctx.rootTable,
5324
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5325
+ )
5326
+ ];
5195
5327
  const outerAlias = ctx.currentAlias ?? ctx.rootTable;
5196
5328
  const { joins, targets } = compileLateralCascade(
5197
5329
  decision,
@@ -7180,9 +7312,14 @@ import { InvalidOperationError as InvalidOperationError2 } from "@dbsp/core";
7180
7312
  import {
7181
7313
  isParamIntent as isParamIntent6,
7182
7314
  NQL_SELECT_SCALAR_FUNCTION_ALLOWLIST,
7183
- NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST
7315
+ NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST,
7316
+ toColumnList as toColumnList8
7184
7317
  } from "@dbsp/types";
7185
- import { getNqlBindingRefName, isNqlBindingRef } from "@dbsp/types/internal";
7318
+ import {
7319
+ getNqlBindingRefName,
7320
+ getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields4,
7321
+ isNqlBindingRef
7322
+ } from "@dbsp/types/internal";
7186
7323
 
7187
7324
  // src/pgsql-deparser.ts
7188
7325
  function deparse(node) {
@@ -8246,6 +8383,7 @@ init_window();
8246
8383
  init_include();
8247
8384
  init_shared();
8248
8385
  init_handlers();
8386
+ init_exists();
8249
8387
  init_types();
8250
8388
  init_utils();
8251
8389
  init_intent_to_decisions();
@@ -8283,7 +8421,27 @@ function assertNqlSelectWindowFunctionAllowed(functionName) {
8283
8421
  throw new UnsupportedNqlSelectFunctionError(functionName);
8284
8422
  }
8285
8423
  }
8424
+ function trustedRelationHasMultipleHops(relation) {
8425
+ if (typeof relation !== "string") return relation.length > 1;
8426
+ return relation.split(".").length > 1;
8427
+ }
8428
+ function isJsonAggOrderBy(orderBy) {
8429
+ return Array.isArray(orderBy) && orderBy.every((item) => typeof item === "string");
8430
+ }
8431
+ function isExpressionOrderBy(orderBy) {
8432
+ return Array.isArray(orderBy) && orderBy.every(
8433
+ (item) => typeof item === "object" && item !== null && "field" in item && typeof item.field === "string"
8434
+ );
8435
+ }
8286
8436
  function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8437
+ const jsonAggOrderBy = isJsonAggOrderBy(pd.orderBy) ? pd.orderBy : void 0;
8438
+ const expressionOrderBy = isExpressionOrderBy(pd.orderBy) ? pd.orderBy : void 0;
8439
+ const derivedFkColumns = deriveFkColumns(
8440
+ pd,
8441
+ pd.sourceTable ?? rootTable,
8442
+ defaultPk,
8443
+ deriveFk
8444
+ );
8287
8445
  return {
8288
8446
  type: pd.type,
8289
8447
  table: pd.table,
@@ -8294,7 +8452,8 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8294
8452
  paramIndex: pd.paramIndex,
8295
8453
  direction: pd.direction,
8296
8454
  joinType: pd.joinType,
8297
- ...deriveFkColumns(pd, pd.sourceTable ?? rootTable, defaultPk, deriveFk),
8455
+ sourceColumn: pd.sourceColumn ?? derivedFkColumns.sourceColumn,
8456
+ targetColumn: pd.targetColumn ?? derivedFkColumns.targetColumn,
8298
8457
  targetTable: pd.targetTable,
8299
8458
  function: pd.function,
8300
8459
  args: pd.args,
@@ -8311,6 +8470,8 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8311
8470
  relationType: pd.relationType,
8312
8471
  foreignKey: pd.foreignKey,
8313
8472
  parentKey: pd.parentKey,
8473
+ targetPrimaryKey: pd.targetPrimaryKey ?? jsonAggOrderBy,
8474
+ orderByFallback: pd.orderByFallback,
8314
8475
  dataType: pd.dataType,
8315
8476
  traversal: pd.traversal,
8316
8477
  pkColumn: pd.pkColumn,
@@ -8325,7 +8486,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8325
8486
  include: pd.include?.map(
8326
8487
  (c) => mapToHandlerDecision(c, rootTable, defaultPk, deriveFk)
8327
8488
  ),
8328
- orderBy: pd.orderBy?.map((o) => ({
8489
+ orderBy: expressionOrderBy?.map((o) => ({
8329
8490
  column: o.field,
8330
8491
  direction: o.direction?.toUpperCase() ?? "ASC"
8331
8492
  })),
@@ -8488,6 +8649,8 @@ var PlanCompiler = class _PlanCompiler {
8488
8649
  rawJoins = [];
8489
8650
  /** CTE nodes from include handlers (e.g., CTE strategy) */
8490
8651
  pendingCtes = [];
8652
+ /** Local aliases for binding relation-column scalar subqueries. */
8653
+ bindingRelationColumnSubqueryIndex = 0;
8491
8654
  /**
8492
8655
  * Maps relation-dotted include path → JOIN alias for multi-hop FK resolution.
8493
8656
  * Populated as join decisions are compiled so later hops can find the
@@ -8705,6 +8868,13 @@ var PlanCompiler = class _PlanCompiler {
8705
8868
  this.defaultPk,
8706
8869
  this.deriveFk
8707
8870
  );
8871
+ if (handlerDecision.strategy === "json_agg") {
8872
+ assertDialectCapability(
8873
+ this.dialectCapabilities,
8874
+ "supportsJsonAgg",
8875
+ "JSON aggregation for relation includes"
8876
+ );
8877
+ }
8708
8878
  const handler = getIncludeHandler(
8709
8879
  handlerDecision.strategy
8710
8880
  );
@@ -8875,6 +9045,162 @@ var PlanCompiler = class _PlanCompiler {
8875
9045
  this.naming
8876
9046
  );
8877
9047
  }
9048
+ isNqlBindingRoot(plan) {
9049
+ return hasBindingName(this.bindingNames, plan.rootTable, this.naming);
9050
+ }
9051
+ buildCorrelatedRelationRefs(fields, plan) {
9052
+ const relatedAlias = `rc_${this.bindingRelationColumnSubqueryIndex++}`;
9053
+ const relatedTable = rangeVar(
9054
+ fields.targetTable,
9055
+ relatedAlias,
9056
+ this.schemaForRangeVar(plan, fields.targetTable),
9057
+ this.naming
9058
+ );
9059
+ const relatedColumn = columnRef(
9060
+ fields.selectedColumn,
9061
+ relatedAlias,
9062
+ void 0,
9063
+ this.naming
9064
+ );
9065
+ return {
9066
+ relatedAlias,
9067
+ relatedTable,
9068
+ relatedColumn
9069
+ };
9070
+ }
9071
+ compileBindingRelationColumnSubquery(fields, plan, dialectCapabilities) {
9072
+ if (fields.selectedColumn === void 0) {
9073
+ throw new Error(
9074
+ `NQL binding relation-column proof for '${plan.rootTable}' is missing selectedColumn.`
9075
+ );
9076
+ }
9077
+ if (fields.cardinality === "one") {
9078
+ const { relatedAlias, relatedTable, relatedColumn } = this.buildCorrelatedRelationRefs(fields, plan);
9079
+ if (fields.hops.length === 0 && trustedRelationHasMultipleHops(fields.relation)) {
9080
+ throw new Error(
9081
+ `NQL binding relation-column proof for '${plan.rootTable}' is dotted but missing resolved hops.`
9082
+ );
9083
+ }
9084
+ if (fields.hops.length > 0) {
9085
+ let fromNode = relatedTable;
9086
+ let previousAlias = relatedAlias;
9087
+ for (let i = 0; i < fields.hops.length; i++) {
9088
+ const hop = fields.hops[i];
9089
+ const hopAlias = `${relatedAlias}_h${i + 1}`;
9090
+ const hopTable = rangeVar(
9091
+ hop.target,
9092
+ hopAlias,
9093
+ this.schemaForRangeVar(plan, hop.target),
9094
+ this.naming
9095
+ );
9096
+ fromNode = innerJoin(
9097
+ fromNode,
9098
+ hopTable,
9099
+ buildKeyCorrelation(
9100
+ hopAlias,
9101
+ hop.joinColumn,
9102
+ previousAlias,
9103
+ hop.fkColumn,
9104
+ this.createHandlerContext(plan)
9105
+ )
9106
+ );
9107
+ previousAlias = hopAlias;
9108
+ }
9109
+ return {
9110
+ SubLink: {
9111
+ subLinkType: "EXPR_SUBLINK",
9112
+ subselect: selectStmt({
9113
+ targetList: [
9114
+ {
9115
+ ResTarget: {
9116
+ val: columnRef(
9117
+ fields.selectedColumn,
9118
+ previousAlias,
9119
+ void 0,
9120
+ this.naming
9121
+ )
9122
+ }
9123
+ }
9124
+ ],
9125
+ from: [fromNode],
9126
+ where: buildKeyCorrelation(
9127
+ relatedAlias,
9128
+ fields.targetColumn,
9129
+ plan.rootTable,
9130
+ fields.sourceColumn,
9131
+ this.createHandlerContext(plan)
9132
+ )
9133
+ })
9134
+ }
9135
+ };
9136
+ }
9137
+ return {
9138
+ SubLink: {
9139
+ subLinkType: "EXPR_SUBLINK",
9140
+ subselect: selectStmt({
9141
+ targetList: [{ ResTarget: { val: relatedColumn } }],
9142
+ from: [relatedTable],
9143
+ where: buildKeyCorrelation(
9144
+ relatedAlias,
9145
+ fields.targetColumn,
9146
+ plan.rootTable,
9147
+ fields.sourceColumn,
9148
+ this.createHandlerContext(plan)
9149
+ )
9150
+ })
9151
+ }
9152
+ };
9153
+ }
9154
+ if (fields.cardinality === "many") {
9155
+ if (fields.relationType !== "hasMany") {
9156
+ throw new Error(
9157
+ `NQL binding relation-column proof for '${plan.rootTable}' has cardinality 'many' but relationType '${fields.relationType ?? "unknown"}' is not supported; only hasMany can be aggregated (ref-#192).`
9158
+ );
9159
+ }
9160
+ if (dialectCapabilities?.supportsJsonAgg === false) {
9161
+ throw new Error(
9162
+ "JSON aggregation for NQL binding relation columns is not supported by this adapter"
9163
+ );
9164
+ }
9165
+ const { relatedAlias, relatedTable, relatedColumn } = this.buildCorrelatedRelationRefs(fields, plan);
9166
+ return {
9167
+ SubLink: {
9168
+ subLinkType: "EXPR_SUBLINK",
9169
+ subselect: selectStmt({
9170
+ targetList: [
9171
+ {
9172
+ ResTarget: {
9173
+ val: coalesceExpr([
9174
+ funcCall("json_agg", [relatedColumn], {
9175
+ orderBy: [
9176
+ sortBy(
9177
+ typeCast(relatedColumn, "text"),
9178
+ "DEFAULT",
9179
+ "LAST"
9180
+ )
9181
+ ]
9182
+ }),
9183
+ typeCast({ A_Const: { sval: { sval: "[]" } } }, "json")
9184
+ ])
9185
+ }
9186
+ }
9187
+ ],
9188
+ from: [relatedTable],
9189
+ where: buildKeyCorrelation(
9190
+ relatedAlias,
9191
+ fields.targetColumn,
9192
+ plan.rootTable,
9193
+ fields.sourceColumn,
9194
+ this.createHandlerContext(plan)
9195
+ )
9196
+ })
9197
+ }
9198
+ };
9199
+ }
9200
+ throw new Error(
9201
+ `NQL binding relation-column proof for '${plan.rootTable}' is not scalar (cardinality: ${fields.cardinality ?? "unknown"}).`
9202
+ );
9203
+ }
8878
9204
  compileExpressionSubquery(query, paramOffset) {
8879
9205
  const innerCompiler = new _PlanCompiler(this.childCompilerOptions());
8880
9206
  const innerPlan = {
@@ -9252,6 +9578,28 @@ var PlanCompiler = class _PlanCompiler {
9252
9578
  case "selectRelationColumn":
9253
9579
  case "selectPseudoColumn":
9254
9580
  case "selectArithmetic": {
9581
+ if (decision.type === "selectRelationColumn") {
9582
+ const trusted = getTrustedNqlRelationFilterFields4(decision);
9583
+ if (trusted?.selectedColumn !== void 0) {
9584
+ const node2 = this.compileBindingRelationColumnSubquery(
9585
+ trusted,
9586
+ plan,
9587
+ this.dialectCapabilities
9588
+ );
9589
+ targetList.push({
9590
+ ResTarget: {
9591
+ val: node2,
9592
+ ...decision.alias ? { name: this.naming.toDatabase(decision.alias) } : {}
9593
+ }
9594
+ });
9595
+ break;
9596
+ }
9597
+ if (this.isNqlBindingRoot(plan)) {
9598
+ throw new Error(
9599
+ `NQL binding-final query '${plan.rootTable}' cannot select relation column '${decision.relation ?? "unknown"}.${decision.column ?? "unknown"}' without a trusted compiler proof.`
9600
+ );
9601
+ }
9602
+ }
9255
9603
  ensureExpressionHandlersRegistered();
9256
9604
  const exprType = decision.type === "selectRelationColumn" ? "relationColumn" : decision.type === "selectPseudoColumn" ? "pseudoColumn" : "arithmetic";
9257
9605
  const handler = getExpressionHandler(exprType);
@@ -9973,12 +10321,22 @@ var PlanCompiler = class _PlanCompiler {
9973
10321
  registerJoinFilter(decision) {
9974
10322
  const targetTable = decision.targetTable;
9975
10323
  const sourceTable = this.currentRootTable;
9976
- const fkColumn = decision.foreignKey ?? this.deriveFk(targetTable, this.defaultPk);
9977
- const onCondition = eqExpr(
9978
- columnRef(this.defaultPk, targetTable, void 0, this.naming),
9979
- columnRef(fkColumn, sourceTable, void 0, this.naming)
9980
- );
9981
10324
  const alias = targetTable === sourceTable ? decision.relationName ?? `${targetTable}_join` : void 0;
10325
+ const targetAlias = alias ?? targetTable;
10326
+ const fkColumn = decision.foreignKey ?? [
10327
+ this.deriveFk(targetTable, this.defaultPk)
10328
+ ];
10329
+ const targetKey = decision.parentKey ?? [this.defaultPk];
10330
+ const onCondition = buildKeyCorrelation(
10331
+ targetAlias,
10332
+ targetKey,
10333
+ sourceTable,
10334
+ fkColumn,
10335
+ this.createHandlerContext({
10336
+ rootTable: sourceTable,
10337
+ decisions: []
10338
+ })
10339
+ );
9982
10340
  this.pendingJoins.push({
9983
10341
  type: "JOIN",
9984
10342
  table: targetTable,
@@ -9999,19 +10357,21 @@ var PlanCompiler = class _PlanCompiler {
9999
10357
  this.schemaForRangeVar(plan, decision.targetTable ?? ""),
10000
10358
  this.naming
10001
10359
  );
10002
- const onCondition = eqExpr(
10003
- columnRef(
10004
- requiredColumn(decision.sourceColumn, "sourceColumn", "compileJoin"),
10005
- void 0,
10006
- void 0,
10007
- this.naming
10008
- ),
10009
- columnRef(
10010
- requiredColumn(decision.targetColumn, "targetColumn", "compileJoin"),
10011
- decision.alias ?? decision.targetTable,
10012
- void 0,
10013
- this.naming
10014
- )
10360
+ const sourceColumn = toColumnList8(decision.sourceColumn);
10361
+ const targetColumn = toColumnList8(decision.targetColumn);
10362
+ if (sourceColumn.length === 0) {
10363
+ throw new Error("Missing required column 'sourceColumn' in compileJoin");
10364
+ }
10365
+ if (targetColumn.length === 0) {
10366
+ throw new Error("Missing required column 'targetColumn' in compileJoin");
10367
+ }
10368
+ const sourceAlias = sourceColumn.length > 1 ? plan.rootTable : "";
10369
+ const onCondition = buildKeyCorrelation(
10370
+ sourceAlias,
10371
+ sourceColumn,
10372
+ decision.alias ?? decision.targetTable ?? "",
10373
+ targetColumn,
10374
+ this.createHandlerContext(plan)
10015
10375
  );
10016
10376
  if (decision.joinType === "left") {
10017
10377
  return leftJoin(baseTable, targetTable, onCondition, decision.alias);
@@ -13129,6 +13489,8 @@ function inferRelations(fksByConstraint, filteredTables) {
13129
13489
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
13130
13490
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
13131
13491
  const fkCol = fk.cols.length === 1 ? fk.cols[0] : fk.cols;
13492
+ const refCol = fk.refs.length === 1 ? fk.refs[0] : fk.refs;
13493
+ const keyOverride = Array.isArray(refCol) || refCol !== DEFAULT_PK_COLUMN ? refCol : void 0;
13132
13494
  const belongsToKey = `${fk.source}.${belongsToName}`;
13133
13495
  if (!relations.has(belongsToKey)) {
13134
13496
  relations.set(belongsToKey, {
@@ -13137,6 +13499,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13137
13499
  source: fk.source,
13138
13500
  target: fk.target,
13139
13501
  foreignKey: fkCol,
13502
+ ...keyOverride !== void 0 && { targetKey: keyOverride },
13140
13503
  cardinality: "one",
13141
13504
  optionality: "optional",
13142
13505
  includeStrategy: "auto",
@@ -13153,6 +13516,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13153
13516
  source: fk.target,
13154
13517
  target: fk.source,
13155
13518
  foreignKey: fkCol,
13519
+ ...keyOverride !== void 0 && { sourceKey: keyOverride },
13156
13520
  cardinality: "many",
13157
13521
  optionality: "optional",
13158
13522
  includeStrategy: "auto",
@@ -14055,6 +14419,7 @@ import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNql
14055
14419
 
14056
14420
  // src/adapter-compiler-includes.ts
14057
14421
  init_ast_helpers();
14422
+ import { toColumnList as toColumnList9 } from "@dbsp/types";
14058
14423
  init_handlers();
14059
14424
  function compileSubqueryInclude(info, parentIds, _options, deps) {
14060
14425
  const schemaName = deps.schemaName;
@@ -14066,7 +14431,12 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14066
14431
  parameters: []
14067
14432
  };
14068
14433
  }
14069
- const fkColumns = Array.isArray(info.foreignKey) ? info.foreignKey : [info.foreignKey];
14434
+ const fkColumns = toColumnList9(info.foreignKey);
14435
+ if (fkColumns.length === 0) {
14436
+ throw new Error(
14437
+ "Subquery include requires at least one foreignKey column."
14438
+ );
14439
+ }
14070
14440
  if (info.through && info.throughSourceKey && info.throughTargetKey) {
14071
14441
  return compileSubqueryIncludeManyToMany(
14072
14442
  info,
@@ -14114,6 +14484,11 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14114
14484
  };
14115
14485
  } else {
14116
14486
  const conditions = parentIds.map((id) => {
14487
+ if (!Array.isArray(id) || id.length !== fkColumns.length) {
14488
+ throw new Error(
14489
+ `Subquery include composite key parameter width (${Array.isArray(id) ? id.length : 1}) must match foreignKey width (${fkColumns.length}).`
14490
+ );
14491
+ }
14117
14492
  const idValues = id;
14118
14493
  const colConditions = fkColumns.map((col, idx) => {
14119
14494
  state.parameters.push(idValues[idx]);
@@ -14154,7 +14529,13 @@ function compileSubqueryIncludeManyToMany(info, parentIds, schemaName, state, de
14154
14529
  const throughTable = info.through;
14155
14530
  const throughSourceKey = info.throughSourceKey;
14156
14531
  const throughTargetKey = info.throughTargetKey;
14157
- const targetPk = Array.isArray(info.sourceKey) ? info.sourceKey[0] : info.sourceKey;
14532
+ const targetPkColumns = toColumnList9(info.sourceKey);
14533
+ if (targetPkColumns.length !== 1) {
14534
+ throw new Error(
14535
+ `Many-to-many subquery include requires a single-column target key; got ${JSON.stringify(targetPkColumns)}.`
14536
+ );
14537
+ }
14538
+ const targetPk = targetPkColumns[0];
14158
14539
  const paramRefs = parentIds.map((id) => {
14159
14540
  state.parameters.push(id);
14160
14541
  state.paramIndex++;
@@ -14243,6 +14624,7 @@ import {
14243
14624
  POSTGRESQL_CAPABILITIES,
14244
14625
  plan as planFn
14245
14626
  } from "@dbsp/core";
14627
+ import { toColumnList as toColumnList12 } from "@dbsp/types";
14246
14628
 
14247
14629
  // src/adapter-compiler-select.ts
14248
14630
  init_assert_field();
@@ -14250,6 +14632,7 @@ init_ast_helpers();
14250
14632
  init_binding_registry();
14251
14633
  init_compile_where();
14252
14634
  import { countDistinctRelationPathsByName } from "@dbsp/core";
14635
+ import { toColumnList as toColumnList11 } from "@dbsp/types";
14253
14636
  init_compiler_utils();
14254
14637
  init_types();
14255
14638
  init_intent_to_decisions();
@@ -14259,6 +14642,7 @@ init_param_ref();
14259
14642
  init_assert_field();
14260
14643
  init_intent_to_decisions();
14261
14644
  import { deriveRelationPathFromIntentPath } from "@dbsp/core";
14645
+ import { toColumnList as toColumnList10 } from "@dbsp/types";
14262
14646
  function findExistsIntents(where) {
14263
14647
  if (!where || typeof where !== "object") return [];
14264
14648
  const w = where;
@@ -14279,7 +14663,8 @@ function findExistsIntents(where) {
14279
14663
  function resolveRelation(model, sourceTable, relationName) {
14280
14664
  const rel = model.getRelation(`${sourceTable}.${relationName}`);
14281
14665
  if (!rel) return void 0;
14282
- const foreignKey = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
14666
+ const foreignKeyColumns = toColumnList10(rel.foreignKey);
14667
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14283
14668
  const relationType = rel.type;
14284
14669
  return { target: rel.target, foreignKey, relationType };
14285
14670
  }
@@ -14502,7 +14887,8 @@ function buildMultiHopExistsChain(hops, rootTable, outerOperator, innerCondition
14502
14887
  const hopRelations = [];
14503
14888
  for (const hop of hops) {
14504
14889
  const rel = model.getRelation(`${currentSource}.${hop}`);
14505
- const foreignKey = rel ? typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0] : void 0;
14890
+ const foreignKeyColumns = rel ? toColumnList10(rel.foreignKey) : [];
14891
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14506
14892
  const relationType = rel?.type === "belongsTo" ? "belongsTo" : rel?.type === "hasMany" || rel?.type === "hasOne" ? rel.type : void 0;
14507
14893
  const parentKey = relationType === "belongsTo" ? rel?.targetKey : rel?.sourceKey;
14508
14894
  const target = rel ? rel.target : hop;
@@ -14612,7 +14998,8 @@ function enrichExistsStubsInConditions(conditions, sourceTable, model) {
14612
14998
  `exists('${relation}'): no relation '${relation}' is declared on table '${sourceTable}'. Use rawExists(subquery(...)) for an EXISTS over an undeclared or uncorrelated subquery.`
14613
14999
  );
14614
15000
  }
14615
- const foreignKey = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
15001
+ const foreignKeyColumns = toColumnList10(rel.foreignKey);
15002
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14616
15003
  const relationType = rel.type === "belongsTo" ? "belongsTo" : rel.type === "hasMany" || rel.type === "hasOne" ? rel.type : void 0;
14617
15004
  const parentKey = relationType === "belongsTo" ? rel.targetKey : rel.sourceKey;
14618
15005
  const targetTable = rel.target;
@@ -14671,7 +15058,8 @@ function buildEnrichedExistsDecision(d, matchingIntent, rootTable, model) {
14671
15058
  const relIR = model && context.relation ? model.getRelation(
14672
15059
  `${sourceTableForRelation}.${context.relation}`
14673
15060
  ) : void 0;
14674
- const foreignKey = relIR ? typeof relIR.foreignKey === "string" ? relIR.foreignKey : relIR.foreignKey?.[0] : void 0;
15061
+ const foreignKeyColumns = relIR ? toColumnList10(relIR.foreignKey) : [];
15062
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14675
15063
  const relationType = relIR?.type === "belongsTo" ? "belongsTo" : relIR?.type === "hasMany" || relIR?.type === "hasOne" ? relIR.type : void 0;
14676
15064
  const parentKey = relationType === "belongsTo" ? relIR?.targetKey : relIR?.sourceKey;
14677
15065
  let operator = "exists";
@@ -14984,6 +15372,7 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
14984
15372
  const relationName = resolveIncludeAlias(context);
14985
15373
  if (!context.target || !relationName) return void 0;
14986
15374
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15375
+ const parentKey = context.parentKey ?? defaultPk;
14987
15376
  const relationType = context.relationType;
14988
15377
  const effectiveChoice = choice === "subquery" ? "json_agg" : choice;
14989
15378
  const includeIntent = resolveIncludeByPath(
@@ -15004,8 +15393,13 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15004
15393
  targetTable: context.target,
15005
15394
  ...context.sourceTable && { sourceTable: context.sourceTable },
15006
15395
  ...relationType && { relationType },
15007
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15008
- parentKey: defaultPk,
15396
+ foreignKey,
15397
+ parentKey,
15398
+ ...context.targetPrimaryKey && context.targetPrimaryKey.length > 0 ? {
15399
+ targetPrimaryKey: context.targetPrimaryKey,
15400
+ orderBy: context.targetPrimaryKey
15401
+ } : {},
15402
+ ...context.orderByFallback ? { orderByFallback: true } : {},
15009
15403
  ...context.intentPath && { intentPath: context.intentPath },
15010
15404
  ...limit != null && { limit }
15011
15405
  };
@@ -15031,6 +15425,7 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15031
15425
  columns = [defaultPk, ...fields];
15032
15426
  }
15033
15427
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15428
+ const parentKey = context.parentKey ?? defaultPk;
15034
15429
  const relationType = context.relationType;
15035
15430
  let conditions;
15036
15431
  if (includeIntent?.where) {
@@ -15051,8 +15446,8 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15051
15446
  targetTable: context.target,
15052
15447
  ...context.sourceTable && { sourceTable: context.sourceTable },
15053
15448
  ...relationType && { relationType },
15054
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15055
- parentKey: defaultPk,
15449
+ foreignKey,
15450
+ parentKey,
15056
15451
  columns,
15057
15452
  ...joinType && { joinType },
15058
15453
  ...conditions && { conditions }
@@ -15075,10 +15470,13 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15075
15470
  (r) => r.name === alias || snakeToCamel(r.name) === alias
15076
15471
  );
15077
15472
  if (!rel) continue;
15078
- const rawFk = rel.foreignKey ? Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : rel.foreignKey : deriveFk(
15079
- rel.type === "belongsTo" ? rel.target : sourceTable,
15080
- defaultPk
15081
- );
15473
+ const rawFkColumns = toColumnList10(rel.foreignKey);
15474
+ const rawFk = rawFkColumns.length > 0 ? rawFkColumns : [
15475
+ deriveFk(
15476
+ rel.type === "belongsTo" ? rel.target : sourceTable,
15477
+ defaultPk
15478
+ )
15479
+ ];
15082
15480
  let columns = [defaultPk];
15083
15481
  if (inc.select?.type === "fields" && inc.select.fields) {
15084
15482
  const extraFields = inc.select.fields.filter((f) => f !== defaultPk);
@@ -15099,8 +15497,8 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15099
15497
  ...rel.type && {
15100
15498
  relationType: rel.type
15101
15499
  },
15102
- foreignKey: Array.isArray(rawFk) ? rawFk[0] : rawFk,
15103
- parentKey: defaultPk,
15500
+ foreignKey: rawFk,
15501
+ parentKey: rel.type === "belongsTo" ? toColumnList10(rel.targetKey).length > 0 ? toColumnList10(rel.targetKey) : [defaultPk] : toColumnList10(rel.sourceKey).length > 0 ? toColumnList10(rel.sourceKey) : [defaultPk],
15104
15502
  columns,
15105
15503
  joinType: inc.join,
15106
15504
  ...conditions && { conditions }
@@ -15257,9 +15655,12 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
15257
15655
  );
15258
15656
  }
15259
15657
  const isBelongsTo = rel.type === "belongsTo";
15260
- const rawFk = rel.foreignKey ? Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : rel.foreignKey : deriveFk(isBelongsTo ? rootTable : rel.target, defaultPk);
15261
- const sourceColumn = isBelongsTo ? rawFk : defaultPk;
15262
- const targetColumn = isBelongsTo ? defaultPk : rawFk;
15658
+ const rawFk = toColumnList11(rel.foreignKey);
15659
+ const fkColumns = rawFk.length > 0 ? rawFk : [deriveFk(isBelongsTo ? rootTable : rel.target, defaultPk)];
15660
+ const sourceKey = toColumnList11(rel.sourceKey);
15661
+ const targetKey = toColumnList11(rel.targetKey);
15662
+ const sourceColumn = isBelongsTo ? fkColumns : sourceKey.length > 0 ? sourceKey : [defaultPk];
15663
+ const targetColumn = isBelongsTo ? targetKey.length > 0 ? targetKey : [defaultPk] : fkColumns;
15263
15664
  const alias = intent.alias ?? intent.relation;
15264
15665
  results.push({
15265
15666
  type: "join",
@@ -15622,10 +16023,11 @@ function compileWithIncludes(plan, options, deps) {
15622
16023
  const relationName = ctx.includeAlias ?? ctx.relation;
15623
16024
  if (!relationName) continue;
15624
16025
  const rawFk = deriveForeignKey(ctx, deps.deriveFk, deps.defaultPk) ?? deps.defaultPk;
15625
- const fk = Array.isArray(rawFk) ? rawFk[0] : rawFk;
16026
+ const fk = toColumnList11(rawFk);
16027
+ const parentKey = toColumnList11(ctx.parentKey);
15626
16028
  const isBelongsTo = ctx.relationType === "belongsTo";
15627
- const sourceKey = isBelongsTo ? fk : "id";
15628
- const targetFk = isBelongsTo ? "id" : fk;
16029
+ const sourceKey = isBelongsTo ? fk : parentKey.length > 0 ? parentKey : [deps.defaultPk];
16030
+ const targetFk = isBelongsTo ? parentKey.length > 0 ? parentKey : [deps.defaultPk] : fk;
15629
16031
  const includeIntent = plan.intent?.include?.find(
15630
16032
  (i) => i.relation === relationName || i.relation === ctx.includeAlias
15631
16033
  );
@@ -15688,14 +16090,14 @@ function prependSourceCte(sql, parameters, sourceName, sourceCte, naming) {
15688
16090
  };
15689
16091
  }
15690
16092
  function resolveMutationExistsForeignKey(foreignKey, relationName) {
15691
- if (typeof foreignKey === "string") return foreignKey;
15692
- if (!Array.isArray(foreignKey)) return void 0;
15693
- if (foreignKey.length > 1) {
16093
+ const columns = toColumnList12(foreignKey);
16094
+ if (columns.length === 0) return void 0;
16095
+ if (columns.some((column) => column.length === 0)) {
15694
16096
  throw new Error(
15695
- `Mutation exists()/notExists() guards do not support composite foreignKey relations yet: relation '${relationName}' has foreignKey ${JSON.stringify(foreignKey)}. Composite FK correlation is tracked by #179.`
16097
+ `Mutation exists()/notExists() guard relation '${relationName}' has an empty foreignKey column.`
15696
16098
  );
15697
16099
  }
15698
- return foreignKey[0];
16100
+ return columns;
15699
16101
  }
15700
16102
  function resolveExistsRelation(sourceTable, relation, model) {
15701
16103
  if (!model) return { targetTable: relation };
@@ -15708,13 +16110,13 @@ function resolveExistsRelation(sourceTable, relation, model) {
15708
16110
  return {
15709
16111
  targetTable,
15710
16112
  ...fk2 !== void 0 && { sourceColumn: fk2 },
15711
- targetColumn: "id"
16113
+ targetColumn: toColumnList12(rel.targetKey).length > 0 ? toColumnList12(rel.targetKey) : ["id"]
15712
16114
  };
15713
16115
  }
15714
16116
  const fk = resolveMutationExistsForeignKey(rel.foreignKey, relationName);
15715
16117
  return {
15716
16118
  targetTable,
15717
- sourceColumn: rel.sourceKey ?? "id",
16119
+ sourceColumn: toColumnList12(rel.sourceKey).length > 0 ? toColumnList12(rel.sourceKey) : ["id"],
15718
16120
  ...fk !== void 0 && { targetColumn: fk }
15719
16121
  };
15720
16122
  }
@@ -17953,7 +18355,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
17953
18355
  bundle.query.from,
17954
18356
  deps.naming
17955
18357
  );
17956
- const planReport = queryFromBinding ? createNqlBindingSelectPlan(bundle.query) : planFn2(bundle.query, this.requireNqlCompileModel(options), {
18358
+ const planReport = queryFromBinding ? bundle.plan ?? createNqlBindingSelectPlan(bundle.query) : planFn2(bundle.query, this.requireNqlCompileModel(options), {
17957
18359
  dialectCapabilities: options?.dialectCapabilities ?? this.dialectCapabilities
17958
18360
  });
17959
18361
  return guardCompiledQuery(
@@ -18047,6 +18449,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18047
18449
  }
18048
18450
  const leafBundle = {
18049
18451
  ...bundle.query !== void 0 && { query: bundle.query },
18452
+ ...bundle.plan !== void 0 && { plan: bundle.plan },
18050
18453
  ...bundle.cteQuery !== void 0 && { cteQuery: bundle.cteQuery },
18051
18454
  ...bundle.mutation !== void 0 && { mutation: bundle.mutation },
18052
18455
  ...bundle.returning !== void 0 && { returning: bundle.returning },