The Hidden Blog

As it turns out, I do have a hostname.

Check Postgres table dependencies for cascade

Published on Jul 13, 2022

Currently I'm working on a better staging setup for Birdfeeder. A big part of that is syncing the production database to the staging database while cleaning out user specific information. An easy way to do that is to use SQL to truncate the users table.

When I tried to truncate my users table I noticed that my foreign keys don't have an ON DELETE action set. In that case deleting a row that is referenced from another row would fail instead of cascading the delete action further up the tree.

I was wondering what's the best way to see the dependencies between entities in Postgres and found pg_depend which does exactly that.

Query

with recursive chain as (
    select classid, objid, objsubid, conrelid
    from pg_depend d
    join pg_constraint c on c.oid = objid
    where refobjid = 'users'::regclass and deptype = 'n'
union all
    select d.classid, d.objid, d.objsubid, c.conrelid
    from pg_depend d
    join pg_constraint c on c.oid = objid
    join chain on d.refobjid = chain.conrelid and d.deptype = 'n'
    )
select pg_describe_object(classid, objid, objsubid), pg_get_constraintdef(objid)
from chain;

Source: Thanks to klin on StackOverflow for that neat snippet.

Output

pg_describe_object pg_get_constraintdef
constraint fk_rails_c1ff6fa4ac on table bookmarks FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE

Example from Birdfeeder

The bookmark mapping table maps a tweet to a user. If I would delete the user from the users table that is referenced in the bookmarks table it would fail.

CREATE TABLE birdfeederdb.bookmarks (
    id BIGSERIAL PRIMARY KEY,
    tweet_id bigint NOT NULL REFERENCES birdfeederdb.tweets(id),
    user_id bigint NOT NULL REFERENCES birdfeederdb.users(id),
);

We have to add a migration to tell the database that it can also delete the row if the row it's referencing got deleted ("If someone deletes the user, delete the bookmarks of that user").

After running the migration the schema will look like this:

CREATE TABLE birdfeederdb.bookmarks (
    id BIGSERIAL PRIMARY KEY,
    tweet_id bigint NOT NULL REFERENCES birdfeederdb.tweets(id),
    user_id bigint NOT NULL REFERENCES birdfeederdb.users(id) ON DELETE CASCADE,);

If you use Rails the migration is as simple as that:

class AddCascadeToBookmarks < ActiveRecord::Migration[7.0]
  def change
    remove_foreign_key "bookmarks", "users"
    add_foreign_key "bookmarks", "users", on_delete: :cascade
  end
end

Now you can just delete a user from the users table and the database will take care of cleaning up the entities that reference that user.