Skip to content

Commit 5fe2121

Browse files
authored
Add pg_upgrade support functions for PostgreSQL (#2326)
Add pg_upgrade support functions for PostgreSQL for major version upgrades NOTE: This PR was created with AI tools and a human. The ag_graph.namespace column uses the regnamespace type, which pg_upgrade cannot handle in user tables. This commit adds four SQL functions to enable seamless PostgreSQL major version upgrades while preserving all graph data. New functions in ag_catalog: - age_prepare_pg_upgrade(): Converts namespace from regnamespace to oid, creates backup table with graph-to-namespace mappings (stores nspname directly to avoid quoting issues) - age_finish_pg_upgrade(): Remaps stale OIDs after upgrade, restores regnamespace type, invalidates AGE caches while preserving schema ownership - age_revert_pg_upgrade_changes(): Cancels preparation if upgrade is aborted - age_pg_upgrade_status(): Returns current upgrade readiness status Usage: 1. Before pg_upgrade: SELECT age_prepare_pg_upgrade(); 2. Run pg_upgrade as normal 3. After pg_upgrade: SELECT age_finish_pg_upgrade(); Key implementation details: - Uses transaction-level advisory locks (pg_advisory_xact_lock) for safety - Preserves original schema ownership during cache invalidation - Validates all backup rows are mapped before proceeding - Handles zero-graph edge case gracefully - Handles insufficient privileges gracefully with informative notices - Backup table deleted only after all steps succeed Files changed: - sql/age_pg_upgrade.sql: New file with function implementations - sql/sql_files: Added age_pg_upgrade entry - age--1.7.0--y.y.y.sql: Added functions for extension upgrades All regression tests pass.
1 parent 858747c commit 5fe2121

3 files changed

Lines changed: 854 additions & 0 deletions

File tree

age--1.7.0--y.y.y.sql

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,381 @@
3030
--* Please add all additions, deletions, and modifications to the end of this
3131
--* file. We need to keep the order of these changes.
3232
--* REMOVE ALL LINES ABOVE, and this one, that start with --*
33+
34+
--
35+
-- pg_upgrade support functions
36+
--
37+
-- These functions help users upgrade PostgreSQL major versions using pg_upgrade
38+
-- while preserving Apache AGE graph data.
39+
--
40+
41+
CREATE FUNCTION ag_catalog.age_prepare_pg_upgrade()
42+
RETURNS void
43+
LANGUAGE plpgsql
44+
SET search_path = ag_catalog, pg_catalog
45+
AS $function$
46+
DECLARE
47+
graph_count integer;
48+
BEGIN
49+
-- Check if namespace column is already oid type (already prepared)
50+
IF EXISTS (
51+
SELECT 1 FROM information_schema.columns
52+
WHERE table_schema = 'ag_catalog'
53+
AND table_name = 'ag_graph'
54+
AND column_name = 'namespace'
55+
AND data_type = 'oid'
56+
) THEN
57+
RAISE NOTICE 'Database already prepared for pg_upgrade (namespace is oid type).';
58+
RETURN;
59+
END IF;
60+
61+
-- Drop existing backup table if it exists (from a previous failed attempt)
62+
DROP TABLE IF EXISTS public._age_pg_upgrade_backup;
63+
64+
-- Create backup table with graph names mapped to namespace names
65+
-- We store nspname directly (not regnamespace::text) to avoid quoting issues
66+
-- Names survive pg_upgrade while OIDs don't
67+
CREATE TABLE public._age_pg_upgrade_backup AS
68+
SELECT
69+
g.graphid AS old_graphid,
70+
g.name AS graph_name,
71+
n.nspname AS namespace_name
72+
FROM ag_catalog.ag_graph g
73+
JOIN pg_namespace n ON n.oid = g.namespace::oid;
74+
75+
SELECT count(*) INTO graph_count FROM public._age_pg_upgrade_backup;
76+
77+
RAISE NOTICE 'Created backup table public._age_pg_upgrade_backup with % graph(s)', graph_count;
78+
79+
-- Even with zero graphs, we still need to convert the column type
80+
-- because the regnamespace type itself blocks pg_upgrade
81+
82+
-- Drop the existing regnamespace-based index
83+
DROP INDEX IF EXISTS ag_catalog.ag_graph_namespace_index;
84+
85+
-- Convert namespace column from regnamespace to oid
86+
ALTER TABLE ag_catalog.ag_graph
87+
ALTER COLUMN namespace TYPE oid USING namespace::oid;
88+
89+
-- Recreate the index with oid type
90+
CREATE UNIQUE INDEX ag_graph_namespace_index
91+
ON ag_catalog.ag_graph USING btree (namespace);
92+
93+
-- Create a view for backward-compatible display of namespace as schema name
94+
CREATE OR REPLACE VIEW ag_catalog.ag_graph_view AS
95+
SELECT graphid, name, namespace::regnamespace AS namespace
96+
FROM ag_catalog.ag_graph;
97+
98+
RAISE NOTICE 'Successfully prepared database for pg_upgrade.';
99+
RAISE NOTICE 'The ag_graph.namespace column has been converted from regnamespace to oid.';
100+
RAISE NOTICE 'You can now run pg_upgrade.';
101+
RAISE NOTICE 'After pg_upgrade completes, run: SELECT age_finish_pg_upgrade();';
102+
END;
103+
$function$;
104+
105+
COMMENT ON FUNCTION ag_catalog.age_prepare_pg_upgrade() IS
106+
'Prepares an AGE database for pg_upgrade by converting ag_graph.namespace from regnamespace to oid type. Run this before pg_upgrade.';
107+
108+
CREATE FUNCTION ag_catalog.age_finish_pg_upgrade()
109+
RETURNS void
110+
LANGUAGE plpgsql
111+
SET search_path = ag_catalog, pg_catalog
112+
AS $function$
113+
DECLARE
114+
mapping_count integer;
115+
updated_labels integer;
116+
updated_graphs integer;
117+
BEGIN
118+
-- Check if backup table exists
119+
IF NOT EXISTS (
120+
SELECT 1 FROM information_schema.tables
121+
WHERE table_schema = 'public'
122+
AND table_name = '_age_pg_upgrade_backup'
123+
) THEN
124+
RAISE EXCEPTION 'Backup table public._age_pg_upgrade_backup not found. '
125+
'Did you run age_prepare_pg_upgrade() before pg_upgrade?';
126+
END IF;
127+
128+
-- Check if namespace column is oid type (was properly prepared)
129+
IF NOT EXISTS (
130+
SELECT 1 FROM information_schema.columns
131+
WHERE table_schema = 'ag_catalog'
132+
AND table_name = 'ag_graph'
133+
AND column_name = 'namespace'
134+
AND data_type = 'oid'
135+
) THEN
136+
RAISE EXCEPTION 'ag_graph.namespace is not oid type. '
137+
'Did you run age_prepare_pg_upgrade() before pg_upgrade?';
138+
END IF;
139+
140+
-- Create temporary mapping table with old and new OIDs
141+
CREATE TEMP TABLE _graphid_mapping AS
142+
SELECT
143+
b.old_graphid,
144+
b.graph_name,
145+
n.oid AS new_graphid
146+
FROM public._age_pg_upgrade_backup b
147+
JOIN pg_namespace n ON n.nspname = b.namespace_name;
148+
149+
GET DIAGNOSTICS mapping_count = ROW_COUNT;
150+
151+
-- Verify all backup rows were mapped (detect missing schemas)
152+
DECLARE
153+
backup_count integer;
154+
BEGIN
155+
SELECT count(*) INTO backup_count FROM public._age_pg_upgrade_backup;
156+
IF mapping_count < backup_count THEN
157+
RAISE EXCEPTION 'Only % of % graphs could be mapped. Some schema names may have changed or been dropped.',
158+
mapping_count, backup_count;
159+
END IF;
160+
END;
161+
162+
-- Handle zero-graph case (still need to restore schema)
163+
IF mapping_count = 0 THEN
164+
RAISE NOTICE 'No graphs to remap (empty backup table).';
165+
DROP TABLE _graphid_mapping;
166+
-- Skip to schema restoration
167+
ELSE
168+
RAISE NOTICE 'Found % graph(s) to remap', mapping_count;
169+
170+
-- Temporarily drop foreign key constraint
171+
ALTER TABLE ag_catalog.ag_label DROP CONSTRAINT IF EXISTS fk_graph_oid;
172+
173+
-- Update ag_label.graph references to use new OIDs
174+
UPDATE ag_catalog.ag_label l
175+
SET graph = m.new_graphid
176+
FROM _graphid_mapping m
177+
WHERE l.graph = m.old_graphid;
178+
179+
GET DIAGNOSTICS updated_labels = ROW_COUNT;
180+
RAISE NOTICE 'Updated % label record(s)', updated_labels;
181+
182+
-- Update ag_graph.graphid and ag_graph.namespace to new OIDs
183+
UPDATE ag_catalog.ag_graph g
184+
SET graphid = m.new_graphid,
185+
namespace = m.new_graphid
186+
FROM _graphid_mapping m
187+
WHERE g.graphid = m.old_graphid;
188+
189+
GET DIAGNOSTICS updated_graphs = ROW_COUNT;
190+
RAISE NOTICE 'Updated % graph record(s)', updated_graphs;
191+
192+
-- Restore foreign key constraint
193+
ALTER TABLE ag_catalog.ag_label
194+
ADD CONSTRAINT fk_graph_oid
195+
FOREIGN KEY(graph) REFERENCES ag_catalog.ag_graph(graphid);
196+
197+
-- Clean up temporary mapping table
198+
DROP TABLE _graphid_mapping;
199+
200+
RAISE NOTICE 'Successfully completed pg_upgrade OID remapping.';
201+
END IF;
202+
203+
--
204+
-- Restore original schema (revert namespace to regnamespace)
205+
--
206+
RAISE NOTICE 'Restoring original schema...';
207+
208+
-- Drop the view (no longer needed with regnamespace)
209+
DROP VIEW IF EXISTS ag_catalog.ag_graph_view;
210+
211+
-- Drop the existing oid-based index
212+
DROP INDEX IF EXISTS ag_catalog.ag_graph_namespace_index;
213+
214+
-- Convert namespace column back to regnamespace
215+
ALTER TABLE ag_catalog.ag_graph
216+
ALTER COLUMN namespace TYPE regnamespace USING namespace::regnamespace;
217+
218+
-- Recreate the index with regnamespace type
219+
CREATE UNIQUE INDEX ag_graph_namespace_index
220+
ON ag_catalog.ag_graph USING btree (namespace);
221+
222+
RAISE NOTICE 'Successfully restored ag_graph.namespace to regnamespace type.';
223+
224+
--
225+
-- Invalidate AGE's internal caches by touching each graph's namespace
226+
-- AGE registers a syscache callback on NAMESPACEOID, so altering a schema
227+
-- triggers cache invalidation. This ensures cypher queries work immediately
228+
-- without requiring a session reconnect.
229+
--
230+
-- We use xact-level advisory lock (auto-released at transaction end)
231+
-- and preserve original schema ownership.
232+
--
233+
RAISE NOTICE 'Invalidating AGE caches...';
234+
PERFORM pg_catalog.pg_advisory_xact_lock(hashtext('age_finish_pg_upgrade'));
235+
DECLARE
236+
graph_rec RECORD;
237+
cache_invalidated boolean := false;
238+
BEGIN
239+
FOR graph_rec IN
240+
SELECT n.nspname AS ns_name, r.rolname AS owner_name
241+
FROM ag_catalog.ag_graph g
242+
JOIN pg_namespace n ON n.oid = g.namespace
243+
JOIN pg_roles r ON r.oid = n.nspowner
244+
LOOP
245+
BEGIN
246+
-- Touch schema by changing owner to current_user then back to original
247+
-- This triggers cache invalidation without permanently changing ownership
248+
EXECUTE format('ALTER SCHEMA %I OWNER TO %I', graph_rec.ns_name, current_user);
249+
EXECUTE format('ALTER SCHEMA %I OWNER TO %I', graph_rec.ns_name, graph_rec.owner_name);
250+
cache_invalidated := true;
251+
EXCEPTION WHEN insufficient_privilege THEN
252+
-- If we can't change ownership, skip this schema
253+
-- The cache will be invalidated on first use anyway
254+
RAISE NOTICE 'Could not invalidate cache for schema % (insufficient privileges)', graph_rec.ns_name;
255+
END;
256+
END LOOP;
257+
IF NOT cache_invalidated AND (SELECT count(*) FROM ag_catalog.ag_graph) > 0 THEN
258+
RAISE NOTICE 'Cache invalidation skipped. You may need to reconnect for cypher queries to work.';
259+
END IF;
260+
END;
261+
262+
-- Now that all steps succeeded, clean up the backup table
263+
DROP TABLE IF EXISTS public._age_pg_upgrade_backup;
264+
265+
RAISE NOTICE '';
266+
RAISE NOTICE 'pg_upgrade complete. All graph data has been preserved.';
267+
END;
268+
$function$;
269+
270+
COMMENT ON FUNCTION ag_catalog.age_finish_pg_upgrade() IS
271+
'Completes pg_upgrade by remapping stale OIDs and restoring the original schema. Run this after pg_upgrade.';
272+
273+
CREATE FUNCTION ag_catalog.age_revert_pg_upgrade_changes()
274+
RETURNS void
275+
LANGUAGE plpgsql
276+
SET search_path = ag_catalog, pg_catalog
277+
AS $function$
278+
BEGIN
279+
-- Check if namespace column is oid type (needs reverting)
280+
IF NOT EXISTS (
281+
SELECT 1 FROM information_schema.columns
282+
WHERE table_schema = 'ag_catalog'
283+
AND table_name = 'ag_graph'
284+
AND column_name = 'namespace'
285+
AND data_type = 'oid'
286+
) THEN
287+
RAISE NOTICE 'ag_graph.namespace is already regnamespace type. Nothing to revert.';
288+
RETURN;
289+
END IF;
290+
291+
-- Drop the view (no longer needed with regnamespace)
292+
DROP VIEW IF EXISTS ag_catalog.ag_graph_view;
293+
294+
-- Drop the existing oid-based index
295+
DROP INDEX IF EXISTS ag_catalog.ag_graph_namespace_index;
296+
297+
-- Convert namespace column back to regnamespace
298+
ALTER TABLE ag_catalog.ag_graph
299+
ALTER COLUMN namespace TYPE regnamespace USING namespace::regnamespace;
300+
301+
-- Recreate the index with regnamespace type
302+
CREATE UNIQUE INDEX ag_graph_namespace_index
303+
ON ag_catalog.ag_graph USING btree (namespace);
304+
305+
--
306+
-- Invalidate AGE's internal caches by touching each graph's namespace
307+
-- We use xact-level advisory lock and preserve original ownership
308+
--
309+
PERFORM pg_catalog.pg_advisory_xact_lock(hashtext('age_revert_pg_upgrade'));
310+
DECLARE
311+
graph_rec RECORD;
312+
BEGIN
313+
FOR graph_rec IN
314+
SELECT n.nspname AS ns_name, r.rolname AS owner_name
315+
FROM ag_catalog.ag_graph g
316+
JOIN pg_namespace n ON n.oid = g.namespace
317+
JOIN pg_roles r ON r.oid = n.nspowner
318+
LOOP
319+
BEGIN
320+
-- Touch schema by changing owner to current_user then back to original
321+
EXECUTE format('ALTER SCHEMA %I OWNER TO %I', graph_rec.ns_name, current_user);
322+
EXECUTE format('ALTER SCHEMA %I OWNER TO %I', graph_rec.ns_name, graph_rec.owner_name);
323+
EXCEPTION WHEN insufficient_privilege THEN
324+
RAISE NOTICE 'Could not invalidate cache for schema % (insufficient privileges)', graph_rec.ns_name;
325+
END;
326+
END LOOP;
327+
END;
328+
329+
RAISE NOTICE 'Successfully reverted ag_graph.namespace to regnamespace type.';
330+
RAISE NOTICE '';
331+
RAISE NOTICE 'Upgrade preparation has been cancelled.';
332+
RAISE NOTICE 'You may want to drop the backup table: DROP TABLE IF EXISTS public._age_pg_upgrade_backup;';
333+
END;
334+
$function$;
335+
336+
COMMENT ON FUNCTION ag_catalog.age_revert_pg_upgrade_changes() IS
337+
'Reverts schema changes if you need to cancel after age_prepare_pg_upgrade() but before pg_upgrade. Not needed after age_finish_pg_upgrade().';
338+
339+
CREATE FUNCTION ag_catalog.age_pg_upgrade_status()
340+
RETURNS TABLE (
341+
status text,
342+
namespace_type text,
343+
graph_count bigint,
344+
backup_exists boolean,
345+
message text
346+
)
347+
LANGUAGE plpgsql
348+
SET search_path = ag_catalog, pg_catalog
349+
AS $function$
350+
DECLARE
351+
ns_type text;
352+
g_count bigint;
353+
backup_exists boolean;
354+
BEGIN
355+
-- Get namespace column type
356+
SELECT data_type INTO ns_type
357+
FROM information_schema.columns
358+
WHERE table_schema = 'ag_catalog'
359+
AND table_name = 'ag_graph'
360+
AND column_name = 'namespace';
361+
362+
-- Get graph count
363+
SELECT count(*) INTO g_count FROM ag_catalog.ag_graph;
364+
365+
-- Check for backup table
366+
SELECT EXISTS(
367+
SELECT 1 FROM information_schema.tables
368+
WHERE table_schema = 'public'
369+
AND table_name = '_age_pg_upgrade_backup'
370+
) INTO backup_exists;
371+
372+
-- Determine status and message
373+
IF ns_type = 'regnamespace' AND NOT backup_exists THEN
374+
-- Normal state - ready for use, needs prep before pg_upgrade
375+
RETURN QUERY SELECT
376+
'NORMAL'::text,
377+
ns_type,
378+
g_count,
379+
backup_exists,
380+
'Run SELECT age_prepare_pg_upgrade(); before pg_upgrade'::text;
381+
ELSIF ns_type = 'regnamespace' AND backup_exists THEN
382+
-- Unusual state - backup exists but schema wasn't converted
383+
RETURN QUERY SELECT
384+
'WARNING'::text,
385+
ns_type,
386+
g_count,
387+
backup_exists,
388+
'Backup table exists but schema not converted. Run age_prepare_pg_upgrade() again.'::text;
389+
ELSIF ns_type = 'oid' AND backup_exists THEN
390+
-- Prepared and ready for pg_upgrade, or awaiting finish after pg_upgrade
391+
RETURN QUERY SELECT
392+
'PREPARED - AWAITING FINISH'::text,
393+
ns_type,
394+
g_count,
395+
backup_exists,
396+
'After pg_upgrade, run SELECT age_finish_pg_upgrade();'::text;
397+
ELSE
398+
-- oid type without backup - manually converted or partial state
399+
RETURN QUERY SELECT
400+
'CONVERTED'::text,
401+
ns_type,
402+
g_count,
403+
backup_exists,
404+
'Namespace is oid type. If upgrading, ensure backup table exists.'::text;
405+
END IF;
406+
END;
407+
$function$;
408+
409+
COMMENT ON FUNCTION ag_catalog.age_pg_upgrade_status() IS
410+
'Returns the current pg_upgrade readiness status of the AGE installation.';

0 commit comments

Comments
 (0)