@dbsp/adapter-pgsql 1.5.0 → 1.7.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
  }
@@ -669,9 +663,6 @@ function funcCall(name, args = [], options = {}) {
669
663
  function coalesceExpr(args) {
670
664
  return { CoalesceExpr: { args } };
671
665
  }
672
- function coalesce(...args) {
673
- return funcCall("coalesce", args);
674
- }
675
666
  function sortBy(expr, direction = "DEFAULT", nulls = "DEFAULT") {
676
667
  const sb = {
677
668
  node: expr,
@@ -885,10 +876,23 @@ function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = ide
885
876
  }
886
877
  };
887
878
  }
879
+ const aggOrder = options?.orderBy && options.orderBy.length > 0 ? options.orderBy.map(
880
+ (col) => sortBy(
881
+ jsonAggOrderByExpression(
882
+ col,
883
+ targetAlias,
884
+ naming,
885
+ options.orderByFallback === true
886
+ ),
887
+ "ASC",
888
+ "LAST"
889
+ )
890
+ ) : void 0;
888
891
  const jsonAggCall = {
889
892
  FuncCall: {
890
893
  funcname: [stringNode("json_agg")],
891
- args: [toJsonbCall]
894
+ args: [toJsonbCall],
895
+ ...aggOrder && { agg_order: aggOrder }
892
896
  }
893
897
  };
894
898
  const fromTable = rangeVar(targetTable, targetAlias, schemaName, naming);
@@ -923,11 +927,9 @@ function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = ide
923
927
  }
924
928
  };
925
929
  }
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
- );
930
+ function jsonAggOrderByExpression(entry, targetAlias, naming, castToText) {
931
+ const ref = columnRef(entry, targetAlias, void 0, naming);
932
+ return castToText ? typeCast(ref, "text") : ref;
931
933
  }
932
934
  var STRENGTH_MAP, POLICY_MAP;
