Repository navigation
TRUNCATE fails with schema "ag_catalog" does not exist in databases without AGE, when AGE is in shared_preload_libraries #2520
Description
Summary
When age is in shared_preload_libraries, TRUNCATE fails in any database that does not have the AGE extension installed:
ERROR: schema "ag_catalog" does not exist
This makes AGE unsafe to preload on a shared PostgreSQL cluster: a single database wanting graph support breaks TRUNCATE for every other database in the instance.
This is the same class of problem as #2180 (fixed for the object_access_hook path by #2161), but on a different code path that was added later and does not have the guard.
Environment
- PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2), x86_64
- Apache AGE 1.8.0 (
postgresql-18-pgdg-age, PGDG apt) shared_preload_libraries = age
Reproduction
$ psql -U postgres -c "CREATE DATABASE app;"
CREATE DATABASE
$ psql -U postgres -d app -c "CREATE TABLE t (i int);"
CREATE TABLE
$ psql -U postgres -d app -c "TRUNCATE t;"
ERROR: schema "ag_catalog" does not existThe database app never had CREATE EXTENSION age run in it.
Installing the extension in that database makes it work again:
$ psql -U postgres -d app -c "CREATE EXTENSION age;"
CREATE EXTENSION
$ psql -U postgres -d app -c "TRUNCATE t;"
TRUNCATE TABLEOnly TRUNCATE is affected. CREATE TABLE / INSERT / UPDATE / DELETE / CREATE INDEX / ALTER TABLE / VACUUM / ANALYZE / REINDEX / CLUSTER / DROP TABLE / COPY / CREATE EXTENSION <other> / pg_dump / pg_restore all behave normally in the same database.
Root cause
ag_ProcessUtility_hook() in src/backend/catalog/ag_catalog.c handles T_TruncateStmt and calls get_graph_oid_for_table() — which resolves ag_catalog — without first checking whether the AGE extension exists in the current database:
case T_TruncateStmt:
{
TruncateStmt *tstmt = (TruncateStmt *) parsetree;
ListCell *lc;
foreach(lc, tstmt->relations)
{
RangeVar *rv = (RangeVar *) lfirst(lc);
Oid rel_oid = RangeVarGetRelid(rv, AccessShareLock, true);
if (OidIsValid(rel_oid))
{
Oid graph_oid = get_graph_oid_for_table(rel_oid); /* <-- resolves ag_catalog */
if (OidIsValid(graph_oid))
{
increment_graph_version(graph_oid);
}
}
}
}
break;The file already provides the guard for exactly this purpose — is_age_extension_exists(), with the comment:
We don't want most of hooks to do anything if the "age" extension isn't created.
but in the PG18 branch it is only called from is_age_drop() and object_access() — the T_TruncateStmt branch (lines 172–202) calls get_graph_oid_for_table() with no guard at all. Since shared_preload_libraries loads the library instance-wide, the hook runs in every database, including those where ag_catalog does not exist.
Suggested fix
Bail out early in the T_TruncateStmt branch, consistent with how the other hooks guard themselves:
case T_TruncateStmt:
{
TruncateStmt *tstmt = (TruncateStmt *) parsetree;
ListCell *lc;
if (!is_age_extension_exists())
break;
...
}Guarding the whole switch (or the hook entry point) would also work and would protect any future branch from the same mistake.
Impact / workaround
Workaround is to run CREATE EXTENSION age in every database of the instance, including template1 so that newly created databases inherit it. That is fragile: databases created with TEMPLATE template0 — which pg_dump/pg_restore output commonly uses — do not inherit it, and TRUNCATE starts failing there with an error that gives no hint that AGE is involved.
Happy to submit a PR if the maintainers agree with the approach.
Activity
gregfelice commentedon Aug 21, 2026
Confirmed on current master (
4d9b2506, AGE 1.8.0) built against PostgreSQL 18.4, withshared_preload_libraries = ageand a database that never hadCREATE EXTENSION agerun in it:I also reproduced your control result — in the same database, with the same preload, everything else works:
So the failure really is specific to
TRUNCATE, which matches your root-cause reading thatag_ProcessUtility_hook()handlesT_TruncateStmtand reachesget_graph_oid_for_table()without the extension-exists guard that #2161 added on theobject_access_hookpath.Worth underlining the operational impact you noted, because it is easy to miss: this is not confined to graph workloads. One database on the cluster wanting AGE means
TRUNCATEbreaks in every other database on that instance, including ones with no graph data at all. That makes preloading AGE unsafe on any shared cluster.Reported against 1.8.0 from PGDG apt on PG 18.6; confirmed here on master against PG 18.4, so it is neither fixed nor packaging-specific.
crdv7 commentedon Sep 18, 2026
Hi @luckydududu, we also ran into this issue and have prepared a fix with regression tests. Our patch also covers the VACUUM FULL and CLUSTER hook paths added in #2551 which did not exist when you opened this issue. I noticed you mentioned being happy to submit a PR. Have you already prepared one, or are you currently working on it? We'd be happy to share our patch and tests to avoid duplicating work.
luckydududu commentedon Sep 27, 2026
Hi @crdv7 — sorry for the late reply. I haven't prepared a PR, so no duplication. Thanks for taking it on; #2578 matches the fix I had in mind (guard each utility branch with
is_age_extension_exists()), and covering the VACUUM FULL / CLUSTER paths from #2551 is the right call. Happy to see this close #2520 once it lands.