Postgres Trigger to INSERT UPDATE DELETE on similar derivative table - postgresql

Description:
I am running postgresql 13
I have two tables under different schemas, t1 and t2.
t2 is derivative of t1 in the sense that they share all the same
columns and data, but t2 is always downstream of t1 as far as
validity.
The rows in both tables share the same primary key, which is what I assume would be used as the link between them.
The ask:
I would like to create a trigger that reflects any changes in t1 and syncs t2 to be the same.
I started with INSERT or UPDATE, but if DELETE is easily added, I would like to implement that as well.
Trigger Code:
-- Trigger for t1 to t2 --
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync
AFTER INSERT OR UPDATE ON t1_schema.t1
FOR EACH ROW
EXECUTE PROCEDURE t1_schema.sync_trigger()
When I execute this code and do a test UPDATE on t1, the same row on t2 does not reflect the changes or give me any errors.
I have tried:
Discretely labeling all rows as updated with NEW. format, but run into the problem of primary key column not being editable in t2.
Adding a WHERE clause after the VALUES clause, something like WHERE primary_key=NEW.primary_key, but I get an error.
Another option I have seen is adding an IF statement before the
INSERT, or adding a WHEN clause in the trigger, but neither have
worked.

Your best approach is to not create t2 as a table. Instead create it as a VIEW on t1. This totally eliminates triggers to keep them synchronized because the actual source is the same. Follows the concept to store a single data point in only 1 place. Keep in mind that if you store a single piece in 2 places, 1 on them will be wrong at some point. (see demo).
create view soq2.t2 as
select *
from soq1.t1;
Also if you need column names to change then use an alias during the create view;
create view soq2.t2a as
select t1_id as t2_id
, name as t2_name
, status as t2_status
from soq1.t1;

(A) Solution based on triggers
You maybe get an error when updating a row in t1 because your trigger function tries to insert a new row in t2 which has alreday been inserted in t2 by the same trigger function when it has been inserted in t1. You need to duplicate and specialize your trigger functions, one for insert, one for update, one for delete because the treatment to be triggered on t2 is different :
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_insert()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_insert
AFTER INSERT ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_insert() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_update()
RETURNS TRIGGER AS
$$
BEGIN
UPDATE t2
SET col1 = NEW.col1
, col2 = NEW.col2
, col3 = NEW.col3
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_update
AFTER UPDATE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_update() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_delete()
RETURNS TRIGGER AS
$$
BEGIN
DELETE FROM t2
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN OLD; -- NEW is not available for triggers ON DELETE
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_delete
AFTER DELETE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_delete() ;
(B) Solution based on foreign key
It is possible that a foreign key on t2 (col1,col2,col3) referencing t1 (col1, col2, col3) with the options ON UPDATE CASCADE ON DELETE CASCADE may deliver your expected result in a much more simple and efficient way, see the manual. In this case, you don't need the triggers ON UPDATE and ON DELETE anymore, but you still need the trigger ON INSERT.

Related

Update all column in a table using a record

I have two identical tables: table1 and table2. I need to replicate all updates from one table to another using an after trigger. But I don't want to list all columns names in the update statement(except the PK). Is it possible to do something like this?
CREATE FUNCTION replicate_changes()
RETURNS trigger
LANGUAGE 'plpgsql' AS
$BODY$
BEGIN
UPDATE table2
SET (*) = NEW.*;
WHERE table2.id = NEW.id;
RETURN NULL;
END;
$BODY$;
CREATE TRIGGER trigger_replicate_changes
AFTER UPDATE
ON table1
FOR EACH ROW
EXECUTE FUNCTION replicate_changes();
You cannot do this directly, but you can delete the existing row and insert/select the new row. (see demo).
create or replace function replicate_changes()
returns trigger
language 'plpgsql' as
$body$
begin
delete from table2 where id = old.id;
insert into table2
select * from table1
where id = new.id;
return null;
end;
$body$;
But be careful, this is a dangerous process. What happens when you insert or delete from table1. As it stand those actions will not be reflected in table2. And what happens when DML is issued directly against table2, or only 1 of the table definitions gets updated (ddl) bu not the other.

