1

We want to maintain referential integrity between two tables, but we also want to soft delete records. SQL to create the tables:

CREATE TABLE table_a (
    id           SERIAL PRIMARY KEY,
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at   TIMESTAMP WITH TIME ZONE,
    uuid         UUID DEFAULT uuid_generate_v4()
);

CREATE UNIQUE INDEX table_a_uuid ON table_a (uuid) WHERE deleted_at IS NULL;

CREATE TABLE association (
    id               SERIAL PRIMARY KEY,
    created_at       TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at       TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at       TIMESTAMP WITH TIME ZONE,
    table_a_uuid     UUID REFERENCES table_a (uuid) NOT NULL
);

The problem is, Postgres does not allow foreign key references to partial indexes, of which association.table_a_uuid is. Is there any way I can maintain referential integrity between these tables in PSQL, or do I have to do it in the application layer?

Edit: To add some extra context as to why we want to use uuid as the FK ref, soft-deleted records on table_a represent older "versions" of the record, but all "versions" have the same uuid. The referencing association table should "point" to the latest version of the table_a record.

1 Answer 1

4

There is a rather kludgy way to do what you want. The idea is a two-part index, one with the uuid and the second with the id -- but only for deleted records. For the non-deleted, it has a constant value and -1 is convenient.

They you can construct a foreign key reference to that:

CREATE TABLE table_a (
    id           SERIAL PRIMARY KEY,
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at   TIMESTAMP WITH TIME ZONE,
    id_if_deleted  int generated always as (case when deleted_at is null then -1 else id end) stored,
    uuid         UUID DEFAULT gen_random_uuid()
);

CREATE UNIQUE INDEX table_a_uuid ON table_a (uuid, id_if_deleted) ;

CREATE TABLE association (
    id               SERIAL PRIMARY KEY,
    created_at       TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at       TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at       TIMESTAMP WITH TIME ZONE,
    const_neg_one    int generated always as (-1) stored,
    table_a_uuid     UUID,
    foreign key (table_a_uuid, const_neg_one) REFERENCES table_a (uuid, id_if_deleted )
);
Sign up to request clarification or add additional context in comments.

4 Comments

Ah, that's an interesting idea. It could even been simplified to a is_deleted boolean field.
@ericso . . . That would not work because it would allow. only one deleted record.
it would work if you set it to null on deleted records instead of to true.
is live might be a better name than is_deleted however. it also saves on storage as null columns use none.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.