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.