-- This file repeats the simple movies example database (with
-- movie_info and movie_actors), but showing what is necessary in
-- PostgreSQL 6.5 in order to enforce the referential integrity
-- constraint that the id column of movie_actors serve as a foreign
-- key into movie_info.  We could do that with the three clauses
--
--    references movie_info
--      on delete cascade 
--      on update cascade
--
-- in the creation of the movie_actors table, except PostgreSQL
-- doesn't support that yet.  On the other hand, doing this constraint
-- the hard way will let us see some more general mechanisms that can
-- be used to enforce other constraints as well.

-- First we drop everything we are going to create, in case there are old
-- versions.  We get error messages if there aren't old versions, but that
-- is no big deal.  This way we know we have a clean slate.  One exception
-- is that we don't drop the procedural language PL/pgSQL or its handler
-- function, plpgsql_call_handler(), because those are general purpose
-- mechanisms that may be in use for other purposes than just the trigger
-- we are writing in PL/pgSQL.  If some other trigger (or procedure) were
-- written in PL/pgSQL, and we dropped the language and recreated (reloaded)
-- it, the old triggers/procedures would no longer work.

drop trigger propagate_movie_id_change;
drop function propagate_id_change;
drop rule cascade_movie_deletion;
drop trigger check_actor_fk on movie_actors;
drop function check_actor_foreign_key();
drop table movie_actors;
drop table movie_info;

-- Next we create the tables themselves, much like earlier in the course.

create table movie_info (title varchar(40),
                         director varchar(40),
                         year_made int,
                         id int primary key);

create table movie_actors (id int not null,
                           actor varchar(40) not null);

-- Now to load in a little machinery we need in order to write our trigger:
-- PL/pgSQL, a procedural language based on SQL.  Note that these next
-- two create statements may result in errors if PL/pgSQL is already loaded
-- in, because we didn't drop the language or its call handler function
-- in the drop statements above, for the reason explained there.

create function plpgsql_call_handler() returns opaque as
       '/usr/local/pgsql/lib/plpgsql.so' language 'c';
          
create trusted procedural language 'plpgsql'
       handler plpgsql_call_handler
       lancompiler 'PL/pgSQL';

-- With this infrastructure in place, we can create a trigger to
-- maintain referential integrity in the face of insertions into the
-- movie_actors table, or updates of that table's id column.  We want
-- to check that each row in movie_actors has an id that really
-- corresponds with a row in movie_info.  If this integrity constraint
-- would be violated by an insertion into or update of movie_actors,
-- we prevent the operation from proceding.

-- The trigger is handled by the below function (procedure), written in
-- PL/pgSQL.

create function check_actor_foreign_key() returns opaque as '
declare
    info_row  movie_info%rowtype;
begin
    -- the below check for null, which duplicates what is already
    --  specified in the create table statement, seems to be necessary
    --  here to prevent PostgreSQL from bombing out
    if new.id isnull then
        raise exception ''id can not be null'';
    end if;

    select * into info_row from movie_info where id = new.id;
    if not found then
        raise exception ''id = % is not in table movie_info'' , new.id;
    end if;
    return new;
end;
' language 'plpgsql';


-- Now we install the above function as an actual trigger:

create trigger check_actor_fk before insert or update on movie_actors
 for each row execute procedure check_actor_foreign_key();

-- There is a flip side to referential integrity: we need to worry not only
-- about insertions into movie_actors, but also about deletions from
-- movie_info.  In this case, we'll take the "cascading" approach of also
-- deleting the movie_actors rows that correspond to the deleted movie.
-- It turns out we don't need the heavy artillery of a "trigger" written
-- in PL/pgSQL for this job: we can use PostgreSQL's "rule" mechanism:

create rule cascade_movie_deletion as on delete to movie_info
 do delete from movie_actors where id = old.id;

-- By the way, the "old.id" above refers to the id column in the old row
-- that just got deleted from movie_info.  The PostgreSQL documentation,
-- last I noticed, claimed that the correct syntax was "current.id."  But
-- that doesn't work.

-- A related problem to deletion is updating of the id column in
-- movie_info.  We'll handle this one again with the "cascade" option,
-- which in this case means to propagate the change in id number
-- through to the movie_actors, so that they stay associated with the
-- same movie, even though its id number has changed.  At first blush,
-- it appears that we can again do this with a rule:

-- create rule propagate_movie_id_change as on update to movie_info
--  do update movie_actors set id = new.id where id = old.id;

-- However, as you can guess by the commenting out of that rule, and
-- the words "at first blush", this doesn't work.  The problem is that
-- the rule tries to change the id numbers in the movie_actors table
-- *before* the id number is changed in the movie_info table.  This
-- update to the id in movie_actors is rejected by our earlier trigger.

-- Instead, we apparently need to go back to using the trigger
-- mechanism, which can be set to fire "after" an update.

create function propagate_id_change() returns opaque as '
declare
begin
    update movie_actors set id = new.id where id = old.id;
    return new;
end;
' language 'plpgsql';

create trigger propagate_movie_id_change after update on movie_info
 for each row execute procedure propagate_id_change();

-- Finally, with all the mechanisms in place to protect referential integrity,
-- we can load the data into the tables, just like in the old days.  One
-- key difference is now it really matters that we load the data into the
-- movie_info table first, and then the movie_actors table afterwards.

\copy movie_info from /Net/solen/home/m/a/max/MC39/movie_info
\copy movie_actors from /Net/solen/home/m/a/max/MC39/movie_actors
