Soft delete cascade in PostgreSQL🐘 and YugabyteDB🚀

franckpachot

Franck Pachot

Posted on April 17, 2022

Soft delete cascade in PostgreSQL🐘 and YugabyteDB🚀

This is a quick example to answer this remark about soft deletes:

I don't know which database Kelly Sommers uses, but PostgreSQL has many features that, combined, help implementing this data processing logic, in a declarative way. I know people don't like to put code in the databases, but this is not business logic. This is pure data logic: implementing soft deletes. SQL has huge benefit for this: it is a declarative language. You declare it once, test it, and you are done. No need for additional code or additional tests.

I've run this on YugabyteDB to verify that it works the same as in PostgreSQL. Of course, no suprise, YugabyteDB re-uses the postgres SQL processing layer, for the best compatibility.

Tables

Here is the parent table which has a parent_deleted timestamp set to the date of deletion. The default, infinity, is for valid records. This column is part of the primary key because there may be multiple deletion for the same parent_id. But only one valid.

CREATE TABLE parent (
 parent_id int, parent_deleted timestamptz default 'infinity',
 primary key (parent_id,parent_deleted)
);

Enter fullscreen mode Exit fullscreen mode

The child table inherits the parent primary key and adds a child_number to it as its primary key. The foreign key is declared with on update cascade as the soft deletes will be cascaded as updates to this primary key.

CREATE TABLE child (
 parent_id int, parent_deleted timestamptz default 'infinity',
 child_number int,
 primary key (parent_id,parent_deleted, child_number),
 foreign key (parent_id,parent_deleted) 
  references parent(parent_id,parent_deleted)
  on update cascade
);

Enter fullscreen mode Exit fullscreen mode

Views

Tables could be sufficient. But the beauty of SQL is the logical independence. I want to query my tables, from the application or by the user, without caring about the soft delete implementation.

I declare views for that. The application will query valid_parent and valid_child to see the current versions, filtering out the soft deleted rows:

create view valid_parent as
 select parent_id from parent where parent_deleted>=now();

create view valid_child as
 select parent_id,child_number from child where parent_deleted>=now();

Enter fullscreen mode Exit fullscreen mode

Thanks to re-using the primary key, there is no need to join the tables there. This is the right choice when deletes are rare (the cascading update overhead is acceptable) but selects are frequent. And people tend to think that joins don't scale.

Procedure

I want to encapsulate this logic in the database and create a procedure to do be called for this soft deletion:

create procedure soft_delete_parent(id int) as $SQL$
update parent
set parent_deleted=now()
where parent_id=id;
$SQL$ language sql;

Enter fullscreen mode Exit fullscreen mode

I'll show an alternative later if you don't like stored procedures. But, personally, I like this procedure encapsulation because the semantic is clear: the application calls a specific procedure.

Data

I'm inserting few rows there. I'm inserting valid rows, and insert them though the view, because a view is a virtual table, with all DML allowed. The default infinity value is set automatically:

insert into valid_parent
 select n from generate_series(1,3) n;

insert into valid_child
 select parent_id,n from valid_parent,generate_series(1,2) n;

Enter fullscreen mode Exit fullscreen mode

Here is a screenshot from my test:
screenshot1

You can easily reproduce it - did you try the YugabyteDB managed free tier?

Test

When you implement data logic in SQL, a simple unit test is usually sufficient, because the database takes care of all multi-user consistency.

select * from valid_parent;

select * from valid_child;
Enter fullscreen mode Exit fullscreen mode

This shows only the valid rows. I call the procedure to soft-delete one parent:

call soft_delete_parent(2);
Enter fullscreen mode Exit fullscreen mode

When querying the views, the rows have been virtually deleted:

select * from valid_parent;

select * from valid_child;
Enter fullscreen mode Exit fullscreen mode

Here is the result:
screenshot2

In the tables behind the views, we can see all the rows, with the soft-deleted ones:

yugabyte=# select * from parent;

 parent_id |        parent_deleted
-----------+-------------------------------
         1 | infinity
         2 | 2022-04-15 10:21:45.635693+00
         3 | infinity
(3 rows)

yugabyte=# select * from child;

 parent_id |        parent_deleted         | child_number
-----------+-------------------------------+--------------
         1 | infinity                      |            1
         1 | infinity                      |            2
         2 | 2022-04-15 10:21:45.635693+00 |            1
         2 | 2022-04-15 10:21:45.635693+00 |            2
         3 | infinity                      |            1
         3 | infinity                      |            2
(6 rows)
Enter fullscreen mode Exit fullscreen mode

Note that, with GRANT and REVOKE, you can give access to the views only, or to these tables. And revoke the right to hard delete.

Rule

You can make this completely transparent, so that users don't have to call the procedure, but simply run DELETE on the view, with a DO INSTEAD code:

create or replace rule soft_delete_parent as 
on delete to valid_parent do instead
update parent
set parent_deleted=now()
where parent_id=old.parent_id;
Enter fullscreen mode Exit fullscreen mode

This is simple. Now any delete will actually do a soft delete:

screenshot3

This looks great, as the application is just interacting with the standard SQL API (SELECT, INSERT, UPDATE, DELETE). And it comes handy when the application cannot be modified. But, for better code quality, I prefer a procedure so that the application developer knows what she does (my procedure name is explicit about soft deletes). You can also see that this RULE is not 100% transparent in its output, showing DELETE 0.

PostgreSQL-compatible

This technique is easy on PostgreSQL and PostgreSQL-compatible databases which re-use the PostgreSQL open-source code, like YugabyteDB. Here is the list of SQL features that makes it easy, declarative, and transparent:

SQL Feature 🐘 PostgreSQL 🚀 YugabyteDB 🪳 CockroachDB 🅾 Oracle
composite PK
default infinity (4) (1)
on update cascade (2)
stored procedure
insert into view
rule / instead of view (3)
grant/revoke

(1) Temporal Validity is an alternative
(2) Triggers and deferred constraints may be an alternative
(3) Oracle has no equivalent of now() which is the start of transaction
(4) Displayed as 294276-12-31 23:59:59.999999+00

Note that, to be fair, I compared only with the database where I know more than the basics. Here is my guess for SQL Server:

This is where having all PostgreSQL features in YugabyteDB makes it the right solution for many enterprise applications. Even when you don't want to put business logic into the database, there is one day where you will need a stored procedure, triggers, rule, or any of those modern SQL features that have proven their value on monolithic databases and are now available in distributed SQL.

💖 💪 🙅 🚩
franckpachot
Franck Pachot

Posted on April 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related