To my mind, it doesn't make sense to use NULL to represent a Boolean value. The semantics of "is_active" suggest that the only sensible values are True and False. Also, NULL interferes with cascading updates.
So I'm not using NULL.
First, create the "parent" table with both a primary key and a unique constraint on the primary key and "is_active".
create table parent (
p_id integer primary key,
other_columns char(1) default 'x',
is_active boolean not null default true,
unique (p_id, is_deleted)
);
insert into parent (p_id) values
(1), (2), (3);
Create the child table with an "is_active" column. Declare a foreign key constraint referencing the columns in the parent table's unique constraint (last line in the CREATE TABLE statement above), and cascade updates.
create table child (
p_id integer not null,
is_active boolean not null default true,
foreign key (p_id, is_active) references parent (p_id, is_active)
on update cascade,
some_other_key_col char(1) not null default '!',
primary key (p_id, some_other_key_col)
);
insert into child (p_id, some_other_key_col) values
(1, 'a'), (1, 'b'), (2, 'a'), (2, 'c'), (2, 'd'), (3, '!');
Now you can set the "parent" to false, and that will cascade to all referencing tables.
update parent
set is_active = false
where p_id = 1;
select *
from child
order by p_id;
p_id is_active some_other_key_col
--
1 f a
1 f b
2 t a
2 t c
2 t d
3 t !
Soft deletes are a lot simpler and have much better semantics if you implement them as valid-time state tables. FWIW, I think the terms soft delete, undelete, and undo are all misleading in this context, and I think you should avoid them.
PostgreSQL's range data types are particularly useful for this kind of work. I'm using date ranges, but timestamp ranges work the same way.
For this example, I'm treating only "parent" as a valid-time state table. That means that invalidating a particular row (soft deleting a particular row) also invalidates all the rows that reference it through foreign keys. It doesn't matter whether they reference it directly or indirectly.
I'm not implementing soft deletes on "child". I can do that, but I think that would make the essential technique unreasonably hard to understand.
create extension btree_gist; -- Necessary for the kind of exclusion
-- constraint below.
create table parent (
p_id integer not null,
other_columns char(1) not null default 'x',
valid_from_to daterange not null,
primary key (p_id, valid_from_to),
-- No overlapping date ranges for a given value of p_id.
exclude using gist (p_id with =, valid_from_to with &&)
);
create table child (
p_id integer not null,
valid_from_to daterange not null,
foreign key (p_id, valid_from_to) references parent on update cascade,
other_key_columns char(1) not null default 'x',
primary key (p_id, valid_from_to, other_key_columns),
other_columns char(1) not null default 'x'
);
Insert some sample data. In PostgreSQL, the daterange data type has a special value 'infinity'. In this context, it means that the row that has the value 1 for "parent"."p_id" is valid from '2015-01-01' until forever.
insert into parent values
(1, 'x', daterange('2015-01-01', 'infinity'));
insert into child values
(1, daterange('2015-01-01', 'infinity'), 'a', 'x'),
(1, daterange('2015-01-01', 'infinity'), 'b', 'y');
This query will show you the joined rows.
select *
from parent p
left join child c
on p.p_id = c.p_id
and p.valid_from_to = c.valid_from_to;
To invalidate a row, update the date range. This row (below) was valid from '2015-01-01' to '2015-01-31'. That is, it was soft deleted on 2015-01-31.
update parent
set valid_from_to = daterange('2015-01-01', '2015-01-31')
where p_id = 1 and valid_from_to = daterange('2015-01-01', 'infinity');
Insert a new valid row for p_id 1, and pick up the child rows that were invalidated on Jan 31.
insert into parent values (1, 'r', daterange(current_date, 'infinity'));
update child set valid_from_to = daterange(current_date, 'infinity')
where p_id = 1 and valid_from_to = daterange('2015-01-01', '2015-01-31');
Richard T Snodgrass's seminal book Developing Time-Oriented Database Applications in SQL is available free from his university web page.