PostgreSQL Function/Trigger assign same ID for every row per call - sql

I want to insert into a child table every time the parent table is updated. But when this happens, all of the new records inserted into the child table should have the same id. The ids will only increment if the parent table is separately updated another time. How can I do this?
In this case, I want to insert into the child table every new My_Date field from when the parent table is updated. Below is an example of what this would look like.
When parent table gains two new rows...
My_Date
old
old
new
new
Child table gains two new rows, both assigned to same ID (the ID autoincrements in table definition)
My_Date ID
...
new 4
new 4
When parent table gains two new rows again...
My_Date
old
old
old
old
new
new
Child table gains two new rows, both assigned to same new ID
My_Date ID
...
old 4
old 4
new 5
new 5
Here is what I have so far.
CREATE or replace FUNCTION update_child() RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO child
SET My_Date = NEW.My_Date /*Not sure if this is correct*/
/*Give every row the same ID*/
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_child_after_update
AFTER UPDATE
ON parent
FOR EACH ROW
EXECUTE PROCEDURE update_child();

I believe what you want is a combination of a sequence and a statement-level (as opposed to row level) trigger:
https://www.postgresql.org/docs/current/trigger-definition.html#:~:text=PostgreSQL%20offers%20both%20per%2Drow,statement%20that%20fired%20the%20trigger.
PostgreSQL offers both per-row triggers and per-statement triggers.
With a per-row trigger, the trigger function is invoked once for each
row that is affected by the statement that fired the trigger. In
contrast, a per-statement trigger is invoked only once when an
appropriate statement is executed, regardless of the number of rows
affected by that statement. In particular, a statement that affects
zero rows will still result in the execution of any applicable
per-statement triggers. These two types of triggers are sometimes
called row-level triggers and statement-level triggers, respectively.
This is really bare bones, but I think it will demonstrate the desired behavior you described in your question.
create sequence child_id;
CREATE or replace FUNCTION update_child()
RETURNS trigger AS
$BODY$
DECLARE
r1 record;
new_id int;
BEGIN
new_id := nextval ('child_id');
FOR r1 IN SELECT * FROM new_table
LOOP
insert into child
select r1.my_date, new_id;
END loop;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_child_after_update
AFTER UPDATE
ON parent
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
FOR EACH statement
EXECUTE PROCEDURE update_child();
CREATE TRIGGER update_child_after_insert
AFTER INSERT
ON parent
REFERENCING NEW TABLE AS new_table
FOR EACH statement
EXECUTE PROCEDURE update_child();
There are a lot of restrictions on statement-level triggers, and it's worth reading up on them. For example, only the "after" is supported.
Presumably your parent table also has some form of PK, which you would also be passing to the child, but I'm hopeful that's easy enough to see where you would insert that in the code example above.

Related

How to select all inserted rows to execute an insert trigger with a stored procedure in postgresql?

I'm trying to set an "after insert" trigger that executes a procedure. The procedure would take all inserted rows in table A, group them by a column and insert the result in a table B. I know about "new" variable but it gets inserted rows one by one. Is it possible to get all of them?
I think I can't use a for each row statement as I need to group rows depending on the "trackCode" variable, shared by different rows in tableA.
CREATE OR REPLACE PROCEDURE Public.my_procedure(**inserted rows in tableA?**)
LANGUAGE 'plpgsql'
AS $$
BEGIN
INSERT INTO Public."tableB" ("TrackCode", "count")
SELECT "TrackCode", count(*) as "count" FROM Public."tableA" --new inserted rows in this table
GROUP BY "vmsint"."TrackCode" ;
COMMIT;
END;
$$;
create trigger Public.my_trigger
after insert ON Public.tableA
execute procedure Public.my_procedure(**inserted rows in tableA?**)
Thank you!
You create a statement lever trigger, but do not attempt to pass parameters. Instead use the clause referencing new table as reference_table_name. In the trigger function you use the reference_table_name in place of the actual table name. Something like: (see demo)
create or replace function group_a_ais()
returns trigger
language 'plpgsql'
as $$
begin
insert into table_b(track_code, items)
select track_code, count(*)
from rows_inserted_to_a
group by track_code ;
return null;
end;
$$;
create trigger table_a_ais
after insert on table_a
referencing new table as rows_inserted_to_a
for each statement
execute function group_a_ais();
Do not attempt to commit in a trigger, it is a very bad id even if allowed. Suppose the insert to the main table is part of a larger transaction, which fails later in its process.
Be sure to refer to links provided by Adrian.

PLS-00049: bad bind variable 'NEW.REQUEST_DATETIME' Issue

