Resolving 0A000 Feature-Not-Supported Errors During Extension Upgrades
What a 0A000 (feature_not_supported) error means during ALTER EXTENSION — an unsupported relocation, a downgrade the extension never shipped, or an operation the target server major cannot perform — and why it is a terminal signal to change the plan, not the role.
Up: Error Categorization Frameworks — this page is the 0A000 entry in that framework’s taxonomy; the general classifier is described in categorizing extension upgrade errors for automated triage.
Context & When This Applies
A 0A000 tells you the operation is well-formed and permitted but the server or extension simply does not implement it. During extension work it shows up in a handful of stable situations: asking a non-relocatable extension to SET SCHEMA, requesting a downgrade path an extension never authored, or invoking an extension feature that the target PostgreSQL major dropped or has not yet added. This page applies to PostgreSQL 12–17. Unlike a privilege error (42501) or a definition error (42P13), a 0A000 is not fixable by editing a grant or a function body — the capability is absent, so the plan itself must change.
That makes 0A000 a DEPENDENCY_BLOCK-class terminal in the categorizer: retrying is pointless, and the resolution is a different upgrade route — often the choice between an in-place and a logical path covered in pg_upgrade vs a logical replication upgrade path. Surfacing it in async upgrade simulation is what keeps it from becoming a stalled maintenance window.
Concept: The Capability Is Absent, So the Plan Must Move
0A000 is PostgreSQL’s way of saying “I understood you, and you were allowed, but this is not implemented.” For extensions, three capability gaps produce nearly all real occurrences. A non-relocatable extension declares relocatable = false in its control file, so ALTER EXTENSION ... SET SCHEMA is refused — the objects are pinned to their install schema. A missing downgrade path occurs because extensions ship forward --from--to-- scripts but rarely reverse ones, so ALTER EXTENSION ... UPDATE TO '<older>' finds no script. A target-major gap appears when an extension operation relies on a server feature the target PostgreSQL major removed or does not yet provide.
The unifying property — and the reason 0A000 is terminal in the classifier — is that none of these is a transient or a role problem. The resolution always changes the plan: keep the extension in its schema, restore prior state through snapshot and point-in-time recovery instead of downgrading, or switch to a different upgrade route. Because the fix is architectural, catching 0A000 at planning time is far cheaper than at execution time, which is exactly what the four compatibility surfaces of the compatibility validation pipeline exist to do.
Runnable Implementation
The helper below inspects the catalog to confirm which 0A000 origin applies before your automation gives up, so the triage ticket names the required plan change rather than a generic failure.
#!/usr/bin/env python3
"""Diagnose the plan change a 0A000 feature_not_supported requires."""
import psycopg2
def diagnose_0a000(conn, extname: str, requested_version: str | None) -> dict:
with conn.cursor() as cur:
# Is the extension relocatable? A non-relocatable one cannot SET SCHEMA.
cur.execute(
"SELECT extrelocatable, extversion FROM pg_extension WHERE extname = %s",
(extname,),
)
row = cur.fetchone()
if row is None:
return {"origin": "unknown", "plan": "extension not installed here"}
relocatable, installed = row
# Does a path to the requested (older) version exist on this node?
downgrade_available = None
if requested_version is not None:
cur.execute(
"""
SELECT 1 FROM pg_extension_update_paths(%s)
WHERE source = %s AND target = %s AND path IS NOT NULL
""",
(extname, installed, requested_version),
)
downgrade_available = cur.fetchone() is not None
if requested_version is not None and downgrade_available is False:
return {"origin": "unsupported_downgrade",
"plan": "no downgrade script; restore via snapshot/PITR instead"}
if not relocatable:
return {"origin": "non_relocatable",
"plan": "cannot SET SCHEMA; keep the extension in its fixed schema"}
return {"origin": "target_major_gap",
"plan": "feature absent on this server major; change the upgrade route"}
if __name__ == "__main__":
conn = psycopg2.connect("dbname=appdb")
import json
print(json.dumps(diagnose_0a000(conn, "postgis", "3.3.0"), indent=2))
Route the returned plan into your gate: a downgrade block hands off to snapshot and point-in-time recovery, while a target-major gap hands off to the route decision in pg_upgrade vs a logical replication upgrade path.
Expected Output & Verification
For a requested downgrade with no reverse script, the helper names the plan change:
{
"origin": "unsupported_downgrade",
"plan": "no downgrade script; restore via snapshot/PITR instead"
}
Verify the downgrade path really is absent — the most-actionable 0A000 — by listing the update paths the node actually has:
-- Every version transition this node can perform for the extension.
-- A NULL path means the transition is unsupported (a 0A000 if attempted).
SELECT source, target, path
FROM pg_extension_update_paths('postgis')
ORDER BY source, target;
A NULL in the path column for your requested transition confirms the 0A000 is structural, not incidental. Record the confirmed limitation in compatibility matrix synchronization so the matrix never proposes that transition again.
Edge Cases & Gotchas
ALTER EXTENSION ... SET SCHEMA on a non-relocatable extension is a hard no. The message is explicit:
ERROR: extension "postgis" does not support SET SCHEMA
There is no flag to override it. Design the deployment so the extension installs into its intended schema the first time, tracked under version control and branching, because moving it later is not an option.
A downgrade 0A000 is your cue to use PITR, not to hunt for a flag. When rollback means going to an older extension version:
ERROR: extension "timescaledb" has no update path from version "2.14.2" to "2.13.1"
there is no supported in-place downgrade for most extensions. The correct rollback is a restore to a pre-upgrade checkpoint via snapshot and point-in-time recovery, which is exactly why the pipeline snapshots before any non-transactional upgrade.
A target-major gap only appears after pg_upgrade, at first use. pg_upgrade copies the catalog, so an operation the new major no longer supports surfaces as 0A000 the first time it runs post-upgrade, not during the upgrade itself. Simulate the workload, not just the ALTER EXTENSION, on a topology-matched clone via test environment routing to catch these before cutover.
Do not let a 0A000 loop as if it were LOCK_CONTENTION. Because it is terminal, a classifier that miscategorizes it as retryable will burn the whole maintenance window re-attempting an impossible operation. Keep the explicit 0A000 rule ahead of any broad retry rule, per categorizing extension upgrade errors for automated triage.