Skip to content

Object-level enforcement

object_access_hook fires as PostgreSQL acts on an individual catalogue object, with the object's class, its OID, and what is being done to it.

This layer owns DROP and TRUNCATE outright rather than backstopping a check elsewhere.


Why not the statement hook

Because a statement only names what the user typed.

Statement What it names What it destroys
DROP SCHEMA x CASCADE the schema every table in it
TRUNCATE parent CASCADE the parent every FK-referencing table
DROP EXTENSION ... CASCADE the extension every vault table

By the time the object hook fires, the cascade has been expanded and each doomed table arrives individually. That is the only place the guarantee can be made to hold, so duplicating a weaker check in the statement hook would add a second thing to keep correct without adding coverage.


What is gated

Event Permission required
OAT_DROP drop
OAT_TRUNCATE truncate

Only ordinary tables are considered. A vault table's TOAST relation and its indexes are dropped as dependencies of the table itself, authorised by the check on that table — gating them separately makes a permitted DROP impossible to complete.


Internal deletions are skipped

Deletions carrying PERFORM_DELETION_INTERNAL are exempt, and this is not a convenience.

VACUUM FULL and CLUSTER build a transient relation — pg_temp_<oid> — that inherits the table's access method, copy the live rows into it, swap the files, and drop the transient one. That transient table carries no permissions of its own, so gating it refused the drop and made both commands impossible on any vault table. Bloat could never be reclaimed on exactly the long-lived tables this extension exists for.

The flag is set only by PostgreSQL for its own bookkeeping and cannot be requested from SQL, so it discriminates precisely rather than opening a loophole. A user DROP, and every cascade reached from one, arrives without it and is still gated.


Hook chaining

Both hooks save whatever was installed before them and call it first, so another extension's hook is never skipped — including when this extension is about to raise.