Check Grants on Removed PG11 Catalog Tables Before a PG15 Upgrade

When upgrading from a PostgreSQL 11-based YugabyteDB release to a PostgreSQL 15-based YugabyteDB release, the upgrade includes a PostgreSQL major-version upgrade phase.

One issue you may encounter involves catalog tables that existed in the PG11-based release but no longer exist in the PG15-based release.

For example, the upgrade may encounter an error like:

				
					ERROR: relation "pg_catalog.pg_pltemplate" does not exist

STATEMENT:
GRANT SELECT ON TABLE "pg_catalog"."pg_pltemplate" TO "appdba_role";
				
			

Why?

The source database contains an explicit grant on pg_catalog.pg_pltemplate, but that relation no longer exists in the PG15 catalog. When the upgrade process attempts to recreate the grant against the PG15-based target, it fails.

🐛 Related YugabyteDB Issue: #32356
This scenario is related to YSQL ACL preflight check is incomplete for removed/renamed catalog objects. During a PG11-to-PG15 YSQL major upgrade, ACL changes can reference catalog objects that no longer exist in the target catalog, causing the upgrade to fail. The issue tracks expanding the preflight checks to catch additional ACL-bearing objects, including catalog relations, before the upgrade proceeds.

Two Catalog Relations to Check

For this upgrade path, check for explicit grants on:
  • ● pg_pltemplate
  • ● yb_int_pg_stats_v11

Before starting the upgrade, run the following query on the PG11-based YugabyteDB universe. It identifies explicit privileges on the two catalog relations granted to non-owner, non-superuser roles and generates the corresponding REVOKE command. PUBLIC, relation owners, and superusers are intentionally excluded so the results focus on grants that may have been manually added to user-created roles.

				
					SELECT
    n.nspname AS schema_name,
    c.relname AS relation_name,
    r.rolname AS grantee,
    a.privilege_type,
    a.is_grantable,
    format(
        'REVOKE %s ON TABLE %I.%I FROM %I;',
        a.privilege_type,
        n.nspname,
        c.relname,
        r.rolname
    ) AS revoke_command
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n
  ON n.oid = c.relnamespace
CROSS JOIN LATERAL pg_catalog.aclexplode(c.relacl) a
JOIN pg_catalog.pg_roles r
  ON r.oid = a.grantee
WHERE n.nspname = 'pg_catalog'
  AND c.relname IN (
      'pg_pltemplate',
      'yb_int_pg_stats_v11'
  )
  AND c.relacl IS NOT NULL

  -- Ignore PUBLIC
  AND a.grantee <> 0

  -- Ignore the relation owner
  AND a.grantee <> c.relowner

  -- Ignore all superusers / administrative users
  AND r.rolsuper = false
ORDER BY
    c.relname,
    r.rolname,
    a.privilege_type;
				
			

If no rows are returned, there are no matching non-owner, non-superuser grants on these two catalog relations that require review.

If rows are returned, review each entry and its generated revoke_command. The query intentionally excludes PUBLIC, the relation owner, and all superuser roles so the results focus on privileges granted to ordinary user-created roles.

Example output:

				
					.schema_name | relation_name | grantee     | privilege_type | is_grantable | revoke_command
-------------+---------------+-------------+----------------+--------------+---------------------------------------------------------------
 pg_catalog  | pg_pltemplate | appdba_role | SELECT         | f            | REVOKE SELECT ON TABLE pg_catalog.pg_pltemplate FROM appdba_role;
				
			

Review each generated revoke_command before running it. If the privilege was manually granted to a user-created role, revoke it before beginning the upgrade.

For example:

				
					REVOKE SELECT ON TABLE pg_catalog.pg_pltemplate FROM appdba_role;
				
			
⚠️ Important Caution
Only remove manually added privileges from ordinary user-created roles, such as application or support roles. Do not revoke baseline privileges from superusers, the relation owner, PUBLIC, or other built-in administrative roles.

And, if applicable:

				
					REVOKE SELECT
ON TABLE pg_catalog.yb_int_pg_stats_v11
FROM appdba_role;
				
			

Then rerun the check:

				
					SELECT
    n.nspname AS schema_name,
    c.relname AS relation_name,
    r.rolname AS grantee,
    a.privilege_type,
    a.is_grantable,
    format(
        'REVOKE %s ON TABLE %I.%I FROM %I;',
        a.privilege_type,
        n.nspname,
        c.relname,
        r.rolname
    ) AS revoke_command
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n
  ON n.oid = c.relnamespace
CROSS JOIN LATERAL pg_catalog.aclexplode(c.relacl) a
JOIN pg_catalog.pg_roles r
  ON r.oid = a.grantee
WHERE n.nspname = 'pg_catalog'
  AND c.relname IN (
      'pg_pltemplate',
      'yb_int_pg_stats_v11'
  )
  AND c.relacl IS NOT NULL

  -- Ignore PUBLIC
  AND a.grantee <> 0

  -- Ignore the relation owner
  AND a.grantee <> c.relowner

  -- Ignore all superusers / administrative users
  AND r.rolsuper = false
ORDER BY
    c.relname,
    r.rolname,
    a.privilege_type;
				
			

Ideally, the query should now return no rows.

💡 Upgrade Readiness Check
Before a PG11-to-PG15 YugabyteDB upgrade, check for manually assigned privileges on catalog relations that do not exist in the PG15-based release. Removing stale grants ahead of time can prevent the major-upgrade phase from attempting to recreate privileges on objects that no longer exist.

Final Takeaway

PostgreSQL major-version upgrades involve more than just migrating application tables. The PostgreSQL system catalog changes too.

A simple pre-upgrade check for explicit grants on:

				
					pg_catalog.pg_pltemplate
pg_catalog.yb_int_pg_stats_v11
				
			

can catch a problem before the upgrade window.

If grants exist, revoke them in the PG11-based YugabyteDB universe first, verify the ACLs are gone, and then proceed with the upgrade.

Have Fun!

There we are! 😀 YugabyteDB on the directory at our Sunnyvale, CA office.

Always cool seeing the YB name right in the heart of Silicon Valley!