Postgres getting duplicate key exception (DELETE AND INSERT) from inside psql function

I am trying to update the table with huge data. On checking I figured that upsert (Update and insert) is slow compared to using the temp table to delete and then insert from temp table. But I am facing issue with duplicate data in such scenario.
As far as I understand the delete and insert is happening in same transaction, so I could not understand why I am facing duplicate data issue when I have already deleted the data with the delete.
This is multithreaded scenario but i guess the other transaction will have its own set of data.
Any help is appreciated here.
Sample code
CREATE OR REPLACE FUNCTION insertUsingTempTable(a_id int, s_obj int[], p bigint[])
RETURNS BOOLEAN AS $BODY$
DECLARE passed BOOLEAN;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS temp_lists AS SELECT * FROM acm_lists WHERE 0=1;
TRUNCATE TABLE temp_lists;
INSERT INTO temp_lists(a_id, s_obj_id, eff_p)
SELECT a_id , unnest($2::int[]), unnest($3::bit(64)[]) ;
IF NOT EXISTS(SELECT t.a_id, s_obj_id, count(1) FROM temp_lists t
GROUP BY t.a_id, s_obj_id HAVING count(1) > 1) THEN
DELETE FROM acm_lists t WHERE EXISTS (SELECT 1 FROM temp_lists t1
WHERE t1.a_id=t.a_id AND s_obj_id=t.s_obj_id);
INSERT INTO acm_lists (a_id, s_obj_id, eff_p)
SELECT t.a_id, t.s_obj_id, t.eff_p FROM temp_lists t;
RETURN true;
END IF;
RETURN false;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Uniqueness is on a_id, s_obj_id in the above code.
I would like to know why duplicate exception is coming at times, if I am already deleting the data before inserting. This happens only when multiple transactions are running at the same time on the table.
INSERT ... ON CONFLICT DO UPDATE resolves the issue, but seems there is considerable performance hit. so I don't plan to use ON CONFLICT DO UPDATE approach.

Rewrite Do-Block to trigger function in PostgreSQL/plpgsql [duplicate]

This question already has an answer here:
PostgreSQL functions and triggers
(1 answer)
Closed 2 years ago.
I created a script to update multiple columns of a table in my database . Originally I run this manually but I'd like to apply some automatization with the help of a TRIGGER function. Basically I have an empty table where I INSERT values. Based on this INSERT i'd like to update the same table afterwards.
Thats my old code:
-- Create example table
CREATE TABLE table1(
column1 INTEGER,
column2 INTEGER,
column3 INTEGER,
column4 INTEGER);
-- Insert example values
INSERT INTO table1 (column1, column2, column3)
VALUES
(1,2,3),
(4,5,6),
(7,8,9),
(10,11,12);
-- Update the table
DO $$
DECLARE
b INTEGER := 5;
c INTEGER := 11;
BEGIN
UPDATE table1
SET column2 = column1 + b;
UPDATE table1
SET column3 = column2 - c;
UPDATE table1
SET column4 = column1 + column2 +column3;
END $$ language plpgsql;
I tried to rewrite it in this form:
--Create Trigger function
CREATE OR REPLACE FUNCTION example_trigger()
RETURNS TRIGGER AS
$func$
DECLARE
b INTEGER := 5;
c INTEGER := 11;
BEGIN
UPDATE table1
SET column2 = column1 + b;
UPDATE table1
SET column3 = column2 - c;
UPDATE table1
SET column4 = column1 + column2 +column3;
END $func$ language plpgsql;
-- Create Trigger
CREATE TRIGGER atest AFTER INSERT ON table1
EXECUTE PROCEDURE example_trigger();
-- DELETE FROM table1
--INSERT values in the hope to trigger "example_trigger"
INSERT INTO table1 (column1, column2, column3)
VALUES
(1,2,3),
(4,5,6),
(7,8,9),
(10,11,12);
But I get
ERROR: control reached end of trigger procedure without RETURN
CONTEXT: PL/pgSQL function example_trigger()
SQL state: 2F005
How is RETURN causing problems in my query? Additionally I'd like to know if the UPDATE statements in the DO Blocks are working row-wise or column wise.
In a PostgreSQL trigger function you need to use RETURN statement:
A trigger function must return either NULL or a record/row value
having exactly the structure of the table the trigger was fired for.
Row-level triggers fired BEFORE can return null to signal the trigger
manager to skip the rest of the operation for this row (i.e.,
subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does
not occur for this row). If a nonnull value is returned then the
operation proceeds with that row value. Returning a row value
different from the original value of NEW alters the row that will be
inserted or updated. Thus, if the trigger function wants the
triggering action to succeed normally without altering the row value,
NEW (or a value equal thereto) has to be returned. To alter the row to
be stored, it is possible to replace single values directly in NEW and
return the modified NEW, or to build a complete new record/row to
return. 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.
INSTEAD OF triggers (which are always row-level triggers, and may only
be used on views) can return null to signal that they did not perform
any updates, and that the rest of the operation for this row should be
skipped (i.e., subsequent triggers are not fired, and the row is not
counted in the rows-affected status for the surrounding
INSERT/UPDATE/DELETE). Otherwise a nonnull value should be returned,
to signal that the trigger performed the requested operation. For
INSERT and UPDATE operations, the return value should be NEW, which
the trigger function may modify to support INSERT RETURNING and UPDATE
RETURNING (this will also affect the row value passed to any
subsequent triggers, or passed to a special EXCLUDED alias reference
within an INSERT statement with an ON CONFLICT DO UPDATE clause). For
DELETE operations, the return value should be OLD.
The return value of a row-level trigger fired AFTER or a
statement-level trigger fired BEFORE or AFTER is always ignored; it
might as well be null. However, any of these types of triggers might
still abort the entire operation by raising an error.

