Re: Each foo must have a bar

From: Michael Glaesemann <grzm(at)myrealbox(dot)com>
To: Michael Fuhr <mike(at)fuhr(dot)org>
Cc: David Fetter <david(at)fetter(dot)org>, PostgreSQL General <pgsql-general(at)postgresql(dot)org>
Subject: Re: Each foo must have a bar
Date: 2006-02-12 12:09:35
Message-ID: BC96C81F-8C37-4026-80AD-4D1DFDD809F4@myrealbox.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-general

I haven't seen this hit the lists yet, though I sent it nearly 12
hours ago. Resending for completeness.

On Feb 12, 2006, at 6:59 , Michael Fuhr wrote:

> On Sat, Feb 11, 2006 at 12:56:36PM -0800, David Fetter wrote:
>
>> I'm trying to figure out how to enforce the following. Table foo has
>> a primary key. Table bar has a foreign key to foo. So far so good.
>> I'd also like to say, "for each row in foo, there must be at least
>> one
>> row in bar."
>>
>
> Possibly something involving CREATE CONSTRAINT TRIGGER, specifying
> DEFERRABLE INITIALLY DEFERRED? The documentation says it's not for
> general use; I've used it only in simple experiments so I'm not
> sure how problematic it could be. Anybody?
>

I've used constraint triggers to handle multi-statement updates for
temporal tables. I also haven't had any in production but am planning
to soon. I have tested it quite a bit and haven't seen any problems.
Caveat being that I tend to have small databases, so I'm not sure
about the performance of the PL/pgSQL function I use to enforce the
assertion. I suspect the except should be more performant than the
count, but that's speculation.

I don't know if the SQL spec allows for statements with multiple
updates, e.g. something like

insert into foo (foo) values ('blurfl'), -- note comma
insert into bar (bar, foo_id)
select 'bat', foo_id
from foo where id = 'blurfl';

(And I definitely don't know if that kind of recursive assignment
would even work; maybe it would have to be more like:

insert into foo (foo_id, foo) values (1, 'blurfl'),
insert into bar (bar, foo_id) values ('bat', 1);
)

That might help get rid of the need to use a constraint trigger
rather than a normal assertion. Not that PostgreSQL has assertions
yet anyway :)

Please find an example below. Hope this helps.

Michael Glaesemann
grzm myrealbox com

create table foo
(
foo_id serial primary key
, foo text not null unique
);

create table bar
(
bar_id serial primary key
, bar text not null unique
, foo_id integer not null
references foo (foo_id)
on update cascade on delete cascade
);

create function assert_bar_for_each_foo()
returns trigger
language plpgsql as $$
begin
if exists (
select foo_id
from foo
except
select foo_id
from bar
)
then raise exception 'Every foo must have a bar';
end if;
return null;
end;
$$;

create constraint trigger assert_bar_for_each_foo_tr
after insert on foo
-- With the on update on delete cascade I don't believe you
-- need to check on update or delete here.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();

create constraint trigger assert_bar_for_each_foo_tr
after delete on bar
-- The foreign key on bar takes care of inserts and updates.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();

insert into foo (foo) values ('blurfl');
ERROR: Every foo must have a bar

begin;
insert into foo (foo) values ('blurfl');
INSERT 0 1
insert into bar (bar, foo_id)
INSERT 0 1
select 'bat', foo_id
from foo
where foo = 'blurfl';
commit;

select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
2 | blurfl | 1 | bat
(1 row)

update foo
set foo_id = 3
where foo = 'blurfl';
UPDATE 1

select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
3 | blurfl | 1 | bat
(1 row)

update bar
set foo_id = 2
where bar = 'bat';
ERROR: insert or update on table "bar" violates foreign key
constraint "bar_foo_id_fkey"
DETAIL: Key (foo_id)=(2) is not present in table "foo".

delete from bar where bar = 'bat';
ERROR: Every foo must have a bar

delete from foo where foo = 'blurfl';
DELETE 1

select * from foo;
foo_id | foo
--------+-----
(0 rows)

select * from bar;

bar_id | bar | foo_id
--------+-----+--------
(0 rows)

select version();

version
------------------------------------------------------------------------
----------------------------------------------------------------------
PostgreSQL 8.1.0 on powerpc-apple-darwin8.3.0, compiled by GCC
powerpc-apple-darwin8-gcc-4.0.0 (GCC) 4.0.0 (Apple Computer, Inc.
build 5026)
(1 row)

In response to

Browse pgsql-general by date

  From Date Subject
Next Message Doug McNaught 2006-02-12 13:57:41 Re: Last modification time
Previous Message Joe Conway 2006-02-12 07:08:01 Re: Seeking comments on schema design and data integrity