Extension Upgrade Planning & Compatibility Validation

Promoting a PostgreSQL extension from one version to the next is not an isolated ALTER EXTENSION UPDATE; it is a gated validation pipeline that intersects schema migration ordering, connection pooler behaviour, replication topology, and SLO-bound maintenance windows. This guide is the anchor reference for building that pipeline across a production fleet: it explains the catalog surfaces that decide whether an upgrade is even reachable, walks every stage from compatibility mapping through dry-run simulation to a pass/fail gate, and shows exactly where Python and CI/CD logic hook in so that a promotion either advances deterministically or blocks and routes to a rollback — never half-applies silently.

If you have not yet modelled extensions as managed infrastructure, start with PostgreSQL Extension Architecture & Lifecycle Fundamentals; this page assumes that on-disk artifact model and focuses on the validation discipline that decides whether a candidate is safe to ship.

The Validation Pipeline at a Glance

Upgrade planning is a staged pipeline: a candidate only promotes after every gate passes, and any failure routes to triage and rollback rather than a partial write.

The gated upgrade validation pipeline A candidate moves left to right through four ordered stages — compatibility matrix, dependency resolution, asynchronous simulation, and error categorization — into a gate. The gate emits one binary decision: pass routes to a threshold check and promotion, fail routes to block and triage. Block and triage loops back on a dashed rollback path to the compatibility matrix so the candidate is re-planned rather than half-applied. Compatibility matrix is the tuple allowed? Dependency resolution topological apply order Async simulation dry-run + lock capture Error categorization transient vs terminal Gate promote? Block + triage no partial write Threshold + promote fits the window? pass fail rollback & re-plan

Every box is an automation boundary where the pipeline reads state, makes a decision, and records a verifiable result. The compatibility matrix decides whether the version tuple is allowed; dependency resolution decides in what order transitions run; simulation decides what would actually happen against production-sized data; error categorization decides whether a failure is transient or terminal; and threshold tuning decides whether the operation fits the window. The rest of this guide decomposes each boundary and names the deeper reference for it.

Core Concepts: What “Compatible” Actually Means

Compatibility validation fails when teams treat “the new version installs” as equivalent to “the new version is safe.” Four distinct surfaces must each be validated independently, because a candidate can pass three and still break production on the fourth.

  • Update-path reachability. PostgreSQL computes the sequence of migration scripts (extension--A--B.sql) from files present on disk on that specific node. A version is only reachable if an unbroken chain of --from--to-- scripts exists locally. A node that received a partial package upgrade will report extension "x" has no update path from version "A" to version "B" while its neighbours succeed — the single most common source of fleet-wide inconsistency.
  • ABI and shared-library compatibility. The compiled .so referenced by module_pathname exposes C symbols that the SQL-level function definitions call. A library built against a different PostgreSQL major, or a mismatched libc/libpq, produces could not find function ... in file errors that surface only at call time, not at install time — which is why static linkage checks (ldd) belong in the pipeline, not the incident review.
  • Catalog-state compatibility. An update rewrites rows in pg_proc, pg_class, pg_type, and pg_extension. Some of those rewrites (function signature changes, dropped operators) invalidate dependent objects, cached plans, or prepared statements in live sessions.
  • PostgreSQL-version compatibility. Each extension version supports a bounded band of server majors. Promotion has to cross-reference extension version against server version explicitly, which is the job of a maintained compatibility matrix rather than tribal knowledge.

The pipeline’s entire purpose is to convert these four latent, call-time or restart-time failure classes into deterministic, pre-flight decisions.

Four compatibility surfaces, each an independent gate A candidate upgrade descends through four sequential gates, and every gate must be cleared. Gate one, update-path reachability, inspects pg_available_extension_versions and the on-disk migration scripts and catches "has no update path from A to B". Gate two, ABI and shared library, inspects the module_pathname .so and its ldd symbol table and catches "could not find function in file". Gate three, catalog-state, inspects rewrites of pg_proc, pg_class and pg_type and catches invalidated cached plans and prepared statements. Gate four, PostgreSQL-version, inspects the extension version against the supported server-major band and catches an unsupported server and extension tuple. Only a candidate that clears all four is safe to promote. Candidate upgrade SURFACE INSPECTS CATCHES 1 Update-path reachability is the target hop present on this node? pg_available_extension_versions on-disk name--A--B.sql scripts has no update path from version "A" to "B" 2 ABI / shared library do C symbols resolve at call time? module_pathname .so ldd symbol + libc / libpq check could not find function ... in file 3 Catalog-state do rewrites invalidate live objects? pg_proc · pg_class · pg_type signature + operator rewrites stale cached plans and prepared statements 4 PostgreSQL-version does this server major fall in band? ext version × server major maintained compatibility matrix unsupported server / extension tuple Safe to promote — all four cleared