How can i get my table to populate using a trigger function?

i have a function:
CREATE OR REPLACE FUNCTION delete_student()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF (TG_OP = 'DELETE')
THEN
INSERT INTO cancel(eno, excode,sno,cdate,cuser)
VALUES ((SELECT entry.eno FROM entry
JOIN student ON (entry.sno = student.sno)
WHERE entry.sno = OLD.sno),(SELECT entry.excode FROM entry
JOIN student ON (entry.sno = student.sno)
WHERE entry.sno = OLD.sno),
OLD.sno,current_timestamp,current_user);
END IF;
RETURN OLD;
END; $BODY$ LANGUAGE plpgsql;
and i also have the trigger:
CREATE TRIGGER delete_student
BEFORE DELETE
on student
FOR EACH ROW
EXECUTE PROCEDURE delete_student();
the idea is when i delete a student from the student relation then the entry in the entry relation also delete and my cancel relation updates.
this is what i put into my student relation:
INSERT INTO
student(sno, sname, semail) VALUES (1, 'a. adedeji', 'ayooladedeji#live.com');
and this is what i put into my entry relation:
INSERT INTO
entry(excode, sno, egrade) VALUES (1, 1, 98.56);
when i execute the command
DELETE FROM student WHERE sno = 1;
it deletes the student and also the corresponding entry and the query returns with no errors however when i run a select on my cancel table the table shows up empty?
You do not show how the corresponding entry is deleted. If the entry is deleted before the student record then that causes the problem because then the INSERT in the trigger will fail because the SELECT statement will not provide any values to insert. Is the corresponding entry deleted through a CASCADING delete on student?
Also, your trigger can be much simpler:
CREATE OR REPLACE FUNCTION delete_student() RETURNS trigger AS $BODY$
BEGIN
INSERT INTO cancel(eno, excode, sno, cdate, cuser)
VALUES (SELECT eno, excode, sno, current_timestamp, current_user
FROM entry
WHERE sno = OLD.sno);
RETURN OLD;
END; $BODY$ LANGUAGE plpgsql;
First of all, the function only fires on a DELETE trigger, so you do not have to test for TG_OP. Secondly, in the INSERT statement you never access any data from the student relation so there is no need to JOIN to that relation; the sno does come from the student relation, but through the OLD implicit parameter.
You didn't post your DB schema and it's not very clear what your problem is, but it looks like a cascade delete is interfering somewhere. Specifically:
Before deleting the student, you insert something into cancel that references it.
Postgres proceeds to delete the row in student.
Postgres proceeds to honors all applicable cascade deletes.
Postgres deletes rows in entry and ... cancel (including the one you just inserted).
A few remarks:
Firstly, and as a rule of thumb, before triggers should never have side-effects on anything but the row itself. Inserting row in a before delete trigger is a big no no: besides introducing potential problems related such as Postgres reporting an incorrect FOUND value or incorrect row counts upon completing the query, consider the case where a separate before trigger cancels the delete altogether by returning NULL. As such, your trigger function should be running on an after trigger -- only at that point can you be sure that the row is indeed deleted.
Secondly, you don't need these inefficient, redundant, and ugly-as-sin sub-select statements. Use the insert ... select ... variety of inserts instead:
INSERT INTO cancel(eno, excode,sno,cdate,cuser)
SELECT entry.eno entry.excode, OLD.sno, current_timestamp, current_user
FROM entry
WHERE entry.sno = OLD.sno;
Thirdly, your trigger should probably be running on the entry table, like so:
INSERT INTO cancel(eno, excode,sno,cdate,cuser)
SELECT OLD.eno OLD.excode, OLD.sno, current_timestamp, current_user;
Lastly, there might be a few problems in your schema. If there is a unique row in entry for each row in student, and you need information in entry to make your trigger work in order to fill in cancel, it probably means the two tables (student and entry) ought to be merged. Whether you merge them or not, you might also need to remove (or manually manage) some cascade deletes where applicable, in order to enforce the business logic in the order you need it to run.

