Skip to content

Commit 7bd854d

Browse files
committed
feat(migrate-ts): runner — end-to-end apply/rollback + advisory lock + multi-tenant (real PG)
1 parent c86fdf6 commit 7bd854d

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// src/runner/index.ts
2+
export { contentChecksum } from "./checksum.js";
3+
export { loadMigrations, type Migration } from "./migration-source.js";
4+
export { type AppliedRow, type HistoryStore, InMemoryHistoryStore } from "./history-store.js";
5+
export { applyMigrations, rollbackTo, type SqlExecutor, type ApplyResult, type RollbackResult } from "./apply.js";
6+
export { PgExecutor } from "./pg-executor.js";
7+
export { PgHistoryStore, type PgHistoryStoreOptions } from "./pg-history-store.js";

server/typescript/packages/migrate-ts/test/integration/runner-pg.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// test/integration/runner-pg.test.ts
22
import { test, expect, describe } from "bun:test";
33
import { newDb } from "pg-mem";
4+
import { Pool } from "pg";
45
import { PgExecutor } from "../../src/runner/pg-executor.js";
56
import { PgHistoryStore } from "../../src/runner/pg-history-store.js";
7+
import { applyMigrations, rollbackTo } from "../../src/runner/apply.js";
8+
import type { Migration } from "../../src/runner/migration-source.js";
69

710
/** A pg-mem-backed Pool-compatible object. */
811
function memPool() {
@@ -69,3 +72,66 @@ describe("PgHistoryStore tracking (pg-mem)", () => {
6972
await pool.end();
7073
});
7174
});
75+
76+
const REAL_PG = process.env.MIGRATE_TS_PG_URL;
77+
const realDescribe = REAL_PG ? describe : describe.skip;
78+
79+
realDescribe("runner end-to-end (real Postgres)", () => {
80+
const migs: Migration[] = [
81+
{ version: "20260101000000", name: "create-widgets", dir: "/t/1",
82+
upSql: 'CREATE TABLE "widgets" ("id" int primary key);', downSql: 'DROP TABLE "widgets";' },
83+
{ version: "20260102000000", name: "add-label", dir: "/t/2",
84+
upSql: 'ALTER TABLE "widgets" ADD COLUMN "label" text;', downSql: 'ALTER TABLE "widgets" DROP COLUMN "label";' },
85+
];
86+
87+
test("apply creates tables + history rows; rollback reverts; advisory lock round-trips", async () => {
88+
const pool = new Pool({ connectionString: REAL_PG });
89+
try {
90+
// clean slate
91+
await pool.query('DROP TABLE IF EXISTS "widgets"');
92+
await pool.query("DROP TABLE IF EXISTS metaobjects_migrations");
93+
94+
const store = new PgHistoryStore(pool);
95+
const exec = new PgExecutor(pool);
96+
97+
const applied = await applyMigrations(migs, store, exec);
98+
expect(applied.applied).toEqual(["20260101000000", "20260102000000"]);
99+
100+
const cols = await pool.query(
101+
`SELECT column_name FROM information_schema.columns
102+
WHERE table_name = 'widgets' ORDER BY column_name`,
103+
);
104+
expect(cols.rows.map((r) => r.column_name)).toEqual(["id", "label"]);
105+
expect((await store.applied()).map((r) => r.version)).toEqual(["20260101000000", "20260102000000"]);
106+
107+
// rollback the second migration only
108+
const rb = await rollbackTo("20260101000000", migs, store, exec);
109+
expect(rb.rolledBack).toEqual(["20260102000000"]);
110+
const cols2 = await pool.query(
111+
`SELECT column_name FROM information_schema.columns WHERE table_name = 'widgets'`,
112+
);
113+
expect(cols2.rows.map((r) => r.column_name)).toEqual(["id"]); // label dropped
114+
} finally {
115+
await pool.end();
116+
}
117+
});
118+
119+
test("a second store with a different schema tracks independently (multi-tenant)", async () => {
120+
const pool = new Pool({ connectionString: REAL_PG });
121+
try {
122+
await pool.query('CREATE SCHEMA IF NOT EXISTS tenant_x');
123+
await pool.query('DROP TABLE IF EXISTS tenant_x."metaobjects_migrations"');
124+
const storeX = new PgHistoryStore(pool, { schema: "tenant_x" });
125+
const storeDefault = new PgHistoryStore(pool);
126+
await storeDefault.ensure();
127+
await storeX.ensure();
128+
await storeX.record({ version: "20260101000000", name: "x", checksum: "c", appliedAt: new Date().toISOString(), executionMs: 1, success: true });
129+
expect((await storeX.applied()).length).toBe(1);
130+
// default store unaffected (clean it first so the assertion is meaningful)
131+
await pool.query('DELETE FROM "public"."metaobjects_migrations"');
132+
expect((await storeDefault.applied()).length).toBe(0);
133+
} finally {
134+
await pool.end();
135+
}
136+
});
137+
});

0 commit comments

Comments
 (0)