Architecture & State Model: How the Catalog Exposes Upgrade Candidacy

Automation that plans upgrades reads three catalog surfaces, and the critical one for planning is the third — the one that enumerates reachable versions and their per-version constraints.

Catalog view What it reports for upgrade planning What it does NOT tell you
pg_extension The version currently installed in this database and its target schema Whether a newer version is reachable; state in other databases across the same cluster
pg_available_extensions The default_version the control file would install and the installed_version here Every intermediate hop; whether the path is transaction-safe
pg_available_extension_versions Every version the on-disk scripts can install or reach, with per-version superuser and requires Whether applying that path is safe under load or fits the maintenance window

Three facts shape every planning query:

  1. pg_extension is per-database, not per-cluster. A candidate must be planned and validated per database, iterating across the whole cluster rather than assuming a single global state.
  2. Availability and installed state are decoupled. Dropping a new .so and control file makes a version available but does not apply it; planning must diff “reachable default” against “installed” to know whether work is pending.
  3. The reachable-version graph is computed from disk. Because pg_available_extension_versions reflects the scripts present on this node, reconciling those artifacts across every node before planning is a prerequisite — a discipline developed as Extension Registry Mapping.

A single query snapshots a database’s upgrade candidacy for the first gate — installed version, reachable default, and whether the target hop even exists:

SELECT
    e.extname,
    e.extversion                         AS installed,
    ae.default_version                   AS reachable_default,
    (e.extversion IS DISTINCT FROM ae.default_version) AS upgrade_pending,
    n.nspname                            AS schema
FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
LEFT JOIN pg_available_extensions ae ON ae.name = e.extname
ORDER BY upgrade_pending DESC, e.extname;

To confirm the path itself is reachable rather than just the endpoint, planning also inspects the version graph directly:

-- Does an update path from the installed version to the target exist on THIS node?
SELECT version, superuser, requires
FROM pg_available_extension_versions
WHERE name = 'postgis'
ORDER BY version;

If the target version is absent from that result on any node, the candidate is unreachable there and the gate must block before a single ALTER EXTENSION runs.

Pipeline Stages Walkthrough

The upgrade pipeline moves a candidate through five ordered stages. Each has a distinct failure mode, and the job of the pipeline is to make each one either a clean block or a recoverable rollback rather than a production incident.

Stage 1 — Compatibility mapping

The pipeline resolves the target version tuple against a maintained matrix that binds PostgreSQL minor releases, extension binaries, and shared-library dependencies. This is where an unsupported server/extension combination is rejected before anything is staged. Because static spreadsheets go stale, the matrix is generated from control files and artifact registries and checked in as a version-locked artifact — the mechanics of which live in Compatibility Matrix Synchronization.

Failure mode: a version tuple absent from the matrix, or a mismatched server major. The gate rejects the candidate at pull-request time with a structured error, never at 02:00 in production.

Stage 2 — Dependency resolution

Extensions are not independent: postgis_topology requires postgis, which may require others, and every hop must be applied in dependency order. The pipeline topologically sorts the transitive requires graph and validates each member’s version against the target, exactly as developed in Dependency Tree Analysis.

Failure mode: applying an upgrade out of dependency order, which cascades into required extension "x" is not installed or an ABI break in a dependent. A correct topological sort prevents the cascade entirely.

Stage 3 — Asynchronous simulation and dry-run

Rather than trusting the plan, the pipeline replays the full upgrade sequence against a snapshot clone, capturing the exact SQL execution order, lock acquisition sequence, and catalog diffs without blocking live transactions. This is where a missing update path or an unexpected AccessExclusiveLock is discovered in staging instead of production, and it is the subject of Async Upgrade Simulation. Routing that simulation onto an environment that mirrors production topology, configuration, and data cardinality is itself a discipline — see Test Environment Routing — because a simulation on a mismatched cluster validates nothing.

Failure mode: a non-transactional step (background-worker registration, shared-memory allocation) that commits immediately and cannot be rolled back. Simulation surfaces these so the plan can stage an explicit downgrade script and snapshot before the real run.

Stage 4 — Error categorization and the gate

Simulation output is only useful if the pipeline can decide whether a surfaced error is transient (retry), terminal-but-recoverable (rollback), or terminal (block and page a human). That taxonomy — mapping PostgreSQL SQLSTATE classes to automated responses — is the job of Error Categorization Frameworks. The gate consumes the categorized result and emits a single binary decision: promote or block.

