Skip to content

Commit e8e5ab8

Browse files
dmealingclaude
andcommitted
feat(migrate-ts): drop-check restore down + allow.dropCheck gating
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b4cfc8 commit e8e5ab8

4 files changed

Lines changed: 36 additions & 3 deletions

File tree

server/typescript/packages/migrate-ts/src/diff/status.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ function blockedReasonFor(c: Change, allow: AllowOptions): string | null {
2727
return allow.dropIndex ? null : "destructive: drop-index not allowed (pass allow.dropIndex)";
2828
case "drop-fk":
2929
return allow.dropFk ? null : "destructive: drop-fk not allowed (pass allow.dropFk)";
30+
case "drop-check":
31+
return allow.dropCheck ? null : "destructive: drop-check not allowed (pass allow.dropCheck)";
3032

3133
case "change-column-type":
3234
if (isWidening(c.from, c.to)) return null; // widening always allowed
@@ -48,7 +50,6 @@ function blockedReasonFor(c: Change, allow: AllowOptions): string | null {
4850
case "add-index":
4951
case "add-fk":
5052
case "add-check":
51-
case "drop-check":
5253
case "create-view":
5354
case "drop-view":
5455
case "replace-view":

server/typescript/packages/migrate-ts/src/emit/postgres.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ function renderDown(c: Change): string {
123123
// add-check / drop-check down arms: declared but not yet produced by the diff
124124
// (checks are create-time-only; see renderUp note).
125125
case "add-check": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.check.name)};`;
126-
case "drop-check": return `-- WARNING: down migration cannot restore the original CHECK definition`;
126+
case "drop-check":
127+
return c.restore
128+
? `ALTER TABLE ${quoteQualified(c.table, c.schema)} ADD CONSTRAINT ${quote(c.restore.name)} CHECK (${c.restore.expression});`
129+
: `-- WARNING: down migration cannot restore the original CHECK definition`;
127130
case "create-view": return `DROP VIEW ${quoteQualifiedView(c.view.name, c.schema)};`;
128131
case "drop-view": return `-- WARNING: down migration cannot restore the original view definition`;
129132
case "replace-view": return `-- WARNING: down migration cannot restore the original view definition`;

server/typescript/packages/migrate-ts/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export type Change =
139139
| { kind: "add-fk"; table: string; schema?: string; fk: FkDescriptor; status: ChangeStatus }
140140
| { kind: "drop-fk"; table: string; schema?: string; fk: string; restore?: FkDescriptor; status: ChangeStatus }
141141
| { kind: "add-check"; table: string; schema?: string; check: CheckDescriptor; status: ChangeStatus }
142-
| { kind: "drop-check"; table: string; schema?: string; check: string; status: ChangeStatus }
142+
| { kind: "drop-check"; table: string; schema?: string; check: string; restore?: CheckDescriptor; status: ChangeStatus }
143143
// Declared for v0.3, never produced in v0.1:
144144
| { kind: "create-view"; view: ViewDescriptor; schema?: string; status: ChangeStatus }
145145
| { kind: "drop-view"; view: string; schema?: string; status: ChangeStatus }
@@ -163,6 +163,7 @@ export interface AllowOptions {
163163
typeChange?: boolean;
164164
dropIndex?: boolean;
165165
dropFk?: boolean;
166+
dropCheck?: boolean;
166167
/** Existing data must satisfy NOT NULL; diff cannot verify this. */
167168
nullableToNotNull?: boolean;
168169
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, test, expect } from "bun:test";
2+
import type { Change } from "../../src/types.js";
3+
import { emit } from "../../src/emit/index.js";
4+
import { applyStatus } from "../../src/diff/status.js";
5+
6+
const CHK = { name: "orders_qty_numeric_chk", expression: "qty >= 1" };
7+
8+
describe("drop-check: restore down + allow gating", () => {
9+
test("drop-check with restore → down re-adds the constraint", () => {
10+
const c = { kind: "drop-check", table: "orders", check: CHK.name, restore: CHK, status: { state: "allowed" } } as unknown as Change;
11+
const r = emit([c], { dialect: "postgres" });
12+
expect(r.up).toContain(`ALTER TABLE "orders" DROP CONSTRAINT "orders_qty_numeric_chk";`);
13+
expect(r.down).toContain(`ALTER TABLE "orders" ADD CONSTRAINT "orders_qty_numeric_chk" CHECK (qty >= 1);`);
14+
});
15+
test("drop-check is blocked unless allow.dropCheck", () => {
16+
const blocked = [{ kind: "drop-check", table: "orders", check: CHK.name, status: { state: "allowed" } } as unknown as Change];
17+
applyStatus(blocked, {});
18+
expect(blocked[0]!.status.state).toBe("blocked");
19+
const allowed = [{ kind: "drop-check", table: "orders", check: CHK.name, status: { state: "allowed" } } as unknown as Change];
20+
applyStatus(allowed, { dropCheck: true });
21+
expect(allowed[0]!.status.state).toBe("allowed");
22+
});
23+
test("add-check stays always-allowed", () => {
24+
const c = [{ kind: "add-check", table: "orders", check: CHK, status: { state: "allowed" } } as unknown as Change];
25+
applyStatus(c, {});
26+
expect(c[0]!.status.state).toBe("allowed");
27+
});
28+
});

0 commit comments

Comments
 (0)