@dbsp/adapter-pgsql 1.6.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
@@ -663,9 +663,6 @@ function funcCall(name, args = [], options = {}) {
663
663
  function coalesceExpr(args) {
664
664
  return { CoalesceExpr: { args } };
665
665
  }
666
- function coalesce(...args) {
667
- return funcCall("coalesce", args);
668
- }
669
666
  function sortBy(expr, direction = "DEFAULT", nulls = "DEFAULT") {
670
667
  const sb = {
671
668
  node: expr,
@@ -4486,6 +4483,11 @@ var init_relation_filter = __esm({
4486
4483
 
4487
4484
  // src/subquery-emission.ts
4488
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
+ }
4489
4491
  function assertNoDroppedDecisionModifiers(decision, use) {
4490
4492
  const d = decision;
4491
4493
  const unsupported = [];
@@ -4604,8 +4606,9 @@ function buildPredicateSubquerySelect(use, sourceIntent, decision, ctx, state, d
4604
4606
  ],
4605
4607
  ...whereClause && { whereClause }
4606
4608
  };
4607
- if (decision.orderBy && decision.orderBy.length > 0) {
4608
- 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(
4609
4612
  (o) => sortBy(
4610
4613
  columnRef(o.column, targetAlias, void 0, ctx.naming),
4611
4614
  o.direction ?? "ASC",
@@ -5098,19 +5101,19 @@ var init_shared = __esm({
5098
5101
  });
5099
5102
 
5100
5103
  // src/handlers/include/json-agg.ts
5101
- import { toColumnList as toColumnList6 } from "@dbsp/types";
5104
+ import { resolveJsonAggOrderKey } from "@dbsp/types";
5105
+ function isJsonAggOrderBy(orderBy) {
5106
+ return Array.isArray(orderBy) && orderBy.every((item) => typeof item === "string");
5107
+ }
5102
5108
  function resolveJsonAggOrderBy(decision, targetTable, ctx) {
5103
5109
  const table = ctx.model?.getTable(targetTable);
5104
5110
  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
+ const orderKey = resolveJsonAggOrderKey(table);
5112
+ return orderKey.columns.length > 0 ? orderKey : void 0;
5111
5113
  }
5112
- return decision.targetPrimaryKey && decision.targetPrimaryKey.length > 0 ? {
5113
- columns: decision.targetPrimaryKey,
5114
+ const decisionOrderBy = isJsonAggOrderBy(decision.orderBy) ? decision.orderBy : void 0;
5115
+ return decisionOrderBy && decisionOrderBy.length > 0 ? {
5116
+ columns: decisionOrderBy,
5114
5117
  fallback: decision.orderByFallback === true
5115
5118
  } : void 0;
5116
5119
  }
@@ -5210,7 +5213,7 @@ var init_json_agg = __esm({
5210
5213
  });
5211
5214
 
5212
5215
  // src/handlers/include/lateral.ts
5213
- import { toColumnList as toColumnList7 } from "@dbsp/types";
5216
+ import { toColumnList as toColumnList6 } from "@dbsp/types";
5214
5217
  function buildLateralTargets(columns, alias, ctx) {
5215
5218
  if (columns && columns.length > 0 && !(columns.length === 1 && columns[0] === "*")) {
5216
5219
  return columns.map((col) => ({
@@ -5312,7 +5315,7 @@ var init_lateral = __esm({
5312
5315
  lateralIncludeHandler = {
5313
5316
  strategy: "lateral",
5314
5317
  compile(decision, ctx, state) {
5315
- const sourceColumn = toColumnList7(decision.sourceColumn);
5318
+ const sourceColumn = toColumnList6(decision.sourceColumn);
5316
5319
  if (sourceColumn.length === 0) {
5317
5320
  throw new Error(
5318
5321
  "Missing required column 'sourceColumn' in lateral include"
@@ -6267,6 +6270,7 @@ function buildRecursiveScalarSubquery(config) {
6267
6270
  pkColumn,
6268
6271
  fkColumn,
6269
6272
  outerAlias,
6273
+ outerSeedColumn,
6270
6274
  isAncestors,
6271
6275
  maxDepth,
6272
6276
  selectColumn,
@@ -6277,6 +6281,9 @@ function buildRecursiveScalarSubquery(config) {
6277
6281
  const dbPk = naming.toDatabase(pkColumn);
6278
6282
  const dbFk = naming.toDatabase(fkColumn);
6279
6283
  const dbOuter = naming.toDatabase(outerAlias);
6284
+ const dbOuterSeed = naming.toDatabase(
6285
+ outerSeedColumn ?? (isAncestors ? fkColumn : pkColumn)
6286
+ );
6280
6287
  const innerAlias = "__n";
6281
6288
  const anchorSelect = {
6282
6289
  targetList: [
@@ -6345,7 +6352,7 @@ function buildRecursiveScalarSubquery(config) {
6345
6352
  ColumnRef: {
6346
6353
  fields: [
6347
6354
  { String: { sval: dbOuter } },
6348
- { String: { sval: dbFk } }
6355
+ { String: { sval: dbOuterSeed } }
6349
6356
  ]
6350
6357
  }
6351
6358
  }
@@ -6365,7 +6372,7 @@ function buildRecursiveScalarSubquery(config) {
6365
6372
  ColumnRef: {
6366
6373
  fields: [
6367
6374
  { String: { sval: dbOuter } },
6368
- { String: { sval: dbPk } }
6375
+ { String: { sval: dbOuterSeed } }
6369
6376
  ]
6370
6377
  }
6371
6378
  }
@@ -6429,15 +6436,7 @@ function buildRecursiveScalarSubquery(config) {
6429
6436
  }
6430
6437
  ],
6431
6438
  fromClause: [
6432
- // FROM __rc
6433
- {
6434
- RangeVar: {
6435
- relname: cteAlias,
6436
- inh: true,
6437
- relpersistence: "p"
6438
- }
6439
- },
6440
- // INNER JOIN table AS __n
6439
+ // FROM __rc INNER JOIN table AS __n
6441
6440
  {
6442
6441
  JoinExpr: {
6443
6442
  jointype: "JOIN_INNER",
@@ -6520,7 +6519,7 @@ function buildRecursiveScalarSubquery(config) {
6520
6519
  },
6521
6520
  integerNode(maxDepth)
6522
6521
  ),
6523
- // pk <> ALL(__visited) cycle detection
6522
+ // pk <> ALL(__visited) - cycle detection
6524
6523
  {
6525
6524
  A_Expr: {
6526
6525
  kind: "AEXPR_OP_ALL",
@@ -6565,20 +6564,35 @@ function buildRecursiveScalarSubquery(config) {
6565
6564
  targetList: [
6566
6565
  {
6567
6566
  ResTarget: {
6568
- val: coalesce(
6569
- funcCall("json_agg", [
6570
- {
6571
- ColumnRef: {
6572
- fields: [
6573
- { String: { sval: cteAlias } },
6574
- { String: { sval: dbSelectCol } }
6575
- ]
6567
+ val: coalesceExpr([
6568
+ funcCall(
6569
+ "json_agg",
6570
+ [
6571
+ {
6572
+ ColumnRef: {
6573
+ fields: [
6574
+ { String: { sval: cteAlias } },
6575
+ { String: { sval: dbSelectCol } }
6576
+ ]
6577
+ }
6576
6578
  }
6579
+ ],
6580
+ {
6581
+ orderBy: [
6582
+ sortBy({
6583
+ ColumnRef: {
6584
+ fields: [
6585
+ { String: { sval: cteAlias } },
6586
+ { String: { sval: "__depth" } }
6587
+ ]
6588
+ }
6589
+ })
6590
+ ]
6577
6591
  }
6578
- ]),
6592
+ ),
6579
6593
  // Empty array fallback: '[]'::json
6580
- typeCast(stringNode("[]"), "json")
6581
- )
6594
+ typeCast({ A_Const: { sval: { sval: "[]" } } }, "json")
6595
+ ])
6582
6596
  }
6583
6597
  }
6584
6598
  ],
@@ -7037,6 +7051,11 @@ var init_relation = __esm({
7037
7051
  });
7038
7052
 
7039
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
+ }
7040
7059
  function buildSortBy(column, direction, ctx) {
7041
7060
  const tableAlias = ctx.currentAlias ?? ctx.rootTable;
7042
7061
  const colRef = columnRef(column, tableAlias, void 0, ctx.naming);
@@ -7049,7 +7068,7 @@ function buildSortBy(column, direction, ctx) {
7049
7068
  }
7050
7069
  function buildWindowDef(decision, ctx) {
7051
7070
  const partition = decision.partition;
7052
- const orderBy = decision.orderBy;
7071
+ const orderBy = isWindowOrderBy(decision.orderBy) ? decision.orderBy : void 0;
7053
7072
  const frame = decision.frame;
7054
7073
  const tableAlias = ctx.currentAlias ?? ctx.rootTable;
7055
7074
  const windowDef = { frameOptions: WINDOW_FRAME_DEFAULT };
@@ -7313,7 +7332,7 @@ import {
7313
7332
  isParamIntent as isParamIntent6,
7314
7333
  NQL_SELECT_SCALAR_FUNCTION_ALLOWLIST,
7315
7334
  NQL_SELECT_WINDOW_FUNCTION_ALLOWLIST,
7316
- toColumnList as toColumnList8
7335
+ toColumnList as toColumnList7
7317
7336
  } from "@dbsp/types";
7318
7337
  import {
7319
7338
  getNqlBindingRefName,
@@ -8379,6 +8398,7 @@ init_case_value();
8379
8398
  init_custom();
8380
8399
  init_expression();
8381
8400
  init_param_value();
8401
+ init_pseudo();
8382
8402
  init_window();
8383
8403
  init_include();
8384
8404
  init_shared();
@@ -8425,7 +8445,7 @@ function trustedRelationHasMultipleHops(relation) {
8425
8445
  if (typeof relation !== "string") return relation.length > 1;
8426
8446
  return relation.split(".").length > 1;
8427
8447
  }
8428
- function isJsonAggOrderBy(orderBy) {
8448
+ function isJsonAggOrderBy2(orderBy) {
8429
8449
  return Array.isArray(orderBy) && orderBy.every((item) => typeof item === "string");
8430
8450
  }
8431
8451
  function isExpressionOrderBy(orderBy) {
@@ -8434,8 +8454,12 @@ function isExpressionOrderBy(orderBy) {
8434
8454
  );
8435
8455
  }
8436
8456
  function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8437
- const jsonAggOrderBy = isJsonAggOrderBy(pd.orderBy) ? pd.orderBy : void 0;
8457
+ const jsonAggOrderBy = isJsonAggOrderBy2(pd.orderBy) ? pd.orderBy : void 0;
8438
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
+ }));
8439
8463
  const derivedFkColumns = deriveFkColumns(
8440
8464
  pd,
8441
8465
  pd.sourceTable ?? rootTable,
@@ -8470,7 +8494,6 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8470
8494
  relationType: pd.relationType,
8471
8495
  foreignKey: pd.foreignKey,
8472
8496
  parentKey: pd.parentKey,
8473
- targetPrimaryKey: pd.targetPrimaryKey ?? jsonAggOrderBy,
8474
8497
  orderByFallback: pd.orderByFallback,
8475
8498
  dataType: pd.dataType,
8476
8499
  traversal: pd.traversal,
@@ -8486,10 +8509,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
8486
8509
  include: pd.include?.map(
8487
8510
  (c) => mapToHandlerDecision(c, rootTable, defaultPk, deriveFk)
8488
8511
  ),
8489
- orderBy: expressionOrderBy?.map((o) => ({
8490
- column: o.field,
8491
- direction: o.direction?.toUpperCase() ?? "ASC"
8492
- })),
8512
+ orderBy: handlerOrderBy,
8493
8513
  partition: pd.partitionBy,
8494
8514
  jsonPath: pd.jsonPath,
8495
8515
  jsonMode: pd.jsonMode,
@@ -9048,8 +9068,11 @@ var PlanCompiler = class _PlanCompiler {
9048
9068
  isNqlBindingRoot(plan) {
9049
9069
  return hasBindingName(this.bindingNames, plan.rootTable, this.naming);
9050
9070
  }
9071
+ allocateBindingRelationAlias() {
9072
+ return `rc_${this.bindingRelationColumnSubqueryIndex++}`;
9073
+ }
9051
9074
  buildCorrelatedRelationRefs(fields, plan) {
9052
- const relatedAlias = `rc_${this.bindingRelationColumnSubqueryIndex++}`;
9075
+ const relatedAlias = this.allocateBindingRelationAlias();
9053
9076
  const relatedTable = rangeVar(
9054
9077
  fields.targetTable,
9055
9078
  relatedAlias,
@@ -9068,12 +9091,93 @@ var PlanCompiler = class _PlanCompiler {
9068
9091
  relatedColumn
9069
9092
  };
9070
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"
9130
+ );
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
+ }
9071
9151
  compileBindingRelationColumnSubquery(fields, plan, dialectCapabilities) {
9072
9152
  if (fields.selectedColumn === void 0) {
9073
9153
  throw new Error(
9074
9154
  `NQL binding relation-column proof for '${plan.rootTable}' is missing selectedColumn.`
9075
9155
  );
9076
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
+ );
9176
+ }
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
+ }
9077
9181
  if (fields.cardinality === "one") {
9078
9182
  const { relatedAlias, relatedTable, relatedColumn } = this.buildCorrelatedRelationRefs(fields, plan);
9079
9183
  if (fields.hops.length === 0 && trustedRelationHasMultipleHops(fields.relation)) {
@@ -9152,9 +9256,14 @@ var PlanCompiler = class _PlanCompiler {
9152
9256
  };
9153
9257
  }
9154
9258
  if (fields.cardinality === "many") {
9155
- if (fields.relationType !== "hasMany") {
9259
+ if (fields.relationType !== "hasMany" && fields.relationType !== "manyToMany" && fields.relationType !== "belongsToMany") {
9156
9260
  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).`
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).`
9158
9267
  );
9159
9268
  }
9160
9269
  if (dialectCapabilities?.supportsJsonAgg === false) {
@@ -9163,6 +9272,37 @@ var PlanCompiler = class _PlanCompiler {
9163
9272
  );
9164
9273
  }
9165
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
+ );
9166
9306
  return {
9167
9307
  SubLink: {
9168
9308
  subLinkType: "EXPR_SUBLINK",
@@ -9185,14 +9325,8 @@ var PlanCompiler = class _PlanCompiler {
9185
9325
  }
9186
9326
  }
9187
9327
  ],
9188
- from: [relatedTable],
9189
- where: buildKeyCorrelation(
9190
- relatedAlias,
9191
- fields.targetColumn,
9192
- plan.rootTable,
9193
- fields.sourceColumn,
9194
- this.createHandlerContext(plan)
9195
- )
9328
+ from: [fromNode],
9329
+ where: whereNode
9196
9330
  })
9197
9331
  }
9198
9332
  };
@@ -10357,8 +10491,8 @@ var PlanCompiler = class _PlanCompiler {
10357
10491
  this.schemaForRangeVar(plan, decision.targetTable ?? ""),
10358
10492
  this.naming
10359
10493
  );
10360
- const sourceColumn = toColumnList8(decision.sourceColumn);
10361
- const targetColumn = toColumnList8(decision.targetColumn);
10494
+ const sourceColumn = toColumnList7(decision.sourceColumn);
10495
+ const targetColumn = toColumnList7(decision.targetColumn);
10362
10496
  if (sourceColumn.length === 0) {
10363
10497
  throw new Error("Missing required column 'sourceColumn' in compileJoin");
10364
10498
  }
@@ -10797,6 +10931,12 @@ function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
10797
10931
  }
10798
10932
  return `${parts.join(" ")};`;
10799
10933
  }
10934
+ function failSafeUnknownDownChange(kind) {
10935
+ return {
10936
+ sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${kind}"`,
10937
+ destructive: true
10938
+ };
10939
+ }
10800
10940
  function isChangeSupported(kind, caps) {
10801
10941
  switch (kind) {
10802
10942
  case "create_enum":
@@ -11249,188 +11389,381 @@ function changeToUpSQL(change, schemaName) {
11249
11389
  function changeToDownSQL(change, schemaName) {
11250
11390
  switch (change.kind) {
11251
11391
  case "create_table":
11252
- 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
+ };
11253
11396
  case "drop_table":
11254
- 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
+ };
11255
11401
  case "add_column":
11256
- 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
+ };
11257
11406
  case "drop_column":
11258
- 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
+ };
11259
11411
  case "alter_column_type": {
11260
11412
  const fromType = change.meta?.fromType;
11261
11413
  if (!fromType) {
11262
- 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
+ };
11263
11418
  }
11264
- 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
+ };
11265
11423
  }
11266
11424
  case "alter_column_nullable": {
11267
11425
  const oldNullable = change.meta?.oldNullable;
11268
11426
  if (oldNullable === void 0) {
11269
- 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
+ };
11270
11431
  }
11271
11432
  const action = oldNullable ? "DROP NOT NULL" : "SET NOT NULL";
11272
- 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
+ };
11273
11438
  }
11274
11439
  case "alter_column_default": {
11275
11440
  const oldDefault = change.meta?.oldDefault;
11276
11441
  if (oldDefault === void 0) {
11277
- 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
+ };
11278
11446
  }
11279
11447
  if (oldDefault === null) {
11280
- 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
+ };
11281
11453
  }
11282
- 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
+ };
11283
11459
  }
11284
11460
  case "add_primary_key": {
11285
- 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
+ };
11286
11480
  }
11287
- case "drop_primary_key":
11288
- return `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`;
11289
11481
  case "add_foreign_key": {
11290
11482
  const fk = change.meta?.fk;
11291
- if (!fk) return void 0;
11483
+ if (!fk) return { sql: void 0, destructive: true };
11292
11484
  const constraintName = quoteIdent2(
11293
11485
  fkName(change.table, fk.columns),
11294
11486
  "alias"
11295
11487
  );
11296
- 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
+ };
11297
11506
  }
11298
- case "drop_foreign_key":
11299
- return `-- WARNING: Cannot reverse drop_foreign_key "${change.table}" -- FK definition was lost`;
11300
11507
  case "alter_foreign_key": {
11301
11508
  const oldFk = change.meta?.oldFk;
11302
- if (!oldFk) {
11303
- return `-- WARNING: Cannot reverse alter_foreign_key "${change.table}" -- missing migration metadata`;
11304
- }
11305
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
+ }
11306
11516
  const constraintName = quoteIdent2(
11307
11517
  fkName(change.table, fk.columns),
11308
11518
  "alias"
11309
11519
  );
11310
11520
  const drop = `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${constraintName};`;
11311
11521
  const add = generateAddFKSQL(change.table, oldFk, schemaName);
11312
- return `${drop}
11313
- ${add}`;
11522
+ return { sql: `${drop}
11523
+ ${add}`, destructive: false };
11314
11524
  }
11315
11525
  case "create_index": {
11316
11526
  const idx = change.meta?.index;
11317
- if (!idx) return void 0;
11527
+ if (!idx) return { sql: void 0, destructive: true };
11318
11528
  const indexName = quoteIdent2(
11319
11529
  idxName(change.table, idx.columns, idx.name),
11320
11530
  "alias"
11321
11531
  );
11322
11532
  const schemaPrefix = schemaName ? `${quoteIdent2(schemaName, "alias")}.` : "";
11323
- 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
+ };
11324
11551
  }
11325
- case "drop_index":
11326
- return `-- WARNING: Cannot reverse drop_index "${change.table}" -- index definition was lost`;
11327
11552
  case "add_check_constraint": {
11328
11553
  const check = change.meta?.check;
11329
- if (!check) return void 0;
11330
- 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
+ };
11331
11559
  }
11332
11560
  case "drop_check_constraint": {
11333
11561
  const check = change.meta?.check;
11334
- if (!check) return void 0;
11562
+ if (!check) return { sql: void 0, destructive: true };
11335
11563
  validateSqlExpression(
11336
11564
  check.expression,
11337
11565
  "migration check constraint (down)"
11338
11566
  );
11339
- 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
+ };
11340
11572
  }
11341
11573
  case "create_enum": {
11342
11574
  const enumDef = change.meta?.enum;
11343
- if (!enumDef) return void 0;
11575
+ if (!enumDef) return { sql: void 0, destructive: true };
11344
11576
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
11345
- return `DROP TYPE IF EXISTS ${enumName} CASCADE;`;
11577
+ return {
11578
+ sql: `DROP TYPE IF EXISTS ${enumName} CASCADE;`,
11579
+ destructive: true
11580
+ };
11346
11581
  }
11347
11582
  case "drop_enum": {
11348
11583
  const enumDef = change.meta?.enum;
11349
- if (!enumDef) return void 0;
11584
+ if (!enumDef) return { sql: void 0, destructive: true };
11350
11585
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
11351
11586
  const values = enumDef.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
11352
- 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
+ };
11353
11592
  }
11354
11593
  case "alter_enum_add_value":
11355
- 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
+ };
11356
11598
  case "alter_column_collation": {
11357
- 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
+ };
11358
11603
  }
11359
11604
  case "alter_column_identity": {
11360
11605
  const col = change.meta?.column;
11361
11606
  const prevIdentity = change.meta?.previousIdentity;
11362
- if (!col) return void 0;
11607
+ if (!col) return { sql: void 0, destructive: true };
11363
11608
  const table = qualifyTable(change.table, schemaName);
11364
11609
  const column = quoteIdent2(change.column, "alias");
11365
11610
  if (prevIdentity && !col.identity) {
11366
11611
  const gen2 = prevIdentity === "always" ? "ALWAYS" : "BY DEFAULT";
11367
- 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
+ };
11368
11617
  }
11369
11618
  if (!prevIdentity && col.identity) {
11370
- 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
+ };
11371
11629
  }
11372
11630
  const gen = prevIdentity === "always" ? "ALWAYS" : "BY DEFAULT";
11373
- 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
+ };
11374
11636
  }
11375
11637
  case "add_comment": {
11376
11638
  const target = change.meta?.target;
11377
11639
  if (target === "table") {
11378
- 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
+ };
11379
11644
  }
11380
- 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
+ };
11381
11649
  }
11382
11650
  case "drop_comment": {
11383
- 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
+ };
11384
11674
  }
11385
11675
  case "create_extension": {
11386
11676
  const ext = change.meta?.extension;
11387
- if (ext == null) return void 0;
11388
- 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
+ };
11389
11682
  }
11390
11683
  case "drop_extension": {
11391
11684
  const ext = change.meta?.extension;
11392
- if (ext == null) return void 0;
11393
- 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
+ };
11394
11691
  }
