How to avoid "blank" insert when inserting data into a view with a trigger & insert procedure? - sql

I'm trying to update tables from insert or update call on a PostgreSQL view. Here's a simplified example of what I do:
[Person] table:
id | lastname | firstname | city | age
[Person_View] table:
id | lastname | firstname | city
Here is the trigger and the related procedure :
CREATE TRIGGER tg_update_person_view
INSTEAD OF INSERT OR UPDATE OR DELETE ON
Person_View FOR EACH ROW EXECUTE PROCEDURE update_person_view_table();
CREATE OR REPLACE FUNCTION update_person_view_table()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $function$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO Person (id, lastname, firstname)
VALUES(NEW.id, NEW.lastname, NEW.firstname);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE Person
SET id=NEW.id, lastname=NEW.lastname, firstname=NEW.firstname
WHERE id=OLD.id;
RETURN NEW;
END IF;
RETURN NEW;
END;
$function$;
If I do:
INSERT INTO Person_View (id, city) VALUES ('3', 'Berlin')
A row with only the ID is added to the view and the parent table.
How can I check in the procedure that columns in which values are being inserted have a "mapping" defined in the procedure and if there ain't any mapped columns, it does not proceed ?

You can define a check constraint on the table, e.g.:
create table person(
id int primary key,
lastname text,
firstname text,
city text,
age int,
check(coalesce(lastname, firstname, city, age::text) is not null)
);
insert into person (id)
values (1);
ERROR: new row for relation "person" violates check constraint "person_check"
DETAIL: Failing row contains (1, null, null, null, null).
The solution works regardless whether any views based on the table were created or not.

Have a separate trigger & trigger function for ON DELETE to simplify. (You are not doing anything ON DELETE anyway?)
A CHECK constraint like klin suggested seems like a good idea. You don't need COALESCE and casting, though. Check a row value for NULL.
CHECK (NOT ROW(lastname, firstname) IS NULL) -- ROW keyword is noise
This enforces at least one notnull value in the row. Works for any number of columns and any data type.
Note in particular that ROW(lastname, firstname) IS NOT NULL is not the same and would not work. Detailed explanation:
NOT NULL constraint over a set of columns
If the CHECK constraint is not an option, you can use the same expression in a trigger - which should be faster than adding it to the trigger function. The manual on CREATE TRIGGER:
Also, a trigger definition can specify a Boolean WHEN condition, which
will be tested to see whether the trigger should be fired. In
row-level triggers the WHEN condition can examine the old and/or new
values of columns of the row.
CREATE TRIGGER tg_update_person_view
INSTEAD OF INSERT OR UPDATE ON Person_View
FOR EACH ROW
WHEN (NOT (NEW.lastname, NEW.firstname) IS NULL) -- more columns?
EXECUTE PROCEDURE update_person_view_table();
If the WHEN expression does not evaluate to TRUE, the trigger function is not even called - so it does not proceed like requested.
However, I missed your trigger INSTEAD OF. The manual:
INSTEAD OF triggers do not support WHEN conditions.
In this case you have to move the check into the function body:
IF NOT (NEW.lastname, NEW.firstname) IS NULL THEN
-- do stuff
END IF;

Related

PSQL Constraint based on column value

Is it possible to have a Constraint but only when one column is set to a particular value. For example take this pseudo-code example of a President which checks to make sure there is never more than 1 President at any time (note, this is NOT valid psql syntax)
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL,
CONSTRAINT there_can_be_only_one CHECK(COUNT(current=true)<=1)
);
You can use the so called partial index to enforce this specific constraint. In SQL Server they are called filtered indexes.
CREATE UNIQUE INDEX IX ON president (current)
WHERE current = true;
This index should prevent having more than one row in a table with current value set to true, because it is defined as unique.
Unfortunately NO as far as I know and anyway it already tells us,
ERROR: aggregate functions are not allowed in check constraints.
But we can use BEFORE trigger to check that the data you are trying to insert should meets the criteria COUNT(current=true)<=1
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL
);
---------------------------------------------------------------------
CREATE FUNCTION check_current_flag()
RETURNS trigger
AS $current_president$
DECLARE
current_flag_count integer;
BEGIN
SELECT COUNT(*) FILTER (WHERE current = true )
INTO current_flag_count
FROM president;
IF new.current = true
and current_flag_count >= 1 THEN
RAISE EXCEPTION 'There can be only one current president';
-- RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$current_president$ LANGUAGE plpgsql;
---------------------------------------------------------------------
CREATE TRIGGER current_president BEFORE INSERT OR UPDATE ON president
FOR EACH ROW EXECUTE PROCEDURE check_current_flag();
Db<>Fiddle for reference
Note:
You can either throw exception in case of preconditions doesn't match ore simply returning NULL will skip the insert and do nothing. as official document says also here

postgres updatable view and unique constraints

in my simple application I would like to create a view in order to allow users filling data of my db.
Here a little example of my data
CREATE TABLE specie
(
specie_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_comune TEXT UNIQUE,
nome_scientifico TEXT UNIQUE
);
CREATE TABLE rilevatore
(
rilevatore_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_cognome TEXT UNIQUE,
telefono INTEGER,
email TEXT,
ente_appartenenza TEXT
);
CREATE TABLE evento_investimento
(
evento_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
data DATE,
ora TIME WITHOUT TIME ZONE,
rilevatore_id INT REFERENCES rilevatore (rilevatore_id),
specie_id INT REFERENCES specie(specie_id),
);
This is the VIEW I created
CREATE VIEW investimenti_vista AS
SELECT
evento_investimento.evento_id,
evento_investimento.ora,
evento_investimento.data,
rilevatore.nome_cognome,
rilevatore.telefono,
rilevatore.email,
rilevatore.ente_appartenenza,
specie.nome_comune,
specie.nome_scientifico
from
evento_investimento
JOIN specie ON evento_investimento.specie_id = specie.specie_id
JOIN rilevatore ON evento_investimento.rilevatore_id = rilevatore.rilevatore_id;
When I attempt to fill the data I receive an error from postgres since view generated from different tables aren't updatable by default.
Thus, I implemetend the following trigger to overcome this issue.
CREATE OR REPLACE FUNCTION inserimento_vista() RETURNS trigger AS $$
BEGIN
INSERT INTO evento_investimento (data,ora)
VALUES (NEW.data,NEW.ora);
INSERT INTO rilevatore (nome_cognome, telefono, email, ente_appartenenza)
VALUES (NEW.nome_cognome, NEW.telefono, NEW.email, NEW.ente_appartenenza);
INSERT INTO specie (nome_comune, nome_scientifico)
VALUES (NEW.nome_comune, NEW.nome_scientifico);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create trigger inserimento_vista_trg
instead of insert on investimenti_vista for each row EXECUTE procedure inserimento_vista();
However this is not working due to unique contraints I have in the rilevatore and specie tables. How I can solve this?
Thanks
You might try to check for the existence of the conflicting values like this:
CREATE OR REPLACE FUNCTION inserimento_vista() RETURNS trigger AS $$
BEGIN
INSERT INTO evento_investimento (data,ora)
VALUES (NEW.data,NEW.ora);
if not exists(select * from rilevatore where rilevatore.nome_cognome=new.nome_cognome) then
INSERT INTO rilevatore (nome_cognome, telefono, email, ente_appartenenza)
VALUES (NEW.nome_cognome, NEW.telefono, NEW.email, NEW.ente_appartenenza);
end if;
if not exists(select * from specie where specie.nome_comune=new.nome_comune) then
INSERT INTO specie (nome_comune, nome_scientifico)
VALUES (NEW.nome_comune, NEW.nome_scientifico);
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
You might want to add to this an update the specie and/or rilevatore tables with the non-conflicting values but that's up to you :-)

TRIGGER BEFORE DELETE, doesn't delete data in table

