Creating a Trigger To Delete Records - sql

I have a table which we can call decks. Decks has an id used as its primary key along with some other attributes. The cards table contains a foreign key reference to the deck id and has a primary key of cardid as well. Another table exists called answers where its foreign key is the cardid.
So in order to delete from decks, the database requires I delete from answers first, then cards, and then finally from decks.
I would like to create a trigger which takes care of the first and second delete so that I only have to specify a delete statement from the decks table to completely destroy a deck.
Below is an example PostgreSQL trigger I've found, but I am not sure if its even possible to do what I am asking as I can find no examples online of anyone creating a trigger this way.
CREATE OR REPLACE FUNCTION autoCalculate() RETURNS TRIGGER AS $$
BEGIN
IF NEW.wins < 0 THEN
RAISE EXCEPTION 'Wins cannot be negative';
END IF;
IF(OLD.wins <> NEW.wins_ OR (OLD.losses <> NEW.losses) THEN
NEW.Winning_Percentage := calc_winning_percentage(NEW.Wins, NEW.Losses);
END IF
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER update_winning_percentage ON standings;
CREATE TRIGGER update_winning_percentage BEFORE INSERT OR UPDATE ON standings
FOR EACH ROW EXECUTE PROCEDURE autoCalculate();
If anyone has knowledge to do this, if they could nudge me in the right direction or provide an example of how to do this I'd be grateful!

On your foreign key definitions use ON DELETE CASCADE and it will take care of this for you

In Postgres 8.2 you can specify On Delete Casacde when you define your foreign key relationships.
http://www.postgresql.org/docs/8.2/static/ddl-constraints.html

Related

SQL trigger for replicating actions of an ON DELETE CASCADE

I am working on a project that requires creating a trigger that basically replicates the actions of an ON DELETE CASCADE clause of a FOREIGN KEY CONSTRAINT. The parameter of interest is "account_number", referencing relationship is "depositor" and referenced relationship is "account". Basically, when an "account_number" is deleted from "account" table, the trigger is to delete all occurences of that "account_number" in the depositor table ("all" specified since there could be joint accounts with multiple customer_ID's listed in the depositor table with that "account_number".
Seems simple enough and I tried a few approaches that I thought should all work; yet none of them are working - that is, when I delete an "account_number" from the "account" table, the corresponding "account_number"s in the depositor table are not getting deleted (despite not getting any error messages along the way). I tried the trigger funciton a couple ways, with the commented out sections below used in one case and commented out in another case - neither worked. What am I missing?
CREATE OR REPLACE FUNCTION Demirci_07_bankTriggerFunction()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
/*
DECLARE
acct_no_deleted INTEGER;
*/
BEGIN
/*
PERFORM account_number
FROM account
WHERE account_number = OLD.account_number;
DELETE FROM depositor
WHERE account_number = acct_no_deleted;
*/
DELETE FROM depositor
WHERE account_number = OLD.account_number;
RETURN OLD;
END;
$$;
CREATE TRIGGER Demirci_07_bankTrigger
AFTER DELETE ON account
FOR EACH ROW
EXECUTE PROCEDURE Demirci_07_bankTriggerFunction();
The problem is that it is an AFTER DELETE trigger. You have to remove the dependent rows before you delete the referenced row.

How to add constraint to sql table so that table has exactly one row

Parameter table is initially created and one row is added in Postgres.
This table should have always one row, otherwise SQL queries using this table will produce incorrect results. DELETE or INSERT to this table are disallowed, only UPDATE is allowed.
How to add single row constraint to this table?
Maybe DELETE and INSERT triggers can raise an exception or is there simpler way?
The following will create a table where you can only insert one single row. Any update of the id column will result in an error, as will any insert with a different value than 42. The actual id value doesn't matter actually (unless there is some special meaning that you need).
create table singleton
(
id integer not null primary key default 42,
parameter_1 text,
parameter_2 text,
constraint only_one_row check (id = 42)
);
insert into singleton values (default);
To prevent deletes you can use a rule:
create or replace rule ignore_delete
AS on delete to singleton
do instead nothing;
You could also use a rule to make insert do nothing as well if you want to make an insert "fail" silently. Without the rule, an insert would generate an error. If you want a delete to generate an error as well, you would need to create a trigger that simply raises an exception.
Edit
If you want an error to be thrown for inserts or deletes, you need a trigger for that:
create table singleton
(
id integer not null primary key,
parameter_1 text,
parameter_2 text
);
insert into singleton (id) values (42);
create or replace function raise_error()
returns trigger
as
$body$
begin
RAISE EXCEPTION 'No changes allowed';
end;
$body$
language plpgsql;
create trigger singleton_trg
before insert or delete on singleton
for each statement execute procedure raise_error();
Note that you have to insert the single row before you create the trigger, otherwise you can't insert that row.
This will only partially work for a superuser or the owner of the table. Both have the privilege to drop or disable the trigger. But that is the nature of a superuser - he can do anything.
To make any table a singleton just add this column:
just_me bool NOT NULL DEFAULT TRUE UNIQUE CHECK (just_me)
This allows exactly one row. Plus add the trigger #a_horse provided.
But I would rather use a function instead of the table for this purpose. Simpler and cheaper.
CREATE OR REPLACE FUNCTION one_row()
RETURNS TABLE (company_id int, company text) LANGUAGE sql IMMUTABLE AS
$$SELECT 123, 'The Company'$$
ALTER FUNCTION one_row() OWNER TO postgres;
Set the owner to the user that should be allowed to change it.
Give a user permission to ALTER a function
Nobody else change it - except superusers of course. Superusers can do anything.
You can use this function just like you would use the table:
SELECT * FROM one_row();
If you need a "table", create a view (which is actually a special table internally):
CREATE VIEW one_row AS SELECT * FROM one_row();
I guess you will not use the PostgreSQL root user in your application so you could simply limit the permissions of your application user on UPDATE for this table.
An INSERT or DELETE will then cause an Insufficient privilege exception.

SQL: How to check if foreign key was really changed

I had FK with wrong constraints and I have to change it:
ALTER TABLE user_login_logout_fact DROP CONSTRAINT user_fk;
ALTER TABLE user_login_logout_fact
ADD CONSTRAINT user_fk FOREIGN KEY (user_id)
REFERENCES uuser (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
There's no problem with it, but I have to apply this using patch file with checking if it was applied already. So I have to create function like this:
CREATE FUNCTION tryUpgrade(patch varchar) RETURNS integer AS $$
DECLARE testRecord RECORD;
BEGIN
RAISE NOTICE 'checking %', patch;
SELECT INTO testRecord * FROM patchlog where basename = patch;
IF FOUND THEN
RAISE NOTICE 'patch % has already been applied', patch;
RETURN 0;
END IF;
//check if constraints are ok
IF ok THEN
RAISE NOTICE 'upgraded but not noted';
INSERT INTO patchlog VALUES (patch, now());
RETURN 0;
END IF;
SELECT INTO testRecord upgrade(); //this function will alter table
INSERT INTO patchlog VALUES (patch, now());
RAISE NOTICE 'upgraded';
RETURN 1;
END;
$$ LANGUAGE plpgsql;
So, the question is - how to check if FK will use ON DELETE CASCADE but not old NO ACTION?
How to check if FK will use ON DELETE CASCADE but not old NO ACTION?
You can check the confdeltype in the system catalog table pg_constraint. The manual:
Foreign key deletion action code: a = no action, r = restrict, c = cascade, n = set null, d = set default
In PL/pgSQL code use it like:
IF EXISTS (
SELECT FROM pg_constraint
WHERE conname = 'user_fk'
AND conrelid = 'user_login_logout_fact'::regclass
AND confdeltype = 'c'
) THEN
-- all done
ELSE
-- run ALTER TABLE ...
END IF;
Table name can optionally be schema-qualified. See:
How do I speed up counting rows in a PostgreSQL table?
Information schema versus system catalog
As you answered yourself, the same can be achieved by querying the information schema, which adheres to an SQL standard.
However:
Not all major RDBMS implement it. Oracle doesn't, for one.
The information schema is implemented with (sometimes hugely complex) views (not tables), which make look-ups a lot slower than accessing tables in pg_catalog directly. A quick test in a real live db showed factor 10 for the example at hand. I have seem factor 1000 and more.
The aim of using the information schema is often to keep your implementation "portable". But this hardly ever works to begin with. The various RDBMS are just too far from the SQL standard in too many ways.
There is another benefit, though: Postgres does not assert anything concerning the structure of tables in pg_catalog across major versions. By using the information schema, one would be on the safe side across major versions of Postgres.
However, basic structures hardly change. While possible, it is rather unlikely, that a query like this would break in a future version.
See:
How to check if a table exists in a given schema
Related answer on dba.SE discussing "Information schema vs. system catalogs"
Also, second solution whitch is seems to be less postgresql-specific.
Info about foreign keys stores in referential_constraints table:
SELECT * FROM information_schema.referential_constraints
WHERE CAST(constraint_name AS TEXT) LIKE 'user_fk'
AND delete_rule LIKE 'CASCADE'
AND update_rule LIKE 'CASCADE';

Conditional CASCADE operation for foreign key constraint?

I have parent and child table where child has a FK pointing to the PK of parent table. When I delete something in parent table I can have child records deleted as well by having ON DELETE CASCADE.
However, in my parent table I don't delete records at all. Instead I set the column state = "passive". I want to delete related entries in the child table.
Do we have something like a "conditional CASCADE" in Postgres? Or is the solution to manually delete entries in the child table?
You would have to do this in a trigger that takes action ON UPDATE. Where the NEW.state = "passive", delete the child rows.
There is nothing like "conditional CASCADE". The closest thing that comes to mind would be to disable triggers. But that's not helpful in your case.
Assumptions:
- state is defined NOT NULL.
- parent_id never changes. If it does you'll want to cascade that UPDATE as well.
The condition to fire the trigger ON UPDATE should be:
NEW.state = "passive"
AND OLD.state <> "passive"
.. since you do not want to trigger it over and over again, only once when parent is set to "passive".
CREATE OR REPLACE FUNCTION trg_upbef()
RETURNS TRIGGER AS
$func$
BEGIN
DELETE FROM child
WHERE parent_id = OLD.parent_id; -- OLD works for UPDATE & DELETE
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Instead of checking the condition in the trigger function, you can do that in the trigger directly since Postgres 9.0, thereby saving a bit of overhead:
CREATE TRIGGER upd_cascade_del
BEFORE UPDATE ON parent
FOR EACH ROW
WHEN (NEW.state = "passive" AND
OLD.state <> "passive") -- parenthesis required
EXECUTE PROCEDURE trg_upbef();
Don't forget to add a trigger for ON DELETE as well. You don't normally DELETE, but if you do, you want to either raise an exception or cascade the operation.
Trigger function has to RETURN OLD, since NEW is not defined in this case. Otherwise the same:
CREATE OR REPLACE FUNCTION trg_delbef()
...
RETURN OLD;
...
$func$ LANGUAGE plpgsql;
CREATE TRIGGER del_cascade_del
BEFORE DELETE ON parent
FOR EACH ROW
WHEN (OLD.state <> "passive") -- only if not already passive
EXECUTE PROCEDURE trg_delbef();

PL/SQL DB triggers - Make insertion fail if a given condition is not met

I have a small test database I'm using for learning SQL.
There's a Duel table, which contains two Pilot foreign keys (the duelists). I want to check if the duelists haven't already "met" before insertion.
Pseudocode:
before insertion on Duel
for each row already in the table
if ((new_row.numpilot1 = old_row.numpilot1 and new_row.numpilot2 = old_row.numpilot2) OR
(new_row.numpilot1 = old_row.numpilot2 and new_row.numpilot2 = old_row.numpilot1)
)
insertion fails
One other alternative would be
tempnum integer;
select numpilot1 into tempnum from duel
where (:NEW.numpilot1 = numpilot1 and :NEW.numpilot2 = numpilot2) OR
(:NEW.numpilot1 = numpilot2 and :NEW.numpilot2 = numpilot1);
if tempnum == null
fail insertion
What is the PL/SQL (Oracle DBMS) version of this?
Normally, you wouldn't use a trigger for this sort of requirement. Instead, you'd create a couple of constraints on the table. I would suggest a unique constraint on (numpilot1, numpilot2) along with a check constraint that ensures that numpilot1 < numpilot2.
ALTER TABLE duel
ADD CONSTRAINT unique_pilot_combination
UNIQUE( numpilot1, numpilot2 );
ALTER TABLE duel
ADD CONSTRAINT chk_pilot1_lt_pilot2
CHECK( numpilot1_fk < numpilot2_fk );
If you wanted to do this sort of thing in a trigger, it would be quite a bit more complicated. In general, a row-level trigger on DUEL cannot query the DUEL table-- doing so would create a mutating table exception. You would need to create a collection in a package, a before statement trigger that initializes the collection, a row-level trigger that inserts the new pilot keys into the collection, and an after statement trigger that reads the data in the collection and does the validation. That's quite a few moving pieces to manage in addition to the potential performance hit. If you're really stuck with the trigger solution, however, there is an example of using the three trigger solution to work around mutating table exceptions on Tim Hall's site.
You could use a function-based index:
create unique index duel_uk on duel
( least(numpilot1, numpilot2), greatest(numpilot1, numpilot2));
The answer you are clearly seeking is a trigger like this:
create trigger duel_trg
before insert on duel
for each row
declare
dummy number;
begin
select count(*)
into dummy
from duel
where (numpilot1 = :new.numpilot1 and numpilot2 = :new.numpilot2)
or (numpilot1 = :new.numpilot2 and numpilot2 = :new.numpilot1);
if dummy > 0 then
raise_application_error(-20001,'You lose');
end if;
end;
However, that will fail to ensure integrity in a multi-user (or multi-session) environment, as this can happen:
User1> insert into duel (numpilot1, numpilot2) values (1,2);
-- trigger checks, all seems OK
User2> insert into duel (numpilot1, numpilot2) values (1,2);
-- trigger checks, all seems OK (can't see User1's new row
-- as it hasn't been committed)
User1> commit;
User2> commit;
Result: corrupt database. So while this trigger may satisfy the teacher, it is a bad solution and constraints should be used instead (preferably Justin's solution rather than mine!)