How options are stored¶
permissions and retention live in pg_class.reloptions, but they never pass through PostgreSQL's reloption machinery. This page explains why, because the arrangement looks odd until you know.
Why they are not registered reloptions¶
PostgreSQL 18 has no per-access-method reloptions callback. heap_reloptions() runs for every ordinary relation regardless of its access method.
Registering against RELOPT_KIND_HEAP does not work either. That routes to default_reloptions(), which parses against a static const relopt_parse_elt tab[] containing core's options only. An option registered for the kind but absent from that table:
- trips
Assert(numoptions <= num_relopt_elems)in an assert build, taking the postmaster down, and - reaches
elog(ERROR, "reloption \"%s\" not found in parse table")infillRelOptions()in any build,
because DefineRelation() validates with validate = true for every ordinary table.
There is no way to extend that table from an extension.
What happens instead¶
- The utility hook strips the options from the
CREATE TABLEstatement before PostgreSQL sees it. - It validates them itself.
- Once the table exists, it writes them into
pg_class.reloptionsdirectly.
The catalogue read path uses validate = false and silently ignores names it does not recognise, which is what makes storing them there safe.
Why pg_class.reloptions and not an extension table¶
Because pg_dump emits WITH (...) straight from pg_class.reloptions.
A dump therefore round-trips the declaration through the same hook with no special handling, no pg_extension_config_dump, and no restore-ordering hazard. A partial restore cannot produce a table that looks protected but is not.
An extension catalogue table would have needed its rows dumped separately, restored in the right order, and kept in step with DROP.
The quoting trap¶
pg_dump quotes a reloption value only when it needs to, so a restore presents three different literal forms:
WITH (permissions='insert,delete') -- quoted: contains a comma
WITH (permissions=insert, retention='90') -- bare, and a quoted integer
The integer arrives as a String node where the original DDL gave an Integer. Parsing with defGetInt64() therefore produces dumps that cannot be restored; defGetString() handles every form. There is a regression test for exactly this literal.
Reading the values back¶
Via untransformRelOptions(), which does not consult the option registry at all and splits each element on its first = — so a value containing commas, as permissions always does, survives intact.
One parser serves everything: enforcement, the creation-time checks, and the SQL accessor. A second implementation that drifted would be a silent hole in the guarantee.
A consequence worth stating¶
Because the values live in pg_class, which PostgreSQL makes world-readable, a table's permissions and retention are visible to any role. pgvault_tables.view_vault_tables() is a convenience that returns them in a sensible shape, not an access control. Nothing here is confidential.