I'm working on history of my database when a row is modify/delete.
My main table is "bati" and history table "bati_history", when a row is delete or modify, the trigger is suppose to insert into bati_history all the old data, then delete in the main table (bati). But with my code, the row is insert into the history but not delete in the main table and I don't know why.
I'm on PostgreSQL 11.2 64-bit
The code :
Main table:
CREATE TABLE IF NOT EXISTS bati(
id_bati BIGSERIAL NOT NULL UNIQUE,
code_bati VARCHAR(25) NOT NULL,
code_parcelle CHAR(50) NOT NULL,
...);
History table:
CREATE TABLE IF NOT EXISTS bati_history(
id_history BIGSERIAL NOT NULL PRIMARY KEY,
event CHAR(10) NOT NULL,
date_save_history TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
id_bati BIGINT NOT NULL,
code_bati VARCHAR(25) NOT NULL,
code_parcelle CHAR(50) NOT NULL,
...);
The function
CREATE FUNCTION archive_bati() RETURNS TRIGGER AS $BODY$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle,...)
VALUES ('DELETE',OLD.id_bati,OLD.code_bati,OLD.code_parcelle,OLD....);
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle,...)
VALUES ('UPDATE',OLD.id_bati,OLD.code_bati,OLD.code_parcelle,OLD....);
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
TRIGGERS:
CREATE TRIGGER bati_trigger_before_delete BEFORE DELETE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
CREATE TRIGGER bati_trigger_before_update BEFORE UPDATE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
When I try DELETE FROM bati;, I expect to copy every row in bati_history, then delete them from bati, but they are not delete from bati, and I have this output without error :
test=# INSERT INTO bati (id_bati,code_bati,code_parcelle,id_interne) VALUES (5,'CODEBATI001','CODEPARC001',02);
INSERT 0 1
test=# INSERT INTO bati (id_bati,code_bati,code_parcelle,id_interne) VALUES (6,'CODEBATI002','CODEPARC002',02);
INSERT 0 1
test=# DELETE FROM bati;
DELETE 0
(sorry for my english I'm french)
You should use the if-else branching to either return NEW OR OLD depending on the trigger operation. The variable TG_OP has a text type & could be used in the insert query directly.
So, the function definition becomes:
CREATE FUNCTION archive_bati()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO bati_history (event,id_bati,code_bati,code_parcelle)
VALUES (TG_OP, OLD.id_bati, OLD.code_bati, OLD.code_parcelle);
IF TG_OP = 'DELETE'
THEN RETURN OLD;
ELSE RETURN NEW;
END IF;
END;
$$ LANGUAGE PLPGSQL;
Also, it seems unnecessary to me to define two triggers when 1 will suffice:
CREATE TRIGGER bati_trigger_before_update BEFORE UPDATE OR DELETE
ON bati FOR EACH ROW
EXECUTE PROCEDURE archive_bati();
When you delete a row, NEW is null. If the before trigger returns a null, it means the operation should be cancelled. You should then return OLD for deletions, and NEW for updates.
From the doc:
In the case of a before-trigger on DELETE, the returned value has no
direct effect, but it has to be nonnull to allow the trigger action to
proceed. Note that NEW is null in DELETE triggers, so returning that
is usually not sensible. The usual idiom in DELETE triggers is to
return OLD.

Insert inserted id to another table

Here's the scenario:
create table a (
id serial primary key,
val text
);
create table b (
id serial primary key,
a_id integer references a(id)
);
create rule a_inserted as on insert to a do also insert into b (a_id) values (new.id);
I'm trying to create a record in b referencing to a on insertion to a table. But what I get is that new.id is null, as it's automatically generated from a sequence. I also tried a trigger AFTER insert FOR EACH ROW, but result was the same. Any way to work this out?
To keep it simple, you could also just use a data-modifying CTE (and no trigger or rule):
WITH ins_a AS (
INSERT INTO a(val)
VALUES ('foo')
RETURNING a_id
)
INSERT INTO b(a_id)
SELECT a_id
FROM ins_a
RETURNING b.*; -- last line optional if you need the values in return
Related answer with more details:
PostgreSQL multi INSERT...RETURNING with multiple columns
Or you can work with currval() and lastval():
How to get the serial id just after inserting a row?
Reference value of serial column in another column during same INSERT
Avoid rules, as they'll come back to bite you.
Use an after trigger on table a that runs for each row. It should look something like this (untested):
create function a_ins() returns trigger as $$
begin
insert into b (a_id) values (new.id);
return null;
end;
$$ language plpgsql;
create trigger a_ins after insert on a
for each row execute procedure a_ins();
Don't use triggers or other database Kung fu. This situation happens every moment somewhere in the world - there is a simple solution:
After the insertion, use the LASTVAL() function, which returns the value of the last sequence that was auto-incremented.
Your code would look like:
insert into a (val) values ('foo');
insert into b (a_id, val) values (lastval(), 'bar');
Easy to read, maintain and understand.

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();