Skip to content

TRUNCATE fails with schema "ag_catalog" does not exist in databases without AGE, when AGE is in shared_preload_libraries #2520

Closed

Description

@luckydududu

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 exist

The 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 TABLE

Only 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

  1. gregfelice commented on Aug 21, 2026

    @gregfelice
    Contributor

    Confirmed on current master (4d9b2506, AGE 1.8.0) built against PostgreSQL 18.4, with shared_preload_libraries = age and a database that never had CREATE EXTENSION age run in it:

    $ psql -d noage -c "CREATE TABLE t (i int);" -c "INSERT INTO t VALUES (1);" -c "TRUNCATE t;"
    CREATE TABLE
    INSERT 0 1
    ERROR:  schema "ag_catalog" does not exist
    

    I also reproduced your control result — in the same database, with the same preload, everything else works:

    UPDATE 1
    DELETE 1
    VACUUM
    DROP TABLE
    

    So the failure really is specific to TRUNCATE, which matches your root-cause reading that ag_ProcessUtility_hook() handles T_TruncateStmt and reaches get_graph_oid_for_table() without the extension-exists guard that #2161 added on the object_access_hook path.

    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 TRUNCATE breaks 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.

  2. crdv7 commented on Sep 18, 2026

    @crdv7
    Contributor

    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.

  3. luckydududu commented on Sep 27, 2026

    @luckydududu
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions