How to create a conditional trigger - sql

I have a table with an id as auto incremented primary key and another id.
CREATE TABLE tester (
"id" integer PRIMARY KEY AUTOINCREMENT,
"refId" integer DEFAULT 0
);
refId should be able to either be 0 (the default) or reference id if refId > 0 (i.e. act as foreign key).
Now I need two constraints:
A row should only be deletable if its id is not used (referenced?) by any other row's refId
A row should only be deletable if its refId is 0.
From what I have understood, I need to create a trigger that checks for these constraints before a DELETE event happens. And depending on refId's value either abort the delete action or allow it.
However, I have a hard time understanding the syntax for this and how to do a conditional check. But what I have so far (in mind!) is concerning 1.):
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE "refId" = OLD."id") IS NOT NULL;
END;
And concerning 2.)
CREATE TRIGGER no_delete_if_ref
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
IF OLD."refId" > 0 THEN RAISE(ABORT, "cannot delete tester because it refers to an existing tester");
END;
Does this make sense and is valid?
I am totally not sure, to me it does but well, I am all noob.
Also as a last question, can I alternatively combine this into a single trigger? For example would this be a valid query:
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE ("refId" = OLD."id" OR "refId" > 0) ) IS NOT NULL;
END;

You can define a foreign key referring to the same table. Use null instead of 0 for rows without a reference:
create table tester(
id int primary key,
refid int references tester,
check (id <> refid)
);
insert into tester values
(1, null),
(2, null),
(3, 1),
(4, 3);
You need a trigger to ensure that a row which references another one cannot be deleted.
create or replace function before_delete_on_tester()
returns trigger language plpgsql as $$
begin
if old.refid is not null then
raise exception
'Cannot delete: (id)=(%) references (id)=(%)', old.id, old.refid;
end if;
return old;
end $$;
create trigger before_delete_on_tester
before delete on tester
for row execute procedure before_delete_on_tester();
Test:
delete from tester where id = 1;
ERROR: update or delete on table "tester" violates foreign key constraint "tester_refid_fkey" on table "tester"
DETAIL: Key (id)=(1) is still referenced from table "tester".
delete from tester where id = 4;
ERROR: Cannot delete from tester. (id)=(4) references (id)=(3)
CONTEXT: PL/pgSQL function before_delete_on_tester() line 4 at RAISE
In Postgres you have to define a trigger function. Read more:
Overview of Trigger Behavior
Trigger Procedures
Create Trigger

Related

PostgreSQL trigger function not working properly