Create Alias for PostgreSQL Table

I have a table called assignments. I would like to be able to read/write to all the columns in this table using either assignments.column or homework.column, how can I do this?
I know this is not something you would normally do. I need to be able to do this to provide backwards compatibility for a short period of time.
We have an iOS app that currently does direct postgresql queries against the DB. We're updating all of our apps to use an API. In the process of building the API the developer decided to change the name of the tables because we (foolishly) thought we didn't need backwards compatibility.
Now, V1.0 and the API both need to be able to write to this table so I don't have to do some voodoo later to transfer/combine data later...
We're using Ruby on Rails for the API.
With Postgres 9.3 the following should be enough:
CREATE VIEW homework AS SELECT * FROM assignments;
It works because simple views are automatically updatable (see docs).
In Postgres 9.3 or later, a simple VIEW is "updatable" automatically. The manual:
Simple views are automatically updatable: the system will allow
INSERT, UPDATE and DELETE statements to be used on the view in
the same way as on a regular table. A view is automatically updatable
if it satisfies all of the following conditions:
The view must have exactly one entry in its FROM list, which must be a table or another updatable view.
The view definition must not contain WITH, DISTINCT, GROUP BY, HAVING, LIMIT, or OFFSET clauses at the top level.
The view definition must not contain set operations (UNION, INTERSECT or EXCEPT) at the top level.
The view's select list must not contain any aggregates, window functions or set-returning functions.
If one of these conditions is not met (or for the now outdated Postgres 9.2 or older), a manual setup may do the job.
Building on your work in progress:
Trigger function
CREATE OR REPLACE FUNCTION trg_ia_insupdel()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
DECLARE
_tbl CONSTANT regclass := 'iassignments_assignments';
_cols text;
_vals text;
BEGIN
CASE TG_OP
WHEN 'INSERT' THEN
INSERT INTO iassignments_assignments
VALUES (NEW.*);
RETURN NEW;
WHEN 'UPDATE' THEN
SELECT INTO _cols, _vals
string_agg(quote_ident(attname), ', ') -- incl. pk col!
, string_agg('n.' || quote_ident(attname), ', ')
FROM pg_attribute
WHERE attrelid = _tbl -- _tbl converted to oid automatically
AND attnum > 0 -- no system columns
AND NOT attisdropped; -- no dropped (dead) columns
EXECUTE format('
UPDATE %s t
SET (%s) = (%s)
FROM (SELECT ($1).*) n
WHERE t.published_assignment_id
= ($2).published_assignment_id' -- match to OLD value of pk
, _tbl, _cols, _vals) -- _tbl converted to text automatically
USING NEW, OLD;
RETURN NEW;
WHEN 'DELETE' THEN
DELETE FROM iassignments_assignments
WHERE published_assignment_id = OLD.published_assignment_id;
RETURN OLD;
END CASE;
RETURN NULL; -- control should never reach this
END
$func$;
Trigger
CREATE TRIGGER insupbef
INSTEAD OF INSERT OR UPDATE OR DELETE ON assignments_published
FOR EACH ROW EXECUTE PROCEDURE trg_ia_insupdel();
Notes
assignments_published must be a VIEW, an INSTEAD OF trigger is only allowed for views.
Dynamic SQL (in the UPDATE section) is not strictly necessary, only to cover future changes to the table layout automatically. The names of table and PK are still hard coded.
Simpler and probably cheaper without sub-block (like you had).
Using (SELECT ($1).*) instead of the shorter VALUES ($1.*) to preserve column names.
My naming convention: I prepend trg_ for trigger functions, followed by an abbreviation indicating the target table and finally one or more of the the tokens ins, up and del for INSERT, UPDATE and DELETE respectively. The name of the trigger is a copy of the function name, stripped of the first two parts. This is purely a matter of convention and taste but has proven useful for me since the names tell the purpose and are still short.
More explanation in the related answer that has already been mentioned:
Update multiple columns in a trigger function in plpgsql
This is where I am with the trigger functions so far, any feedback would be greatly appreciated. It's a combination of http://vibhorkumar.wordpress.com/2011/10/28/instead-of-trigger/ and Update multiple columns in a trigger function in plpgsql
Table: iassignments_assignments
Columns:
published_assignment_id
name
filepath
filename
link
teacher
due date
description
published
classrooms
View: assignments_published - SELECT * FROM iassignments_assignments
Trigger Function for assignments_published
CREATE OR REPLACE FUNCTION assignments_published_trigger_func()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $function$
BEGIN
IF TG_OP = 'INSERT' THEN
EXECUTE format('INSERT INTO %s SELECT ($1).*', 'iassignments_assignments')
USING NEW;
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
DECLARE
tbl = 'iassignments_assignments';
cols text;
vals text;
BEGIN
SELECT INTO cols, vals
string_agg(quote_ident(attname), ', ')
,string_agg('x.' || quote_ident(attname), ', ')
FROM pg_attribute
WHERE attrelid = tbl
AND NOT attisdropped -- no dropped (dead) columns
AND attnum > 0; -- no system columns
EXECUTE format('
UPDATE %s t
SET (%s) = (%s)
FROM (SELECT ($1).*) x
WHERE t.published_assignment_id = ($2).published_assignment_id'
, tbl, cols, vals)
USING NEW, OLD;
RETURN NEW;
END
ELSIF TG_OP = 'DELETE' THEN
DELETE FROM iassignments_assignments WHERE published_assignment_id=OLD.published_assignment_id;
RETURN NULL;
END IF;
RETURN NEW;
END;
$function$;
Trigger
CREATE TRIGGER assignments_published_trigger
INSTEAD OF INSERT OR UPDATE OR DELETE ON
assignments_published FOR EACH ROW EXECUTE PROCEDURE assignments_published_trigger_func();
Table: iassignments_classes
Columns:
class_assignment_id
guid
assignment_published_id
View: assignments_class - SELECT * FROM assignments_classes
Trigger Function for assignments_class
**I'll create this function once I have received feedback on the other and know it's create, so I (hopefully) need very little changes to this function.
Trigger
CREATE TRIGGER assignments_class_trigger
INSTEAD OF INSERT OR UPDATE OR DELETE ON
assignments_class FOR EACH ROW EXECUTE PROCEDURE assignments_class_trigger_func();