11395
11692
  case "create_sequence": {
11396
11693
  const seq = change.meta?.sequence;
11397
- if (!seq) return void 0;
11694
+ if (!seq) return { sql: void 0, destructive: true };
11398
11695
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(seq.name, "alias")}` : quoteIdent2(seq.name, "alias");
11399
- return `DROP SEQUENCE IF EXISTS ${seqName} CASCADE;`;
11696
+ return {
11697
+ sql: `DROP SEQUENCE IF EXISTS ${seqName} CASCADE;`,
11698
+ destructive: true
11699
+ };
11400
11700
  }
11401
11701
  case "alter_sequence": {
11402
11702
  const prevSeq = change.meta?.previousSequence;
11403
11703
  if (!prevSeq) {
11404
- 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
+ };
11405
11708
  }
11406
11709
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(prevSeq.name, "alias")}` : quoteIdent2(prevSeq.name, "alias");
11407
- 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
+ };
11408
11715
  }
11409
11716
  case "drop_sequence": {
11410
11717
  const seq = change.meta?.sequence;
11411
- if (!seq) return void 0;
11718
+ if (!seq) return { sql: void 0, destructive: true };
11412
11719
  const seqName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(seq.name, "alias")}` : quoteIdent2(seq.name, "alias");
11413
- 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
+ };
11414
11725
  }
11415
11726
  case "validate_constraint":
11416
- 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
+ };
11417
11731
  case "enable_rls":
11418
- 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
+ };
11419
11736
  case "disable_rls":
11420
- 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
+ };
11421
11742
  case "create_policy": {
11422
11743
  const policy = change.meta?.policy;
11423
- if (!policy) return void 0;
11424
- 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
+ };
11425
11749
  }
11426
11750
  case "drop_policy": {
11427
11751
  const policy = change.meta?.policy;
11428
- if (!policy) return void 0;
11429
- 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
+ };
11430
11758
  }
11759
+ default:
11760
+ return failSafeUnknownDownChange(change.kind);
11431
11761
  }
11432
11762
  }
11433
11763
  function generateDownSQL(diff, options) {
11764
+ return generateDownMigrationSQL(diff, options).statements;
11765
+ }
11766
+ function generateDownMigrationSQL(diff, options) {
11434
11767
  const schemaName = options?.schemaName;
11435
11768
  const includeDestructive = options?.includeDestructive ?? true;
11436
11769
  const changes = includeDestructive ? diff.changes : diff.changes.filter((c) => !c.destructive);
@@ -11479,13 +11812,15 @@ function generateDownSQL(diff, options) {
11479
11812
  phases[phase].push(change);
11480
11813
  }
11481
11814
  const statements = [];
11815
+ let destructive = false;
11482
11816
  for (let i = phases.length - 1; i >= 0; i--) {
11483
11817
  for (const change of phases[i]) {
11484
- const sql = changeToDownSQL(change, schemaName);
11485
- if (sql) statements.push(sql);
11818
+ const down = changeToDownSQL(change, schemaName);
11819
+ destructive = down.destructive || destructive;
11820
+ if (down.sql) statements.push(down.sql);
11486
11821
  }
11487
11822
  }
11488
- return statements;
11823
+ return { statements, destructive };
11489
11824
  }
11490
11825
  function generateCreateTableSQL(table, schemaName) {
11491
11826
  const qualTable = qualifyTable(table.name, schemaName);
@@ -11778,29 +12113,36 @@ function generateCreatePolicy(tableName, policy, schemaName, naming) {
11778
12113
  // src/ddl/migration-file.ts
11779
12114
  function generateMigrationFile(diff, options) {
11780
12115
  const upStatements = generateMigrationSQL(diff, options);
11781
- 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"}`;
11782
12119
  const header = options?.name ? `-- Migration: ${options.name}
11783
12120
  -- Generated by: dbsp migrate dev
11784
12121
  -- Date: ${(/* @__PURE__ */ new Date()).toISOString()}
12122
+ ${destructiveHeader}
11785
12123
 
11786
- ` : "";
12124
+ ` : `${destructiveHeader}
12125
+
12126
+ `;
11787
12127
  const upSection = upStatements.length > 0 ? `${upStatements.join(";\n\n")};
