Rules

What is held

Every statement goes through the Postgres grammar compiled into the binary. There is no regex on the decision path. If it does not parse, it does not run. A WHERE with no column reference (WHERE true, WHERE 1=1) counts as unscoped. EXPLAIN ANALYZE is classified as the statement it would run.

Built-in actions

The same table on Free, Pro, and Team when rules.builtin is default.

KindExamplesActionSnapshot
selectSELECT, SHOW, EXPLAIN without ANALYZEallownone
insertINSERT, INSERT ... ON CONFLICTallownone
updateUPDATE with a column in WHEREallownone
deleteDELETE with a column in WHEREallownone
update_unscopedUPDATE with no column in WHEREholddata and DDL of the target
delete_unscopedDELETE FROM t, or WHERE trueholddata and DDL of the target
mergeMERGE with matched-only actionsallownone
merge_delete_unscopedWHEN NOT MATCHED BY SOURCE THEN DELETEholdtarget
truncateTRUNCATE, including CASCADEholdnamed tables, plus FK children on CASCADE
drop_tableDROP TABLEholdeach table
drop_schemaDROP SCHEMAholdevery ordinary table in the schema
drop_databaseDROP DATABASEholdnone. See restore.
drop_otherDROP VIEW, INDEX, FUNCTION, ROLE, ...holdnone, or DDL only for a policy or trigger on a table
alter_drop_columnALTER TABLE t DROP COLUMNholddata and DDL of t
alter_drop_constraintALTER TABLE t DROP CONSTRAINTholdDDL only
alter_column_typeALTER COLUMN ... TYPEallownone. A hold rule is commented in the init template.
alter_otherother ALTER TABLE, RENAMEallownone
rls_disableDISABLE ROW LEVEL SECURITY, NO FORCEholdDDL only
createCREATE TABLE, INDEX, VIEW, SQL functionsallownone
grant_revokeGRANT, REVOKE, ALTER DEFAULT PRIVILEGESallownone
copy_from_stdin, copy_to_stdoutCOPY with STDIN or STDOUTallownone
copy_programCOPY ... PROGRAM, or a server filedenynone
do_blockDO $$ ... $$holdnone
create_function_opaqueCREATE FUNCTION in plpgsql or another non-SQL languageholdnone
create_trigger, create_ruleCREATE TRIGGER, CREATE RULEholdnone
prepare_heldPREPARE of a statement that would be helddenynone
setSET, RESET, SET ROLEallownone
transaction_controlBEGIN, COMMIT, ROLLBACK, SAVEPOINTallownone
utilityVACUUM, ANALYZE, LISTEN, CALL, othersallownone. CALL is a documented limit.
protected_objectanything in schema tablebeltdenynot overridable
unparsedparse error, or statement over the size limitdenynot overridable

custom is a hold from a user rule on a kind that would otherwise be allowed. large_write (an EXPLAIN row estimate) is not in this version. A scoped DELETE ... WHERE id > 0 that matches every row is allowed.

Evaluation order

  1. unparsed and protected_object are always deny. Nothing overrides them.
  2. Hosted extra rules (Team), in order. They may only hold or deny.
  3. Local rules.custom, in order. First match wins. allow can relax a built-in hold, for example TRUNCATE public.sessions. It cannot relax step 1 or a hosted rule.
  4. The built-in table above.

If a hosted rule and a local rule both match, the more severe action wins. A hosted hold beats a local allow.

rules.custom
rules:
  builtin: default
  protected_schemas: [tablebelt]
  custom:
    - id: deny-drop-database
      description: Never drop a database through the agent path
      match: { kinds: [drop_database] }
      action: deny
    - id: allow-truncate-sessions
      match: { kinds: [truncate], tables: ["public.sessions", "public.cache_*"] }
      action: allow
    - id: hold-type-changes
      match: { kinds: [alter_column_type] }
      action: hold
    - id: hold-migrations-tool-drops
      match: { kinds: [drop_other], client_applications: ["prisma*"] }
      action: hold

kinds matches if any statement kind is in the list. tables is a glob on schema.table after resolution. client_applications globs the startup application_name. Empty match fields match everything. Unknown keys fail config validation.

tablebelt init writes deny-drop-database enabled, so a fresh install denies DROP DATABASE. See Restore.

rules test

tablebelt rules test
tablebelt rules test "DELETE FROM users; SELECT 1"
tablebelt rules test --file migration.sql
printf '%s' "TRUNCATE public.sessions" | tablebelt rules test --resolve

Without --resolve, unqualified names show as ?.name. Exit code 4 means a statement would be denied, which is useful in CI. tablebelt rules list prints built-in, hosted, and local rules in evaluation order.

Limits