Statement-level enforcement¶
ProcessUtility_hook is a global function pointer PostgreSQL consults for every statement that is not a plain query. This extension uses it for two things.
Creation-time handling of the options¶
permissions and retention are not registered reloptions — they cannot be, for reasons covered in How Options Are Stored. So the hook removes them from the statement before PostgreSQL sees it, validates them itself, and writes the accepted values to the catalogue once the table exists.
This is also where creation-time validation happens: unknown permission names, empty lists, insert with insertonce, retention below 1, a vault table with no permissions, the options on a non-vault table, and any combination of vault with partitioning.
A useful side effect: without the library loaded, CREATE TABLE ... WITH (permissions = ...) fails as an unrecognised parameter. A vault table cannot be created by a server that is not in a position to enforce anything about it.
Gating ALTER TABLE¶
This must happen in the statement hook rather than the object hook, because OAT_POST_ALTER fires after the fact — and ALTER TABLE ... SET ACCESS METHOD heap has to be stopped before the rewrite begins.
The gate walks the statement's subcommands and classifies each one:
| Subcommand | Outcome |
|---|---|
OWNER TO, ADD CONSTRAINT |
Always permitted — the forms a dump replays |
Column SET STORAGE/SET COMPRESSION/SET STATISTICS, column SET/RESET (...), CLUSTER ON, SET WITHOUT CLUSTER, REPLICA IDENTITY |
Always permitted — tuning that cannot reach a row |
Trigger enable/disable, row security, logged/unlogged, tablespace, ADD COLUMN, column defaults |
Permitted where the table grants trigger, rls, log, tablespace, addcolumn or coldefault |
SET (...), RESET (...) |
Refused, and cannot be opened up — see below |
| Everything else | Refused, with no permission that grants it |
The first refusal takes the whole statement with it, so mixing a permitted form with a refused one leaves neither applied.
CREATE, ALTER and DROP POLICY are gated by policy on their own path, since they are separate statement types rather than AlterTableStmt subcommands. DROP POLICY names its table as the front of a qualified name list, which is rebuilt into a RangeVar to look the options up.
Why table-level storage parameters cannot be permitted¶
ATExecSetRelOptions validates a table's whole merged reloptions array with validate = true. This extension keeps permissions and retention in that array without registering them with core, so core refuses the statement — even SET (fillfactor = 70), which has nothing to do with this extension — before the hook is consulted.
That is not a policy the gate could relax. It is also load-bearing: the same validation is what puts permissions and retention beyond the reach of any ALTER TABLE, so neither can be widened or stripped. Storage parameters given in the CREATE TABLE statement work normally, because the hook removes its own options before core sees them.
Indexes are never intercepted. ALTER INDEX names the index, whose relam is btree rather than vault, so the gate returns early — deliberate, not incidental.
The rewrite that has to be handled¶
SET LOGGED, SET UNLOGGED, and ADD COLUMN with a volatile default or a generated column, rebuild the table: PostgreSQL creates a fresh relfilenode and copies every live row through table_tuple_insert, which lands in the vault access method's own insert callback. Left alone, each copied row is treated as new and given a fresh retention deadline — silently extending every row's retention by a full period.
A flag set around the rewriting statement makes the insert path preserve a deadline that is already present, exactly as restore_mode does on the restore path. VACUUM FULL and CLUSTER need none of this: they copy through table_relation_copy_for_cluster, which is delegated to heap and never reaches the insert callback.
"ALTER TABLE" is three node types¶
The manual's ALTER TABLE is not one thing in the parser:
| Statement | Node |
|---|---|
ALTER TABLE ... OWNER TO, SET ACCESS METHOD, ADD COLUMN … |
AlterTableStmt |
ALTER TABLE ... RENAME |
RenameStmt |
ALTER TABLE ... SET SCHEMA |
AlterObjectSchemaStmt |
Covering only the first leaves a vault table renameable and movable between schemas.
Internally generated subcommands are exempt¶
PostgreSQL builds its own AlterTableStmt for work done on a table's behalf — adding a REFERENCES constraint named in CREATE TABLE, for one — and routes it back through ProcessUtility with PROCESS_UTILITY_SUBCOMMAND.
Refusing those makes a vault table with a foreign key impossible to create. Only that context is exempt: a statement written inside a function arrives as PROCESS_UTILITY_QUERY and is still refused.
Two permitted forms¶
OWNER TO and ADD CONSTRAINT are accepted, because pg_dump emits both and without them a backup cannot be restored. A statement mixing either with anything else is refused whole.
Neither reaches the data. An owner cannot exceed the table's permissions, and a constraint can only restrict what may be stored.
What is deliberately not intercepted¶
COMMENT ON and GRANT/REVOKE pass straight through.
Enforcement sits below PostgreSQL's permission system, so a GRANT cannot widen what the access method allows. The most it can do is let a broader set of roles attempt something that is still refused.