Failure mode: miscategorising a terminal error as transient, which loops a doomed upgrade until the window closes. The taxonomy exists precisely to prevent that.

Stage 5 — Threshold check and promotion

A candidate that passes every prior gate still must fit the maintenance window. The pipeline estimates lock-hold duration, pooler drain time, and replication-lag tolerance against a computed threshold, promoting only if the operation fits — the calculation developed in Threshold Tuning for Downtime Windows. On promotion, execution is handed to the automated execution and rollback workflows; on any mid-flight failure it routes to Fallback Routing Strategies.

Failure mode: an operation whose real lock window exceeds the estimate, aborting mid-flight when the window closes. Historical lock-metric baselines keep the estimate honest.

Dependency & Compatibility Surface

The reason batch promotions fail is that three coupling mechanisms compound risk, and the validation pipeline must model all three before issuing any command.

  • Transitive requires chains. The full directed acyclic graph must be resolved and applied in topological order; validating version constraints across the whole graph is the guard against out-of-order cascades.
  • shared_preload_libraries ordering. Extensions with C-level interdependencies must appear in the correct order in the preload list. Reordering or inserting an entry changes shared-memory allocation and, if wrong, aborts cluster bootstrap — and because it lives in postgresql.conf, it requires a restart that the threshold check must account for.
  • Version constraint bands. Each extension version supports a bounded range of server majors, maintained as a live matrix rather than assumed.

A minimal constraint view — the authoritative version is generated and kept current in the matrix synchronization guide:

Extension Extension version Supported PostgreSQL Preload required Cross-major upgrade path
PostGIS 3.4.x 12–17 no ALTER EXTENSION UPDATE
pg_partman 5.x 14–17 pg_partman_bgw if using BGW ALTER EXTENSION UPDATE
TimescaleDB 2.14.x 13–16 timescaledb (required) dump/reload across majors
pgvector 0.7.x 12–17 no ALTER EXTENSION UPDATE

As documented in the official PostgreSQL Extension Development Guide, extensions may register background workers, custom GUCs, or catalog modifications that must load in a precise sequence; misordered preload directives or unresolved symbols abort bootstrap before any matrix can help, which is why preload ordering is validated in simulation on a production-mirroring topology.

Dependency order and version constraints as one resolution problem On the left, a requires chain in apply order: postgis_topology requires postgis, which requires address_standardizer. On the right, a grid of PostgreSQL majors 14, 15, 16 and 17 marks which server versions each extension supports. postgis_topology and postgis support 14 through 17, but address_standardizer is unsupported on 14 and supports only 15 through 17. The chain is promotable only in the highlighted band where all three supported ranges overlap, PostgreSQL 15 to 17, so the pipeline must satisfy dependency order and every version constraint together before promotion. REQUIRES CHAIN (apply order ↓) SUPPORTED PostgreSQL MAJOR promotable band: PG 15–17 (all ranges overlap) 14 15 16 17 postgis_topology top of the chain postgis middle dependency address_standardizer narrowest range — drops PG 14 requires requires unsupported

Automation Integration Points

The pipeline is driven from CI/CD, not a psql prompt, and there are three points where Python logic hooks into every candidate.

1. Pre-flight gates. Before any command runs, the pipeline snapshots pg_extension, confirms the target is reachable in pg_available_extension_versions, resolves the dependency graph, and validates the tuple against the matrix. Any failure blocks the deployment rather than attempting it. Storing control-file checksums alongside deployment manifests — aligned with Version Control & Branching — lets the gate assert strict environment parity so the version that was validated is the version that ships.

2. Dry-run payloads. The pipeline emits a structured plan — every command it would run, in order, with from/to versions — and asserts it against expectations before committing. A dry run is the difference between discovering a missing update path in staging versus at 02:00 in production.

3. Post-check assertions. After promotion the pipeline re-reads the catalog and runtime, confirms the observed version matches the intent, and only then releases dependent workloads; divergence routes to a fallback restore.

A minimal planner illustrates the pre-flight decision — it plans from catalog state and refuses to act when the target is unreachable:

#!/usr/bin/env python3
"""Upgrade planner: emit a dry-run plan only when the target is reachable."""
import psycopg2