I am trying to write a Trigger which basically updates one table when an entry is made on another table.
CREATE OR REPLACE TRIGGER "DTISCDB_OWNER"."REQUEST_CONTEXT_TR"
AFTER INSERT OR UPDATE ON REQUEST_CONTEXT
FOR EACH ROW
BEGIN
:NEW.REQUEST_DATETIME := SYSDATE;
:NEW.ID := TRUNC(DBMS_RANDOM.VALUE(100000000000000000000000000000000000,999999999999999999999999999999999999));
SELECT bre_conditions_seq.NEXTVAL INTO :OLD.seq_number FROM dual;
SELECT REQUEST_CONTEXT.CURRENT_STATE INTO :NEW.STATE FROM REQUEST_CONTEXT;
SELECT REQUEST_CONTEXT.REQUEST_ID INTO :NEW.REQUEST_ID FROM REQUEST_CONTEXT;
INSERT INTO REQUEST_LIFECYCLES(ID,SEQ_NUMBER,STATE,REQUEST_ID,REQUEST_DATETIME)
VALUES(:NEW.ID,:NEW.seq_number,:NEW.STATE,:NEW.REQUEST_ID,:NEW.REQUEST_DATETIME);
END;
You seem to be confused about what the correlation pseudorows are for and what they hold and can do. It looks like you're treating :NEW as if it is related to the REQUEST_LIFECYCLES table you are inserting into inside the trigger, and :OLD as if it is related to the REQUEST_CONTEXT row that has been inserted or updated and caused the trigger to fire.
Both OLD and NEW refer to the table the trigger is against, REQUEST_CONTEXT. If the trigger was fired by an update then OLD has the pre-update values for the affected row; if it was fired by an insert then it is empty as there is no old state. Either way NEW has the current state, with the newly-inserted or post-update values. You can't change the OLD values, and it doesn't make sense to change the NEW values in an 'after' trigger. You also don't need to query the table the trigger is fired against, as the NEW pseudorow already makes that information available.
So if you are trying to use the inserted/updated values in REQUEST_CONTEXT to create a row in REQUEST_LIFECYCLES, you would do something like:
CREATE OR REPLACE TRIGGER "DTISCDB_OWNER"."REQUEST_CONTEXT_TR"
AFTER INSERT OR UPDATE ON REQUEST_CONTEXT
FOR EACH ROW
BEGIN
INSERT INTO REQUEST_LIFECYCLES(ID, SEQ_NUMBER, STATE, REQUEST_ID,
REQUEST_DATETIME)
VALUES(TRUNC(DBMS_RANDOM.VALUE(100000000000000000000000000000000000, 999999999999999999999999999999999999)),
bre_conditions_seq.NEXTVAL, :NEW.CURRENT_STATE, :NEW.REQUEST_ID,SYSDATE);
END;
/
I'm assuming you wanted to set the 'lifecycle' SEQ_NUMBER value from the trigger, despite you trying to set the :OLD value - hopefully the old reference was a mistake. If you were trying to set that value in both REQUEST_CONTEXT and REQUEST_LIFECYCLES you would need a before insert/update trigger instead, and would set :NEW.SEQ_NUMBER rather than the :OLD value, before using it in the values clause.
As Justin said using a random value for the ID is rather strange, not least because it won't be unique, and a sequence is much more common. You may actually want the ID from the inserted/updated row, in which case you can just refer to :NEW.ID in the values clause instead of generating a new value. (It's also possible you are trying to set that ID in both REQUEST_CONTEXT and REQUEST_LIFECYCLES too, but that would be even stranger, and you'd need a before-insert/update trigger to do that anyway).

On postgreSQL update trigger audit, original table updates fine, but multiple rows appear in the audit table.

I have a table with a trigger applied to it, that populates an audit table if it changes. I created the original table with just two fields:
CREATE TABLE events(
code serial8 NOT NULL,
event date NOT NULL,
);
And I want a trigger to populate the audit table whenever the event date on this first table is updated:
CREATE TABLE audit(
date_log date,
time_log time,
userid char(20),
event_code int,
action_log text,
old_date date,
new_date date
);
The trigger function:
CREATE OR REPLACE FUNCTION auditing() RETURNS trigger AS $$ BEGIN
INSERT INTO audit(date_log,time_log,userid,event_code,action_log,old_date,new_date)
SELECT current_date,current_time,current_user,code,tg_op,OLD.event, NEW.event FROM events;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
And I set the trigger like this:
CREATE TRIGGER audit_dates
AFTER UPDATE OF event ON events
FOR EACH ROW EXECUTE PROCEDURE auditing();
The problem I am having is that whenever I run an update function such as:
UPDATE events SET event = '2013-03-01' WHERE code = 1;
The original table updates just fine, but the table that should audit this, generates one audit row for each row that the original table had, so if the original table has 200 rows and I update just one of them, 200 new rows are generated in the audit table.
I am trying to think whether this is because I created the trigger as FOR EACH ROW , but at the same time I have been reading the docs on this and it looks like this is the syntax I need.
What is the right way to get just one new row in the audit table for each time that I update the original table?
You have SELECT ... FROM events in the trigger, so it's selecting all events. You can just use a simple INSERT ... VALUES since NEW and OLD are variables.