11788
12128
  ` : "";
11789
12129
  const downSection = downStatements.length > 0 ? `${downStatements.join(";\n\n")};
11790
12130
  ` : "";
11791
- return `${header}${upSection}
11792
- -- DOWN
12131
+ const separatorPrefix = upSection.length > 0 ? "\n" : "";
12132
+ return `${header}${upSection}${separatorPrefix}-- DOWN
11793
12133
 
11794
12134
  ${downSection}`;
11795
12135
  }
11796
12136
  function parseMigrationFile(content) {
12137
+ const destructive = parseDestructiveHeader(content);
11797
12138
  const separatorRegex = /^\s*-- DOWN\s*$/m;
11798
12139
  const match = separatorRegex.exec(content);
11799
12140
  if (!match) {
11800
12141
  return {
11801
12142
  upStatements: splitStatements(content),
11802
12143
  downStatements: [],
11803
- hasDown: false
12144
+ hasDown: false,
12145
+ destructive
11804
12146
  };
11805
12147
  }
11806
12148
  const upContent = content.slice(0, match.index);
@@ -11808,9 +12150,36 @@ function parseMigrationFile(content) {
11808
12150
  return {
11809
12151
  upStatements: splitStatements(upContent),
11810
12152
  downStatements: splitStatements(downContent),
11811
- hasDown: true
12153
+ hasDown: true,
12154
+ destructive
11812
12155
  };
11813
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
+ }
11814
12183
  function isDestructiveDown(downStatements) {
11815
12184
  const destructivePatterns = [
11816
12185
  /DROP\s+TABLE/i,
@@ -12933,6 +13302,7 @@ function vectorDims(column) {
12933
13302
  // src/introspection.ts
12934
13303
  init_assert_field();
12935
13304
  import { ModelIRImpl } from "@dbsp/core";
13305
+ import { buildRelationKeyFields } from "@dbsp/types";
12936
13306
  function parseExpressionsList(raw) {
12937
13307
  const results = [];
12938
13308
  let depth = 0;
@@ -13488,9 +13858,10 @@ function inferRelations(fksByConstraint, filteredTables) {
13488
13858
  for (const [, fk] of fksByConstraint) {
13489
13859
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
13490
13860
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
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;
13861
+ const relationFk = {
13862
+ columns: fk.cols,
13863
+ references: { table: fk.target, columns: fk.refs }
13864
+ };
13494
13865
  const belongsToKey = `${fk.source}.${belongsToName}`;
13495
13866
  if (!relations.has(belongsToKey)) {
13496
13867
  relations.set(belongsToKey, {
@@ -13498,8 +13869,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13498
13869
  type: "belongsTo",
13499
13870
  source: fk.source,
13500
13871
  target: fk.target,
13501
- foreignKey: fkCol,
13502
- ...keyOverride !== void 0 && { targetKey: keyOverride },
13872
+ ...buildRelationKeyFields(relationFk, "belongsTo"),
13503
13873
  cardinality: "one",
13504
13874
  optionality: "optional",
13505
13875
  includeStrategy: "auto",
@@ -13515,8 +13885,7 @@ function inferRelations(fksByConstraint, filteredTables) {
13515
13885
  type: "hasMany",
13516
13886
  source: fk.target,
13517
13887
  target: fk.source,
13518
- foreignKey: fkCol,
13519
- ...keyOverride !== void 0 && { sourceKey: keyOverride },
13888
+ ...buildRelationKeyFields(relationFk, "inverse"),
13520
13889
  cardinality: "many",
13521
13890
  optionality: "optional",
13522
13891
  includeStrategy: "auto",
@@ -14419,7 +14788,7 @@ import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNql
14419
14788
 
14420
14789
  // src/adapter-compiler-includes.ts
14421
14790
  init_ast_helpers();
14422
- import { toColumnList as toColumnList9 } from "@dbsp/types";
14791
+ import { toColumnList as toColumnList8 } from "@dbsp/types";
14423
14792
  init_handlers();
14424
14793
  function compileSubqueryInclude(info, parentIds, _options, deps) {
14425
14794
  const schemaName = deps.schemaName;
@@ -14431,7 +14800,7 @@ function compileSubqueryInclude(info, parentIds, _options, deps) {
14431
14800
  parameters: []
14432
14801
  };
14433
14802
  }
14434
- const fkColumns = toColumnList9(info.foreignKey);
14803
+ const fkColumns = toColumnList8(info.foreignKey);
14435
14804
  if (fkColumns.length === 0) {
14436
14805
  throw new Error(
14437
14806
  "Subquery include requires at least one foreignKey column."
@@ -14529,7 +14898,7 @@ function compileSubqueryIncludeManyToMany(info, parentIds, schemaName, state, de
14529
14898
  const throughTable = info.through;
14530
14899
  const throughSourceKey = info.throughSourceKey;
14531
14900
  const throughTargetKey = info.throughTargetKey;
14532
- const targetPkColumns = toColumnList9(info.sourceKey);
14901
+ const targetPkColumns = toColumnList8(info.sourceKey);
14533
14902
  if (targetPkColumns.length !== 1) {
14534
14903
  throw new Error(
14535
14904
  `Many-to-many subquery include requires a single-column target key; got ${JSON.stringify(targetPkColumns)}.`
@@ -14624,7 +14993,7 @@ import {
14624
14993
  POSTGRESQL_CAPABILITIES,
14625
14994
  plan as planFn
14626
14995
  } from "@dbsp/core";
14627
- import { toColumnList as toColumnList12 } from "@dbsp/types";
14996
+ import { toColumnList as toColumnList11 } from "@dbsp/types";
14628
14997
 
14629
14998
  // src/adapter-compiler-select.ts
14630
14999
  init_assert_field();
@@ -14632,7 +15001,7 @@ init_ast_helpers();
14632
15001
  init_binding_registry();
14633
15002
  init_compile_where();
14634
15003
  import { countDistinctRelationPathsByName } from "@dbsp/core";
14635
- import { toColumnList as toColumnList11 } from "@dbsp/types";
15004
+ import { toColumnList as toColumnList10 } from "@dbsp/types";
14636
15005
  init_compiler_utils();
14637
15006
  init_types();
14638
15007
  init_intent_to_decisions();
@@ -14642,7 +15011,7 @@ init_param_ref();
14642
15011
  init_assert_field();
14643
15012
  init_intent_to_decisions();
14644
15013
  import { deriveRelationPathFromIntentPath } from "@dbsp/core";
14645
- import { toColumnList as toColumnList10 } from "@dbsp/types";
15014
+ import { toColumnList as toColumnList9 } from "@dbsp/types";
14646
15015
  function findExistsIntents(where) {
14647
15016
  if (!where || typeof where !== "object") return [];
14648
15017
  const w = where;
@@ -14663,7 +15032,7 @@ function findExistsIntents(where) {
14663
15032
  function resolveRelation(model, sourceTable, relationName) {
14664
15033
  const rel = model.getRelation(`${sourceTable}.${relationName}`);
14665
15034
  if (!rel) return void 0;
14666
- const foreignKeyColumns = toColumnList10(rel.foreignKey);
15035
+ const foreignKeyColumns = toColumnList9(rel.foreignKey);
14667
15036
  const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14668
15037
  const relationType = rel.type;
14669
15038
  return { target: rel.target, foreignKey, relationType };
@@ -14887,7 +15256,7 @@ function buildMultiHopExistsChain(hops, rootTable, outerOperator, innerCondition
14887
15256
  const hopRelations = [];
14888
15257
  for (const hop of hops) {
14889
15258
  const rel = model.getRelation(`${currentSource}.${hop}`);
14890
- const foreignKeyColumns = rel ? toColumnList10(rel.foreignKey) : [];
15259
+ const foreignKeyColumns = rel ? toColumnList9(rel.foreignKey) : [];
14891
15260
  const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
14892
15261
  const relationType = rel?.type === "belongsTo" ? "belongsTo" : rel?.type === "hasMany" || rel?.type === "hasOne" ? rel.type : void 0;
14893
15262
  const parentKey = relationType === "belongsTo" ? rel?.targetKey : rel?.sourceKey;
@@ -14998,7 +15367,7 @@ function enrichExistsStubsInConditions(conditions, sourceTable, model) {
14998
15367
  `exists('${relation}'): no relation '${relation}' is declared on table '${sourceTable}'. Use rawExists(subquery(...)) for an EXISTS over an undeclared or uncorrelated subquery.`
14999
15368
  );
15000
15369
  }
15001
- const foreignKeyColumns = toColumnList10(rel.foreignKey);
15370
+ const foreignKeyColumns = toColumnList9(rel.foreignKey);
15002
15371
  const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
15003
15372
  const relationType = rel.type === "belongsTo" ? "belongsTo" : rel.type === "hasMany" || rel.type === "hasOne" ? rel.type : void 0;
15004
15373
  const parentKey = relationType === "belongsTo" ? rel.targetKey : rel.sourceKey;
@@ -15058,7 +15427,7 @@ function buildEnrichedExistsDecision(d, matchingIntent, rootTable, model) {
15058
15427
  const relIR = model && context.relation ? model.getRelation(
15059
15428
  `${sourceTableForRelation}.${context.relation}`
15060
15429
  ) : void 0;
15061
- const foreignKeyColumns = relIR ? toColumnList10(relIR.foreignKey) : [];
15430
+ const foreignKeyColumns = relIR ? toColumnList9(relIR.foreignKey) : [];
15062
15431
  const foreignKey = foreignKeyColumns.length > 0 ? foreignKeyColumns : void 0;
15063
15432
  const relationType = relIR?.type === "belongsTo" ? "belongsTo" : relIR?.type === "hasMany" || relIR?.type === "hasOne" ? relIR.type : void 0;
15064
15433
  const parentKey = relationType === "belongsTo" ? relIR?.targetKey : relIR?.sourceKey;
@@ -15395,10 +15764,7 @@ function toIncludeDecision(d, choice, plan, defaultPk = DEFAULT_PK_COLUMN, deriv
15395
15764
  ...relationType && { relationType },
15396
15765
  foreignKey,
15397
15766
  parentKey,
15398
- ...context.targetPrimaryKey && context.targetPrimaryKey.length > 0 ? {
15399
- targetPrimaryKey: context.targetPrimaryKey,
15400
- orderBy: context.targetPrimaryKey
15401
- } : {},
15767
+ ...context.targetOrderKey && context.targetOrderKey.length > 0 ? { orderBy: context.targetOrderKey } : {},
15402
15768
  ...context.orderByFallback ? { orderByFallback: true } : {},
15403
15769
  ...context.intentPath && { intentPath: context.intentPath },
15404
15770
  ...limit != null && { limit }
@@ -15470,7 +15836,7 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15470
15836
  (r) => r.name === alias || snakeToCamel(r.name) === alias
15471
15837
  );
15472
15838
  if (!rel) continue;
15473
- const rawFkColumns = toColumnList10(rel.foreignKey);
15839
+ const rawFkColumns = toColumnList9(rel.foreignKey);
15474
15840
  const rawFk = rawFkColumns.length > 0 ? rawFkColumns : [
15475
15841
  deriveFk(
15476
15842
  rel.type === "belongsTo" ? rel.target : sourceTable,
@@ -15498,7 +15864,7 @@ function synthesizeMissingJoinDecisions(plan, coveredRelations, model, defaultPk
15498
15864
  relationType: rel.type
15499
15865
  },
15500
15866
  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],
15867
+ parentKey: rel.type === "belongsTo" ? toColumnList9(rel.targetKey).length > 0 ? toColumnList9(rel.targetKey) : [defaultPk] : toColumnList9(rel.sourceKey).length > 0 ? toColumnList9(rel.sourceKey) : [defaultPk],
15502
15868
  columns,
15503
15869
  joinType: inc.join,
15504
15870
  ...conditions && { conditions }
@@ -15655,10 +16021,10 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
15655
16021
  );
15656
16022
  }
15657
16023
  const isBelongsTo = rel.type === "belongsTo";
15658
- const rawFk = toColumnList11(rel.foreignKey);
16024
+ const rawFk = toColumnList10(rel.foreignKey);
15659
16025
  const fkColumns = rawFk.length > 0 ? rawFk : [deriveFk(isBelongsTo ? rootTable : rel.target, defaultPk)];
15660
- const sourceKey = toColumnList11(rel.sourceKey);
15661
- const targetKey = toColumnList11(rel.targetKey);
16026
+ const sourceKey = toColumnList10(rel.sourceKey);
16027
+ const targetKey = toColumnList10(rel.targetKey);
15662
16028
  const sourceColumn = isBelongsTo ? fkColumns : sourceKey.length > 0 ? sourceKey : [defaultPk];
15663
16029
  const targetColumn = isBelongsTo ? targetKey.length > 0 ? targetKey : [defaultPk] : fkColumns;
15664
16030
  const alias = intent.alias ?? intent.relation;
@@ -16023,8 +16389,8 @@ function compileWithIncludes(plan, options, deps) {
16023
16389
  const relationName = ctx.includeAlias ?? ctx.relation;
16024
16390
  if (!relationName) continue;
16025
16391
  const rawFk = deriveForeignKey(ctx, deps.deriveFk, deps.defaultPk) ?? deps.defaultPk;
16026
- const fk = toColumnList11(rawFk);
16027
- const parentKey = toColumnList11(ctx.parentKey);
16392
+ const fk = toColumnList10(rawFk);
16393
+ const parentKey = toColumnList10(ctx.parentKey);
16028
16394
  const isBelongsTo = ctx.relationType === "belongsTo";
16029
16395
  const sourceKey = isBelongsTo ? fk : parentKey.length > 0 ? parentKey : [deps.defaultPk];
16030
16396
  const targetFk = isBelongsTo ? parentKey.length > 0 ? parentKey : [deps.defaultPk] : fk;
@@ -16090,7 +16456,7 @@ function prependSourceCte(sql, parameters, sourceName, sourceCte, naming) {
16090
16456
  };
16091
16457
  }
16092
16458
  function resolveMutationExistsForeignKey(foreignKey, relationName) {
16093
- const columns = toColumnList12(foreignKey);
16459
+ const columns = toColumnList11(foreignKey);
16094
16460
  if (columns.length === 0) return void 0;
16095
16461
  if (columns.some((column) => column.length === 0)) {
16096
16462
  throw new Error(
@@ -16110,13 +16476,13 @@ function resolveExistsRelation(sourceTable, relation, model) {
16110
16476
  return {
16111
16477
  targetTable,
16112
16478
  ...fk2 !== void 0 && { sourceColumn: fk2 },
16113
- targetColumn: toColumnList12(rel.targetKey).length > 0 ? toColumnList12(rel.targetKey) : ["id"]
16479
+ targetColumn: toColumnList11(rel.targetKey).length > 0 ? toColumnList11(rel.targetKey) : ["id"]
16114
16480
  };
16115
16481
  }
16116
16482
  const fk = resolveMutationExistsForeignKey(rel.foreignKey, relationName);
16117
16483
  return {
16118
16484
  targetTable,
16119
- sourceColumn: toColumnList12(rel.sourceKey).length > 0 ? toColumnList12(rel.sourceKey) : ["id"],
16485
+ sourceColumn: toColumnList11(rel.sourceKey).length > 0 ? toColumnList11(rel.sourceKey) : ["id"],
16120
16486
  ...fk !== void 0 && { targetColumn: fk }
16121
16487
  };
16122
16488
  }
@@ -18038,6 +18404,9 @@ function orderedNqlBindingNames(bundle) {
18038
18404
  }
18039
18405
  return names;
18040
18406
  }
18407
+ function runtimeBindingSourceTable(bundle, name) {
18408
+ return bundle.mutationBindings?.get(name)?.table ?? bundle.bindings?.get(name)?.from;
18409
+ }
18041
18410
  function mapRuntimeBindingColumnType(type) {
18042
18411
  switch (type) {
18043
18412
  case "string":
@@ -18090,13 +18459,13 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
18090
18459
  );
18091
18460
  if (column === void 0) {
18092
18461
  throw new Error(
18093
- `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}'.`
18094
18463
  );
18095
18464
  }
18096
18465
  const dbType = column.originalDbType?.trim() || mapRuntimeBindingColumnType(column.type);
18097
18466
  if (dbType === void 0 || dbType.trim() === "") {
18098
18467
  throw new Error(
18099
- `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}'.`
18100
18469
  );
18101
18470
  }
18102
18471
  const typeName = dbType.trim();
@@ -18119,7 +18488,7 @@ function resolveRuntimeBindingColumnTypes(name, binding, model, sourceTableName)
18119
18488
  const sourceTable = findRuntimeBindingSourceTable(model, sourceTableName);
18120
18489
  if (sourceTable === void 0) {
18121
18490
  throw new Error(
18122
- `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.`
18123
18492
  );
18124
18493
  }
18125
18494
  return binding.columns.map(
@@ -18146,7 +18515,7 @@ function compileNqlRuntimeBindingCte(name, binding, naming, parameterOffset, sou
18146
18515
  const columnSql = binding.columns.map((column) => quoteIdent2(naming.toDatabase(column), "column")).join(", ");
18147
18516
  if (sourceTable === void 0) {
18148
18517
  throw new Error(
18149
- `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.`
18150
18519
  );
18151
18520
  }
18152
18521
  const projectedColumns = binding.columns.map((column) => quoteIdent2(naming.toDatabase(column), "column")).join(", ");
@@ -18421,7 +18790,7 @@ var PgsqlAdapter = class _PgsqlAdapter {
18421
18790
  runtimeBinding,
18422
18791
  naming,
18423
18792
  parameters.length,
18424
- bundle.mutationBindings?.get(name)?.table,
18793
+ runtimeBindingSourceTable(bundle, name),
18425
18794
  deps.schemaName,
18426
18795
  deps.model
18427
18796
  );