def plan_upgrade(conn, name: str, target: str) -> dict:
    with conn.cursor() as cur:
        cur.execute(
            "SELECT extversion FROM pg_extension WHERE extname = %s", (name,)
        )
        row = cur.fetchone()
        installed = row[0] if row else None

        # Confirm the target version is reachable on THIS node before planning.
        cur.execute(
            """
            SELECT 1
            FROM pg_available_extension_versions
            WHERE name = %s AND version = %s
            """,
            (name, target),
        )
        reachable = cur.fetchone() is not None

    if installed == target:
        return {"decision": "noop", "installed": installed}
    if not reachable:
        return {"decision": "block", "reason": "no update path to target",
                "installed": installed, "target": target}

    stmt = (
        f'CREATE EXTENSION "{name}" VERSION %s'
        if installed is None
        else f'ALTER EXTENSION "{name}" UPDATE TO %s'
    )
    return {"decision": "promote", "from": installed, "to": target,
            "stmt": stmt, "params": [target]}

The transactional-safety analysis of the ALTER EXTENSION step itself — which object types commit immediately and how to wrap the rest — is developed in ALTER EXTENSION automation, and the backup side of a failed promotion, including PITR restore, covers how to rewind a catalog that advanced past a partial failure.

Security & Privilege Enforcement

An upgrade executes its migration SQL with elevated privilege and can register functions, composite types, and untrusted procedural languages directly into shared schemas — so the validation pipeline treats privilege as a gate, not an afterthought. Three enforcement practices contain the blast radius before promotion:

  • Static payload analysis. CI scans the migration SQL for privilege-escalation patterns, untrusted-language invocations (plpythonu, plperlu), and unauthorized catalog modifications, rejecting the candidate before it reaches staging.
  • Restricted SUPERUSER delegation. Promotion runs through a controlled installer role or trusted-extension marking rather than a broad superuser grant; the specific hazards of installing as superuser are enumerated in security implications of superuser extension installation.
  • Schema isolation. Relocatable extensions promote into a dedicated schema with a pinned search_path, so an upgraded function cannot be shadowed by an untrusted caller mid-transition.

These practices are the operational core of Security Boundaries & Permissions and are non-negotiable before any candidate promotes into a shared production database.

Observability & Debugging

A promotion is not complete until telemetry confirms a healthy post-state. Because extensions can introduce custom cost functions, index access methods, and background workers, upgrade events must be correlated with runtime behaviour: watch pg_stat_activity for lock waits and long-running backends immediately after the transition, track background-worker memory for extensions that register workers, and tail extension-specific log channels for the immediate-commit errors that non-transactional updates emit.

A practical post-promotion probe confirms the gate’s decision held:

-- Any backend blocked on a lock immediately after the promotion?
SELECT pid, wait_event_type, wait_event, state,
       now() - query_start AS running_for, left(query, 60) AS query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY running_for DESC;

Correlating extension version against latency, lock contention, and cache-hit ratios via structured telemetry lets teams isolate a problematic promotion before it degrades the whole cluster; understanding which ALTER EXTENSION operations are transactional versus not is documented in the official PostgreSQL ALTER EXTENSION documentation and applied throughout the rollback workflows.

FAQ

Why does an upgrade candidate pass validation in staging but fail on one production node?

Because update-path reachability is computed from the migration scripts present on disk on that node. A node with an incomplete package upgrade is missing an intermediate --from--to-- script and reports has no update path from version "A" to version "B", while fully patched nodes succeed. Reconcile on-disk artifacts across every node before planning, and confirm the target version appears in pg_available_extension_versions on each node in the pre-flight gate.

Can I roll back a promotion by wrapping ALTER EXTENSION UPDATE in a transaction?

Only partially. Ordinary catalog DDL inside the update is transactional and rolls back cleanly, but steps that register a background worker, allocate shared memory, or touch cluster-global state commit immediately and are not undone by ROLLBACK. The simulation stage exists to surface those steps so the plan stages an explicit downgrade script and a pre-upgrade snapshot for recovery.

What decides whether a failed promotion retries or rolls back?

The error taxonomy. Each surfaced SQLSTATE class maps to an automated response — transient contention retries, a recoverable terminal error routes to rollback, and an unrecoverable one blocks and pages a human. Miscategorising a terminal error as transient loops a doomed upgrade until the window closes, which is why the categorization stage precedes the gate.

How do I know an upgrade will fit its maintenance window before I run it?

Estimate lock-hold duration, connection-pooler drain time, and replication-lag tolerance against a threshold computed from historical lock-acquisition metrics for that extension and cluster size. If the estimate exceeds the window, the gate blocks and the operation is rescheduled or re-planned as a dump/reload rather than an in-place update.

Does a compatibility matrix replace testing the actual upgrade?

No. The matrix rejects unsupported version tuples cheaply at pull-request time, but it cannot predict lock contention, catalog rewrites, or ABI symbol resolution against real data. The matrix is the first gate; asynchronous simulation on a production-mirroring topology is what proves the promotion is actually safe.