PostgreSQL: Protect tables against accidental deletion

There is a risk of accidentally deleting an important table whenever we interact with a PostgreSQL server.

PostgreSQL: Protect tables against accidental deletion

If you have any questions, comments, corrections, or suggestions - please contact StJohn Piano on Tela:
tela.app/id/stjohn_piano/7c51a6

Follow Tela Network on LinkedIn:
linkedin.com/company/tela-network

Join Tela Network and become a consultant:
telablog.com/how-to-use-tela-for-consulting


The Problem

There is a risk of accidentally deleting an important table whenever we interact with a PostgreSQL server.

We interact with a PostgreSQL server whenever we:

  • write statements manually in a client
  • prepare statements in a text file and copypaste them into a client
  • generate SQL statements in another programming language

We want to add a protective guardrail that prevents accidental deletion.

The Solution

We create an event trigger that fires when the DROP command is used. It calls a function that checks the table name against a list of protected table names.

Why not use privileges instead ?

For normal operation, we usually create a second database user with limited privileges that can insert / delete data, but does not own anything, and thus cannot drop a table.

However, this does not protect against "fat-finger" / "oops" moments when a tired database administrator needs to drop a table but makes a mistake.

Protecting against this possibility with a simple guardrail is cheaper than restoring from a backup.

Background Material

The commands in these notes can be run in the Postgres client psql.

The article PostgreSQL: Background. It covers:

  • how to connect to Postgres with psql
  • the structure of a Postgres database
  • some details about event triggers

Previous articles in this series:

PostgreSQL: Prevent accidental database deletion
When we interact with a PostgreSQL server, there is a risk of accidentally deleting an important database.
PostgreSQL: Protect schemas against accidental deletion
There is a risk of accidentally deleting an important schema whenever we interact with a PostgreSQL server.

The Solution: Actual Code

Create the following function in the database.

Note: A function is an object that belongs to a particular schema in a particular database.

You should be connected as the default superuser to the default postgres database and the default public schema, so that this function is stored here instead of anywhere else.

CREATE OR REPLACE FUNCTION protect_tables()
  RETURNS event_trigger LANGUAGE plpgsql AS $$
DECLARE
    obj record;
    table_name_1 text;
BEGIN
    FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
    LOOP
        IF obj.object_type = 'table' THEN

            table_name_1 := obj.object_name;

            IF EXISTS (SELECT 1 FROM protected_table WHERE table_name = table_name_1) THEN
               RAISE EXCEPTION 'ERROR: Table ''%'' is listed in the table ''protected_table''.', table_name_1;
            END IF;

        END IF;
    END LOOP;
END;
$$;

Create an event trigger that calls this function when a DROP command is used.

DROP EVENT TRIGGER IF EXISTS drop_table_trigger;
CREATE EVENT TRIGGER drop_table_trigger
ON sql_drop
EXECUTE FUNCTION protect_tables();

Create a table that stores the list of protected tables. Again, this table should be stored on postgres.public.

CREATE TABLE protected_table (
    table_name text PRIMARY KEY
);

Now, whenever you have an important table, insert its name into this table. Example:

INSERT INTO protected_table (table_name) VALUES ('my_table_1');

Worked Example

Document Postgres version.

SELECT version();

postgres=# SELECT version();
                                                            version
--------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 14.9 (Homebrew) on x86_64-apple-darwin22.4.0, compiled by Apple clang version 14.0.3 (clang-1403.0.22.14.1), 64-bit
(1 row)

See current role.

SELECT CURRENT_ROLE;

postgres=# SELECT CURRENT_ROLE;
 current_role
--------------
 admin
(1 row)

This is the superuser role.

See current database.

SELECT CURRENT_DATABASE();

postgres=# SELECT CURRENT_DATABASE();
 current_database
------------------
 postgres
(1 row)

See current schema.

SELECT CURRENT_SCHEMA;

postgres=# SELECT CURRENT_SCHEMA;
 current_schema
----------------
 public
(1 row)

Create the function, trigger, and table shown earlier.

Create two new test tables.

CREATE TABLE test_table_1 (
    id SERIAL PRIMARY KEY,
    name VARCHAR NOT NULL
);

CREATE TABLE test_table_2 (
    id SERIAL PRIMARY KEY,
    name VARCHAR NOT NULL
);

Protect the name of the first test table.

INSERT INTO protected_table (table_name) VALUES ('test_table_1');

We try to drop a table that doesn't exist.

postgres=# DROP TABLE foo;
ERROR:  table "foo" does not exist

We try to drop a table that is protected.

postgres=# DROP TABLE test_table_1;
ERROR:  ERROR: Table 'test_table_1' is listed in the table 'protected_table'.
CONTEXT:  PL/pgSQL function protect_tables() line 13 at RAISE

We try to drop a table that is unprotected.

postgres=# DROP TABLE test_table_2;
DROP TABLE

List tables.

postgres=# \dt
              List of relations
 Schema |        Name        | Type  | Owner
--------+--------------------+-------+-------
 public | protected_database | table | admin
 public | protected_schema   | table | admin
 public | protected_table    | table | admin
 public | test_table_1       | table | admin
(4 rows)

So: We are now prevented from accidentally dropping a table, if its name is stored in the table postgres.public.protected_table.

Sources

https://www.postgresql.org/docs/14/sql-droptable.html


If you have any questions, comments, or suggestions - please contact StJohn Piano on Tela:
tela.app/id/stjohn_piano/7c51a6

Follow Tela Network on LinkedIn:
linkedin.com/company/tela-network

Follow Tela Network on Twitter:
twitter.com/tela_updates

Join Tela Network and become a consultant:
telablog.com/how-to-use-tela-for-consulting

Join the Tela Social channel on Telegram to get every new update:
t.me/tela_social

Follow Tela Network on Instagram:
instagram.com/tela_updates

Invest in Tela:
tela.network/invest