I have a trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
DECLARE
act VARCHAR(30);
BEGIN
SELECT account_number INTO act
DELETE FROM depositor
WHERE depositor.account_number = act;
RETURN act;
END;
$$;
\`
and then I have a trigger:
CREATE TRIGGER Day_21_bankTrigger AFTER DELETE on account FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction()
The thought behind this is that is an account was deleted from the account table then this should trigger the function to run and then delete all records on the depositor table where that account is present.
I can create the trigger function and trigger without an issues but if I attempt to delete an account from the account table...I still see the account # in the depositor table when I shouldn't.
Any thoughts?
above is what I tried. Expected results would be to delete an account from the account table and then the trigger function should kick off and remove that account from the depositor table
Usually, relationships of any mode between tables are created by foreign keys. This is the best way and are standards for DBs. Using foreign keys you can control your data. SQL sample:
CREATE TABLE contacts(
contact_id INT GENERATED ALWAYS AS IDENTITY,
customer_id INT,
contact_name VARCHAR(255) NOT NULL,
email VARCHAR(100),
PRIMARY KEY(contact_id),
CONSTRAINT fk_customer
FOREIGN KEY(customer_id)
REFERENCES customers(customer_id)
ON DELETE SET NULL
ON UPDATE SET NULL
);
Third, specify the parent table and parent key columns referenced by the foreign key columns in the REFERENCES clause.
Finally, specify the delete and update actions in the ON DELETE and ON UPDATE clauses.
The delete and update actions determine the behaviors when the primary key in the parent table is deleted and updated. Since the primary key is rarely updated, the ON UPDATE action is not often used in practice. We’ll focus on the ON DELETE action.
PostgreSQL supports the following actions after updating or deleting:
SET NULL (Set values to NULL if data exists on referencing table)
SET DEFAULT (Set values to DEFAULT VALUES of this field if data exists on referencing table)
RESTRICT (Similar to NO ACTION)
NO ACTION (Can not update or delete data if exists on referencing table)
CASCADE (Delete all data if exists on referencing table)
I wrote for you a sample trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare
act varchar(30);
begin
-- get account_number from deleted record
act = old.account_number;
-- SECTION-1 :: Protect deleting if existing data
if (exists(select 1 from depositor where account_number = act)) then
return null;
end if;
-- SECTION-1 :: END
-- SECTION-2 :: Delete all data in the anothers table if exists */
delete from depositor where account_number = act;
return old;
-- SECTION-2 :: END
end
$function$;
CREATE TRIGGER Day_21_bankTrigger
BEFORE DELETE on account
FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction();
Inside my trigger function, I have written two types of SQL codes. (SECTION-1, SECTION-2). You must choose one of them.

How to implement AFTER INSERT Trigger in Oracle PL/SQL?

I am trying to implement after insert trigger in PLSQL. The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
I am struggling with implementing warning-free query, which causes error during insertion. How could I manage this?
Here is my implemented trigger which I guess needs some changes.
CREATE TRIGGER blatrigger
AFTER INSERT
ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT (exception_name, -20999);
BEGIN
if (select count(*) as counter from BLATABLE where CLIENTID = :NEW.CLIENTID and STATUS='PENDING').counter > 1
THEN
raise exception_name;
END IF;
END;
Here is the table itself:
create table BLATABLE
(
ID NUMBER(19) not null primary key,
CLIENTID NUMBER(10),
CREATED TIMESTAMP(6),
STATUS VARCHAR2(255 char)
);
The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
No need for a trigger. It looks like a simple unique constraint should get the job done here:
create table blatable (
id number(19) not null primary key,
clientid number(10),
created timestamp(6),
status varchar2(255 char),
constraint blaconstraint unique (clientid, status)
);
The unique constraint prevents duplicates on (clientid, status) across the whole table. If a DML operation (insert, update) attempts to generate a duplicate, an error is raised and the operation is rolled back.
If, on the other end, you want to allow only one "PENDING" status per user, then you can use a unique index as follows:
create unique index bla_index
on blatable( (case when status = 'PENDING' then clientid end) );
Use a Statement Level Trigger, rather than a Row Level by removing FOR EACH ROW, and converting to your code as below :
CREATE OR REPLACE TRIGGER blatrigger
AFTER INSERT ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
counter INT;
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT(exception_name, -20999);
BEGIN
SELECT MAX(COUNT(*))
INTO counter
FROM BLATABLE
WHERE STATUS = 'PENDING'
GROUP BY CLIENTID;
IF counter > 1 THEN
RAISE exception_name;
END IF;
END;
/
where
the SELECT statement need to be removed from IF .. THEN conditional
Most probably, the mutating table error would raise for Row Level Trigger case
Demo

How to delete/update records inside trigger based on the updated/deleted row?

I have a table with the following format
id | name | supervisor_id
I made a "BEFORE INSERT" trigger that checks if the supervisor_id exists in the id column and if not, then assign a null value to the supervisor_id.
I am trying to write two more triggers. One that checks if the supervisor_id exists in the id column before each update of the supervisor_id, and one that sets the supervisor_id to NULL for each employee if his supervisor is deleted.
This is my code, of course it's not working, help please.
CREATE OR REPLACE TRIGGER EAP_users_TRG3
AFTER DELETE
ON EAP_users
FOR EACH ROW
DECLARE
d NUMBER;
BEGIN
SELECT id INTO d FROM EAP_users WHERE id = :OLD.id;
UPDATE EAP_users SET supervisor = NULL WHERE supervisor = d;
END;
/
This is the "working" trigger:
CREATE OR REPLACE TRIGGER EAP_users_TRG1
BEFORE INSERT
ON EAP_users
FOR EACH ROW
DECLARE
supervisor EAP_users.supervisor%TYPE;
CURSOR supervisor_CUR IS SELECT idFROM EAP_users;
b BOOLEAN := FALSE;
BEGIN
IF ( :NEW.supervisor IS NOT NULL ) THEN
FOR s IN supervisor_CUR LOOP
IF ( :NEW.supervisor = s.id ) THEN
b := TRUE;
END IF;
END LOOP;
IF (b = FALSE) THEN
:NEW.supervisor := NULL;
END IF;
END IF;
END;
/
According to the definition of your problem, you are trying to enforce referential integrity of your data. In that case, a trigger is probably not the right tool. To quote Oracle's documentation:
You can use both triggers and integrity constraints to define and enforce any type of integrity rule. However, Oracle strongly recommends that you use triggers to constrain data input only in the following situations:
[...]
When a required referential integrity rule cannot be enforced using the following integrity constraints:
NOT NULL, UNIQUE
PRIMARY KEY
FOREIGN KEY
CHECK
DELETE CASCADE
DELETE SET NULL
In that particular case you should use FOREIGN KEY constraint using the DELETE SET NULL modifier. Assuming you have an index on id, all you need is:
ALTER TABLE EAP_users
ADD CONSTRAINT EAP_users_supervisor_cst
FOREIGN KEY (supervisor_id)
REFERENCES EAP_users(id)
ON DELETE SET NULL;
This simple referential integrity constraint will perform probably better the same things as your 3 triggers -- namely:
prevent insert/update with a non existing (non-NULL) supervisor_id
set all supervisor_id to NULL when you delete the supervisor
See http://sqlfiddle.com/#!4/1f8fb/1 for a live example.

Why is trigger not fired on every single row when using "insert select" or "merge"

I defined a BEFORE INSERT trigger for a table and it works as expected for single INSERTstatements, but not for INSERT ... SELECT nor MERGE statements.
These are my database objects (simplified):
CREATE TABLE "COMPANY" (
"ID" NUMBER NOT NULL,
"NAME" VARCHAR(100)
);
CREATE TABLE "EMPLOYEE" (
"ID" NUMBER NOT NULL,
"COMPANY_ID" NUMBER NOT NULL
);
CREATE UNIQUE INDEX "EMPLOYEE_PK" ON "EMPLOYEE" ("ID");
CREATE SEQUENCE "EMPLOYEE_SEQUENCE";
CREATE TRIGGER "BI_EMPLOYEE" BEFORE INSERT ON "EMPLOYEE"
REFERENCING NEW AS newrow FOR EACH ROW BEGIN ATOMIC
IF newrow.id IS NULL THEN
SET newrow.id = NEXT VALUE FOR employee_sequence;
END IF;
END;
If single INSERTstatements are executed, everything works as expected, the ÌD is fetched from the sequence. But if I execute something like
INSERT INTO employee (company_id) SELECT id FROM company;
the I get an error:
integrity constraint violation: unique constraint or index violation: "EMPLOYEE_PK"
which could propably mean that it tries to insert the same key from the sequence twice.
I'm using the latests version 2.3.2 of HSQLDB.
Because triggers are set based, not row based.
See details here

Insert trigger ends up inserting duplicate rows in partitioned table

I have a partitioned table with (what I think) the appropriate INSERT trigger and a few constraints on it. Somehow, INSERT statements insert 2 rows for each INSERT: one for the parent and one for the appropriate partition.
The setup briefly is the following:
CREATE TABLE foo (
id SERIAL NOT NULL,
d_id INTEGER NOT NULL,
label VARCHAR(4) NOT NULL);
CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo);
ALTER TABLE ONLY foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo ADD CONSTRAINT foo_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);
CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('foo_id_seq');
END IF;
EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_foo_trigger
BEFORE INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();
Upon further debugging I isolated what's causing it: the fact that the INSERT trigger returns NEW as opposed to just NULL. However I do want my insert statements to return the auto-increment id and if I just return NULL that won't be the case.
What's the solution? Why does returning NEW cause this seemingly "strange" behavior?
UPDATE #1
Well, I know why the rows got inserted twice as it is clear from the documentation of triggers:
Trigger functions invoked by per-statement triggers should always
return NULL. Trigger functions invoked by per-row triggers can return
a table row (a value of type HeapTuple) to the calling executor, if
they choose. A row-level trigger fired before an operation has the
following choices:
It can return NULL to skip the operation for the current row. This
instructs the executor to not perform the row-level operation that
invoked the trigger (the insertion, modification, or deletion of a
particular table row).
For row-level INSERT and UPDATE triggers only, the returned row
becomes the row that will be inserted or will replace the row being
updated. This allows the trigger function to modify the row being
inserted or updated.
But my question is still how is it possible to not return NEW and still be able to get the auto-incremented id, or ROW_COUNT for example?
UPDATE #2
I found a solution, but I sure hope that there's a better one. Basically, you can add an AFTER TRIGGER to delete the row inserted into the parent table. This seems horribly inefficient, so if anyone has a better solution, please post it!
For reference the solution is:
CREATE TRIGGER insert_foo_trigger
BEFORE INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();
CREATE OR REPLACE FUNCTION foo_delete_master()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM ONLY foo WHERE id = NEW.id;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER after_insert_foo_trigger
AFTER INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_delete_master();
A simpler way is to create stored procedure instead of the triggers, for example add_foo( [parameters] ), which would decide which partition is suitable to insert a row to and return id (or the new record values, including id). For example:
CREATE OR REPLACE FUNCTION add_foo(
_d_id INTEGER
, _label VARCHAR(4)
) RETURNS BIGINT AS $$
DECLARE
_rec foo%ROWTYPE;
BEGIN
_rec.id := nextval('foo_id_seq');
_rec.d_id := _d_id;
_rec.label := _label;
EXECUTE 'INSERT INTO foo_' || ( _d_id % 2 ) || ' SELECT $1.*' USING _rec;
RETURN _rec.id;
END $$ LANGUAGE plpgsql;
Another solution to this problem is offered by this question:
Postgres trigger-based insert redirection without breaking RETURNING
In summary, create a view for your table and then you can use INSTEAD OF to handle the update while still being able to return NEW.
Untested code, but you get the idea:
CREATE TABLE foo_base (
id SERIAL NOT NULL,
d_id INTEGER NOT NULL,
label VARCHAR(4) NOT NULL
);
CREATE OR REPLACE VIEW foo AS SELECT * FROM foo_base;
CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo_base);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo_base);
ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);
CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('foo_base_id_seq');
END IF;
EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_foo_trigger
INSTEAD OF INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();