Skip to content

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") in fillRelOptions() 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

  1. The utility hook strips the options from the CREATE TABLE statement before PostgreSQL sees it.
  2. It validates them itself.
  3. Once the table exists, it writes them into pg_class.reloptions directly.

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.