Skip to content

Commit 81ed4e7

Browse files
committed
feat(pgpm): pgpm regen — generate revert/verify scripts from deploys
1 parent 7a37da1 commit 81ed4e7

10 files changed

Lines changed: 3278 additions & 7148 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { teardownPgPools } from 'pg-cache';
4+
5+
import { CLIDeployTestFixture } from '../test-utils';
6+
7+
jest.setTimeout(60000);
8+
9+
afterAll(async () => {
10+
await teardownPgPools();
11+
});
12+
13+
const MODULE_NAME = 'regen-e2e';
14+
15+
const STUB_REVERT = (change: string) =>
16+
`-- Revert ${change} from pg\n\nBEGIN;\n\n-- Add your revert SQL here\n\nCOMMIT;\n`;
17+
const STUB_VERIFY = (change: string) =>
18+
`-- Verify ${change} on pg\n\nBEGIN;\n\nROLLBACK;\n`;
19+
20+
interface ChangeSpec {
21+
name: string;
22+
deps?: string[];
23+
deploy: string;
24+
}
25+
26+
/** Mixed statement kinds: schema, table, ALTER columns/constraints, function,
27+
* trigger, RLS + policy, grant, index — all with empty stub revert/verify. */
28+
const CHANGES: ChangeSpec[] = [
29+
{
30+
name: 'schemas/regen_app/schema',
31+
deploy: 'CREATE SCHEMA regen_app;'
32+
},
33+
{
34+
name: 'schemas/regen_app/tables/users/table',
35+
deps: ['schemas/regen_app/schema'],
36+
deploy: [
37+
'CREATE TABLE regen_app.users (id serial PRIMARY KEY);',
38+
'ALTER TABLE regen_app.users ADD COLUMN email text;',
39+
'ALTER TABLE regen_app.users ADD CONSTRAINT users_email_key UNIQUE (email);'
40+
].join('\n')
41+
},
42+
{
43+
name: 'schemas/regen_app/tables/users/indexes/users_email_idx',
44+
deps: ['schemas/regen_app/tables/users/table'],
45+
deploy: 'CREATE INDEX users_email_idx ON regen_app.users (email);'
46+
},
47+
{
48+
name: 'schemas/regen_app/procedures/touch',
49+
deps: ['schemas/regen_app/schema'],
50+
deploy:
51+
'CREATE FUNCTION regen_app.touch() RETURNS trigger AS $$ BEGIN RETURN NEW; END $$ LANGUAGE plpgsql;'
52+
},
53+
{
54+
name: 'schemas/regen_app/tables/users/triggers/users_touch',
55+
deps: ['schemas/regen_app/tables/users/table', 'schemas/regen_app/procedures/touch'],
56+
deploy:
57+
'CREATE TRIGGER users_touch BEFORE UPDATE ON regen_app.users FOR EACH ROW EXECUTE FUNCTION regen_app.touch();'
58+
},
59+
{
60+
name: 'schemas/regen_app/tables/users/policies/users_self',
61+
deps: ['schemas/regen_app/tables/users/table'],
62+
deploy: [
63+
'ALTER TABLE regen_app.users ENABLE ROW LEVEL SECURITY;',
64+
'CREATE POLICY users_self ON regen_app.users FOR SELECT USING (true);'
65+
].join('\n')
66+
},
67+
{
68+
name: 'schemas/regen_app/tables/users/grants/select',
69+
deps: ['schemas/regen_app/tables/users/table'],
70+
deploy: 'GRANT SELECT ON regen_app.users TO PUBLIC;'
71+
}
72+
];
73+
74+
const writeModule = (moduleDir: string) => {
75+
// enclosing workspace, so deploy/verify/revert resolve the module
76+
const workspaceDir = path.dirname(moduleDir);
77+
fs.mkdirSync(workspaceDir, { recursive: true });
78+
fs.writeFileSync(path.join(workspaceDir, 'pgpm.json'), '{\n "packages": [\n "*"\n ]\n}');
79+
fs.mkdirSync(moduleDir, { recursive: true });
80+
for (const dir of ['deploy', 'revert', 'verify']) {
81+
fs.mkdirSync(path.join(moduleDir, dir), { recursive: true });
82+
}
83+
const planLines = CHANGES.map((c, i) => {
84+
const deps = c.deps && c.deps.length > 0 ? ` [${c.deps.join(' ')}]` : '';
85+
return `${c.name}${deps} 2024-01-0${(i % 9) + 1}T00:00:00Z test <test@example.com> # add ${c.name}`;
86+
});
87+
fs.writeFileSync(
88+
path.join(moduleDir, 'pgpm.plan'),
89+
`%syntax-version=1.0.0\n%project=${MODULE_NAME}\n%uri=https://github.com/test/${MODULE_NAME}\n\n${planLines.join('\n')}\n`
90+
);
91+
fs.writeFileSync(
92+
path.join(moduleDir, `${MODULE_NAME}.control`),
93+
`comment = 'Regen e2e module'\ndefault_version = '0.0.1'\nrelocatable = false\nsuperuser = false\n`
94+
);
95+
const writeScript = (type: string, name: string, content: string) => {
96+
const filePath = path.join(moduleDir, type, `${name}.sql`);
97+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
98+
fs.writeFileSync(filePath, content);
99+
};
100+
for (const c of CHANGES) {
101+
writeScript('deploy', c.name, `-- Deploy ${c.name} to pg\n\nBEGIN;\n\n${c.deploy}\n\nCOMMIT;\n`);
102+
writeScript('revert', c.name, STUB_REVERT(c.name));
103+
writeScript('verify', c.name, STUB_VERIFY(c.name));
104+
}
105+
};
106+
107+
describe('regen e2e round-trip', () => {
108+
let fixture: CLIDeployTestFixture;
109+
let testDb: any;
110+
let moduleDir: string;
111+
112+
beforeAll(async () => {
113+
fixture = new CLIDeployTestFixture();
114+
testDb = await fixture.setupTestDatabase();
115+
moduleDir = path.join(fixture.tempFixtureDir, 'regen-ws', MODULE_NAME);
116+
writeModule(moduleDir);
117+
});
118+
119+
afterAll(async () => {
120+
await fixture.cleanup();
121+
});
122+
123+
it('fills every stub revert/verify from the deploy scripts', async () => {
124+
await fixture.runTerminalCommands(
125+
`
126+
cd regen-ws/${MODULE_NAME}
127+
pgpm regen
128+
`,
129+
{}
130+
);
131+
132+
for (const c of CHANGES) {
133+
const revert = fs.readFileSync(path.join(moduleDir, 'revert', `${c.name}.sql`), 'utf-8');
134+
const verify = fs.readFileSync(path.join(moduleDir, 'verify', `${c.name}.sql`), 'utf-8');
135+
expect(revert).toContain(`-- Revert ${c.name} from pg`);
136+
expect(revert).toContain('COMMIT;');
137+
expect(revert).not.toContain('-- Add your revert SQL here');
138+
expect(verify).toContain(`-- Verify ${c.name} on pg`);
139+
expect(verify).toContain('ROLLBACK;');
140+
expect(verify).toMatch(/SELECT 1\//);
141+
}
142+
});
143+
144+
it('deploys, verifies, and reverts cleanly against live Postgres', async () => {
145+
await fixture.runTerminalCommands(
146+
`
147+
cd regen-ws/${MODULE_NAME}
148+
pgpm deploy --database ${testDb.name} --package ${MODULE_NAME} --yes
149+
`,
150+
{ database: testDb.name }
151+
);
152+
153+
expect(await testDb.exists('schema', 'regen_app')).toBe(true);
154+
expect(await testDb.exists('table', 'regen_app.users')).toBe(true);
155+
156+
await fixture.runTerminalCommands(
157+
`
158+
cd regen-ws/${MODULE_NAME}
159+
pgpm verify --database ${testDb.name} --package ${MODULE_NAME} --yes
160+
`,
161+
{ database: testDb.name }
162+
);
163+
164+
await fixture.runTerminalCommands(
165+
`
166+
cd regen-ws/${MODULE_NAME}
167+
pgpm revert --database ${testDb.name} --package ${MODULE_NAME} --yes
168+
`,
169+
{ database: testDb.name }
170+
);
171+
172+
expect(await testDb.exists('schema', 'regen_app')).toBe(false);
173+
expect(await testDb.exists('table', 'regen_app.users')).toBe(false);
174+
175+
const remaining = await testDb.query(
176+
`SELECT COUNT(*)::int AS count FROM pgpm_migrate.changes WHERE package = $1`,
177+
[MODULE_NAME]
178+
);
179+
expect(remaining.rows[0].count).toBe(0);
180+
});
181+
});

0 commit comments

Comments
 (0)