Summary
After pg_dump + restore of a database containing a graph, every Cypher query fails with graph with oid N does not exist, even though the restore reports no errors and all label tables and rows arrive intact.
The cause is that ag_catalog.ag_graph stores the graph's namespace twice, in two different types, and only one of them survives a logical dump:
| Column |
Type |
Survives pg_dump/restore? |
ag_graph.namespace |
regnamespace |
✅ dumped as the schema name, re-resolves to the new oid |
ag_label.relation |
regclass |
✅ dumped as the table name, re-resolves |
ag_graph.graphid |
oid |
❌ dumped as a bare integer — still the source database's namespace oid |
ag_label.graph |
oid |
❌ same, and FK-bound to graphid |
cypher() resolves the graph through graphid, so it looks for a namespace oid that only ever existed in the source cluster.
The failure is silent at restore time — psql -f dump.sql exits with 0 errors. The first symptom is every query failing later.
Reproduction
AGE 1.5.0, PostgreSQL 16.9 (Debian 16.9-1.pgdg120+1), aarch64.
-- source database
CREATE EXTENSION IF NOT EXISTS age;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT create_graph('demo');
SELECT * FROM cypher('demo', $$ CREATE (:P {name: 'a'}) $$) AS (v agtype);
SELECT * FROM cypher('demo', $$ MATCH (n:P) RETURN n.name $$) AS (name agtype); -- "a"
pg_dump -U postgres -d age_repro_src -f age_repro.sql
createdb -U postgres age_repro_dst
psql -U postgres -d age_repro_dst -f age_repro.sql # 0 errors
-- restored database
SELECT name, graphid, namespace::oid AS namespace_oid, graphid = namespace::oid AS healthy
FROM ag_catalog.ag_graph;
-- name | graphid | namespace_oid | healthy
-- demo | 5616016 | 5616051 | f
SELECT count(*) FROM demo."P"; -- 1 (data restored fine)
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT * FROM cypher('demo', $$ MATCH (n:P) RETURN n.name $$) AS (name agtype);
-- ERROR: graph with oid 5616016 does not exist
Expected
Cypher queries work against a restored database, as they do against the original.
Workaround
The correct oid is already present in the same row, as namespace::oid, so the catalog can be repaired without consulting pg_namespace. ag_label must be updated first (it joins on the old graphid), and fk_graph_oid is not deferrable, so it has to be dropped for the duration:
BEGIN;
ALTER TABLE ag_catalog.ag_label DROP CONSTRAINT fk_graph_oid;
UPDATE ag_catalog.ag_label l
SET graph = g.namespace::oid
FROM ag_catalog.ag_graph g
WHERE l.graph = g.graphid
AND g.graphid IS DISTINCT FROM g.namespace::oid;
UPDATE ag_catalog.ag_graph
SET graphid = namespace::oid
WHERE graphid IS DISTINCT FROM namespace::oid;
ALTER TABLE ag_catalog.ag_label
ADD CONSTRAINT fk_graph_oid FOREIGN KEY (graph) REFERENCES ag_catalog.ag_graph (graphid);
COMMIT;
Verified on both the minimal case above and a ~1.3 GB production snapshot (2 graphs, 18 label tables, 6188 edges): Cypher works again afterwards and the statements are idempotent.
Possible fixes
- Type
graphid as regnamespace (and ag_label.graph to match), so pg_dump emits names and PostgreSQL re-resolves them — the same mechanism that already makes namespace and relation correct. Requires a catalog migration for existing installs.
- Drop the redundant column and resolve the graph through
namespace (already regnamespace), since the two are meant to hold the same value.
- Self-heal on load, treating
namespace::oid as authoritative when the two disagree.
- At minimum, detect and report: a mismatch could raise something like
graph "demo" has a stale graphid (restored from a dump?) instead of an oid that means nothing to the user.
Happy to open a PR for whichever direction maintainers prefer.
Summary
After
pg_dump+ restore of a database containing a graph, every Cypher query fails withgraph with oid N does not exist, even though the restore reports no errors and all label tables and rows arrive intact.The cause is that
ag_catalog.ag_graphstores the graph's namespace twice, in two different types, and only one of them survives a logical dump:pg_dump/restore?ag_graph.namespaceregnamespaceag_label.relationregclassag_graph.graphidoidag_label.graphoidgraphidcypher()resolves the graph throughgraphid, so it looks for a namespace oid that only ever existed in the source cluster.The failure is silent at restore time —
psql -f dump.sqlexits with 0 errors. The first symptom is every query failing later.Reproduction
AGE 1.5.0, PostgreSQL 16.9 (Debian 16.9-1.pgdg120+1), aarch64.
pg_dump -U postgres -d age_repro_src -f age_repro.sql createdb -U postgres age_repro_dst psql -U postgres -d age_repro_dst -f age_repro.sql # 0 errorsExpected
Cypher queries work against a restored database, as they do against the original.
Workaround
The correct oid is already present in the same row, as
namespace::oid, so the catalog can be repaired without consultingpg_namespace.ag_labelmust be updated first (it joins on the oldgraphid), andfk_graph_oidis not deferrable, so it has to be dropped for the duration:Verified on both the minimal case above and a ~1.3 GB production snapshot (2 graphs, 18 label tables, 6188 edges): Cypher works again afterwards and the statements are idempotent.
Possible fixes
graphidasregnamespace(andag_label.graphto match), sopg_dumpemits names and PostgreSQL re-resolves them — the same mechanism that already makesnamespaceandrelationcorrect. Requires a catalog migration for existing installs.namespace(alreadyregnamespace), since the two are meant to hold the same value.namespace::oidas authoritative when the two disagree.graph "demo" has a stale graphid (restored from a dump?)instead of an oid that means nothing to the user.Happy to open a PR for whichever direction maintainers prefer.