933
935
  var init_ast_helpers = __esm({
@@ -3172,6 +3174,7 @@ var init_custom_expression = __esm({
3172
3174
  });
3173
3175
 
3174
3176
  // src/handlers/where/exists.ts
3177
+ import { toColumnList } from "@dbsp/types";
3175
3178
  function createSubLinkExists(subquery, negated) {
3176
3179
  const subLink = {
3177
3180
  subLinkType: "EXISTS_SUBLINK",
@@ -3188,10 +3191,27 @@ function createSubLinkExists(subquery, negated) {
3188
3191
  }
3189
3192
  return existsNode;
3190
3193
  }
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);
3194
+ function buildKeyCorrelation(sourceAlias, sourceCols, targetAlias, targetCols, ctx) {
3195
+ const normalizedSourceCols = toColumnList(sourceCols);
3196
+ const normalizedTargetCols = toColumnList(targetCols);
3197
+ if (normalizedSourceCols.length === 0 || normalizedTargetCols.length === 0 || normalizedSourceCols.length !== normalizedTargetCols.length || normalizedSourceCols.some((column) => column.length === 0) || normalizedTargetCols.some((column) => column.length === 0)) {
3198
+ throw new Error(
3199
+ `Invalid relation correlation: source columns (${normalizedSourceCols.length}) must match target columns (${normalizedTargetCols.length}) and both must be non-empty.`
3200
+ );
3201
+ }
3202
+ const comparisons = normalizedSourceCols.map(
3203
+ (sourceColumn, index) => eqExpr(
3204
+ columnRef(sourceColumn, sourceAlias, void 0, ctx.naming),
3205
+ columnRef(
3206
+ normalizedTargetCols[index],
3207
+ targetAlias,
3208
+ void 0,
3209
+ ctx.naming
3210
+ )
3211
+ )
3212
+ );
3213
+ if (comparisons.length === 1) return comparisons[0];
3214
+ return andExpr(...comparisons);
3195
3215
  }
3196
3216
  function schemaForExistsFromName(ctx, fromName) {
3197
3217
  return schemaForFromName(ctx.schema, fromName, ctx.bindingNames, ctx.naming);
@@ -3199,11 +3219,15 @@ function schemaForExistsFromName(ctx, fromName) {
3199
3219
  function buildExistsSubquery(decision, ctx, state, dispatch) {
3200
3220
  const relation = decision.relation;
3201
3221
  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,
3222
+ const sourceColumn = decision.sourceColumn ?? [
3205
3223
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3206
- );
3224
+ ];
3225
+ const targetColumn = decision.targetColumn ?? [
3226
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3227
+ ctx.rootTable,
3228
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3229
+ )
3230
+ ];
3207
3231
  if (!targetTable) {
3208
3232
  throw new Error("EXISTS handler requires targetTable or relation");
3209
3233
  }
@@ -3211,7 +3235,7 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3211
3235
  const targetAlias = `${targetTable}_exists_${existingAliases}`;
3212
3236
  state.aliases.set(`exists_${targetTable}`, targetAlias);
3213
3237
  const sourceAlias = ctx.currentAlias ?? ctx.rootTable;
3214
- const correlation = buildCorrelation(
3238
+ const correlation = buildKeyCorrelation(
3215
3239
  sourceAlias,
3216
3240
  sourceColumn,
3217
3241
  targetAlias,
@@ -3249,8 +3273,8 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3249
3273
  const joinRelation = inc.relation;
3250
3274
  if (!joinRelation) continue;
3251
3275
  let joinTargetTable = joinRelation;
3252
- let joinSourceCol;
3253
- let joinTargetCol;
3276
+ let joinSourceCols;
3277
+ let joinTargetCols;
3254
3278
  let sourceAliasForJoin = targetAlias;
3255
3279
  const model = ctx.model;
3256
3280
  if (model) {
@@ -3268,31 +3292,41 @@ function buildExistsSubquery(decision, ctx, state, dispatch) {
3268
3292
  if (rel) {
3269
3293
  joinTargetTable = rel.target;
3270
3294
  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;
3295
+ const fk = toColumnList(rel.foreignKey);
3296
+ joinSourceCols = fk.length > 0 ? fk : void 0;
3297
+ const targetKey = toColumnList(rel.targetKey);
3298
+ joinTargetCols = targetKey.length > 0 ? targetKey : [
3299
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3300
+ ];
3274
3301
  } 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;
3302
+ const fk = toColumnList(rel.foreignKey);
3303
+ const sourceKey = toColumnList(rel.sourceKey);
3304
+ joinSourceCols = sourceKey.length > 0 ? sourceKey : [
3305
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3306
+ ];
3307
+ joinTargetCols = fk.length > 0 ? fk : void 0;
3278
3308
  }
3279
3309
  }
3280
3310
  }
3281
- if (!joinSourceCol) {
3282
- joinSourceCol = (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3283
- joinRelation,
3311
+ if (!joinSourceCols || joinSourceCols.length === 0) {
3312
+ joinSourceCols = [
3313
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
3314
+ joinRelation,
3315
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3316
+ )
3317
+ ];
3318
+ joinTargetCols = [
3284
3319
  ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
3285
- );
3286
- joinTargetCol = ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN;
3320
+ ];
3287
3321
  }
3288
3322
  const joinAlias = joinRelation;
3289
- const joinQuals = fkCorrelation(
3290
- joinSourceCol,
3323
+ const joinQuals = buildKeyCorrelation(
3291
3324
  sourceAliasForJoin,
3292
3325
  // resolved source alias (root or intermediate)
3293
- joinTargetCol ?? DEFAULT_PK_COLUMN,
3326
+ joinSourceCols,
3294
3327
  joinAlias,
3295
- ctx.naming
3328
+ joinTargetCols,
3329
+ ctx
3296
3330
  );
3297
3331
  const joinType = inc.joinType === "left" ? "JOIN_LEFT" : "JOIN_INNER";
3298
3332
  const joinRangeVar = rangeVar(
@@ -3774,6 +3808,7 @@ var init_range = __esm({
3774
3808
  });
3775
3809
 
3776
3810
  // src/compile-where.ts
3811
+ import { toColumnList as toColumnList2 } from "@dbsp/types";
3777
3812
  import { getTrustedNqlRelationFilterFields as getTrustedNqlRelationFilterFields3 } from "@dbsp/types/internal";
3778
3813
  function toHandlerContext(ctx) {
3779
3814
  return {
@@ -4039,14 +4074,16 @@ function handleRelationFilterIntent(intent, ctx) {
4039
4074
  let singleHopTargetColumn;
4040
4075
  if (resolvedRelation) {
4041
4076
  const rel = resolvedRelation;
4042
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4077
+ const fk = toColumnList2(rel.foreignKey);
4043
4078
  const defaultPk = DEFAULT_PK_COLUMN;
4044
4079
  if (rel.type === "belongsTo") {
4045
- singleHopSourceColumn = fk ?? defaultFkDerivation(rel.target, defaultPk);
4046
- singleHopTargetColumn = rel.targetKey ?? defaultPk;
4080
+ singleHopSourceColumn = fk.length > 0 ? fk : [defaultFkDerivation(rel.target, defaultPk)];
4081
+ const targetKey = toColumnList2(rel.targetKey);
4082
+ singleHopTargetColumn = targetKey.length > 0 ? targetKey : [defaultPk];
4047
4083
  } else {
4048
- singleHopSourceColumn = rel.sourceKey ?? defaultPk;
4049
- singleHopTargetColumn = fk ?? defaultFkDerivation(ctx.rootTable, defaultPk);
4084
+ const sourceKey = toColumnList2(rel.sourceKey);
4085
+ singleHopSourceColumn = sourceKey.length > 0 ? sourceKey : [defaultPk];
4086
+ singleHopTargetColumn = fk.length > 0 ? fk : [defaultFkDerivation(ctx.rootTable, defaultPk)];
4050
4087
  }
4051
4088
  }
4052
4089
  return compileWhereIntent(
@@ -4083,15 +4120,19 @@ function handleRelationFilterIntent(intent, ctx) {
4083
4120
  );
4084
4121
  }
4085
4122
  hopTargets.push(rel.target);
4086
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : Array.isArray(rel.foreignKey) ? rel.foreignKey[0] : void 0;
4123
+ const fk = toColumnList2(rel.foreignKey);
4087
4124
  const defaultPk = DEFAULT_PK_COLUMN;
4088
4125
  if (rel.type === "belongsTo") {
4089
- hopSourceColumns.push(fk ?? defaultFkDerivation(rel.target, defaultPk));
4090
- hopTargetColumns.push(rel.targetKey ?? defaultPk);
4126
+ hopSourceColumns.push(
4127
+ fk.length > 0 ? fk : [defaultFkDerivation(rel.target, defaultPk)]
4128
+ );
4129
+ const targetKey = toColumnList2(rel.targetKey);
4130
+ hopTargetColumns.push(targetKey.length > 0 ? targetKey : [defaultPk]);
4091
4131
  } else {
4092
- hopSourceColumns.push(rel.sourceKey ?? defaultPk);
4132
+ const sourceKey = toColumnList2(rel.sourceKey);
4133
+ hopSourceColumns.push(sourceKey.length > 0 ? sourceKey : [defaultPk]);
4093
4134
  hopTargetColumns.push(
4094
- fk ?? defaultFkDerivation(currentSource, defaultPk)
4135
+ fk.length > 0 ? fk : [defaultFkDerivation(currentSource, defaultPk)]
4095
4136
  );
4096
4137
  }
4097
4138
  currentSource = rel.target;
@@ -4319,6 +4360,7 @@ var init_raw_exists = __esm({
4319
4360
  });
4320
4361
 
4321
4362
  // src/handlers/where/relation-filter.ts
4363
+ import { toColumnList as toColumnList3 } from "@dbsp/types";
4322
4364
  function getFilterMode(decision) {
4323
4365
  const operator = decision.operator;
4324
4366
  if (operator === "some" || operator === "exists") return "some";
@@ -4330,15 +4372,18 @@ function getFilterMode(decision) {
4330
4372
  function buildJoinFilter(decision, ctx, state, dispatch) {
4331
4373
  const relation = decision.relation;
4332
4374
  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
- );
4375
+ const sourceColumn = toColumnList3(decision.sourceColumn);
4376
+ if (sourceColumn.length === 0) {
4377
+ throw new Error(
4378
+ "Missing required column 'sourceColumn' in relation filter"
4379
+ );
4380
+ }
4381
+ const targetColumn = decision.targetColumn ?? [
4382
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4383
+ ctx.rootTable,
4384
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4385
+ )
4386
+ ];
4342
4387
  if (!targetTable) {
4343
4388
  throw new Error("Relation filter requires targetTable");
4344
4389
  }
@@ -4346,9 +4391,12 @@ function buildJoinFilter(decision, ctx, state, dispatch) {
4346
4391
  const targetAlias = `${targetTable}_rel_${existingAliases}`;
4347
4392
  state.aliases.set(`rel_${targetTable}`, targetAlias);
4348
4393
  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)
4394
+ const joinCondition = buildKeyCorrelation(
4395
+ sourceAlias,
4396
+ sourceColumn,
4397
+ targetAlias,
4398
+ targetColumn,
4399
+ ctx
4352
4400
  );
4353
4401
  const joinExpr2 = {
4354
4402
  jointype: "JOIN_INNER",
@@ -4383,6 +4431,7 @@ var init_relation_filter = __esm({
4383
4431
  "use strict";
4384
4432
  init_assert_field();
4385
4433
  init_ast_helpers();
4434
+ init_exists();
4386
4435
  relationFilterHandler = {
4387
4436
  operators: ["relationFilter", "relation", "is", "isNot"],
4388
4437
  compile(decision, ctx, state, dispatch) {
@@ -4434,6 +4483,11 @@ var init_relation_filter = __esm({
4434
4483
 
4435
4484
  // src/subquery-emission.ts
4436
4485
  import { isParamIntent as isParamIntent5 } from "@dbsp/types";
4486
+ function isColumnOrderBy(orderBy) {
4487
+ return Array.isArray(orderBy) && orderBy.every(
4488
+ (item) => typeof item === "object" && item !== null && "column" in item && typeof item.column === "string"
4489
+ );
4490
+ }
4437
4491
  function assertNoDroppedDecisionModifiers(decision, use) {
4438
4492
  const d = decision;
4439
4493
  const unsupported = [];
@@ -4552,8 +4606,9 @@ function buildPredicateSubquerySelect(use, sourceIntent, decision, ctx, state, d
4552
4606
  ],
4553
4607
  ...whereClause && { whereClause }
4554
4608
  };
4555
- if (decision.orderBy && decision.orderBy.length > 0) {
4556
- stmt.sortClause = decision.orderBy.map(
4609
+ const orderBy = isColumnOrderBy(decision.orderBy) ? decision.orderBy : void 0;
4610
+ if (orderBy && orderBy.length > 0) {
4611
+ stmt.sortClause = orderBy.map(
4557
4612
  (o) => sortBy(
4558
4613
  columnRef(o.column, targetAlias, void 0, ctx.naming),
4559
4614
  o.direction ?? "ASC",
@@ -4812,6 +4867,7 @@ var init_where = __esm({
4812
4867
  });
4813
4868
 
4814
4869
  // src/handlers/include/cte.ts
4870
+ import { toColumnList as toColumnList4 } from "@dbsp/types";
4815
4871
  function buildCteTargets(columns, alias, ctx) {
4816
4872
  if (columns && columns.length > 0 && !columns.every((c) => c === "*")) {
4817
4873
  return columns.filter((col) => col !== "*").map((col) => ({
@@ -4862,12 +4918,12 @@ function buildCTE(cteName, cteSelect, ctx) {
4862
4918
  return { CommonTableExpr: cte };
4863
4919
  }
4864
4920
  function buildCteJoin(cteName, cteAlias, sourceAlias, sourceColumn, targetColumn, ctx) {
4865
- const joinCondition = fkCorrelation(
4866
- sourceColumn,
4921
+ const joinCondition = buildKeyCorrelation(
4867
4922
  sourceAlias,
4868
- targetColumn,
4923
+ sourceColumn,
4869
4924
  cteAlias,
4870
- ctx.naming
4925
+ targetColumn,
4926
+ ctx
4871
4927
  );
4872
4928
  const cteRef = {
4873
4929
  RangeVar: {
@@ -4891,20 +4947,22 @@ var init_cte = __esm({
4891
4947
  init_assert_field();
4892
4948
  init_ast_helpers();
4893
4949
  init_handlers();
4950
+ init_exists();
4894
4951
  cteIncludeHandler = {
4895
4952
  strategy: "cte",
4896
4953
  compile(decision, ctx, state) {
4897
4954
  const relation = decision.relation;
4898
4955
  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
- );
4956
+ const sourceColumn = toColumnList4(decision.sourceColumn);
4957
+ if (sourceColumn.length === 0 || sourceColumn.some((col) => col === "")) {
4958
+ throw new Error("Missing required column 'sourceColumn' in CTE include");
4959
+ }
4960
+ const targetColumn = decision.targetColumn ?? [
4961
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
4962
+ ctx.rootTable,
4963
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
4964
+ )
4965
+ ];
4908
4966
  const columns = decision.columns;
4909
4967
  const conditions = decision.conditions;
4910
4968
  if (!targetTable) {
@@ -4947,13 +5005,14 @@ var init_cte = __esm({
4947
5005
  });
4948
5006
 
4949
5007
  // src/handlers/include/join.ts
5008
+ import { toColumnList as toColumnList5 } from "@dbsp/types";
4950
5009
  function buildJoin(targetTable, targetAlias, sourceAlias, sourceColumn, targetColumn, ctx, joinType = "left") {
4951
- const joinCondition = fkCorrelation(
4952
- sourceColumn,
5010
+ const joinCondition = buildKeyCorrelation(
4953
5011
  sourceAlias,
4954
- targetColumn,
5012
+ sourceColumn,
4955
5013
  targetAlias,
4956
- ctx.naming
5014
+ targetColumn,
5015
+ ctx
4957
5016
  );
4958
5017
  const joinExpr2 = {
4959
5018
  jointype: joinType === "inner" ? "JOIN_INNER" : "JOIN_LEFT",
@@ -4968,6 +5027,7 @@ var init_join = __esm({
4968
5027
  "use strict";
4969
5028
  init_assert_field();
4970
5029
  init_ast_helpers();
5030
+ init_exists();
4971
5031
  joinIncludeHandler = {
4972
5032
  strategy: "join",
4973
5033
  compile(decision, ctx, _state) {
@@ -4978,15 +5038,16 @@ var init_join = __esm({
4978
5038
  }
4979
5039
  const targetAlias = relation ?? targetTable;
4980
5040
  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
- );
5041
+ const sourceColumn = toColumnList5(decision.sourceColumn);
5042
+ if (sourceColumn.length === 0) {
5043
+ throw new Error("Missing required column 'sourceColumn' in JOIN include");
5044
+ }
5045
+ const targetColumn = decision.targetColumn ?? [
5046
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5047
+ ctx.rootTable,
5048
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5049
+ )
5050
+ ];
4990
5051
  const join = buildJoin(
4991
5052
  targetTable,
4992
5053
  targetAlias,
@@ -5040,6 +5101,22 @@ var init_shared = __esm({
5040
5101
  });
5041
5102
 
5042
5103
  // src/handlers/include/json-agg.ts
5104
+ import { resolveJsonAggOrderKey } from "@dbsp/types";
5105
+ function isJsonAggOrderBy(orderBy) {
5106
+ return Array.isArray(orderBy) && orderBy.every((item) => typeof item === "string");
5107
+ }
5108
+ function resolveJsonAggOrderBy(decision, targetTable, ctx) {
5109
+ const table = ctx.model?.getTable(targetTable);
5110
+ if (table) {
5111
+ const orderKey = resolveJsonAggOrderKey(table);
5112
+ return orderKey.columns.length > 0 ? orderKey : void 0;
5113
+ }
5114
+ const decisionOrderBy = isJsonAggOrderBy(decision.orderBy) ? decision.orderBy : void 0;
5115
+ return decisionOrderBy && decisionOrderBy.length > 0 ? {
5116
+ columns: decisionOrderBy,
5117
+ fallback: decision.orderByFallback === true
5118
+ } : void 0;
5119
+ }
5043
5120
  function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5044
5121
  const innerAlias = depth === 0 ? "__t__" : `__t${depth}__`;
5045
5122
  const relation = decision.relation ?? decision.relationName;
@@ -5056,12 +5133,12 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5056
5133
  ctx.defaultPkColumnName,
5057
5134
  ctx.deriveFkColumnName
5058
5135
  );
5059
- let whereExpr = jsonAggCorrelation(
5060
- parentAlias,
5061
- sourceColumn,
5136
+ let whereExpr = buildKeyCorrelation(
5062
5137
  innerAlias,
5063
5138
  targetColumn,
5064
- ctx.naming
5139
+ parentAlias,
5140
+ sourceColumn,
5141
+ ctx
5065
5142
  );
5066
5143
  const compiledFilter = decision._compiledFilterWhere;
5067
5144
  if (compiledFilter) {
@@ -5092,6 +5169,7 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5092
5169
  if (childNodes.length === 0) childNodes = void 0;
5093
5170
  }
5094
5171
  const limit = typeof decision.limit === "number" ? decision.limit : void 0;
5172
+ const orderBy = resolveJsonAggOrderBy(decision, targetTable, ctx);
5095
5173
  return jsonAggSubquery(
5096
5174
  targetTable,
5097
5175
  whereExpr,
@@ -5102,7 +5180,9 @@ function compileJsonAggRecursive(decision, parentAlias, depth, ctx, _state) {
5102
5180
  ...childNodes && { childNodes },
5103
5181
  innerAlias,
5104
5182
  ...limit !== void 0 && { limit },
5105
- ...decision.columns && { columns: decision.columns }
5183
+ ...decision.columns && { columns: decision.columns },
5184
+ ...orderBy && { orderBy: orderBy.columns },
5185
+ ...orderBy?.fallback && { orderByFallback: true }
5106
5186
  }
5107
5187
  );
5108
5188
  }
@@ -5111,6 +5191,7 @@ var init_json_agg = __esm({
5111
5191
  "src/handlers/include/json-agg.ts"() {
5112
5192
  "use strict";
5113
5193
  init_ast_helpers();
5194
+ init_exists();
5114
5195
  init_shared();
5115
5196
  jsonAggIncludeHandler = {
5116
5197
  strategy: "json_agg",
@@ -5132,6 +5213,7 @@ var init_json_agg = __esm({
5132
5213
  });
5133
5214
 
5134
5215
  // src/handlers/include/lateral.ts
5216
+ import { toColumnList as toColumnList6 } from "@dbsp/types";
5135
5217
  function buildLateralTargets(columns, alias, ctx) {
5136
5218
  if (columns && columns.length > 0 && !(columns.length === 1 && columns[0] === "*")) {
5137
5219
  return columns.map((col) => ({
@@ -5143,12 +5225,12 @@ function buildLateralTargets(columns, alias, ctx) {
5143
5225
  return [starTarget(alias, ctx.naming)];
5144
5226
  }
5145
5227
  function buildLateralSubquery(targetTable, innerAlias, outerAlias, sourceColumn, targetColumn, columns, limit, ctx) {
5146
- const whereClause = fkCorrelation(
5147
- targetColumn,
5228
+ const whereClause = buildKeyCorrelation(
5148
5229
  innerAlias,
5149
- sourceColumn,
5230
+ targetColumn,
5150
5231
  outerAlias,
5151
- ctx.naming
5232
+ sourceColumn,
5233
+ ctx
5152
5234
  );
5153
5235
  const targetList = buildLateralTargets(columns, innerAlias, ctx);
5154
5236
  const stmt = {
@@ -5228,19 +5310,23 @@ var init_lateral = __esm({
5228
5310
  "use strict";
5229
5311
  init_assert_field();
5230
5312
  init_ast_helpers();
5313
+ init_exists();
5231
5314
  init_shared();
5232
5315
  lateralIncludeHandler = {
5233
5316
  strategy: "lateral",
5234
5317
  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
- );
5318
+ const sourceColumn = toColumnList6(decision.sourceColumn);
5319
+ if (sourceColumn.length === 0) {
5320
+ throw new Error(
5321
+ "Missing required column 'sourceColumn' in lateral include"
5322
+ );
5323
+ }
5324
+ const targetColumn = decision.targetColumn ?? [
5325
+ (ctx.deriveFkColumnName ?? defaultFkDerivation)(
5326
+ ctx.rootTable,
5327
+ ctx.defaultPkColumnName ?? DEFAULT_PK_COLUMN
5328
+ )
5329
+ ];
5244
5330
  const outerAlias = ctx.currentAlias ?? ctx.rootTable;
5245
5331
  const { joins, targets } = compileLateralCascade(
5246
5332
  decision,
@@ -6184,6 +6270,7 @@ function buildRecursiveScalarSubquery(config) {
6184
6270
  pkColumn,
6185
6271
  fkColumn,
6186
6272
  outerAlias,
6273
+ outerSeedColumn,
6187
6274
  isAncestors,
6188
6275
  maxDepth,
6189
6276
  selectColumn,
@@ -6194,6 +6281,9 @@ function buildRecursiveScalarSubquery(config) {
6194
6281
  const dbPk = naming.toDatabase(pkColumn);
6195
6282
  const dbFk = naming.toDatabase(fkColumn);
6196
6283
  const dbOuter = naming.toDatabase(outerAlias);
6284
+ const dbOuterSeed = naming.toDatabase(
6285
+ outerSeedColumn ?? (isAncestors ? fkColumn : pkColumn)
6286
+ );
6197
6287
  const innerAlias = "__n";
6198
6288
  const anchorSelect = {
6199
6289
  targetList: [
@@ -6262,7 +6352,7 @@ function buildRecursiveScalarSubquery(config) {
6262
6352
  ColumnRef: {
6263
6353
  fields: [
6264
6354
  { String: { sval: dbOuter } },
6265
- { String: { sval: dbFk } }
6355
+ { String: { sval: dbOuterSeed } }
6266
6356
  ]
6267
6357
  }
6268
6358
  }
@@ -6282,7 +6372,7 @@ function buildRecursiveScalarSubquery(config) {
6282
6372
  ColumnRef: {
6283
6373
  fields: [
6284
6374
  { String: { sval: dbOuter } },
6285
- { String: { sval: dbPk } }
6375
+ { String: { sval: dbOuterSeed } }
6286
6376
  ]
6287
6377
  }
6288
6378
  }
@@ -6346,15 +6436,7 @@ function buildRecursiveScalarSubquery(config) {
6346
6436
  }
6347
6437
  ],
6348
6438
  fromClause: [
6349
- // FROM __rc
6350
- {
6351
- RangeVar: {
6352
- relname: cteAlias,
6353
- inh: true,
6354
- relpersistence: "p"
6355
- }
6356
- },
6357
- // INNER JOIN table AS __n
6439
+ // FROM __rc INNER JOIN table AS __n
6358
6440
  {
6359
6441
  JoinExpr: {
6360
6442
  jointype: "JOIN_INNER",
@@ -6437,7 +6519,7 @@ function buildRecursiveScalarSubquery(config) {
6437
6519
  },
6438
6520
  integerNode(maxDepth)
6439
6521
  ),
6440
- // pk <> ALL(__visited) cycle detection
6522
+ // pk <> ALL(__visited) - cycle detection
6441
6523
  {
6442
6524
  A_Expr: {
6443
6525
  kind: "AEXPR_OP_ALL",
@@ -6482,20 +6564,35 @@ function buildRecursiveScalarSubquery(config) {
6482
6564
  targetList: [
6483
6565
  {
6484
6566
  ResTarget: {
6485
- val: coalesce(
6486
- funcCall("json_agg", [
6487
- {
6488
- ColumnRef: {
6489
- fields: [
6490
- { String: { sval: cteAlias } },
6491
- { String: { sval: dbSelectCol } }
6492
- ]
6567
+ val: coalesceExpr([
6568
+ funcCall(
6569
+ "json_agg",
6570
+ [
6571
+ {
6572
+ ColumnRef: {
6573
+ fields: [
6574
+ { String: { sval: cteAlias } },
6575
+ { String: { sval: dbSelectCol } }
6576
+ ]
6577
+ }
6493
6578
  }
6579
+ ],
6580
+ {
6581
+ orderBy: [
6582
+ sortBy({
6583
+ ColumnRef: {
6584
+ fields: [
6585
+ { String: { sval: cteAlias } },
6586
+ { String: { sval: "__depth" } }
6587
+ ]
6588
+ }
6589
+ })
6590
+ ]
6494
6591
  }
6495
- ]),
6592
+ ),
6496
6593
  // Empty array fallback: '[]'::json
6497
- typeCast(stringNode("[]"), "json")
6498
- )
6594
+ typeCast({ A_Const: { sval: { sval: "[]" } } }, "json")
6595
+ ])
6499
6596
  }
6500
6597
  }
6501
6598
  ],
@@ -6954,6 +7051,11 @@ var init_relation = __esm({
6954
7051
  });
6955
7052
 
6956
7053
  // src/handlers/expression/window.ts
7054
+ function isWindowOrderBy(orderBy) {
7055
+ return Array.isArray(orderBy) && orderBy.every(
7056
+ (item) => typeof item === "object" && item !== null && "column" in item && typeof item.column === "string"
7057
+ );
7058
+ }
6957
7059
  function buildSortBy(column, direction, ctx) {
6958
7060
  const tableAlias = ctx.currentAlias ?? ctx.rootTable;
6959
7061
  const colRef = columnRef(column, tableAlias, void 0, ctx.naming);
@@ -6966,7 +7068,7 @@ function buildSortBy(column, direction, ctx) {
6966
7068
  }
6967
7069
  function buildWindowDef(decision, ctx) {
6968
7070
  const partition = decision.partition;
6969
- const orderBy = decision.orderBy;
7071
+ const orderBy = isWindowOrderBy(decision.orderBy) ? decision.orderBy : void 0;
6970
7072
  const frame = decision.frame;
6971
7073
  const tableAlias = ctx.currentAlias ?? ctx.rootTable;
6972
7074
  const windowDef = { frameOptions: WINDOW_FRAME_DEFAULT };
@@ -7229,7 +7331,8 @@ import { InvalidOperationError as InvalidOperationError2 } from "@dbsp/core";
7229
7331
  import {
7230
7332
  isParamIntent as isParamIntent6,
7231
7333
  NQL_SELECT_SCALAR_FUNCTION_ALLOWLIST,
7232
- NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST
7334
+ NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST,
7335
+ toColumnList as toColumnList7
7233
7336
  } from "@dbsp/types";
7234
7337
  import {
7235
7338
  getNqlBindingRefName,
@@ -8295,10 +8398,12 @@ init_case_value();
8295
8398
  init_custom();
8296
8399
  init_expression();
8297
8400
  init_param_value();
8401
+ init_pseudo();
8298
8402
  init_window();
8299
8403
  init_include();
8300
8404
  init_shared();
8301
8405
  init_handlers();
8406
+ init_exists();
8302
8407
  init_types();
8303
8408
  init_utils();
8304
8409
  init_intent_to_decisions();
@@ -8336,7 +8441,25 @@ function assertNqlSelectWindowFunctionAllowed(functionName) {
8336
8441
  throw new UnsupportedNqlSelectFunctionError(functionName);
8337
8442
  }
8338
8443
  }
8444
+ function trustedRelationHasMultipleHops(relation) {
8445
+ if (typeof relation !== "string") return relation.length > 1;
8446
+ return relation.split(".").length > 1;
8447
+ }
8448
+ function isJsonAggOrderBy2(orderBy) {
8449
+ return Array.isArray(orderBy) && orderBy.every((item) => typeof item === "string");
8450
+ }
8451
+ function isExpressionOrderBy(orderBy) {
8452
+ return Array.isArray(orderBy) && orderBy.every(
8453
+ (item) => typeof item === "object" && item !== null && "field" in item && typeof item.field === "string"
8454
+ );
8455
+ }
8339
8456
  function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8457
+ const jsonAggOrderBy = isJsonAggOrderBy2(pd.orderBy) ? pd.orderBy : void 0;
8458
+ const expressionOrderBy = isExpressionOrderBy(pd.orderBy) ? pd.orderBy : void 0;
8459
+ const handlerOrderBy = jsonAggOrderBy ?? expressionOrderBy?.map((o) => ({
8460
+ column: o.field,
8461
+ direction: o.direction?.toUpperCase() ?? "ASC"
8462
+ }));
8340
8463
  const derivedFkColumns = deriveFkColumns(
8341
8464
  pd,
8342
8465
  pd.sourceTable ?? rootTable,
@@ -8371,6 +8494,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8371
8494
  relationType: pd.relationType,
8372
8495
  foreignKey: pd.foreignKey,
8373
8496
  parentKey: pd.parentKey,
8497
+ orderByFallback: pd.orderByFallback,
8374
8498
  dataType: pd.dataType,
8375
8499
  traversal: pd.traversal,
8376
8500
  pkColumn: pd.pkColumn,
@@ -8385,10 +8509,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8385
8509
  include: pd.include?.map(
8386
8510
  (c) => mapToHandlerDecision(c, rootTable, defaultPk, deriveFk)
8387
8511
  ),
8388
- orderBy: pd.orderBy?.map((o) => ({
8389
- column: o.field,
8390
- direction: o.direction?.toUpperCase() ?? "ASC"
8391
- })),
8512
+ orderBy: handlerOrderBy,
8392
8513
  partition: pd.partitionBy,
8393
8514
  jsonPath: pd.jsonPath,
8394
8515
  jsonMode: pd.jsonMode,
@@ -8767,6 +8888,13 @@ var PlanCompiler = class _PlanCompiler {
8767
8888
  this.defaultPk,
8768
8889
  this.deriveFk
8769
8890
  );
8891
+ if (handlerDecision.strategy === "json_agg") {
8892
+ assertDialectCapability(
8893
+ this.dialectCapabilities,
8894
+ "supportsJsonAgg",
8895
+ "JSON aggregation for relation includes"
8896
+ );
8897
+ }
8770
8898
  const handler = getIncludeHandler(
8771
8899
  handlerDecision.strategy
8772
8900
  );
@@ -8940,18 +9068,11 @@ var PlanCompiler = class _PlanCompiler {
8940
9068
  isNqlBindingRoot(plan) {
8941
9069
  return hasBindingName(this.bindingNames, plan.rootTable, this.naming);
8942
9070
  }
8943
- compileBindingRelationColumnSubquery(fields, plan) {
8944
- if (fields.selectedColumn === void 0) {
8945
- throw new Error(
8946
- `NQL binding relation-column proof for '${plan.rootTable}' is missing selectedColumn.`
8947
- );
8948
- }
8949
- if (fields.cardinality !== "one") {
8950
- throw new Error(
8951
- `NQL binding relation-column proof for '${plan.rootTable}' is not scalar (cardinality: ${fields.cardinality ?? "unknown"}).`
8952
- );
8953
- }
8954
- const relatedAlias = `rc_${this.bindingRelationColumnSubqueryIndex++}`;
9071
+ allocateBindingRelationAlias() {
9072
+ return `rc_${this.bindingRelationColumnSubqueryIndex++}`;
9073
+ }
9074
+ buildCorrelatedRelationRefs(fields, plan) {
9075
+ const relatedAlias = this.allocateBindingRelationAlias();
8955
9076
  const relatedTable = rangeVar(
8956
9077
  fields.targetTable,
8957
9078
  relatedAlias,
@@ -8964,28 +9085,255 @@ var PlanCompiler = class _PlanCompiler {
8964
9085
  void 0,
8965
9086
  this.naming
8966
9087
  );
8967
- const relatedJoinColumn = columnRef(
8968
- fields.targetColumn,
9088
+ return {
8969
9089
  relatedAlias,
8970
- void 0,
8971
- this.naming
8972
- );
8973
- const bindingJoinColumn = columnRef(
8974
- fields.sourceColumn,
8975
- plan.rootTable,
8976
- void 0,
8977
- this.naming
9090
+ relatedTable,
9091
+ relatedColumn
9092
+ };
9093
+ }
9094
+ bindingRelationHasCompleteManyToManyProof(fields) {
9095
+ return (fields.relationType === "manyToMany" || fields.relationType === "belongsToMany") && fields.through !== void 0 && fields.throughSourceColumn !== void 0 && fields.throughTargetColumn !== void 0;
9096
+ }
9097
+ bindingRelationName(fields) {
9098
+ return typeof fields.relation === "string" ? fields.relation : fields.relation.join(".");
9099
+ }
9100
+ bindingRelationNameLooksRecursive(fields) {
9101
+ const relationName = this.bindingRelationName(fields).toLowerCase();
9102
+ return relationName === "ascendant" || relationName === "descendant";
9103
+ }
9104
+ compileBindingRecursiveRelationColumnSubquery(fields, plan, dialectCapabilities) {
9105
+ const recursive = fields.recursive;
9106
+ if (recursive === void 0) {
9107
+ throw new Error(
9108
+ `NQL binding recursive relation-column proof for '${plan.rootTable}' is missing recursive metadata.`
9109
+ );
9110
+ }
9111
+ if (fields.cardinality !== "many") {
9112
+ throw new Error(
9113
+ `NQL binding recursive relation-column proof for '${plan.rootTable}' must use cardinality 'many'.`
9114
+ );
9115
+ }
9116
+ if (fields.sourceColumn.length !== 1) {
9117
+ throw new Error(
9118
+ `NQL binding recursive relation-column proof for '${plan.rootTable}' must carry exactly one projected seed column.`
9119
+ );
9120
+ }
9121
+ if (dialectCapabilities?.supportsRecursiveCTE === false) {
9122
+ throw new Error(
9123
+ `Recursive NQL binding relation columns require a dialect with supportsRecursiveCTE; current dialect (${dialectCapabilities.name}) declared it unsupported.`
9124
+ );
9125
+ }
9126
+ assertDialectCapability(
9127
+ dialectCapabilities,
9128
+ "supportsJsonAgg",
9129
+ "JSON aggregation for NQL binding relation columns is"
8978
9130
  );
8979
- return {
8980
- SubLink: {
8981
- subLinkType: "EXPR_SUBLINK",
8982
- subselect: selectStmt({
8983
- targetList: [{ ResTarget: { val: relatedColumn } }],
8984
- from: [relatedTable],
8985
- where: eqExpr(relatedJoinColumn, bindingJoinColumn)
8986
- })
9131
+ const seedColumn = fields.sourceColumn[0];
9132
+ if (!seedColumn) {
9133
+ throw new Error(
9134
+ `NQL binding recursive relation-column proof for '${plan.rootTable}' is missing its projected seed column.`
9135
+ );
9136
+ }
9137
+ const relatedAlias = this.allocateBindingRelationAlias();
9138
+ return buildRecursiveScalarSubquery({
9139
+ cteAlias: `__${relatedAlias}`,
9140
+ table: fields.targetTable,
9141
+ pkColumn: recursive.targetKeyColumn,
9142
+ fkColumn: recursive.selfRefColumn,
9143
+ outerAlias: plan.rootTable,
9144
+ outerSeedColumn: seedColumn,
9145
+ isAncestors: recursive.direction === "up",
9146
+ maxDepth: recursive.maxDepth,
9147
+ selectColumn: fields.selectedColumn,
9148
+ ctx: this.createHandlerContext(plan, plan.rootTable)
9149
+ });
9150
+ }
9151
+ compileBindingRelationColumnSubquery(fields, plan, dialectCapabilities) {
9152
+ if (fields.selectedColumn === void 0) {
9153
+ throw new Error(
9154
+ `NQL binding relation-column proof for '${plan.rootTable}' is missing selectedColumn.`
9155
+ );
9156
+ }
9157
+ if (fields.recursive !== void 0) {
9158
+ return this.compileBindingRecursiveRelationColumnSubquery(
9159
+ fields,
9160
+ plan,
9161
+ dialectCapabilities
9162
+ );
9163
+ }
9164
+ if (this.bindingRelationNameLooksRecursive(fields)) {
9165
+ throw new Error(
9166
+ `NQL binding recursive relation-column proof for '${plan.rootTable}' is missing recursive metadata.`
9167
+ );
9168
+ }
9169
+ const isManyToManyRelation = fields.relationType === "manyToMany" || fields.relationType === "belongsToMany";
9170
+ const hasCompleteManyToManyProof = this.bindingRelationHasCompleteManyToManyProof(fields);
9171
+ if (isManyToManyRelation && (fields.cardinality !== "many" || !hasCompleteManyToManyProof)) {
9172
+ if (fields.cardinality !== "many") {
9173
+ throw new Error(
9174
+ `NQL binding relation-column proof for '${plan.rootTable}' is manyToMany but must use cardinality 'many' with complete junction proof (through, throughSourceColumn, throughTargetColumn) (ref-#192).`
9175
+ );
8987
9176
  }
8988
- };
9177
+ throw new Error(
9178
+ `NQL binding relation-column proof for '${plan.rootTable}' is manyToMany but missing complete junction proof (through, throughSourceColumn, throughTargetColumn) (ref-#192).`
9179
+ );
9180
+ }
9181
+ if (fields.cardinality === "one") {
9182
+ const { relatedAlias, relatedTable, relatedColumn } = this.buildCorrelatedRelationRefs(fields, plan);
9183
+ if (fields.hops.length === 0 && trustedRelationHasMultipleHops(fields.relation)) {
9184
+ throw new Error(
9185
+ `NQL binding relation-column proof for '${plan.rootTable}' is dotted but missing resolved hops.`
9186
+ );
9187
+ }
9188
+ if (fields.hops.length > 0) {
9189
+ let fromNode = relatedTable;
9190
+ let previousAlias = relatedAlias;
9191
+ for (let i = 0; i < fields.hops.length; i++) {
9192
+ const hop = fields.hops[i];
9193
+ const hopAlias = `${relatedAlias}_h${i + 1}`;
9194
+ const hopTable = rangeVar(
9195
+ hop.target,
9196
+ hopAlias,
9197
+ this.schemaForRangeVar(plan, hop.target),
9198
+ this.naming
9199
+ );
9200
+ fromNode = innerJoin(
9201
+ fromNode,
9202
+ hopTable,
9203
+ buildKeyCorrelation(
9204
+ hopAlias,
9205
+ hop.joinColumn,
9206
+ previousAlias,
9207
+ hop.fkColumn,
9208
+ this.createHandlerContext(plan)
9209
+ )
9210
+ );
9211
+ previousAlias = hopAlias;
9212
+ }
9213
+ return {
9214
+ SubLink: {
9215
+ subLinkType: "EXPR_SUBLINK",
9216
+ subselect: selectStmt({
9217
+ targetList: [
9218
+ {
9219
+ ResTarget: {
9220
+ val: columnRef(
9221
+ fields.selectedColumn,
9222
+ previousAlias,
9223
+ void 0,
9224
+ this.naming
9225
+ )
9226
+ }
9227
+ }
9228
+ ],
9229
+ from: [fromNode],
9230
+ where: buildKeyCorrelation(
9231
+ relatedAlias,
9232
+ fields.targetColumn,
9233
+ plan.rootTable,
9234
+ fields.sourceColumn,
9235
+ this.createHandlerContext(plan)
9236
+ )
9237
+ })
9238
+ }
9239
+ };
9240
+ }
9241
+ return {
9242
+ SubLink: {
9243
+ subLinkType: "EXPR_SUBLINK",
9244
+ subselect: selectStmt({
9245
+ targetList: [{ ResTarget: { val: relatedColumn } }],
9246
+ from: [relatedTable],
9247
+ where: buildKeyCorrelation(
9248
+ relatedAlias,
9249
+ fields.targetColumn,
9250
+ plan.rootTable,
9251
+ fields.sourceColumn,
9252
+ this.createHandlerContext(plan)
9253
+ )
9254
+ })
9255
+ }
9256
+ };
9257
+ }
9258
+ if (fields.cardinality === "many") {
9259
+ if (fields.relationType !== "hasMany" && fields.relationType !== "manyToMany" && fields.relationType !== "belongsToMany") {
9260
+ throw new Error(
9261
+ `NQL binding relation-column proof for '${plan.rootTable}' has cardinality 'many' but relationType '${fields.relationType ?? "unknown"}' is not supported; only hasMany or manyToMany can be aggregated (ref-#192).`
9262
+ );
9263
+ }
9264
+ if ((fields.relationType === "manyToMany" || fields.relationType === "belongsToMany") && !hasCompleteManyToManyProof) {
9265
+ throw new Error(
9266
+ `NQL binding relation-column proof for '${plan.rootTable}' is manyToMany but missing complete junction proof (through, throughSourceColumn, throughTargetColumn) (ref-#192).`
9267
+ );
9268
+ }
9269
+ if (dialectCapabilities?.supportsJsonAgg === false) {
9270
+ throw new Error(
9271
+ "JSON aggregation for NQL binding relation columns is not supported by this adapter"
9272
+ );
9273
+ }
9274
+ const { relatedAlias, relatedTable, relatedColumn } = this.buildCorrelatedRelationRefs(fields, plan);
9275
+ const junctionAlias = hasCompleteManyToManyProof ? this.allocateBindingRelationAlias() : void 0;
9276
+ const handlerContext = this.createHandlerContext(plan);
9277
+ const fromNode = hasCompleteManyToManyProof ? innerJoin(
9278
+ relatedTable,
9279
+ rangeVar(
9280
+ fields.through,
9281
+ junctionAlias,
9282
+ this.schemaForRangeVar(plan, fields.through),
9283
+ this.naming
9284
+ ),
9285
+ buildKeyCorrelation(
9286
+ relatedAlias,
9287
+ fields.targetColumn,
9288
+ junctionAlias,
9289
+ [fields.throughTargetColumn],
9290
+ handlerContext
9291
+ )
9292
+ ) : relatedTable;
9293
+ const whereNode = hasCompleteManyToManyProof ? buildKeyCorrelation(
9294
+ junctionAlias,
9295
+ [fields.throughSourceColumn],
9296
+ plan.rootTable,
9297
+ fields.sourceColumn,
9298
+ handlerContext
9299
+ ) : buildKeyCorrelation(
9300
+ relatedAlias,
9301
+ fields.targetColumn,
9302
+ plan.rootTable,
9303
+ fields.sourceColumn,
9304
+ handlerContext
9305
+ );
9306
+ return {
9307
+ SubLink: {
9308
+ subLinkType: "EXPR_SUBLINK",
9309
+ subselect: selectStmt({
9310
+ targetList: [
9311
+ {
9312
+ ResTarget: {
9313
+ val: coalesceExpr([
9314
+ funcCall("json_agg", [relatedColumn], {
9315
+ orderBy: [
9316
+ sortBy(
9317
+ typeCast(relatedColumn, "text"),
9318
+ "DEFAULT",
9319
+ "LAST"
9320
+ )
9321
+ ]
9322
+ }),
9323
+ typeCast({ A_Const: { sval: { sval: "[]" } } }, "json")
9324
+ ])
9325
+ }
9326
+ }
9327
+ ],
9328
+ from: [fromNode],
9329
+ where: whereNode
9330
+ })
9331
+ }
9332
+ };
9333
+ }
9334
+ throw new Error(
9335
+ `NQL binding relation-column proof for '${plan.rootTable}' is not scalar (cardinality: ${fields.cardinality ?? "unknown"}).`
9336
+ );
8989
9337
  }
8990
9338
  compileExpressionSubquery(query, paramOffset) {
8991
9339
  const innerCompiler = new _PlanCompiler(this.childCompilerOptions());
@@ -9369,7 +9717,8 @@ var PlanCompiler = class _PlanCompiler {
9369
9717
  if (trusted?.selectedColumn !== void 0) {
9370
9718
  const node2 = this.compileBindingRelationColumnSubquery(
9371
9719
  trusted,
9372
- plan
9720
+ plan,
9721
+ this.dialectCapabilities
9373
9722
  );
9374
9723
  targetList.push({
9375
9724
  ResTarget: {
@@ -10106,12 +10455,22 @@ var PlanCompiler = class _PlanCompiler {
10106
10455
  registerJoinFilter(decision) {
10107
10456
  const targetTable = decision.targetTable;
10108
10457
  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
10458
  const alias = targetTable === sourceTable ? decision.relationName ?? `${targetTable}_join` : void 0;
10459
+ const targetAlias = alias ?? targetTable;
10460
+ const fkColumn = decision.foreignKey ?? [
10461
+ this.deriveFk(targetTable, this.defaultPk)
10462
+ ];
10463
+ const targetKey = decision.parentKey ?? [this.defaultPk];
10464
+ const onCondition = buildKeyCorrelation(
10465
+ targetAlias,
10466
+ targetKey,
10467
+ sourceTable,
10468
+ fkColumn,
10469
+ this.createHandlerContext({
10470
+ rootTable: sourceTable,
10471
+ decisions: []
10472
+ })
10473
+ );
10115
10474
  this.pendingJoins.push({
10116
10475
  type: "JOIN",
10117
10476
  table: targetTable,
@@ -10132,19 +10491,21 @@ var PlanCompiler = class _PlanCompiler {
10132
10491
  this.schemaForRangeVar(plan, decision.targetTable ?? ""),
10133
10492
  this.naming
10134
10493
  );
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
- )
10494
+ const sourceColumn = toColumnList7(decision.sourceColumn);
10495
+ const targetColumn = toColumnList7(decision.targetColumn);
10496
+ if (sourceColumn.length === 0) {
10497
+ throw new Error("Missing required column 'sourceColumn' in compileJoin");
10498
+ }
10499
+ if (targetColumn.length === 0) {
10500
+ throw new Error("Missing required column 'targetColumn' in compileJoin");
10501
+ }
10502
+ const sourceAlias = sourceColumn.length > 1 ? plan.rootTable : "";
10503
+ const onCondition = buildKeyCorrelation(
10504
+ sourceAlias,
10505
+ sourceColumn,
10506
+ decision.alias ?? decision.targetTable ?? "",
10507
+ targetColumn,
10508
+ this.createHandlerContext(plan)
10148
10509
  );
10149
10510
  if (decision.joinType === "left") {
10150
10511
  return leftJoin(baseTable, targetTable, onCondition, decision.alias);
@@ -10570,6 +10931,12 @@ function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
10570
10931
  }
10571
10932
  return `${parts.join(" ")};`;
10572
10933
  }
10934
+ function failSafeUnknownDownChange(kind) {
10935
+ return {
10936
+ sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${kind}"`,
10937
+ destructive: true
10938
+ };
10939
+ }
10573
10940
  function isChangeSupported(kind, caps) {
10574
10941
  switch (kind) {
10575
10942
  case "create_enum":
@@ -11022,188 +11389,381 @@ function changeToUpSQL(change, schemaName) {
11022
11389
  function changeToDownSQL(change, schemaName) {
11023
11390
  switch (change.kind) {
11024
11391
  case "create_table":
11025
- return `DROP TABLE IF EXISTS ${qualifyTable(change.table, schemaName)} CASCADE;`;
11392
+ return {
11393
+ sql: `DROP TABLE IF EXISTS ${qualifyTable(change.table, schemaName)} CASCADE;`,
11394
+ destructive: true
11395
+ };
11026
11396
  case "drop_table":
11027
- return `-- WARNING: Cannot reverse drop_table "${change.table}" -- table data was lost`;
11397
+ return {
11398
+ sql: `-- WARNING: Cannot reverse drop_table "${change.table}" -- table data was lost`,
11399
+ destructive: true
11400
+ };
11028
11401
  case "add_column":
11029
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP COLUMN ${quoteIdent2(change.column, "alias")} CASCADE;`;
11402
+ return {
11403
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP COLUMN ${quoteIdent2(change.column, "alias")} CASCADE;`,
11404
+ destructive: true
11405
+ };
11030
11406
  case "drop_column":
11031
- return `-- WARNING: Cannot reverse drop_column "${change.table}"."${change.column}" -- column data was lost`;
11407
+ return {
11408
+ sql: `-- WARNING: Cannot reverse drop_column "${change.table}"."${change.column}" -- column data was lost`,
11409
+ destructive: true
11410
+ };
11032
11411
  case "alter_column_type": {
11033
11412
  const fromType = change.meta?.fromType;
11034
11413
  if (!fromType) {
11035
- return `-- WARNING: Cannot reverse alter_column_type "${change.table}"."${change.column}" -- missing migration metadata`;
11414
+ return {
11415
+ sql: `-- WARNING: Cannot reverse alter_column_type "${change.table}"."${change.column}" -- missing migration metadata`,
11416
+ destructive: true
11417
+ };
11036
11418
  }
11037
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${fromType};`;
11419
+ return {
11420
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${fromType};`,
11421
+ destructive: true
11422
+ };
11038
11423
  }
11039
11424
  case "alter_column_nullable": {
11040
11425
  const oldNullable = change.meta?.oldNullable;
11041
11426
  if (oldNullable === void 0) {
11042
- return `-- WARNING: Cannot reverse alter_column_nullable "${change.table}"."${change.column}" -- missing migration metadata`;
11427
+ return {
11428
+ sql: `-- WARNING: Cannot reverse alter_column_nullable "${change.table}"."${change.column}" -- missing migration metadata`,
11429
+ destructive: true
11430
+ };
11043
11431
  }
11044
11432
  const action = oldNullable ? "DROP NOT NULL" : "SET NOT NULL";
11045
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} ${action};`;
11433
+ return {
11434
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} ${action};`,
11435
+ // Allowlisted: restores the recorded prior nullability metadata.
11436
+ destructive: false
11437
+ };
11046
11438
  }
11047
11439
  case "alter_column_default": {
11048
11440
  const oldDefault = change.meta?.oldDefault;
11049
11441
  if (oldDefault === void 0) {
11050
- return `-- WARNING: Cannot reverse alter_column_default "${change.table}"."${change.column}" -- missing migration metadata`;
11442
+ return {
11443
+ sql: `-- WARNING: Cannot reverse alter_column_default "${change.table}"."${change.column}" -- missing migration metadata`,
11444
+ destructive: true
11445
+ };
11051
11446
  }
11052
11447
  if (oldDefault === null) {
11053
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} DROP DEFAULT;`;
11448
+ return {
11449
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} DROP DEFAULT;`,
11450
+ // Allowlisted: restores the recorded prior state of "no default".
11451
+ destructive: false
11452
+ };
11054
11453
  }
11055
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} SET DEFAULT ${formatDefault(oldDefault)};`;
11454
+ return {
11455
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} SET DEFAULT ${formatDefault(oldDefault)};`,
11456
+ // Allowlisted: restores the recorded prior default value.
11457
+ destructive: false
11458
+ };
11056
11459
  }
11057
11460
  case "add_primary_key": {
11058
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(pkName(change.table), "alias")} CASCADE;`;
11461
+ return {
11462
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(pkName(change.table), "alias")} CASCADE;`,
11463
+ destructive: true
11464
+ };
11465
+ }
11466
+ case "drop_primary_key": {
11467
+ const columns = change.meta?.columns;
11468
+ if (Array.isArray(columns) && columns.length > 0) {
11469
+ const pkCols = columns.map((n) => quoteIdent2(n, "alias")).join(", ");
11470
+ return {
11471
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ADD CONSTRAINT ${quoteIdent2(pkName(change.table), "alias")} PRIMARY KEY (${pkCols});`,
11472
+ // Allowlisted: re-adds the dropped primary-key constraint from metadata.
11473
+ destructive: false
11474
+ };
11475
+ }
11476
+ return {
11477
+ sql: `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`,
11478
+ destructive: true
11479
+ };
11059
11480
  }
11060
- case "drop_primary_key":
11061
- return `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`;
11062
11481
  case "add_foreign_key": {
11063
11482
  const fk = change.meta?.fk;
11064
- if (!fk) return void 0;
11483
+ if (!fk) return { sql: void 0, destructive: true };
11065
11484
  const constraintName = quoteIdent2(
11066
11485
  fkName(change.table, fk.columns),
11067
11486
  "alias"
11068
11487
  );
11069
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${constraintName} CASCADE;`;
11488
+ return {
11489
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${constraintName} CASCADE;`,
11490
+ destructive: true
11491
+ };
11492
+ }
11493
+ case "drop_foreign_key": {
11494
+ const fk = change.meta?.fk;
11495
+ if (!fk) {
11496
+ return {
11497
+ sql: `-- WARNING: Cannot reverse drop_foreign_key "${change.table}" -- FK definition was lost`,
11498
+ destructive: true
11499
+ };
11500
+ }
11501
+ return {
11502
+ sql: generateAddFKSQL(change.table, fk, schemaName),
11503
+ // Allowlisted: re-adds the dropped foreign-key constraint from metadata.
11504
+ destructive: false
11505
+ };
11070
11506
  }
11071
- case "drop_foreign_key":
11072
- return `-- WARNING: Cannot reverse drop_foreign_key "${change.table}" -- FK definition was lost`;
11073
11507
  case "alter_foreign_key": {
11074
11508
  const oldFk = change.meta?.oldFk;
11075
- if (!oldFk) {
11076
- return `-- WARNING: Cannot reverse alter_foreign_key "${change.table}" -- missing migration metadata`;
11077
- }
11078
11509
  const fk = change.meta?.fk;
11510
+ if (!oldFk || !fk) {
11511
+ return {
11512
+ sql: `-- WARNING: Cannot reverse alter_foreign_key "${change.table}" -- missing migration metadata`,
11513
+ destructive: true
11514
+ };
11515
+ }
11079
11516
  const constraintName = quoteIdent2(
11080
11517
  fkName(change.table, fk.columns),
11081
11518
  "alias"
11082
11519
  );
11083
11520
  const drop = `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${constraintName};`;
11084
11521
  const add = generateAddFKSQL(change.table, oldFk, schemaName);
11085
- return `${drop}
11086
- ${add}`;
11522
+ return { sql: `${drop}
11523
+ ${add}`, destructive: false };
11087
11524
  }
11088
11525
  case "create_index": {
11089
11526
  const idx = change.meta?.index;
11090
- if (!idx) return void 0;
11527
+ if (!idx) return { sql: void 0, destructive: true };
11091
11528
  const indexName = quoteIdent2(
11092
11529
  idxName(change.table, idx.columns, idx.name),
11093
11530
  "alias"
11094
11531
  );
11095
11532
  const schemaPrefix = schemaName ? `${quoteIdent2(schemaName, "alias")}.` : "";
11096
- return `DROP INDEX IF EXISTS ${schemaPrefix}${indexName};`;
11533
+ return {
11534
+ sql: `DROP INDEX IF EXISTS ${schemaPrefix}${indexName};`,
11535
+ destructive: true
11536
+ };
11537
+ }
11538
+ case "drop_index": {
11539
+ const idx = change.meta?.index;
11540
+ if (!idx) {
11541
+ return {
11542
+ sql: `-- WARNING: Cannot reverse drop_index "${change.table}" -- index definition was lost`,
11543
+ destructive: true
11544
+ };
11545
+ }
11546
+ return {
11547
+ sql: upCreateIndex(change, schemaName),
11548
+ // Allowlisted: re-creates the dropped index from metadata.
11549
+ destructive: false
11550
+ };
11097
11551
  }
11098
- case "drop_index":
11099
- return `-- WARNING: Cannot reverse drop_index "${change.table}" -- index definition was lost`;
11100
11552
  case "add_check_constraint": {
11101
11553
  const check = change.meta?.check;
11102
- if (!check) return void 0;
11103
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(check.name, "alias")};`;
11554
+ if (!check) return { sql: void 0, destructive: true };
11555
+ return {
11556
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(check.name, "alias")};`,
11557
+ destructive: true
11558
+ };
11104
11559
  }
11105
11560
  case "drop_check_constraint": {
11106
11561
  const check = change.meta?.check;
11107
- if (!check) return void 0;
11562
+ if (!check) return { sql: void 0, destructive: true };
11108
11563
  validateSqlExpression(
11109
11564
  check.expression,
11110
11565
  "migration check constraint (down)"
11111
11566
  );
11112
- return "DO $$ BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + check.expression + "; EXCEPTION WHEN duplicate_object THEN NULL; END $$;";
11567
+ return {
11568
+ sql: "DO $$ BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + check.expression + "; EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
11569
+ // Allowlisted: re-adds the dropped CHECK constraint from metadata.
11570
+ destructive: false
11571
+ };
11113
11572
  }
11114
11573
  case "create_enum": {
11115
11574
  const enumDef = change.meta?.enum;
11116
- if (!enumDef) return void 0;
11575
+ if (!enumDef) return { sql: void 0, destructive: true };
11117
11576
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
11118
- return `DROP TYPE IF EXISTS ${enumName} CASCADE;`;
11577
+ return {
11578
+ sql: `DROP TYPE IF EXISTS ${enumName} CASCADE;`,
11579
+ destructive: true
11580
+ };
11119
11581
  }
11120
11582
  case "drop_enum": {
11121
11583
  const enumDef = change.meta?.enum;
11122
- if (!enumDef) return void 0;
11584
+ if (!enumDef) return { sql: void 0, destructive: true };
11123
11585
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
11124
11586
  const values = enumDef.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
11125
- return `CREATE TYPE ${enumName} AS ENUM (${values});`;
11587
+ return {
11588
+ sql: `CREATE TYPE ${enumName} AS ENUM (${values});`,
11589
+ // Allowlisted: re-creates the dropped enum type from metadata.
11590
+ destructive: false
11591
+ };
11126
11592
  }
11127
11593
  case "alter_enum_add_value":
11128
- return `-- ALTER TYPE ADD VALUE cannot be reversed in PostgreSQL`;
11594
+ return {
11595
+ sql: `-- ALTER TYPE ADD VALUE cannot be reversed in PostgreSQL`,
11596
+ destructive: true
11597
+ };
11129
11598
  case "alter_column_collation": {
11130
- return `-- WARNING: Cannot reverse alter_column_collation "${change.table}"."${change.column}" -- previous collation unknown`;
11599
+ return {
11600
+ sql: `-- WARNING: Cannot reverse alter_column_collation "${change.table}"."${change.column}" -- previous collation unknown`,
11601
+ destructive: true
11602
+ };
11131
11603
  }
11132
11604
  case "alter_column_identity": {
11133
11605
  const col = change.meta?.column;
11134
11606
  const prevIdentity = change.meta?.previousIdentity;
11135
- if (!col) return void 0;
11607
+ if (!col) return { sql: void 0, destructive: true };
11136
11608
  const table = qualifyTable(change.table, schemaName);
11137
11609
  const column = quoteIdent2(change.column, "alias");
11138
11610
  if (prevIdentity && !col.identity) {
11139
11611
  const gen2 = prevIdentity === "always" ? "ALWAYS" : "BY DEFAULT";
11140
- return `ALTER TABLE ${table} ALTER COLUMN ${column} ADD GENERATED ${gen2} AS IDENTITY;`;
11612
+ return {
11613
+ sql: `ALTER TABLE ${table} ALTER COLUMN ${column} ADD GENERATED ${gen2} AS IDENTITY;`,
11614
+ // Allowlisted: re-adds the previously recorded identity metadata.
11615
+ destructive: false
11616
+ };
11141
11617
  }
11142
11618
  if (!prevIdentity && col.identity) {
11143
- return `ALTER TABLE ${table} ALTER COLUMN ${column} DROP IDENTITY IF EXISTS;`;
11619
+ return {
11620
+ sql: `ALTER TABLE ${table} ALTER COLUMN ${column} DROP IDENTITY IF EXISTS;`,
11621
+ destructive: true
11622
+ };
11623
+ }
11624
+ if (!prevIdentity && !col.identity) {
11625
+ return {
11626
+ sql: `-- WARNING: Cannot reverse alter_column_identity "${change.table}"."${change.column}" -- missing previous identity metadata`,
11627
+ destructive: true
11628
+ };
11144
11629
  }
11145
11630
  const gen = prevIdentity === "always" ? "ALWAYS" : "BY DEFAULT";
11146
- return `ALTER TABLE ${table} ALTER COLUMN ${column} SET GENERATED ${gen};`;
11631
+ return {
11632
+ sql: `ALTER TABLE ${table} ALTER COLUMN ${column} SET GENERATED ${gen};`,
11633
+ // Allowlisted: restores the recorded prior identity generation mode.
11634
+ destructive: false
11635
+ };
11147
11636
  }
11148
11637
  case "add_comment": {
11149
11638
  const target = change.meta?.target;
11150
11639
  if (target === "table") {
11151
- return `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS NULL;`;
11640
+ return {
11641
+ sql: `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS NULL;`,
11642
+ destructive: true
11643
+ };
11152
11644
  }
11153
- return `COMMENT ON COLUMN ${qualifyTable(change.table, schemaName)}.${quoteIdent2(change.column, "alias")} IS NULL;`;
11645
+ return {
11646
+ sql: `COMMENT ON COLUMN ${qualifyTable(change.table, schemaName)}.${quoteIdent2(change.column, "alias")} IS NULL;`,
11647
+ destructive: true
11648
+ };
11154
11649
  }
11155
11650
  case "drop_comment": {
11156
- return `-- WARNING: Cannot reverse drop_comment "${change.table}"${change.column ? `."${change.column}"` : ""} -- comment text was lost`;
11651
+ const target = change.meta?.target;
11652
+ const comment = change.meta?.comment;
11653
+ if (typeof comment === "string") {
11654
+ const escaped = comment.replace(/'/g, "''");
11655
+ if (target === "table") {
11656
+ return {
11657
+ sql: `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS '${escaped}';`,
11658
+ // Allowlisted: re-adds the dropped table comment from metadata.
11659
+ destructive: false
11660
+ };
11661
+ }
11662
+ if (target === "column") {
11663
+ return {
11664
+ sql: `COMMENT ON COLUMN ${qualifyTable(change.table, schemaName)}.${quoteIdent2(change.column, "alias")} IS '${escaped}';`,
11665
+ // Allowlisted: re-adds the dropped column comment from metadata.
11666
+ destructive: false
11667
+ };
11668
+ }
11669
+ }
11670
+ return {
11671
+ sql: `-- WARNING: Cannot reverse drop_comment "${change.table}"${change.column ? `."${change.column}"` : ""} -- comment text was lost`,
11672
+ destructive: true
11673
+ };
11157
11674
  }
11158
11675
  case "create_extension": {
11159
11676
  const ext = change.meta?.extension;
11160
- if (ext == null) return void 0;
11161
- return `DROP EXTENSION IF EXISTS ${quoteExtensionName(ext)} CASCADE;`;
11677
+ if (ext == null) return { sql: void 0, destructive: true };
11678
+ return {
11679
+ sql: `DROP EXTENSION IF EXISTS ${quoteExtensionName(ext)} CASCADE;`,
11680
+ destructive: true
11681
+ };
11162
11682
  }
11163
11683
  case "drop_extension": {
11164
11684
  const ext = change.meta?.extension;
11165
- if (ext == null) return void 0;
11166
- return `CREATE EXTENSION IF NOT EXISTS ${quoteExtensionName(ext)};`;
11685
+ if (ext == null) return { sql: void 0, destructive: true };
11686
+ return {
11687
+ sql: `CREATE EXTENSION IF NOT EXISTS ${quoteExtensionName(ext)};`,
11688
+ // Allowlisted: re-creates the dropped extension from metadata.
11689
+ destructive: false
11690
+ };
11167
11691
  }
11168
11692
  case "create_sequence": {
11169
11693
  const seq = change.meta?.sequence;
11170
- if (!seq) return void 0;
11694
+ if (!seq) return { sql: void 0, destructive: true };
11171
11695
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(seq.name, "alias")}` : quoteIdent2(seq.name, "alias");
11172
- return `DROP SEQUENCE IF EXISTS ${seqName} CASCADE;`;
11696
+ return {
11697
+ sql: `DROP SEQUENCE IF EXISTS ${seqName} CASCADE;`,
11698
+ destructive: true
11699
+ };
11173
11700
  }
11174
11701
  case "alter_sequence": {
11175
11702
  const prevSeq = change.meta?.previousSequence;
11176
11703
  if (!prevSeq) {
11177
- return `-- WARNING: Cannot reverse alter_sequence "${change.table}" -- missing migration metadata`;
11704
+ return {
11705
+ sql: `-- WARNING: Cannot reverse alter_sequence "${change.table}" -- missing migration metadata`,
11706
+ destructive: true
11707
+ };
11178
11708
  }
11179
11709
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(prevSeq.name, "alias")}` : quoteIdent2(prevSeq.name, "alias");
11180
- return buildSequenceClause("ALTER SEQUENCE", seqName, prevSeq, true);
11710
+ return {
11711
+ sql: buildSequenceClause("ALTER SEQUENCE", seqName, prevSeq, true),
11712
+ // Allowlisted: restores the recorded prior sequence metadata.
11713
+ destructive: false
11714
+ };
11181
11715
  }
11182
11716
  case "drop_sequence": {
11183
11717
  const seq = change.meta?.sequence;
11184
- if (!seq) return void 0;
11718
+ if (!seq) return { sql: void 0, destructive: true };
11185
11719
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(seq.name, "alias")}` : quoteIdent2(seq.name, "alias");
11186
- return buildSequenceClause("CREATE SEQUENCE", seqName, seq);
11720
+ return {
11721
+ sql: buildSequenceClause("CREATE SEQUENCE", seqName, seq),
11722
+ // Allowlisted: re-creates the dropped sequence from metadata.
11723
+ destructive: false
11724
+ };
11187
11725
  }
11188
11726
  case "validate_constraint":
11189
- return `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${change.table}")`;
11727
+ return {
11728
+ sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${change.table}")`,
11729
+ destructive: true
11730
+ };
11190
11731
  case "enable_rls":
11191
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} DISABLE ROW LEVEL SECURITY;`;
11732
+ return {
11733
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DISABLE ROW LEVEL SECURITY;`,
11734
+ destructive: true
11735
+ };
11192
11736
  case "disable_rls":
11193
- return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ENABLE ROW LEVEL SECURITY;`;
11737
+ return {
11738
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ENABLE ROW LEVEL SECURITY;`,
11739
+ // Allowlisted: re-enables the previously present RLS security control.
11740
+ destructive: false
11741
+ };
11194
11742
  case "create_policy": {
11195
11743
  const policy = change.meta?.policy;
11196
- if (!policy) return void 0;
11197
- return `DROP POLICY IF EXISTS ${quoteIdent2(policy.name, "alias")} ON ${qualifyTable(change.table, schemaName)};`;
11744
+ if (!policy) return { sql: void 0, destructive: true };
11745
+ return {
11746
+ sql: `DROP POLICY IF EXISTS ${quoteIdent2(policy.name, "alias")} ON ${qualifyTable(change.table, schemaName)};`,
11747
+ destructive: true
11748
+ };
11198
11749
  }
11199
11750
  case "drop_policy": {
11200
11751
  const policy = change.meta?.policy;
11201
- if (!policy) return void 0;
11202
- return buildPolicySQL(change.table, policy, schemaName);
11752
+ if (!policy) return { sql: void 0, destructive: true };
11753
+ return {
11754
+ sql: buildPolicySQL(change.table, policy, schemaName),
11755
+ // Allowlisted: re-creates the dropped RLS policy from metadata.
11756
+ destructive: false
11757
+ };
11203
11758
  }
11759
+ default:
11760
+ return failSafeUnknownDownChange(change.kind);
11204
11761
  }
11205
11762
  }
11206
11763
  function generateDownSQL(diff, options) {
11764
+ return generateDownMigrationSQL(diff, options).statements;
11765
+ }
11766
+ function generateDownMigrationSQL(diff, options) {
11207
11767
  const schemaName = options?.schemaName;
11208
11768
  const includeDestructive = options?.includeDestructive ?? true;
11209
11769
  const changes = includeDestructive ? diff.changes : diff.changes.filter((c) => !c.destructive);
@@ -11252,13 +11812,15 @@ function generateDownSQL(diff, options) {
11252
11812
  phases[phase].push(change);
11253
11813
  }
11254
11814
  const statements = [];
11815
+ let destructive = false;
11255
11816
  for (let i = phases.length - 1; i >= 0; i--) {
11256
11817
  for (const change of phases[i]) {
11257
- const sql = changeToDownSQL(change, schemaName);
11258
- if (sql) statements.push(sql);
11818
+ const down = changeToDownSQL(change, schemaName);
11819
+ destructive = down.destructive || destructive;
11820
+ if (down.sql) statements.push(down.sql);
11259
11821
  }
11260
11822
  }
11261
- return statements;
11823
+ return { statements, destructive };
11262
11824
  }
11263
11825
  function generateCreateTableSQL(table, schemaName) {
11264
11826
  const qualTable = qualifyTable(table.name, schemaName);
@@ -11551,29 +12113,36 @@ function generateCreatePolicy(tableName, policy, schemaName, naming) {
11551
12113
  // src/ddl/migration-file.ts
11552
12114
  function generateMigrationFile(diff, options) {
11553
12115
  const upStatements = generateMigrationSQL(diff, options);
11554
- const downStatements = generateDownSQL(diff, options);
12116
+ const downMigration = generateDownMigrationSQL(diff, options);
12117
+ const downStatements = downMigration.statements;
12118
+ const destructiveHeader = `-- dbsp:destructive: ${downMigration.destructive ? "true" : "false"}`;
11555
12119
  const header = options?.name ? `-- Migration: ${options.name}
11556
12120
  -- Generated by: dbsp migrate dev
11557
12121
  -- Date: ${(/* @__PURE__ */ new Date()).toISOString()}
12122
+ ${destructiveHeader}
11558
12123
 
11559
- ` : "";
12124
+ ` : `${destructiveHeader}
12125
+
12126
+ `;
11560
12127
  const upSection = upStatements.length > 0 ? `${upStatements.join(";\n\n")};
11561
12128
  ` : "";
11562
12129
  const downSection = downStatements.length > 0 ? `${downStatements.join(";\n\n")};
11563
12130
  ` : "";
11564
- return `${header}${upSection}
11565
- -- DOWN
12131
+ const separatorPrefix = upSection.length > 0 ? "\n" : "";
12132
+ return `${header}${upSection}${separatorPrefix}-- DOWN
11566
12133
 
11567
12134
  ${downSection}`;
11568
12135
  }
11569
12136
  function parseMigrationFile(content) {
12137
+ const destructive = parseDestructiveHeader(content);
11570
12138
  const separatorRegex = /^\s*-- DOWN\s*$/m;
11571
12139
  const match = separatorRegex.exec(content);
11572
12140
  if (!match) {
11573
12141
  return {
11574
12142
  upStatements: splitStatements(content),
11575
12143
  downStatements: [],
11576
- hasDown: false
12144
+ hasDown: false,
12145
+ destructive
11577
12146
  };
11578
12147
  }
11579
12148
  const upContent = content.slice(0, match.index);
@@ -11581,9 +12150,36 @@ function parseMigrationFile(content) {
11581
12150
  return {
11582
12151
  upStatements: splitStatements(upContent),
11583
12152
  downStatements: splitStatements(downContent),
11584
- hasDown: true
12153
+ hasDown: true,
12154
+ destructive
11585
12155
  };
11586
12156
  }
12157
+ function parseDestructiveHeader(content) {
12158
+ let destructive;
12159
+ let found = false;
12160
+ for (const line of content.split(/\r?\n/)) {
12161
+ const trimmed = line.trim();
12162
+ if (trimmed.length === 0) {
12163
+ continue;
12164
+ }
12165
+ if (/^--\s*DOWN\s*$/i.test(trimmed)) {
12166
+ break;
12167
+ }
12168
+ if (!trimmed.startsWith("--")) {
12169
+ break;
12170
+ }
12171
+ const match = /^--\s*dbsp:destructive:\s*(true|false)\s*$/i.exec(trimmed);
12172
+ if (!match) {
12173
+ continue;
12174
+ }
12175
+ if (found) {
12176
+ return void 0;
12177
+ }
12178
+ found = true;
12179
+ destructive = match[1]?.toLowerCase() === "true";
12180
+ }
12181
+ return found ? destructive : void 0;
12182
+ }
11587
12183
  function isDestructiveDown(downStatements) {
11588
12184
  const destructivePatterns = [
11589
12185
  /DROP\s+TABLE/i,
@@ -12706,6 +13302,7 @@ function vectorDims(column) {
12706
13302
  // src/introspection.ts
12707
13303
  init_assert_field();
12708
13304
  import { ModelIRImpl } from "@dbsp/core";
13305
+ import { buildRelationKeyFields } from "@dbsp/types";
12709
13306
  function parseExpressionsList(raw) {
12710
13307
  const results = [];
12711
13308
  let depth = 0;
@@ -13261,7 +13858,10 @@ function inferRelations(fksByConstraint, filteredTables) {
13261
13858
  for (const [, fk] of fksByConstraint) {
13262
13859
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
13263
13860
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
13264
- const fkCol = fk.cols.length === 1 ? fk.cols[0] : fk.cols;
13861
+ const relationFk = {
13862
+ columns: fk.cols,
13863
+ references: { table: fk.target, columns: fk.refs }
13864
+ };
13265
13865
  const belongsToKey = `${fk.source}.${belongsToName}`;
13266
13866
  if (!relations.has(belongsToKey)) {
13267
13867
  relations.set(belongsToKey, {
@@ -13269,7 +13869,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13269
13869
  type: "belongsTo",
13270
13870
  source: fk.source,
13271
13871
  target: fk.target,
13272
- foreignKey: fkCol,
13872
+ ...buildRelationKeyFields(relationFk, "belongsTo"),
13273
13873
  cardinality: "one",
13274
13874
  optionality: "optional",
13275
13875
  includeStrategy: "auto",
@@ -13285,7 +13885,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13285
13885
  type: "hasMany",
13286
13886
  source: fk.target,
13287
13887
  target: fk.source,
13288
- foreignKey: fkCol,
13888
+ ...buildRelationKeyFields(relationFk, "inverse"),
13289
13889
  cardinality: "many",
13290
13890
  optionality: "optional",
13291
13891
  includeStrategy: "auto",
@@ -14188,6 +14788,7 @@ import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNql
14188
14788
 
14189
14789
  // src/adapter-compiler-includes.ts
14190
14790
  init_ast_helpers();
14791
+ import { toColumnList as toColumnList8 } from "@dbsp/types";
14191
14792
  init_handlers();
14192
14793
  function compileSubqueryInclude(info, parentIds, _options, deps) {
14193
14794
  const schemaName = deps.schemaName;
@@ -14199,7 +14800,12 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14199
14800
  parameters: []
14200
14801
  };
14201
14802
  }
14202
- const fkColumns = Array.isArray(info.foreignKey) ? info.foreignKey : [info.foreignKey];
14803
+ const fkColumns = toColumnList8(info.foreignKey);
14804
+ if (fkColumns.length === 0) {
14805
+ throw new Error(
14806
+ "Subquery include requires at least one foreignKey column."
14807
+ );
14808
+ }
14203
14809
  if (info.through && info.throughSourceKey && info.throughTargetKey) {
14204
14810
  return compileSubqueryIncludeManyToMany(
14205
14811
  info,
@@ -14247,6 +14853,11 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14247
14853
  };
14248
14854
  } else {
14249
14855
  const conditions = parentIds.map((id) => {
14856
+ if (!Array.isArray(id) || id.length !== fkColumns.length) {
14857
+ throw new Error(
14858
+ `Subquery include composite key parameter width (${Array.isArray(id) ? id.length : 1}) must match foreignKey width (${fkColumns.length}).`
14859
+ );
14860
+ }
14250
14861
  const idValues = id;
14251
14862
  const colConditions = fkColumns.map((col, idx) => {
14252
14863
  state.parameters.push(idValues[idx]);
@@ -14287,7 +14898,13 @@ function compileSubqueryIncludeManyToMany(info, parentIds, schemaName, state, de
14287
14898
  const throughTable = info.through;
14288
14899
  const throughSourceKey = info.throughSourceKey;
14289
14900
  const throughTargetKey = info.throughTargetKey;
14290
- const targetPk = Array.isArray(info.sourceKey) ? info.sourceKey[0] : info.sourceKey;
14901
+ const targetPkColumns = toColumnList8(info.sourceKey);
14902
+ if (targetPkColumns.length !== 1) {
14903
+ throw new Error(
14904
+ `Many-to-many subquery include requires a single-column target key; got ${JSON.stringify(targetPkColumns)}.`
14905
+ );
14906
+ }
14907
+ const targetPk = targetPkColumns[0];
14291
14908
  const paramRefs = parentIds.map((id) => {
14292
14909
  state.parameters.push(id);
14293
14910
  state.paramIndex++;
@@ -14376,6 +14993,7 @@ import {
14376
14993
  POSTGRESQL_CAPABILITIES,
14377
14994
  plan as planFn
14378
14995
  } from "@dbsp/core";
14996
+ import { toColumnList as toColumnList11 } from "@dbsp/types";
14379
14997
 
14380
14998
  // src/adapter-compiler-select.ts
14381
14999
  init_assert_field();
@@ -14383,6 +15001,7 @@ init_ast_helpers();
14383
15001
  init_binding_registry();
14384
15002
  init_compile_where();
14385
15003
  import { countDistinctRelationPathsByName } from "@dbsp/core";
15004
+ import { toColumnList as toColumnList10 } from "@dbsp/types";
14386
15005
  init_compiler_utils();
14387
15006
  init_types();
14388
15007
  init_intent_to_decisions();
@@ -14392,6 +15011,7 @@ init_param_ref();
14392
15011
  init_assert_field();
14393
15012
  init_intent_to_decisions();
14394
15013
  import { deriveRelationPathFromIntentPath } from "@dbsp/core";
15014
+ import { toColumnList as toColumnList9 } from "@dbsp/types";
14395
15015
  function findExistsIntents(where) {
14396
15016
  if (!where || typeof where !== "object") return [];
14397
15017
  const w = where;
@@ -14412,7 +15032,8 @@ function findExistsIntents(where) {
14412
15032
  function resolveRelation(model, sourceTable, relationName) {
14413
15033
  const rel = model.getRelation(`${sourceTable}.${relationName}`);
14414
15034
  if (!rel) return void 0;
14415
- const foreignKey = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
15035
+ const foreignKeyColumns = toColumnList9(rel.foreignKey);
15036
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14416
15037
  const relationType = rel.type;
14417
15038
  return { target: rel.target, foreignKey, relationType };
14418
15039
  }
@@ -14635,7 +15256,8 @@ function buildMultiHopExistsChain(hops, rootTable, outerOperator, innerCondition
14635
15256
  const hopRelations = [];
14636
15257
  for (const hop of hops) {
14637
15258
  const rel = model.getRelation(`${currentSource}.${hop}`);
14638
- const foreignKey = rel ? typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0] : void 0;
15259
+ const foreignKeyColumns = rel ? toColumnList9(rel.foreignKey) : [];
15260
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14639
15261
  const relationType = rel?.type === "belongsTo" ? "belongsTo" : rel?.type === "hasMany" || rel?.type === "hasOne" ? rel.type : void 0;
14640
15262
  const parentKey = relationType === "belongsTo" ? rel?.targetKey : rel?.sourceKey;
14641
15263
  const target = rel ? rel.target : hop;
@@ -14745,7 +15367,8 @@ function enrichExistsStubsInConditions(conditions, sourceTable, model) {
14745
15367
  `exists('${relation}'): no relation '${relation}' is declared on table '${sourceTable}'. Use rawExists(subquery(...)) for an EXISTS over an undeclared or uncorrelated subquery.`
14746
15368
  );
14747
15369
  }
14748
- const foreignKey = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
15370
+ const foreignKeyColumns = toColumnList9(rel.foreignKey);
15371
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14749
15372
  const relationType = rel.type === "belongsTo" ? "belongsTo" : rel.type === "hasMany" || rel.type === "hasOne" ? rel.type : void 0;
14750
15373
  const parentKey = relationType === "belongsTo" ? rel.targetKey : rel.sourceKey;
14751
15374
  const targetTable = rel.target;
@@ -14804,7 +15427,8 @@ function buildEnrichedExistsDecision(d, matchingIntent, rootTable, model) {
14804
15427
  const relIR = model && context.relation ? model.getRelation(
14805
15428
  `${sourceTableForRelation}.${context.relation}`
14806
15429
  ) : void 0;
14807
- const foreignKey = relIR ? typeof relIR.foreignKey === "string" ? relIR.foreignKey : relIR.foreignKey?.[0] : void 0;
15430
+ const foreignKeyColumns = relIR ? toColumnList9(relIR.foreignKey) : [];
15431
+ const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14808
15432
  const relationType = relIR?.type === "belongsTo" ? "belongsTo" : relIR?.type === "hasMany" || relIR?.type === "hasOne" ? relIR.type : void 0;
14809
15433
  const parentKey = relationType === "belongsTo" ? relIR?.targetKey : relIR?.sourceKey;
14810
15434
  let operator = "exists";
@@ -15117,6 +15741,7 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15117
15741
  const relationName = resolveIncludeAlias(context);
15118
15742
  if (!context.target || !relationName) return void 0;
15119
15743
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15744
+ const parentKey = context.parentKey ?? defaultPk;
15120
15745
  const relationType = context.relationType;
15121
15746
  const effectiveChoice = choice === "subquery" ? "json_agg" : choice;
15122
15747
  const includeIntent = resolveIncludeByPath(
@@ -15137,8 +15762,10 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15137
15762
  targetTable: context.target,
15138
15763
  ...context.sourceTable && { sourceTable: context.sourceTable },
15139
15764
  ...relationType && { relationType },
15140
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15141
- parentKey: defaultPk,
15765
+ foreignKey,
15766
+ parentKey,
15767
+ ...context.targetOrderKey && context.targetOrderKey.length > 0 ? { orderBy: context.targetOrderKey } : {},
15768
+ ...context.orderByFallback ? { orderByFallback: true } : {},
15142
15769
  ...context.intentPath && { intentPath: context.intentPath },
15143
15770
  ...limit != null && { limit }
15144
15771
  };
@@ -15164,6 +15791,7 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15164
15791
  columns = [defaultPk, ...fields];
15165
15792
  }
15166
15793
  const foreignKey = deriveForeignKey(context, deriveFk, defaultPk) ?? defaultPk;
15794
+ const parentKey = context.parentKey ?? defaultPk;
15167
15795
  const relationType = context.relationType;
15168
15796
  let conditions;
15169
15797
  if (includeIntent?.where) {
@@ -15184,8 +15812,8 @@ function toJoinIncludeDecision(d, plan, defaultPk = DEFAULT_PK_COLUMN, deriveFk
15184
15812
  targetTable: context.target,
15185
15813
  ...context.sourceTable && { sourceTable: context.sourceTable },
15186
15814
  ...relationType && { relationType },
15187
- foreignKey: Array.isArray(foreignKey) ? foreignKey[0] : foreignKey,
15188
- parentKey: defaultPk,
15815
+ foreignKey,
15816
+ parentKey,
15189
15817
  columns,
15190
15818
  ...joinType && { joinType },
15191
15819
  ...conditions && { conditions }
@@ -15208,10 +15836,13 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15208
15836
  (r) => r.name === alias || snakeToCamel(r.name) === alias
15209
15837
  );
15210
15838
  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
- );
15839
+ const rawFkColumns = toColumnList9(rel.foreignKey);
15840
+ const rawFk = rawFkColumns.length > 0 ? rawFkColumns : [
15841
+ deriveFk(
15842
+ rel.type === "belongsTo" ? rel.target : sourceTable,
15843
+ defaultPk
15844
+ )
15845
+ ];
15215
15846
  let columns = [defaultPk];
15216
15847
  if (inc.select?.type === "fields" && inc.select.fields) {
15217
15848
  const extraFields = inc.select.fields.filter((f) => f !== defaultPk);
@@ -15232,8 +15863,8 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15232
15863
  ...rel.type && {
15233
15864
  relationType: rel.type
15234
15865
  },
15235
- foreignKey: Array.isArray(rawFk) ? rawFk[0] : rawFk,
15236
- parentKey: defaultPk,
15866
+ foreignKey: rawFk,
15867
+ parentKey: rel.type === "belongsTo" ? toColumnList9(rel.targetKey).length > 0 ? toColumnList9(rel.targetKey) : [defaultPk] : toColumnList9(rel.sourceKey).length > 0 ? toColumnList9(rel.sourceKey) : [defaultPk],
15237
15868
  columns,
15238
15869
  joinType: inc.join,
15239
15870
  ...conditions && { conditions }
@@ -15390,9 +16021,12 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
15390
16021
  );
15391
16022
  }
15392
16023
  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;
16024
+ const rawFk = toColumnList10(rel.foreignKey);
16025
+ const fkColumns = rawFk.length > 0 ? rawFk : [deriveFk(isBelongsTo ? rootTable : rel.target, defaultPk)];
16026
+ const sourceKey = toColumnList10(rel.sourceKey);
16027
+ const targetKey = toColumnList10(rel.targetKey);
16028
+ const sourceColumn = isBelongsTo ? fkColumns : sourceKey.length > 0 ? sourceKey : [defaultPk];
16029
+ const targetColumn = isBelongsTo ? targetKey.length > 0 ? targetKey : [defaultPk] : fkColumns;
15396
16030
  const alias = intent.alias ?? intent.relation;
15397
16031
  results.push({
15398
16032
  type: "join",
@@ -15755,10 +16389,11 @@ function compileWithIncludes(plan, options, deps) {
15755
16389
  const relationName = ctx.includeAlias ?? ctx.relation;
15756
16390
  if (!relationName) continue;
15757
16391
  const rawFk = deriveForeignKey(ctx, deps.deriveFk, deps.defaultPk) ?? deps.defaultPk;
15758
- const fk = Array.isArray(rawFk) ? rawFk[0] : rawFk;
16392
+ const fk = toColumnList10(rawFk);
16393
+ const parentKey = toColumnList10(ctx.parentKey);
15759
16394
  const isBelongsTo = ctx.relationType === "belongsTo";
15760
- const sourceKey = isBelongsTo ? fk : "id";
15761
- const targetFk = isBelongsTo ? "id" : fk;
16395
+ const sourceKey = isBelongsTo ? fk : parentKey.length > 0 ? parentKey : [deps.defaultPk];
16396
+ const targetFk = isBelongsTo ? parentKey.length > 0 ? parentKey : [deps.defaultPk] : fk;
15762
16397
  const includeIntent = plan.intent?.include?.find(
15763
16398
  (i) => i.relation === relationName || i.relation === ctx.includeAlias
15764
16399
  );
@@ -15821,14 +16456,14 @@ function prependSourceCte(sql, parameters, sourceName, sourceCte, naming) {
15821
16456
  };
15822
16457
  }
15823
16458
  function resolveMutationExistsForeignKey(foreignKey, relationName) {
15824
- if (typeof foreignKey === "string") return foreignKey;
15825
- if (!Array.isArray(foreignKey)) return void 0;
15826
- if (foreignKey.length > 1) {
16459
+ const columns = toColumnList11(foreignKey);
16460
+ if (columns.length === 0) return void 0;
16461
+ if (columns.some((column) => column.length === 0)) {
15827
16462
  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.`
16463
+ `Mutation exists()/notExists() guard relation '${relationName}' has an empty foreignKey column.`
15829
16464
  );
15830
16465
  }
15831
- return foreignKey[0];
16466
+ return columns;
15832
16467
  }
15833
16468
  function resolveExistsRelation(sourceTable, relation, model) {
15834
16469
  if (!model) return { targetTable: relation };
@@ -15841,13 +16476,13 @@ function resolveExistsRelation(sourceTable, relation, model) {
15841
16476
  return {
15842
16477
  targetTable,
15843
16478
  ...fk2 !== void 0 && { sourceColumn: fk2 },
15844
- targetColumn: "id"
16479
+ targetColumn: toColumnList11(rel.targetKey).length > 0 ? toColumnList11(rel.targetKey) : ["id"]
15845
16480
  };
15846
16481
  }
15847
16482
  const fk = resolveMutationExistsForeignKey(rel.foreignKey, relationName);
15848
16483
  return {
15849
16484
  targetTable,
15850
- sourceColumn: rel.sourceKey ?? "id",
16485
+ sourceColumn: toColumnList11(rel.sourceKey).length > 0 ? toColumnList11(rel.sourceKey) : ["id"],
15851
16486
  ...fk !== void 0 && { targetColumn: fk }
15852
16487
  };
15853
16488
  }
@@ -17769,6 +18404,9 @@ function orderedNqlBindingNames(bundle) {
17769
18404
  }
17770
18405
  return names;
17771
18406
  }
18407
+ function runtimeBindingSourceTable(bundle, name) {
18408
+ return bundle.mutationBindings?.get(name)?.table ?? bundle.bindings?.get(name)?.from;
18409
+ }
17772
18410
  function mapRuntimeBindingColumnType(type) {
17773
18411
  switch (type) {
17774
18412
  case "string":
@@ -17821,13 +18459,13 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
17821
18459
  );
17822
18460
  if (column === void 0) {
17823
18461
  throw new Error(
17824
- `NQL runtime binding '${bindingName}' cannot resolve projected column '${columnName}' on source mutation table '${sourceTable.name}'.`
18462
+ `NQL runtime binding '${bindingName}' cannot resolve projected column '${columnName}' on source table '${sourceTable.name}'.`
17825
18463
  );
17826
18464
  }
17827
18465
  const dbType = column.originalDbType?.trim() || mapRuntimeBindingColumnType(column.type);
17828
18466
  if (dbType === void 0 || dbType.trim() === "") {
17829
18467
  throw new Error(
17830
- `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${columnName}' on source mutation table '${sourceTable.name}'.`
18468
+ `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${columnName}' on source table '${sourceTable.name}'.`
17831
18469
  );
17832
18470
  }
17833
18471
  const typeName = dbType.trim();
@@ -17850,7 +18488,7 @@ function resolveRuntimeBindingColumnTypes(name, binding, model, sourceTableName)
17850
18488
  const sourceTable = findRuntimeBindingSourceTable(model, sourceTableName);
17851
18489
  if (sourceTable === void 0) {
17852
18490
  throw new Error(
17853
- `NQL runtime binding '${name}' cannot resolve source mutation table '${sourceTableName}' in the model.`
18491
+ `NQL runtime binding '${name}' cannot resolve source table '${sourceTableName}' in the model.`
17854
18492
  );
17855
18493
  }
17856
18494
  return binding.columns.map(
@@ -17877,7 +18515,7 @@ function compileNqlRuntimeBindingCte(name, binding, naming, parameterOffset, sou
17877
18515
  const columnSql = binding.columns.map((column) => quoteIdent2(naming.toDatabase(column), "column")).join(", ");
17878
18516
  if (sourceTable === void 0) {
17879
18517
  throw new Error(
17880
- `NQL runtime binding '${name}' cannot materialize a typed relation because its source mutation table is unavailable.`
18518
+ `NQL runtime binding '${name}' cannot materialize a typed relation because its source table is unavailable.`
17881
18519
  );
17882
18520
  }
17883
18521
  const projectedColumns = binding.columns.map((column) => quoteIdent2(naming.toDatabase(column), "column")).join(", ");
@@ -18086,7 +18724,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18086
18724
  bundle.query.from,
18087
18725
  deps.naming
18088
18726
  );
18089
- const planReport = queryFromBinding ? createNqlBindingSelectPlan(bundle.query) : planFn2(bundle.query, this.requireNqlCompileModel(options), {
18727
+ const planReport = queryFromBinding ? bundle.plan ?? createNqlBindingSelectPlan(bundle.query) : planFn2(bundle.query, this.requireNqlCompileModel(options), {
18090
18728
  dialectCapabilities: options?.dialectCapabilities ?? this.dialectCapabilities
18091
18729
  });
18092
18730
  return guardCompiledQuery(
@@ -18152,7 +18790,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18152
18790
  runtimeBinding,
18153
18791
  naming,
18154
18792
  parameters.length,
18155
- bundle.mutationBindings?.get(name)?.table,
18793
+ runtimeBindingSourceTable(bundle, name),
18156
18794
  deps.schemaName,
18157
18795
  deps.model
18158
18796
  );
@@ -18180,6 +18818,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18180
18818
  }
18181
18819
  const leafBundle = {
18182
18820
  ...bundle.query !== void 0 && { query: bundle.query },
18821
+ ...bundle.plan !== void 0 && { plan: bundle.plan },
18183
18822
  ...bundle.cteQuery !== void 0 && { cteQuery: bundle.cteQuery },
18184
18823
  ...bundle.mutation !== void 0 && { mutation: bundle.mutation },
18185
18824
  ...bundle.returning !== void 0 && { returning: bundle.returning },