postgresql upsert on view with triggers - postgresql

I'm working with a postgresql 14 database where I have access to data through a view schema, and inserts and updates are performed with triggers. There is a unique constraint on the storage table, and I was wondering if there is a way to do upserts in this case?
This replicates the problem in a (much) smaller database:
schema:
CREATE SCHEMA storage;
-- unreachable storage schema
CREATE TABLE storage.data (
id SERIAL PRIMARY KEY,
a INTEGER,
b INTEGER,
c INTEGER,
CONSTRAINT pair UNIQUE(a, b)
);
-- reachable access schema
CREATE SCHEMA access;
CREATE VIEW access.data AS
SELECT id, a, b, c FROM storage.data;
-- data insertion trigger for the access view
CREATE FUNCTION data_insert()
RETURNS TRIGGER AS $data_insert$
BEGIN
INSERT INTO storage.data (a, b, c)
VALUES (NEW.a, NEW.b, NEW.c);
RETURN NEW;
END;
$data_insert$ LANGUAGE plpgsql;
CREATE TRIGGER data_insert_trigger
INSTEAD OF INSERT ON access.data
FOR EACH ROW EXECUTE PROCEDURE data_insert();
-- data update trigger for the access view
CREATE FUNCTION data_update()
RETURNS TRIGGER AS $data_update$
BEGIN
UPDATE storage.data SET
a = NEW.a,
b = NEW.b,
c = NEW.c
WHERE id = OLD.id;
RETURN NEW;
END;
$data_update$ LANGUAGE plpgsql;
CREATE TRIGGER data_update_trigger
INSTEAD OF UPDATE ON access.data
FOR EACH ROW EXECUTE PROCEDURE data_update();
What I would like to do is:
# INSERT INTO access.data(a,b,c) VALUES (1,2,3);
INSERT 0 1
# INSERT INTO access.data(a,b,c) VALUES (1,2,4)
ON CONFLICT ON CONSTRAINT pair
DO UPDATE SET c=EXCLUDED.c;
ERROR: constraint "pair" for table "data" does not exist
Is there any way to do an upsert query in this situation, or should I settle for doing a select followed by insert or update?
EDIT: I can not modify the schemas or add functions, I can only make queries to the access schema.

Have you tried something like this?
CREATE FUNCTION data_insert()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $data_insert$
BEGIN
INSERT INTO storage.data (a, b, c)
VALUES ( NEW.a, NEW.b, NEW.c )
ON CONFLICT DO UPDATE
a = EXCLUDED.a
, b = EXCLUDED.b
, c = EXCLUDED.c
;
RETURN NEW;
END;
$data_insert$;

Related

How to DELETE/INSERT rows in the same table using a UPDATE Trigger?

