|
1 | 1 | // test/integration/runner-pg.test.ts |
2 | 2 | import { test, expect, describe } from "bun:test"; |
3 | 3 | import { newDb } from "pg-mem"; |
| 4 | +import { Pool } from "pg"; |
4 | 5 | import { PgExecutor } from "../../src/runner/pg-executor.js"; |
5 | 6 | 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"; |
6 | 9 |
|
7 | 10 | /** A pg-mem-backed Pool-compatible object. */ |
8 | 11 | function memPool() { |
@@ -69,3 +72,66 @@ describe("PgHistoryStore tracking (pg-mem)", () => { |
69 | 72 | await pool.end(); |
70 | 73 | }); |
71 | 74 | }); |
| 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