Writing an SQL trigger to find if number appears in column more than X times?

I want to write a Postgres SQL trigger that will basically find if a number appears in a column 5 or more times. If it appears a 5th time, I want to throw an exception. Here is how the table looks:
create table tab(
first integer not null constraint pk_part_id primary key,
second integer constraint fk_super_part_id references bom,
price integer);
insert into tab values(1,NULL,100), (2,1,50), (3,1,30), (4,2,20), (5,2,10), (6,3,20);
Above are the original inserts into the table. My trigger will occur upon inserting more values into the table.
Basically if a number appears in the 'second' column more than 4 times after inserting into the table, I want to raise an exception. Here is my attempt at writing the trigger:
create function check() return trigger as '
begin
if(select first, second, price
from tab
where second in (
select second from tab
group by second
having count(second) > 4)
) then
raise exception ''Error, there are more than 5 parts.'';
end if;
return null;
end
'language plpgsql;
create trigger check
after insert or update on tab
for each row execute procedure check();
Could anyone help me out? If so that would be great! Thanks!
CREATE FUNCTION trg_upbef()
RETURN trigger as
$func$
BEGIN
IF (SELECT count(*)
FROM tab
WHERE second = NEW.second ) > 3 THEN
RAISE EXCEPTION 'Error: there are more than 5 parts.';
END IF;
RETURN NEW; -- must be NEW for BEFORE trigger
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER upbef
BEFORE INSERT OR UPDATE ON tab
FOR EACH ROW EXECUTE procedure trg_upbef();
Major points
Keyword is RETURNS, not RETURN.
Use the special variable NEW to refer to the newly inserted / updated row.
Use a BEFORE trigger. Better skip early in case of an exception.
Don't count everything for your test, just what you need. Much faster.
Use dollar-quoting. Makes your live easier.
Concurrency:
If you want to be absolutely sure, you'll have to take an exclusive lock on the table before counting. Else, concurrent inserts / updates might outfox each other under heavy concurrent load. While this is rather unlikely, it's possible.

Oracle trigger- instead of delete, update the row

How do I write an Oracle trigger, than when a user deletes a certain record, the delete doesnt actually happen, but instead performs an update on those rows and sets the status of the record to 'D'?
I tried:
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
UPDATE
M_ITEM_H
SET
ITEM_STAT = 'D'
WHERE
CUST_CODE = 'TEST'
AND ITEM_CODE = 'GDAY'
;
raise_application_error(-20000,'Cannot delete item');
END;
But I am getting mutating table errors. Is this possible?
If you really need a trigger, the more logical approach would be to create a view, create an INSEAD OF DELETE trigger on the view, and to force the applications to issue their deletes against the view rather than against the base table.
CREATE VIEW vw_m_item_h
AS
SELECT *
FROM m_item_h;
CREATE OR REPLACE TRIGGER t4m_item_ondelete
INSTEAD OF DELETE ON vw_m_item_h
FOR EACH ROW
AS
BEGIN
UPDATE m_item_h
SET item_stat = 'D'
WHERE <<primary key>> = :old.<<primary key>>;
END;
Better yet, you would dispense with the trigger, create a delete_item procedure that your application would call rather than issuing a DELETE and that procedure would simply update the row to set the item_stat column rather than deleting the row.
If you really, really, really want a solution that involves a trigger on the table itself, you could
Create a package with a member that is a collection of records that map to the data in the m_item_h table
Create a before delete statement-level trigger that empties this collection
Create a before delete row-level trigger that inserts the :old.<<primary key>> and all the other :old values into the collection
Create an after delete statement-level trigger that iterates through the collection, re-inserts the rows into the table, and sets the item_stat column.
This would involve more work than an instead of trigger since you'd have to delete and then re-insert the row and it would involve way more moving pieces so it would be much less elegant. But it would work.
First of all the trigger you wrote would throw a mutating table error. Technically what you are asking is not possible i.e. delete wouldn't delete but rather update, unless you raise an exception in the middle which could be an ugly way of doing it. I would think users using some sort of application front end which lets them delete data using a delete button, so you may use an update statement there instead of a delete statement.
Another option would be to create a log table, where you could insert the record before deleting it from the actual table and then join the log table with the actual table to retrieve deleted records. Something like-
CRETAE TABLE M_ITEM_H_DEL_LOG as SELECT * FROM M_ITEM_H WHERE 1=2;
And then
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
INSERT INTO
M_ITEM_H_DEL_LOG
VALUES (:old.col1, :old.col2,.....) --col1, col2...are columns in M_ITEM_H
;
END;