I want to create a trigger function, which copies certain columns of an recent updated row and deletes the old data. After that I want to insert the copied columns in exact the same table in the same row (overwrite). I need the data to be INSERTED because this function will be embedded in an existing program, with predefined Triggers.
That's what I have so far:
CREATE OR REPLACE FUNCTION update_table()
RETURNS TRIGGER AS
$func$
BEGIN
WITH tmp AS (DELETE FROM table
WHERE table.id = NEW.id
RETURNING id, geom )
INSERT INTO table (id, geom) SELECT * FROM tmp;
END;
$func$ language plpgsql;
CREATE TRIGGER T_update
AFTER UPDATE OF geom ON table
EXECUTE PROCEDURE update_table();
But I get the Error message:
ERROR: cannot perform DELETE RETURNING on relation "table"
HINT: You need an unconditional ON DELETE DO INSTEAD rule with a RETURNING clause.
Why I should use a rule here?
I'm using PostgreSQL 9.6
UPDATE:
A little bit of clarification. When I have two columns in my table (id, geom), after I updated geom I want to make a copy of this (new)row and insert it into the same table, while overwriting the updated row. (I'm not interested in any value before the update) I know that this is odd but I need this row to be inserted again because the program i embed this function in, listens to a INSERT statement and cannot be changed by me.
Right after you update a row, its old values will no longer be available. So, if you simply want to preserve the old row in case of an update you need to create a BEFORE UPDATE trigger, so that you can still access the OLD values and create a new row, e.g.
CREATE TABLE t (id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (OLD.id,OLD.geom);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
INSERT INTO t VALUES (1,'SRID=4326;POINT(1 1)');
If you update the record 1 ..
UPDATE t SET geom = 'SRID=4326;POINT(2 2)', id = 2 WHERE id = 1;
UPDATE t SET geom = 'SRID=4326;POINT(3 3)', id = 3 WHERE id = 2;
.. you get a new record in the same table as you wished
SELECT id, ST_AsText(geom) FROM t;
id | st_astext
----+------------
1 | POINT(1 1)
2 | POINT(2 2)
3 | POINT(3 3)
Demo: db<>fiddle
Unrelated note: consider upgrading your PostgreSQL version! 9.6 will reach EOL in November, 2021.
First thanks to #JimJones for the answer. I´d like to post his answer modified for this purpose. This code "overwrites" the updated row by inserting a copy of itself and then deleting the old duplicate. That way I can Trigger on INSERT.
CREATE TABLE t (Unique_id SERIAL,id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (NEW.id,NEW.geom);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
CREATE OR REPLACE FUNCTION delete_table() RETURNS TRIGGER AS $$
BEGIN
DELETE FROM t a
USING t b
WHERE a.Unique_id < b.Unique_id
AND a.geom = b.geom;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_delete
AFTER UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE delete_table();
INSERT INTO t VALUES (1,1,'SRID=4326;POINT(1 1)');
UPDATE t SET geom = 'SRID=4326;POINT(2 2)' WHERE id = 1;

postgresql inheritance / insert in parent then same record only in child

I am trying to insert in child table ONLY the same record that is already exsisting in the parent table.
Because ONLY is not aplicable i've tried
to create before insert trigger on parent table that checks for existing id(primary key) but it seems it doesn`t work -> the data are still duplicated:
Example:
parent table:
CREATE TABLE public.store(
id serial PRIMARY KEY,
name text);
child table :
CREATE TABLE public.db_store(
) INHERITS(store);
alter table public.db_store
add constraint db_store_pkey_id primary key (id);
function trigger:
create or replace function store_before_insert()
returns trigger language plpgsql as $$
declare old_s_id integer;
begin
old_s_id=null;
if (new.id is not null ) then
select s.id into old_s_id from store s where s.id=new.id;
if (old_s_id is not null) then
return null;
end if;
end if;
return new;
end $$;
trigger itself:
create trigger insert_storehouse_trg
before insert on storehouse
for each row
execute procedure storehouse_before_insert();
if execute:
insert into store(name) ('test');
insert into db_store(id,name) (1,'test');
(1, 'test') appers two time in store.
Is there are way to achive it without delete record from parent table?

How to pass a record to a PL/pgSQL function?

I have 8 similar PL/pgSQL functions; they are used as INSTEAD OF INSERT/UPDATE/DELETE triggers on views to make them writable. The views each combine columns of one generic table (called "things" in the example below) and one special table ("shaped_things" and "flavored_things" below). PostgreSQL's inheritance feature can't be used in our case, by the way.
The triggers have to insert/update rows in the generic table; these parts are identical across all 8 functions. Since the generic table has ~30 columns, I'm trying to use a helper function there, but I'm having trouble passing the view's NEW record to a function that needs a things record as input.
(Similar questions have been asked here and here, but I don't think I can apply the suggested solutions in my case.)
Simplified schema
CREATE TABLE things (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
-- (plus 30 more columns)
);
CREATE TABLE flavored_things (
thing_id INT PRIMARY KEY REFERENCES things (id) ON DELETE CASCADE,
flavor TEXT NOT NULL
);
CREATE TABLE shaped_things (
thing_id INT PRIMARY KEY REFERENCES things (id) ON DELETE CASCADE,
shape TEXT NOT NULL
);
-- etc...
Writable view implementation for flavored_things
CREATE VIEW flavored_view AS
SELECT t.*,
f.*
FROM things t
JOIN flavored_things f ON f.thing_id = t.id;
CREATE FUNCTION flavored_trig () RETURNS TRIGGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO things VALUES ( -- (A)
DEFAULT,
NEW.name
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
INSERT INTO flavored_things VALUES (
inserted_id,
NEW.flavor
);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE things SET -- (B)
name = NEW.name
-- (plus 30 more columns)
WHERE id = OLD.id;
UPDATE flavored_things SET
flavor = NEW.flavor
WHERE thing_id = OLD.id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
DELETE FROM flavored_things WHERE thing_id = OLD.id;
DELETE FROM things WHERE id = OLD.id;
RETURN OLD;
END IF;
END;
$fun$ LANGUAGE plpgsql;
CREATE TRIGGER write_flavored
INSTEAD OF INSERT OR UPDATE OR DELETE ON flavored_view
FOR EACH ROW EXECUTE PROCEDURE flavored_trig();
The statements marked "(A)" and "(B)" above are what I would like to replace with a call to a helper function.
Helper function for INSERT
My initial attempt was to replace statement "(A)" with
inserted_id = insert_thing(NEW);
using this function
CREATE FUNCTION insert_thing (new_thing RECORD) RETURNS INTEGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
INSERT INTO things (name) VALUES (
new_thing.name
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
RETURN inserted_id;
END;
$fun$ LANGUAGE plpgsql;
This fails with the error message "PL/pgSQL functions cannot accept type record".
Giving the parameter the type things doesn't work when the function is called as insert_thing(NEW): "function insert_thing(flavored_view) does not exist".
Simple casting doesn't seem to be available here; insert_thing(NEW::things) produces "cannot cast type flavored_view to things". Writing a CAST function for each view would remove what we gained by using a helper function.
Any ideas?
There are various options, depending on the complete picture.
Basically, your insert function could work like this:
CREATE FUNCTION insert_thing (_thing flavored_view)
RETURNS int AS
$func$
INSERT INTO things (name) VALUES ($1.name) -- plus 30 more columns
RETURNING id;
$func$ LANGUAGE sql;
Using the row type of the view, because NEW in your trigger is of this type.
Use a simple SQL function, which can be inlined and might perform better.
Demo call:
SELECT insert_thing('(1, foo, 1, bar)');
Inside your trigger flavored_trig ():
inserted_id := insert_thing(NEW);
Or, basically rewritten:
IF TG_OP = 'INSERT' THEN
INSERT INTO flavored_things(thing_id, flavor)
VALUES (insert_thing(NEW), NEW.flavor);
RETURN NEW;
ELSIF ...
record is not a valid type outside PL/pgSQL, it's just a generic placeholder for a yet unknown row type in PL/pgSQL) so you cannot use it for an input parameter in a function declaration.
For a more dynamic function accepting various row types you could use a polymorphic type. Examples:
How to return a table by rowtype in PL/pgSQL
Refactor a PL/pgSQL function to return the output of various SELECT queries
How to write a function that returns text or integer values?
Basically you can convert a record to a hstore variable and pass the hstore variable instead of a record variable to a function. You convert record to hstore i.e. so:
DECLARE r record; h hstore;
h = hstore(r);
Your helper function should also be changed so:
CREATE FUNCTION insert_thing (new_thing hstore) RETURNS INTEGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
INSERT INTO things (name) VALUES (
new_thing -> 'name'
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
RETURN inserted_id;
END;
$fun$ LANGUAGE plpgsql;
And the call:
inserted_id = insert_thing(hstore(NEW));
hope it helps
Composite types. PostgresSQL has documentation on this, you essentially need to use something like
'()' or ROW() to construct the composite type for a row to pass into a function.

Trigger to ensure unique values in column

I have to write a trigger to ensure unique entries in column account of table accounts:
create table accounts (id serial, account int4 default 0);
I tried to write my trigger like this:
create function x_6 () returns trigger as '
begin
IF row(new.account) is distinct from row(OLD.account) THEN
return NEW;
ELSE
raise notice '' Entries are not unique! '';
END IF;
end;
'
language 'plpgsql';
Or:
create function x_6 () returns trigger as '
begin
IF (new.account <> OLD.account) THEN
return NEW;
ELSE
raise notice '' Entries are not unique ! '';
END IF;
end;
'
language 'plpgsql';
And then
create trigger x_6t before insert on accounts for each row execute procedure x_6();
When I try to insert something:
insert into accounts(account) values (20);
I get an error in either case:
ERROR: record "old" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
CONTEXT: PL/pgSQL function "x_6" line 3 at if
How can I fix it?
This is absolutely wrong way. You should not to use triggers for this purpose, you should to use unique indexes.
CREATE TABLE foo(a int PRIMARY KEY, b int);
-- column b has only unique values
CREATE UNIQUE INDEX ON foo(b);
Your code has more than one issue:
bad identifiers - konto instead account
it is table trigger - you has no any access to data there - PostgreSQL triggers are different than MSSQL
If you use row trigger, where there is possible access to record data, then OLD has different meaning than you expect. It is value of record before change - and this value is defined only for UPDATE or DELETE operations - and it is undefined for INSERT, because there previous value of record doesn't exist.

postgresql - cascade copy/insert

I have a question about copying rows in PostgreSQL. My table hierarchy is quite complex, where many tables are linked to each other via foreign keys. For the sake of simplicity, I will explain my question with two tables, but please bear in mind that my actual case requires a lot more complexity.
Say I have the following two tables:
table A
(
integer identifier primary key
... -- other fields
);
table B
(
integer identifier primary key
integer a foreign key references A (identifier)
... -- other fields
);
Say A and B hold the following rows:
A(1)
B(1, 1)
B(2, 1)
My question is: I would like to create a copy of a row in A such that the related rows in B are also copied into a new row. This would give:
A(1) -- the old row
A(2) -- the new row
B(1, 1) -- the old row
B(2, 1) -- the old row
B(3, 2) -- the new row
B(4, 2) -- the new row
Basically I am looking for a COPY/INSERT CASCADE.
Is there a neat trick to achieve this more or less automatically? Maybe by using temporary tables?
I believe that if I have to write all the INSERT INTO ... FROM ... queries myself in the correct order and stuff, I might go mental.
update
Let's answer my own question ;)
I did some try-outs with the RULE mechanisms in PostgreSQL and this is what I came up with:
First, the table definitions:
drop table if exists A cascade;
drop table if exists B cascade;
create table A
(
identifier serial not null primary key,
name varchar not null
);
create table B
(
identifier serial not null primary key,
name varchar not null,
a integer not null references A (identifier)
);
Next, for each table, we create a function and corresponding rule which translates UPDATE into INSERT.
create function A(in A, in A) returns integer as
$$
declare
r integer;
begin
-- A
if ($1.identifier <> $2.identifier) then
insert into A (identifier, name) values ($2.identifier, $2.name) returning identifier into r;
else
insert into A (name) values ($2.name) returning identifier into r;
end if;
-- B
update B set a = r where a = $1.identifier;
return r;
end;
$$ language plpgsql;
create rule A as on update to A do instead select A(old, new);
create function B(in B, in B) returns integer as
$$
declare
r integer;
begin
if ($1.identifier <> $2.identifier) then
insert into B (identifier, name, a) values ($2.identifier, $2.name, $2.a) returning identifier into r;
else
insert into B (name, a) values ($2.name, $2.a) returning identifier into r;
end if;
return r;
end;
$$ language plpgsql;
create rule B as on update to B do instead select B(old, new);
Finally, some testings:
insert into A (name) values ('test_1');
insert into B (name, a) values ('test_1_child', (select identifier from a where name = 'test_1'));
update A set name = 'test_2', identifier = identifier + 50;
update A set name = 'test_3';
select * from A, B where B.a = A.identifier;
This seems to work quite fine. Any comments?
This will work. One thing I note you wisely avoided was DO ALSO rules on inserts and updates. DO ALSO with insert and update is pretty dangerous so avoid that at pretty much all cost.
On further reflection, however, triggers are not going to perform worse and offer fewer hard corners.