@dbsp/adapter-pgsql 1.5.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({
@@ -3172,6 +3177,7 @@ var init_custom_expression = __esm({
3172
3177
  });
3173
3178
 
3174
3179
  // src/handlers/where/exists.ts
3180
+ import { toColumnList } from "@dbsp/types";
3175
3181
  function createSubLinkExists(subquery, negated) {
3176
3182
  const subLink = {
3177
3183
  subLinkType: "EXISTS_SUBLINK",
@@ -3188,10 +3194,27 @@ function createSubLinkExists(subquery, negated) {
3188
3194
  }
3189
3195
  return existsNode;
3190
3196
  }
3191
- function buildCorrelation(sourceAlias, sourceColumn, targetAlias, targetColumn, ctx) {
3192
- const left = columnRef(sourceColumn, sourceAlias, void 0, ctx.naming);
3193
- const right = columnRef(targetColumn, targetAlias, void 0, ctx.naming);
3194
- 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);
3195
3218
  }
3196
3219
  function schemaForExistsFromName(ctx, fromName) {
3197
3220
  return schemaForFromName(ctx.schema, fromName, ctx.bindingNames, ctx.naming);
@@ -3199,11 +3222,15 @@ function schemaForExistsFromName(ctx, fromName) {
3199
3222
  function buildExistsSubquery(decision, ctx, state, dispatch) {
3200
3223
  const relation = decision.relation;
3201
3224
  const targetTable = decision.targetTable ?? relation;
3202
- const sourceColumn = decision.sourceColumn ?? ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3203
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3204
- ctx.rootTable,
3225
+ const sourceColumn = decision.sourceColumn ?? [
3205
3226
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3206
- );
3227
+ ];
3228
+ const targetColumn = decision.targetColumn ?? [
3229
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3230
+ ctx.rootTable,
3231
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3232
+ )
3233
+ ];
3207
3234
  if (!targetTable) {
3208
3235
  throw new Error("EXISTS handler requires targetTable or relation");
3209
3236
  }
@@ -3211,7 +3238,7 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3211
3238
  const targetAlias = `${targetTable}_exists_${existingAliases}`;
3212
3239
  state.aliases.set(`exists_${targetTable}`, targetAlias);
3213
3240
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
3214
- const correlation = buildCorrelation(
3241
+ const correlation = buildKeyCorrelation(
3215
3242
  sourceAlias,
3216
3243
  sourceColumn,
3217
3244
  targetAlias,
@@ -3249,8 +3276,8 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3249
3276
  const joinRelation = inc.relation;
3250
3277
  if (!joinRelation) continue;
3251
3278
  let joinTargetTable = joinRelation;
3252
- let joinSourceCol;
3253
- let joinTargetCol;
3279
+ let joinSourceCols;
3280
+ let joinTargetCols;
3254
3281
  let sourceAliasForJoin = targetAlias;
3255
3282
  const model = ctx.model;
3256
3283
  if (model) {
@@ -3268,31 +3295,41 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3268
3295
  if (rel) {
3269
3296
  joinTargetTable = rel.target;
3270
3297
  if (rel.type === "belongsTo") {
3271
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
3272
- joinSourceCol = fk;
3273
- 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
+ ];
3274
3304
  } else {
3275
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
3276
- joinSourceCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3277
- 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;
3278
3311
  }
3279
3312
  }
3280
3313
  }
3281
- if (!joinSourceCol) {
3282
- joinSourceCol = (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3283
- 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 = [
3284
3322
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3285
- );
3286
- joinTargetCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3323
+ ];
3287
3324
  }
3288
3325
  const joinAlias = joinRelation;
3289
- const joinQuals = fkCorrelation(
3290
- joinSourceCol,
3326
+ const joinQuals = buildKeyCorrelation(
3291
3327
  sourceAliasForJoin,
3292
3328
  // resolved source alias (root or intermediate)
3293
- joinTargetCol ?? DEFAULT_PK_COLUMN,
3329
+ joinSourceCols,
3294
3330
  joinAlias,
3295
- ctx.naming
3331
+ joinTargetCols,
3332
+ ctx
3296
3333
  );
3297
3334
  const joinType = inc.joinType === "left" ? "JOIN_LEFT" : "JOIN_INNER";
3298
3335
  const joinRangeVar = rangeVar(
@@ -3774,6 +3811,7 @@ var init_range = __esm({
3774
3811
  });
3775
3812
 
3776
3813
  // src/compile-where.ts
3814
+ import { toColumnList as toColumnList2 } from "@dbsp/types";
3777
3815
  import { getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields3 } from "@dbsp/types/internal";
3778
3816
  function toHandlerContext(ctx) {
3779
3817
  return {
@@ -4039,14 +4077,16 @@ function handleRelationFilterIntent(intent, ctx) {
4039
4077
  let singleHopTargetColumn;
4040
4078
  if (resolvedRelation) {
4041
4079
  const rel = resolvedRelation;
4042
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4080
+ const fk = toColumnList2(rel.foreignKey);
4043
4081
  const defaultPk = DEFAULT_PK_COLUMN;
4044
4082
  if (rel.type === "belongsTo") {
4045
- singleHopSourceColumn = fk ?? defaultFkDerivation(rel.target, defaultPk);
4046
- 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];
4047
4086
  } else {
4048
- singleHopSourceColumn = rel.sourceKey ?? defaultPk;
4049
- 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)];
4050
4090
  }
4051
4091
  }
4052
4092
  return compileWhereIntent(
@@ -4083,15 +4123,19 @@ function handleRelationFilterIntent(intent, ctx) {
4083
4123
  );
4084
4124
  }
4085
4125
  hopTargets.push(rel.target);
4086
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4126
+ const fk = toColumnList2(rel.foreignKey);
4087
4127
  const defaultPk = DEFAULT_PK_COLUMN;
4088
4128
  if (rel.type === "belongsTo") {
4089
- hopSourceColumns.push(fk ?? defaultFkDerivation(rel.target, defaultPk));
4090
- 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]);
4091
4134
  } else {
4092
- hopSourceColumns.push(rel.sourceKey ?? defaultPk);
4135
+ const sourceKey = toColumnList2(rel.sourceKey);
4136
+ hopSourceColumns.push(sourceKey.length > 0 ? sourceKey : [defaultPk]);
4093
4137
  hopTargetColumns.push(
4094
- fk ?? defaultFkDerivation(currentSource, defaultPk)
4138
+ fk.length > 0 ? fk : [defaultFkDerivation(currentSource, defaultPk)]
4095
4139
  );
4096
4140
  }
4097
4141
  currentSource = rel.target;
@@ -4319,6 +4363,7 @@ var init_raw_exists = __esm({
4319
4363
  });
4320
4364
 
4321
4365
  // src/handlers/where/relation-filter.ts
4366
+ import { toColumnList as toColumnList3 } from "@dbsp/types";
4322
4367
  function getFilterMode(decision) {
4323
4368
  const operator = decision.operator;
4324
4369
  if (operator === "some" || operator === "exists") return "some";
@@ -4330,15 +4375,18 @@ function getFilterMode(decision) {
4330
4375
  function buildJoinFilter(decision, ctx, state, dispatch) {
4331
4376
  const relation = decision.relation;
4332
4377
  const targetTable = decision.targetTable ?? relation;
4333
- const sourceColumn = requiredColumn(
4334
- decision.sourceColumn,
4335
- "sourceColumn",
4336
- "relation filter"
4337
- );
4338
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4339
- ctx.rootTable,
4340
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4341
- );
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
+ ];
4342
4390
  if (!targetTable) {
4343
4391
  throw new Error("Relation filter requires targetTable");
4344
4392
  }
@@ -4346,9 +4394,12 @@ function buildJoinFilter(decision, ctx, state, dispatch) {
4346
4394
  const targetAlias = `${targetTable}_rel_${existingAliases}`;
4347
4395
  state.aliases.set(`rel_${targetTable}`, targetAlias);
4348
4396
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
4349
- const joinCondition = eqExpr(
4350
- columnRef(sourceColumn, sourceAlias, ctx.schema, ctx.naming),
4351
- columnRef(targetColumn, targetAlias, ctx.schema, ctx.naming)
4397
+ const joinCondition = buildKeyCorrelation(
4398
+ sourceAlias,
4399
+ sourceColumn,
4400
+ targetAlias,
4401
+ targetColumn,
4402
+ ctx
4352
4403
  );
4353
4404
  const joinExpr2 = {
4354
4405
  jointype: "JOIN_INNER",
@@ -4383,6 +4434,7 @@ var init_relation_filter = __esm({
4383
4434
  "use strict";
4384
4435
  init_assert_field();
4385
4436
  init_ast_helpers();
4437
+ init_exists();
4386
4438
  relationFilterHandler = {
4387
4439
  operators: ["relationFilter", "relation", "is", "isNot"],
4388
4440
  compile(decision, ctx, state, dispatch) {
@@ -4812,6 +4864,7 @@ var init_where = __esm({
4812
4864
  });
4813
4865
 
4814
4866
  // src/handlers/include/cte.ts
4867
+ import { toColumnList as toColumnList4 } from "@dbsp/types";
4815
4868
  function buildCteTargets(columns, alias, ctx) {
4816
4869
  if (columns && columns.length > 0 && !columns.every((c) => c === "*")) {
4817
4870
  return columns.filter((col) => col !== "*").map((col) => ({
@@ -4862,12 +4915,12 @@ function buildCTE(cteName, cteSelect, ctx) {
4862
4915
  return { CommonTableExpr: cte };
4863
4916
  }
4864
4917
  function buildCteJoin(cteName, cteAlias, sourceAlias, sourceColumn, targetColumn, ctx) {
4865
- const joinCondition = fkCorrelation(
4866
- sourceColumn,
4918
+ const joinCondition = buildKeyCorrelation(
4867
4919
  sourceAlias,
4868
- targetColumn,
4920
+ sourceColumn,
4869
4921
  cteAlias,
4870
- ctx.naming
4922
+ targetColumn,
4923
+ ctx
4871
4924
  );
4872
4925
  const cteRef = {
4873
4926
  RangeVar: {
@@ -4891,20 +4944,22 @@ var init_cte = __esm({
4891
4944
  init_assert_field();
4892
4945
  init_ast_helpers();
4893
4946
  init_handlers();
4947
+ init_exists();
4894
4948
  cteIncludeHandler = {
4895
4949
  strategy: "cte",
4896
4950
  compile(decision, ctx, state) {
4897
4951
  const relation = decision.relation;
4898
4952
  const targetTable = decision.targetTable ?? relation;
4899
- const sourceColumn = requiredColumn(
4900
- decision.sourceColumn,
4901
- "sourceColumn",
4902
- "CTE include"
4903
- );
4904
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4905
- ctx.rootTable,
4906
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4907
- );
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
+ ];
4908
4963
  const columns = decision.columns;
4909
4964
  const conditions = decision.conditions;
4910
4965
  if (!targetTable) {
@@ -4947,13 +5002,14 @@ var init_cte = __esm({
4947
5002
  });
4948
5003
 
4949
5004
  // src/handlers/include/join.ts
5005
+ import { toColumnList as toColumnList5 } from "@dbsp/types";
4950
5006
  function buildJoin(targetTable, targetAlias, sourceAlias, sourceColumn, targetColumn, ctx, joinType = "left") {
4951
- const joinCondition = fkCorrelation(
4952
- sourceColumn,
5007
+ const joinCondition = buildKeyCorrelation(
4953
5008
  sourceAlias,
4954
- targetColumn,
5009
+ sourceColumn,
4955
5010
  targetAlias,
4956
- ctx.naming
5011
+ targetColumn,
5012
+ ctx
4957
5013
  );
4958
5014
  const joinExpr2 = {
4959
5015
  jointype: joinType === "inner" ? "JOIN_INNER" : "JOIN_LEFT",
@@ -4968,6 +5024,7 @@ var init_join = __esm({
4968
5024
  "use strict";
4969
5025
  init_assert_field();
4970
5026
  init_ast_helpers();
5027
+ init_exists();
4971
5028
  joinIncludeHandler = {
4972
5029
  strategy: "join",
4973
5030
  compile(decision, ctx, _state) {
@@ -4978,15 +5035,16 @@ var init_join = __esm({
4978
5035
  }
4979
5036
  const targetAlias = relation ?? targetTable;
4980
5037
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
4981
- const sourceColumn = requiredColumn(
4982
- decision.sourceColumn,
4983
- "sourceColumn",
4984
- "JOIN include"
4985
- );
4986
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4987
- ctx.rootTable,
4988
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4989
- );
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
+ ];
4990
5048
  const join = buildJoin(
4991
5049
  targetTable,
4992
5050
  targetAlias,
@@ -5040,6 +5098,22 @@ var init_shared = __esm({
5040
5098
  });
5041
5099
 
5042
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
+ }
5043
5117
  function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5044
5118
  const innerAlias = depth === 0 ? "__t__" : `__t${depth}__`;
5045
5119
  const relation = decision.relation ?? decision.relationName;
@@ -5056,12 +5130,12 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5056
5130
  ctx.defaultPkColumnName,
5057
5131
  ctx.deriveFkColumnName
5058
5132
  );
5059
- let whereExpr = jsonAggCorrelation(
5060
- parentAlias,
5061
- sourceColumn,
5133
+ let whereExpr = buildKeyCorrelation(
5062
5134
  innerAlias,
5063
5135
  targetColumn,
5064
- ctx.naming
5136
+ parentAlias,
5137
+ sourceColumn,
5138
+ ctx
5065
5139
  );
5066
5140
  const compiledFilter = decision._compiledFilterWhere;
5067
5141
  if (compiledFilter) {
@@ -5092,6 +5166,7 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5092
5166
  if (childNodes.length === 0) childNodes = void 0;
5093
5167
  }
5094
5168
  const limit = typeof decision.limit === "number" ? decision.limit : void 0;
5169
+ const orderBy = resolveJsonAggOrderBy(decision, targetTable, ctx);
5095
5170
  return jsonAggSubquery(
5096
5171
  targetTable,
5097
5172
  whereExpr,
@@ -5102,7 +5177,9 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5102
5177
  ...childNodes && { childNodes },
5103
5178
  innerAlias,
5104
5179
  ...limit !== void 0 && { limit },
5105
- ...decision.columns && { columns: decision.columns }
5180
+ ...decision.columns && { columns: decision.columns },
5181
+ ...orderBy && { orderBy: orderBy.columns },
5182
+ ...orderBy?.fallback && { orderByFallback: true }
5106
5183
  }
5107
5184
  );
5108
5185
  }
@@ -5111,6 +5188,7 @@ var init_json_agg = __esm({
5111
5188
  "src/handlers/include/json-agg.ts"() {
5112
5189
  "use strict";
5113
5190
  init_ast_helpers();
5191
+ init_exists();
5114
5192
  init_shared();
5115
5193
  jsonAggIncludeHandler = {
5116
5194
  strategy: "json_agg",
@@ -5132,6 +5210,7 @@ var init_json_agg = __esm({
5132
5210
  });
5133
5211
 
5134
5212
  // src/handlers/include/lateral.ts
5213
+ import { toColumnList as toColumnList7 } from "@dbsp/types";
5135
5214
  function buildLateralTargets(columns, alias, ctx) {
5136
5215
  if (columns && columns.length > 0 && !(columns.length === 1 && columns[0] === "*")) {
5137
5216
  return columns.map((col) => ({
@@ -5143,12 +5222,12 @@ function buildLateralTargets(columns, alias, ctx) {
5143
5222
  return [starTarget(alias, ctx.naming)];
5144
5223
  }
5145
5224
  function buildLateralSubquery(targetTable, innerAlias, outerAlias, sourceColumn, targetColumn, columns, limit, ctx) {
5146
- const whereClause = fkCorrelation(
5147
- targetColumn,
5225
+ const whereClause = buildKeyCorrelation(
5148
5226
  innerAlias,
5149
- sourceColumn,
5227
+ targetColumn,
5150
5228
  outerAlias,
5151
- ctx.naming
5229
+ sourceColumn,
5230
+ ctx
5152
5231
  );
5153
5232
  const targetList = buildLateralTargets(columns, innerAlias, ctx);
5154
5233
  const stmt = {
@@ -5228,19 +5307,23 @@ var init_lateral = __esm({
5228
5307
  "use strict";
5229
5308
  init_assert_field();
5230
5309
  init_ast_helpers();
5310
+ init_exists();
5231
5311
  init_shared();
5232
5312
  lateralIncludeHandler = {
5233
5313
  strategy: "lateral",
5234
5314
  compile(decision, ctx, state) {
5235
- const sourceColumn = requiredColumn(
5236
- decision.sourceColumn,
5237
- "sourceColumn",
5238
- "lateral include"
5239
- );
5240
- const targetColumn = decision.targetColumn ?? (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5241
- ctx.rootTable,
5242
- ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5243
- );
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
+ ];
5244
5327
  const outerAlias = ctx.currentAlias ?? ctx.rootTable;
5245
5328
  const { joins, targets } = compileLateralCascade(
5246
5329
  decision,
@@ -7229,7 +7312,8 @@ import { InvalidOperationError as InvalidOperationError2 } from "@dbsp/core";
7229
7312
  import {
7230
7313
  isParamIntent as isParamIntent6,
7231
7314
  NQL_SELECT_SCALAR_FUNCTION_ALLOWLIST,
7232
- NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST
7315
+ NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST,
7316
+ toColumnList as toColumnList8
7233
7317
  } from "@dbsp/types";
7234
7318
  import {
7235
7319
  getNqlBindingRefName,
@@ -8299,6 +8383,7 @@ init_window();
8299
8383
  init_include();
8300
8384
  init_shared();
8301
8385
  init_handlers();
8386
+ init_exists();
8302
8387
  init_types();
8303
8388
  init_utils();
8304
8389
  init_intent_to_decisions();
@@ -8336,7 +8421,21 @@ function assertNqlSelectWindowFunctionAllowed(functionName) {
8336
8421
  throw new UnsupportedNqlSelectFunctionError(functionName);
8337
8422
  }
8338
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
+ }
8339
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;
8340
8439
  const derivedFkColumns = deriveFkColumns(
8341
8440
  pd,
8342
8441
  pd.sourceTable ?? rootTable,
@@ -8371,6 +8470,8 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8371
8470
  relationType: pd.relationType,
8372
8471
  foreignKey: pd.foreignKey,
8373
8472
  parentKey: pd.parentKey,
8473
+ targetPrimaryKey: pd.targetPrimaryKey ?? jsonAggOrderBy,
8474
+ orderByFallback: pd.orderByFallback,
8374
8475
  dataType: pd.dataType,
8375
8476
  traversal: pd.traversal,
8376
8477
  pkColumn: pd.pkColumn,
@@ -8385,7 +8486,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8385
8486
  include: pd.include?.map(
8386
8487
  (c) => mapToHandlerDecision(c, rootTable, defaultPk, deriveFk)
8387
8488
  ),
8388
- orderBy: pd.orderBy?.map((o) => ({
8489
+ orderBy: expressionOrderBy?.map((o) => ({
8389
8490
  column: o.field,
8390
8491
  direction: o.direction?.toUpperCase() ?? "ASC"
8391
8492
  })),
@@ -8767,6 +8868,13 @@ var PlanCompiler = class _PlanCompiler {
8767
8868
  this.defaultPk,
8768
8869
  this.deriveFk
8769
8870
  );
8871
+ if (handlerDecision.strategy === "json_agg") {
8872
+ assertDialectCapability(
8873
+ this.dialectCapabilities,
8874
+ "supportsJsonAgg",
8875
+ "JSON aggregation for relation includes"
8876
+ );
8877
+ }
8770
8878
  const handler = getIncludeHandler(
8771
8879
  handlerDecision.strategy
8772
8880
  );
@@ -8940,17 +9048,7 @@ var PlanCompiler = class _PlanCompiler {
8940
9048
  isNqlBindingRoot(plan) {
8941
9049
  return hasBindingName(this.bindingNames, plan.rootTable, this.naming);
8942
9050
  }
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
- }
9051
+ buildCorrelatedRelationRefs(fields, plan) {
8954
9052
  const relatedAlias = `rc_${this.bindingRelationColumnSubqueryIndex++}`;
8955
9053
  const relatedTable = rangeVar(
8956
9054
  fields.targetTable,
@@ -8964,29 +9062,145 @@ var PlanCompiler = class _PlanCompiler {
8964
9062
  void 0,
8965
9063
  this.naming
8966
9064
  );
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
9065
  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
- }
9066
+ relatedAlias,
9067
+ relatedTable,
9068
+ relatedColumn
8988
9069
  };
8989
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
+ }
8990
9204
  compileExpressionSubquery(query, paramOffset) {
8991
9205
  const innerCompiler = new _PlanCompiler(this.childCompilerOptions());
8992
9206
  const innerPlan = {
@@ -9369,7 +9583,8 @@ var PlanCompiler = class _PlanCompiler {
9369
9583
  if (trusted?.selectedColumn !== void 0) {
9370
9584
  const node2 = this.compileBindingRelationColumnSubquery(
9371
9585
  trusted,
9372
- plan
9586
+ plan,
9587
+ this.dialectCapabilities
9373
9588
  );
9374
9589
  targetList.push({
9375
9590
  ResTarget: {
@@ -10106,12 +10321,22 @@ var PlanCompiler = class _PlanCompiler {
10106
10321
  registerJoinFilter(decision) {
10107
10322
  const targetTable = decision.targetTable;
10108
10323
  const sourceTable = this.currentRootTable;
10109
- const fkColumn = decision.foreignKey ?? this.deriveFk(targetTable, this.defaultPk);
10110
- const onCondition = eqExpr(
10111
- columnRef(this.defaultPk, targetTable, void 0, this.naming),
10112
- columnRef(fkColumn, sourceTable, void 0, this.naming)
10113
- );
10114
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
+ );
10115
10340
  this.pendingJoins.push({
10116
10341
  type: "JOIN",
10117
10342
  table: targetTable,
@@ -10132,19 +10357,21 @@ var PlanCompiler = class _PlanCompiler {
10132
10357
  this.schemaForRangeVar(plan, decision.targetTable ?? ""),
10133
10358
  this.naming
10134
10359
  );
10135
- const onCondition = eqExpr(
10136
- columnRef(
10137
- requiredColumn(decision.sourceColumn, "sourceColumn", "compileJoin"),
10138
- void 0,
10139
- void 0,
10140
- this.naming
10141
- ),
10142
- columnRef(
10143
- requiredColumn(decision.targetColumn, "targetColumn", "compileJoin"),
10144
- decision.alias ?? decision.targetTable,
10145
- void 0,
10146
- this.naming
10147
- )
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)
10148
10375
  );
10149
10376
  if (decision.joinType === "left") {
10150
10377
  return leftJoin(baseTable, targetTable, onCondition, decision.alias);
@@ -13262,6 +13489,8 @@ function inferRelations(fksByConstraint, filteredTables) {
13262
13489
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
13263
13490
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
13264
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;
13265
13494
  const belongsToKey = `${fk.source}.${belongsToName}`;
13266
13495
  if (!relations.has(belongsToKey)) {
13267
13496
  relations.set(belongsToKey, {
@@ -13270,6 +13499,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13270
13499
  source: fk.source,
13271
13500
  target: fk.target,
13272
13501
  foreignKey: fkCol,
13502
+ ...keyOverride !== void 0 && { targetKey: keyOverride },
13273
13503
  cardinality: "one",
13274
13504
  optionality: "optional",
13275
13505
  includeStrategy: "auto",
@@ -13286,6 +13516,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13286
13516
  source: fk.target,
13287
13517
  target: fk.source,
13288
13518
  foreignKey: fkCol,
13519
+ ...keyOverride !== void 0 && { sourceKey: keyOverride },
13289
13520
  cardinality: "many",
13290
13521
  optionality: "optional",
13291
13522
  includeStrategy: "auto",
@@ -14188,6 +14419,7 @@ import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNql
14188
14419
 
14189
14420
  // src/adapter-compiler-includes.ts
14190
14421
  init_ast_helpers();
14422
+ import { toColumnList as toColumnList9 } from "@dbsp/types";
14191
14423
  init_handlers();
14192
14424
  function compileSubqueryInclude(info, parentIds, _options, deps) {
14193
14425
  const schemaName = deps.schemaName;
@@ -14199,7 +14431,12 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14199
14431
  parameters: []
14200
14432
  };
14201
14433
  }
14202
- 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
+ }
14203
14440
  if (info.through && info.throughSourceKey && info.throughTargetKey) {
14204
14441
  return compileSubqueryIncludeManyToMany(
14205
14442
  info,
@@ -14247,6 +14484,11 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14247
14484
  };
14248
14485
  } else {
14249
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
+ }
14250
14492
  const idValues = id;
14251
14493
  const colConditions = fkColumns.map((col, idx) => {
14252
14494
  state.parameters.push(idValues[idx]);
@@ -14287,7 +14529,13 @@ function compileSubqueryIncludeManyToMany(info, parentIds, schemaName, state, de
14287
14529
  const throughTable = info.through;
14288
14530
  const throughSourceKey = info.throughSourceKey;
14289
14531
  const throughTargetKey = info.throughTargetKey;
14290
- 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];
14291
14539
  const paramRefs = parentIds.map((id) => {
14292
14540
  state.parameters.push(id);
14293
14541
  state.paramIndex++;
@@ -14376,6 +14624,7 @@ import {
14376
14624
  POSTGRESQL_CAPABILITIES,
14377
14625
  plan as planFn
14378
14626
  } from "@dbsp/core";
14627
+ import { toColumnList as toColumnList12 } from "@dbsp/types";
14379
14628
 
14380
14629
  // src/adapter-compiler-select.ts
14381
14630
  init_assert_field();
@@ -14383,6 +14632,7 @@ init_ast_helpers();
14383
14632
  init_binding_registry();
14384
14633
  init_compile_where();
14385
14634
  import { countDistinctRelationPathsByName } from "@dbsp/core";
14635
+ import { toColumnList as toColumnList11 } from "@dbsp/types";
14386
14636
  init_compiler_utils();
14387
14637
  init_types();
14388
14638
  init_intent_to_decisions();
@@ -14392,6 +14642,7 @@ init_param_ref();
14392
14642
  init_assert_field();
14393
14643
  init_intent_to_decisions();
14394
14644
  import { deriveRelationPathFromIntentPath } from "@dbsp/core";
14645
+ import { toColumnList as toColumnList10 } from "@dbsp/types";
14395
14646
  function findExistsIntents(where) {
14396
14647
  if (!where || typeof where !== "object") return [];
14397
14648
  const w = where;
@@ -14412,7 +14663,8 @@ function findExistsIntents(where) {
14412
14663
  function resolveRelation(model, sourceTable, relationName) {
14413
14664
  const rel = model.getRelation(`${sourceTable}.${relationName}`);
14414
14665
  if (!rel) return void 0;
14415
- 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;
14416
14668
  const relationType = rel.type;
14417
14669
  return { target: rel.target, foreignKey, relationType };
14418
14670
  }
@@ -14635,7 +14887,8 @@ function buildMultiHopExistsChain(hops, rootTable, outerOperator, innerCondition
14635
14887
  const hopRelations = [];
14636
14888
  for (const hop of hops) {
14637
14889
  const rel = model.getRelation(`${currentSource}.${hop}`);
14638
- 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;
14639
14892
  const relationType = rel?.type === "belongsTo" ? "belongsTo" : rel?.type === "hasMany" || rel?.type === "hasOne" ? rel.type : void 0;
14640
14893
  const parentKey = relationType === "belongsTo" ? rel?.targetKey : rel?.sourceKey;
14641
14894
  const target = rel ? rel.target : hop;
@@ -14745,7 +14998,8 @@ function enrichExistsStubsInConditions(conditions, sourceTable, model) {
14745
14998
  `exists('${relation}'): no relation '${relation}' is declared on table '${sourceTable}'. Use rawExists(subquery(...)) for an EXISTS over an undeclared or uncorrelated subquery.`
14746
14999
  );
14747
15000
  }
14748
- 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;
14749
15003
  const relationType = rel.type === "belongsTo" ? "belongsTo" : rel.type === "hasMany" || rel.type === "hasOne" ? rel.type : void 0;
14750
15004
  const parentKey = relationType === "belongsTo" ? rel.targetKey : rel.sourceKey;
14751
15005
  const targetTable = rel.target;
@@ -14804,7 +15058,8 @@ function buildEnrichedExistsDecision(d, matchingIntent, rootTable, model) {
14804
15058
  const relIR = model && context.relation ? model.getRelation(
14805
15059
  `${sourceTableForRelation}.${context.relation}`
14806
15060
  ) : void 0;
14807
- 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;
14808
15063
  const relationType = relIR?.type === "belongsTo" ? "belongsTo" : relIR?.type === "hasMany" || relIR?.type === "hasOne" ? relIR.type : void 0;
14809
15064
  const parentKey = relationType === "belongsTo" ? relIR?.targetKey : relIR?.sourceKey;
14810
15065
  let operator = "exists";
@@ -15117,6 +15372,7 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15117
15372
  const relationName = resolveIncludeAlias(context);
15118
15373
  if (!context.target || !relationName) return void 0;
15119
15374
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15375
+ const parentKey = context.parentKey ?? defaultPk;
15120
15376
  const relationType = context.relationType;
15121
15377
  const effectiveChoice = choice === "subquery" ? "json_agg" : choice;
15122
15378
  const includeIntent = resolveIncludeByPath(
@@ -15137,8 +15393,13 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15137
15393
  targetTable: context.target,
15138
15394
  ...context.sourceTable && { sourceTable: context.sourceTable },
15139
15395
  ...relationType && { relationType },
15140
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15141
- 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 } : {},
15142
15403
  ...context.intentPath && { intentPath: context.intentPath },
15143
15404
  ...limit != null && { limit }
15144
15405
  };
@@ -15164,6 +15425,7 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15164
15425
  columns = [defaultPk, ...fields];
15165
15426
  }
15166
15427
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15428
+ const parentKey = context.parentKey ?? defaultPk;
15167
15429
  const relationType = context.relationType;
15168
15430
  let conditions;
15169
15431
  if (includeIntent?.where) {
@@ -15184,8 +15446,8 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15184
15446
  targetTable: context.target,
15185
15447
  ...context.sourceTable && { sourceTable: context.sourceTable },
15186
15448
  ...relationType && { relationType },
15187
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15188
- parentKey: defaultPk,
15449
+ foreignKey,
15450
+ parentKey,
15189
15451
  columns,
15190
15452
  ...joinType && { joinType },
15191
15453
  ...conditions && { conditions }
@@ -15208,10 +15470,13 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15208
15470
  (r) => r.name === alias || snakeToCamel(r.name) === alias
15209
15471
  );
15210
15472
  if (!rel) continue;
15211
- const rawFk = rel.foreignKey ? Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : rel.foreignKey : deriveFk(
15212
- rel.type === "belongsTo" ? rel.target : sourceTable,
15213
- defaultPk
15214
- );
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
+ ];
15215
15480
  let columns = [defaultPk];
15216
15481
  if (inc.select?.type === "fields" && inc.select.fields) {
15217
15482
  const extraFields = inc.select.fields.filter((f) => f !== defaultPk);
@@ -15232,8 +15497,8 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15232
15497
  ...rel.type && {
15233
15498
  relationType: rel.type
15234
15499
  },
15235
- foreignKey: Array.isArray(rawFk) ? rawFk[0] : rawFk,
15236
- 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],
15237
15502
  columns,
15238
15503
  joinType: inc.join,
15239
15504
  ...conditions && { conditions }
@@ -15390,9 +15655,12 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
15390
15655
  );
15391
15656
  }
15392
15657
  const isBelongsTo = rel.type === "belongsTo";
15393
- const rawFk = rel.foreignKey ? Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : rel.foreignKey : deriveFk(isBelongsTo ? rootTable : rel.target, defaultPk);
15394
- const sourceColumn = isBelongsTo ? rawFk : defaultPk;
15395
- 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;
15396
15664
  const alias = intent.alias ?? intent.relation;
15397
15665
  results.push({
15398
15666
  type: "join",
@@ -15755,10 +16023,11 @@ function compileWithIncludes(plan, options, deps) {
15755
16023
  const relationName = ctx.includeAlias ?? ctx.relation;
15756
16024
  if (!relationName) continue;
15757
16025
  const rawFk = deriveForeignKey(ctx, deps.deriveFk, deps.defaultPk) ?? deps.defaultPk;
15758
- const fk = Array.isArray(rawFk) ? rawFk[0] : rawFk;
16026
+ const fk = toColumnList11(rawFk);
16027
+ const parentKey = toColumnList11(ctx.parentKey);
15759
16028
  const isBelongsTo = ctx.relationType === "belongsTo";
15760
- const sourceKey = isBelongsTo ? fk : "id";
15761
- 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;
15762
16031
  const includeIntent = plan.intent?.include?.find(
15763
16032
  (i) => i.relation === relationName || i.relation === ctx.includeAlias
15764
16033
  );
@@ -15821,14 +16090,14 @@ function prependSourceCte(sql, parameters, sourceName, sourceCte, naming) {
15821
16090
  };
15822
16091
  }
15823
16092
  function resolveMutationExistsForeignKey(foreignKey, relationName) {
15824
- if (typeof foreignKey === "string") return foreignKey;
15825
- if (!Array.isArray(foreignKey)) return void 0;
15826
- 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)) {
15827
16096
  throw new Error(
15828
- `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.`
15829
16098
  );
15830
16099
  }
15831
- return foreignKey[0];
16100
+ return columns;
15832
16101
  }
15833
16102
  function resolveExistsRelation(sourceTable, relation, model) {
15834
16103
  if (!model) return { targetTable: relation };
@@ -15841,13 +16110,13 @@ function resolveExistsRelation(sourceTable, relation, model) {
15841
16110
  return {
15842
16111
  targetTable,
15843
16112
  ...fk2 !== void 0 && { sourceColumn: fk2 },
15844
- targetColumn: "id"
16113
+ targetColumn: toColumnList12(rel.targetKey).length > 0 ? toColumnList12(rel.targetKey) : ["id"]
15845
16114
  };
15846
16115
  }
15847
16116
  const fk = resolveMutationExistsForeignKey(rel.foreignKey, relationName);
15848
16117
  return {
15849
16118
  targetTable,
15850
- sourceColumn: rel.sourceKey ?? "id",
16119
+ sourceColumn: toColumnList12(rel.sourceKey).length > 0 ? toColumnList12(rel.sourceKey) : ["id"],
15851
16120
  ...fk !== void 0 && { targetColumn: fk }
15852
16121
  };
15853
16122
  }
@@ -18086,7 +18355,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18086
18355
  bundle.query.from,
18087
18356
  deps.naming
18088
18357
  );
18089
- 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), {
18090
18359
  dialectCapabilities: options?.dialectCapabilities ?? this.dialectCapabilities
18091
18360
  });
18092
18361
  return guardCompiledQuery(
@@ -18180,6 +18449,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18180
18449
  }
18181
18450
  const leafBundle = {
18182
18451
  ...bundle.query !== void 0 && { query: bundle.query },
18452
+ ...bundle.plan !== void 0 && { plan: bundle.plan },
18183
18453
  ...bundle.cteQuery !== void 0 && { cteQuery: bundle.cteQuery },
18184
18454
  ...bundle.mutation !== void 0 && { mutation: bundle.mutation },
18185
18455
  ...bundle.returning !== void 0 && { returning: bundle.returning },