Skip to content

Commit 4a016f6

Browse files
committed
Make both output identical. Fix ordering issue. Fix missing package for graphql flow
1 parent 6742fcd commit 4a016f6

10 files changed

Lines changed: 427 additions & 143 deletions

File tree

pgpm/cli/src/commands/export.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,32 @@ export default async (
181181
AND datname !~ '^pg_';
182182
`);
183183

184+
// If --database_ids is provided but --databases is not, auto-detect
185+
// which postgres database contains the matching metaschema database name.
186+
if (argv.database_ids && !argv.databases) {
187+
const targetName = argv.database_ids;
188+
for (const row of databasesResult.rows) {
189+
try {
190+
const candidatePool = await getPgPool({ database: row.datname });
191+
const check = await candidatePool.query(
192+
`SELECT name FROM metaschema_public.database WHERE name = $1 LIMIT 1`,
193+
[targetName]
194+
);
195+
if (check.rows.length > 0) {
196+
argv.databases = row.datname;
197+
break;
198+
}
199+
} catch {
200+
// Skip databases that don't have metaschema_public
201+
}
202+
}
203+
if (!argv.databases) {
204+
console.error(`Could not find database "${targetName}" in any postgres database.`);
205+
prompter.close();
206+
return;
207+
}
208+
}
209+
184210
const { databases: dbname } = await prompter.prompt(argv, [
185211
{
186212
type: 'list',
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
-- Register a package (auto-called by deploy if needed)
2+
CREATE PROCEDURE pgpm_migrate.register_package(p_package TEXT)
3+
LANGUAGE plpgsql AS $$
4+
BEGIN
5+
INSERT INTO pgpm_migrate.packages (package)
6+
VALUES (p_package)
7+
ON CONFLICT (package) DO NOTHING;
8+
END;
9+
$$;
10+
11+
-- Check if a change is deployed (handles both local and cross-package dependencies)
12+
CREATE FUNCTION pgpm_migrate.is_deployed(
13+
p_package TEXT,
14+
p_change_name TEXT
15+
)
16+
RETURNS BOOLEAN
17+
LANGUAGE plpgsql STABLE AS $$
18+
DECLARE
19+
v_actual_package TEXT;
20+
v_actual_change TEXT;
21+
v_colon_pos INT;
22+
BEGIN
23+
-- Check if change_name contains a package prefix (cross-package dependency)
24+
v_colon_pos := position(':' in p_change_name);
25+
26+
IF v_colon_pos > 0 THEN
27+
-- Split into package and change name
28+
v_actual_package := substring(p_change_name from 1 for v_colon_pos - 1);
29+
v_actual_change := substring(p_change_name from v_colon_pos + 1);
30+
ELSE
31+
-- Use provided package as default
32+
v_actual_package := p_package;
33+
v_actual_change := p_change_name;
34+
END IF;
35+
36+
RETURN EXISTS (
37+
SELECT 1 FROM pgpm_migrate.changes
38+
WHERE package = v_actual_package
39+
AND change_name = v_actual_change
40+
);
41+
END;
42+
$$;
43+
44+
-- Deploy a change
45+
CREATE PROCEDURE pgpm_migrate.deploy(
46+
p_package TEXT,
47+
p_change_name TEXT,
48+
p_script_hash TEXT,
49+
p_requires TEXT[],
50+
p_deploy_sql TEXT,
51+
p_log_only BOOLEAN DEFAULT FALSE
52+
)
53+
LANGUAGE plpgsql AS $$
54+
DECLARE
55+
v_change_id TEXT;
56+
BEGIN
57+
-- Ensure package exists
58+
CALL pgpm_migrate.register_package(p_package);
59+
60+
-- Generate simple ID
61+
v_change_id := encode(sha256((p_package || p_change_name || p_script_hash)::bytea), 'hex');
62+
63+
-- Check if already deployed
64+
IF pgpm_migrate.is_deployed(p_package, p_change_name) THEN
65+
-- Check if it's the same script (by hash)
66+
IF EXISTS (
67+
SELECT 1 FROM pgpm_migrate.changes
68+
WHERE package = p_package
69+
AND change_name = p_change_name
70+
AND script_hash = p_script_hash
71+
) THEN
72+
-- Same change with same content, skip silently
73+
RETURN;
74+
ELSE
75+
-- Different content, this is an error
76+
RAISE EXCEPTION 'Change % already deployed in package % with different content', p_change_name, p_package;
77+
END IF;
78+
END IF;
79+
80+
-- Check dependencies
81+
IF p_requires IS NOT NULL THEN
82+
DECLARE
83+
missing_changes TEXT[];
84+
BEGIN
85+
SELECT array_agg(req) INTO missing_changes
86+
FROM unnest(p_requires) AS req
87+
WHERE NOT pgpm_migrate.is_deployed(p_package, req);
88+
89+
IF array_length(missing_changes, 1) > 0 THEN
90+
RAISE EXCEPTION 'Missing required changes for %: %', p_change_name, array_to_string(missing_changes, ', ');
91+
END IF;
92+
END;
93+
END IF;
94+
95+
-- Execute deploy (skip if log-only mode)
96+
IF NOT p_log_only THEN
97+
BEGIN
98+
EXECUTE p_deploy_sql;
99+
EXCEPTION WHEN OTHERS THEN
100+
-- Re-raise the original exception to preserve full context including SQL statement
101+
RAISE;
102+
END;
103+
END IF;
104+
105+
-- Record deployment
106+
INSERT INTO pgpm_migrate.changes (change_id, change_name, package, script_hash)
107+
VALUES (v_change_id, p_change_name, p_package, p_script_hash);
108+
109+
-- Record dependencies (INSERTED AFTER SUCCESSFUL DEPLOYMENT)
110+
IF p_requires IS NOT NULL THEN
111+
INSERT INTO pgpm_migrate.dependencies (change_id, requires)
112+
SELECT v_change_id, req FROM unnest(p_requires) AS req;
113+
END IF;
114+
115+
-- Log success
116+
INSERT INTO pgpm_migrate.events (event_type, change_name, package)
117+
VALUES ('deploy', p_change_name, p_package);
118+
END;
119+
$$;
120+
121+
-- Revert a change
122+
CREATE PROCEDURE pgpm_migrate.revert(
123+
p_package TEXT,
124+
p_change_name TEXT,
125+
p_revert_sql TEXT
126+
)
127+
LANGUAGE plpgsql AS $$
128+
BEGIN
129+
-- Check if deployed
130+
IF NOT pgpm_migrate.is_deployed(p_package, p_change_name) THEN
131+
RAISE EXCEPTION 'Change % not deployed in package %', p_change_name, p_package;
132+
END IF;
133+
134+
-- Check if other changes depend on this (including cross-package dependencies)
135+
IF EXISTS (
136+
SELECT 1 FROM pgpm_migrate.dependencies d
137+
JOIN pgpm_migrate.changes c ON c.change_id = d.change_id
138+
WHERE (
139+
-- Local dependency within same package
140+
(d.requires = p_change_name AND c.package = p_package)
141+
OR
142+
-- Cross-package dependency
143+
(d.requires = p_package || ':' || p_change_name)
144+
)
145+
) THEN
146+
-- Get list of dependent changes for better error message
147+
DECLARE
148+
dependent_changes TEXT;
149+
BEGIN
150+
SELECT string_agg(
151+
CASE
152+
WHEN d.requires = p_change_name THEN c.change_name
153+
ELSE c.package || ':' || c.change_name
154+
END,
155+
', '
156+
) INTO dependent_changes
157+
FROM pgpm_migrate.dependencies d
158+
JOIN pgpm_migrate.changes c ON c.change_id = d.change_id
159+
WHERE (
160+
(d.requires = p_change_name AND c.package = p_package)
161+
OR
162+
(d.requires = p_package || ':' || p_change_name)
163+
);
164+
165+
RAISE EXCEPTION 'Cannot revert %: required by %', p_change_name, dependent_changes;
166+
END;
167+
END IF;
168+
169+
-- Execute revert
170+
BEGIN
171+
EXECUTE p_revert_sql;
172+
EXCEPTION WHEN OTHERS THEN
173+
-- Re-raise the original exception to preserve full context including SQL statement
174+
RAISE;
175+
END;
176+
177+
-- Remove from deployed
178+
DELETE FROM pgpm_migrate.changes
179+
WHERE package = p_package AND change_name = p_change_name;
180+
181+
-- Log revert
182+
INSERT INTO pgpm_migrate.events (event_type, change_name, package)
183+
VALUES ('revert', p_change_name, p_package);
184+
END;
185+
$$;
186+
187+
-- Verify a change
188+
CREATE FUNCTION pgpm_migrate.verify(
189+
p_package TEXT,
190+
p_change_name TEXT,
191+
p_verify_sql TEXT
192+
)
193+
RETURNS BOOLEAN
194+
LANGUAGE plpgsql AS $$
195+
BEGIN
196+
EXECUTE p_verify_sql;
197+
RETURN TRUE;
198+
EXCEPTION WHEN OTHERS THEN
199+
RETURN FALSE;
200+
END;
201+
$$;
202+
203+
-- List deployed changes
204+
CREATE FUNCTION pgpm_migrate.deployed_changes(
205+
p_package TEXT DEFAULT NULL
206+
)
207+
RETURNS TABLE(package TEXT, change_name TEXT, deployed_at TIMESTAMPTZ)
208+
LANGUAGE sql STABLE AS $$
209+
SELECT package, change_name, deployed_at
210+
FROM pgpm_migrate.changes
211+
WHERE p_package IS NULL OR package = p_package
212+
ORDER BY deployed_at;
213+
$$;
214+
215+
-- Get changes that depend on a given change
216+
CREATE FUNCTION pgpm_migrate.get_dependents(
217+
p_package TEXT,
218+
p_change_name TEXT
219+
)
220+
RETURNS TABLE(package TEXT, change_name TEXT, dependency TEXT)
221+
LANGUAGE sql STABLE AS $$
222+
SELECT c.package, c.change_name, d.requires as dependency
223+
FROM pgpm_migrate.dependencies d
224+
JOIN pgpm_migrate.changes c ON c.change_id = d.change_id
225+
WHERE (
226+
-- Local dependency within same package
227+
(d.requires = p_change_name AND c.package = p_package)
228+
OR
229+
-- Cross-package dependency
230+
(d.requires = p_package || ':' || p_change_name)
231+
)
232+
ORDER BY c.package, c.change_name;
233+
$$;
234+
235+
-- Get deployment status
236+
CREATE FUNCTION pgpm_migrate.status(
237+
p_package TEXT DEFAULT NULL
238+
)
239+
RETURNS TABLE(
240+
package TEXT,
241+
total_deployed INTEGER,
242+
last_change TEXT,
243+
last_deployed TIMESTAMPTZ
244+
)
245+
LANGUAGE sql STABLE AS $$
246+
WITH latest AS (
247+
SELECT DISTINCT ON (package)
248+
package,
249+
change_name,
250+
deployed_at
251+
FROM pgpm_migrate.changes
252+
WHERE p_package IS NULL OR package = p_package
253+
ORDER BY package, deployed_at DESC
254+
)
255+
SELECT
256+
c.package,
257+
COUNT(*)::INTEGER AS total_deployed,
258+
l.change_name AS last_change,
259+
l.deployed_at AS last_deployed
260+
FROM pgpm_migrate.changes c
261+
JOIN latest l ON l.package = c.package
262+
WHERE p_package IS NULL OR c.package = p_package
263+
GROUP BY c.package, l.change_name, l.deployed_at;
264+
$$;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
-- Create schema
2+
CREATE SCHEMA pgpm_migrate;
3+
4+
-- 1. Packages (minimal - just name and timestamp)
5+
CREATE TABLE pgpm_migrate.packages (
6+
package TEXT PRIMARY KEY,
7+
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
8+
);
9+
10+
-- 2. Deployed changes (what's currently deployed)
11+
CREATE TABLE pgpm_migrate.changes (
12+
change_id TEXT PRIMARY KEY,
13+
change_name TEXT NOT NULL,
14+
package TEXT NOT NULL REFERENCES pgpm_migrate.packages(package),
15+
script_hash TEXT NOT NULL,
16+
deployed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
17+
UNIQUE(package, change_name),
18+
UNIQUE(package, script_hash)
19+
);
20+
21+
-- 3. Dependencies (what depends on what)
22+
CREATE TABLE pgpm_migrate.dependencies (
23+
change_id TEXT NOT NULL REFERENCES pgpm_migrate.changes(change_id) ON DELETE CASCADE,
24+
requires TEXT NOT NULL,
25+
PRIMARY KEY (change_id, requires)
26+
);
27+
28+
-- 4. Event log (minimal history for rollback)
29+
CREATE TABLE pgpm_migrate.events (
30+
event_id SERIAL PRIMARY KEY,
31+
event_type TEXT NOT NULL CHECK (event_type IN ('deploy', 'revert', 'verify')),
32+
change_name TEXT NOT NULL,
33+
package TEXT NOT NULL,
34+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
35+
error_message TEXT,
36+
error_code TEXT
37+
);

0 commit comments

Comments
 (0)