Create trigger to automatically update column with subquery - sql

In my application, I have a couple of tables, lessons and votes. Here's what they look like:
Lessons
+-------------+---------+----------------------+
| Column | Type | Modifiers |
|-------------+---------+----------------------|
| id | uuid | not null |
| votes_total | integer | not null default 0 |
+-------------+---------+----------------------+
Votes
+-------------+---------+-------------+
| Column | Type | Modifiers |
|-------------+---------+-------------|
| positive | boolean | not null |
| user_id | uuid | not null |
| lesson_id | uuid | not null |
+-------------+---------+-------------+
Whenever a row in votes is inserted, updated or deleted, I'd like to update the votes_total column of the related lesson using a subquery. Here's what I've tried:
CREATE PROCEDURE update_lesson_votes_total()
LANGUAGE SQL
AS $$
UPDATE lessons
SET votes_total = (
SELECT SUM(CASE WHEN positive THEN 1 ELSE -1 END)
FROM votes
WHERE lesson_id = NEW.lesson_id
)
WHERE id = NEW.lesson_id;
$$;
CREATE TRIGGER votes_change
AFTER INSERT OR UPDATE OR DELETE ON votes
FOR EACH ROW
EXECUTE PROCEDURE update_lesson_votes_total();
However, when I try to run this in a migration, I get the following error:
(Postgrex.Error) ERROR 42601 (syntax_error) cannot insert multiple commands into a prepared statement

Hi your function must return a trigger. You can't use new in a trigger on delete. You have to create at least 2 triggers calling the same function and use TG_OP to know if the function is triggered by insert, update or delete.
Then in a case or if statement you can use new or old to get the id's value.
CREATE FUNCTION update_lesson_votes_total()
returns trigger
LANGUAGE plpgsql
AS $$
begin
UPDATE lessons
SET votes_total = (
SELECT SUM(CASE WHEN positive THEN 1 ELSE -1 END)
FROM votes
WHERE lesson_id = NEW.lesson_id
)
WHERE id = NEW.lesson_id;
return null;
end;
$$;
CREATE TRIGGER votes_change_u_i
AFTER INSERT OR UPDATE ON votes
FOR EACH ROW
EXECUTE PROCEDURE update_lesson_votes_total();
CREATE TRIGGER votes_change_d
AFTER DELETE ON votes
FOR EACH ROW
EXECUTE PROCEDURE update_lesson_votes_total();

Related

How to use a new serial ID for each new batch of inserted rows?

Is it possible to use a sequence for a batch of rows, versus getting a new ID on each insert? I'm keeping track of a set of details, and I want the sequence to apply for the set, not each individual row. So my data should look like so:
id batch_id name dept
1 99 John Engineering
2 99 Amy Humanities
3 99 Bill Science
4 99 Jack English
It's the batch_id that I want Postgres to issue as a sequence. Is this possible?
Define batch_id as batch_id bigint not null default currval('seqname') and call nextval('seqname') manually before inserting batch of rows.
Or, for the full automation:
1) Create sequence for the batch id:
create sequence mytable_batch_id;
2) Create your table, declare batch id field as below:
create table mytable (
id bigserial not null primary key,
batch_id bigint not null default currval('mytable_batch_id'),
name text not null);
3) Create statement level trigger to increment the batch id sequence:
create function tgf_mytable_batch_id() returns trigger language plpgsql
as $$
begin
perform nextval('mytable_batch_id');
return null;
end $$;
create trigger tg_mytablebatch_id
before insert on mytable
for each statement execute procedure tgf_mytable_batch_id();
Now each single statement when you inserting data into the table will be interpreted as next single batch.
Example:
postgres=# insert into mytable (name) values('John'), ('Amy'), ('Bill');
INSERT 0 3
postgres=# insert into mytable (name) values('Jack');
INSERT 0 1
postgres=# insert into mytable (name) values('Jimmmy'), ('Abigail');
INSERT 0 2
postgres=# table mytable;
id | batch_id | name
----+----------+-------------
1 | 1 | John
2 | 1 | Amy
3 | 1 | Bill
4 | 2 | Jack
5 | 3 | Jimmy
6 | 3 | Abigail
(6 rows)

How to AUTOMATICALLY update a value of a column based on condition on another table's column?

So basically, I'm using Postgresql and what I want to do is this:
Say, we have 2 tables, the inventory and buyList
create table inventory
(item_id serial primary key,
name text not null,
quantity int not null,
price int not null);
insert into inventory values
(1,'a',44,10000),
(2,'b',12,12000),
(3,'c',11,5000),
(4,'d',6,3000);
create table buyList
(buy_id serial primary key,
item_id not null references inventory(item_id),
quantity int not null);
insert into buyList values
(1,2,4),
(2,2,5),
(3,1,1);
so I want to have the inventory.quantity value to be subtracted by the buyList.quantity of relevant item (based of item_id ofcourse)
for example, when there is someone who buy 4 of item 'a', then the value of item 'a' quantity column in table inventory will be 40 (automatically updated).
EDIT :
THANKS A LOT to krithikaGopalakrisnan for the answer,
so I use the trigger made by krithikaGopalakrisnan (and modified it a little)
CREATE OR REPLACE FUNCTION trigger() RETURNS trigger AS $$
BEGIN
UPDATE inventory SET quantity = quantity-NEW.quantity WHERE inventory.item_id = NEW.item_id ;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
BEGIN
EXECUTE format('CREATE TRIGGER trigger BEFORE INSERT OR UPDATE ON buylist FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE trigger()');
END;
$$ LANGUAGE plpgsql;
But now a new problem arises, when the quantity of the item in inventory table (inventory.quantity) is 0, and there is a new purchase of that item in the buylist table, the inventory.quantity of that item becomes a negative number! (of course we can't have that), how do I fix this so that when the item quantity is 0 in the inventory table, the buylist table can't accept another tuple indicating someone buying that item (maybe a function to return error message or something)
thanks in advance, I am still a total novice so I will really appreciate any help and guidance from you guys.
A trigger is what u need..
CREATE FUNCTION trigger() RETURNS trigger AS $$
BEGIN
UPDATE inventory SET quantity = NEW.quantity WHERE inventory.item_id = NEW.item_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
BEGIN
EXECUTE format('CREATE TRIGGER trigger BEFORE INSERT OR UPDATE ON buylist FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE trigger()');
END;
$$ LANGUAGE plpgsql;
Sample data:
postgres=# select * from inventory;
item_id | name | quantity | price
---------+------+----------+-------
1 | a | 44 | 10000
2 | b | 12 | 12000
3 | c | 11 | 5000
4 | d | 6 | 3000
(4 rows)
postgres=# select * from buylist;
buy_id | item_id | quantity
--------+---------+----------
1 | 2 | 4
2 | 2 | 5
3 | 1 | 1
(3 rows)
postgres=# update buylist set quantity=4 where item_id=1;
postgres=# select * from inventory;
item_id | name | quantity | price
---------+------+----------+-------
2 | b | 12 | 12000
3 | c | 11 | 5000
4 | d | 6 | 3000
1 | a | 40 | 10000
Hope it helps

Creating a trigger to copy data from 1 table to another

I am trying to create a trigger that will copy data from table 1 when and paste it into table 2, when a new entry has been put into table 1:
Table 1
id | first_name | last_name | email | uid | pwd
----+------------+-----------+-------+-----+-----
Table 2
user_id | user_first_name | user_last_name | user_uid
---------+-----------------+----------------+---------
the code i am using is this :
DROP TRIGGER IF EXISTS usersetup_identifier ON users;
CREATE OR REPLACE FUNCTION usersetup_identifier_insert_update() RETURNS trigger AS $BODY$
BEGIN
if NEW.identifier is null then
NEW.identifier := "INSERT INTO users_setup (user_id, user_first_name, user_last_name, user_uid)
SELECT id, first_name, last_name, uid
FROM users";
end if;
RETURN NEW;
end
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER usersetup_identifier
AFTER INSERT OR UPDATE ON users FOR EACH ROW
EXECUTE PROCEDURE usersetup_identifier_insert_update();
but when i insert data into table 1 i am getting this error message :
NOTICE: identifier "INSERT INTO users_setup (user_id, user_first_name, user_last_name, user_uid)
SELECT id, first_name, last_name, uid
FROM users" will be truncated to "INSERT INTO users_setup (user_id, user_first_name, user_last_na"
ERROR: record "new" has no field "identifier"
CONTEXT: SQL statement "SELECT NEW.identifier is null"
PL/pgSQL function usersetup_identifier_insert_update() line 3 at IF
the table descriptions are:
Table "public.users"
Column | Type | Collation | Nullable | Default
------------+---------------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('users_id_seq'::regclass)
first_name | character(20) | | not null |
last_name | character(20) | | not null |
email | character(60) | | not null |
uid | character(20) | | not null |
pwd | character(20) | | not null |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"users_email_key" UNIQUE CONSTRAINT, btree (email)
"users_pwd_key" UNIQUE CONSTRAINT, btree (pwd)
"users_uid_key" UNIQUE CONSTRAINT, btree (uid)
Triggers:
usersetup_identifier AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE usersetup_identifier_insert_update()
All the columns match there corresponding columns
can any one help and tell me where i am going wrong?
Table "public.users_setup"
Column | Type | Collation | Nullable | Default
-----------------+------------------------+-----------+----------+-----------------------------------------
id | integer | | not null | nextval('users_setup_id_seq'::regclass)
user_id | integer | | |
user_first_name | character(20) | | |
user_last_name | character(20) | | |
user_uid | character(20) | | |
Can any one help me with where I am going wrong?
There are multiple errors in your code
the table users has a column named identifier so the expression NEW.identifier is invalid
You are assigning a value to a (non-existing) column with the expression new.identifier := ... - but you want to run an INSERT statement, not assign a value.
String values need to be enclosed in single quotes, e.g. 'Arthur', double quotes denote identifiers (e.g. a table or column name). But there is no column named "INSERT INTO use ..."
To access the values of the row being inserted you need to use the new record and the column names. No need to select from the table:
As far as I can tell, this is what you want:
CREATE OR REPLACE FUNCTION usersetup_identifier_insert_update()
RETURNS trigger
AS
$BODY$
BEGIN
INSERT INTO users_setup (user_id, user_first_name, user_last_name, user_uid)
values (new.id, new.first_name, new.last_name, new.uid);
RETURN NEW;
end
$BODY$
LANGUAGE plpgsql;
Unrelated, but:
copying data around like that is bad database design. What happens if you change the user's name? Then you would need to UPDATE the user_setup table as well. It is better to only store a (foreign key) reference in the user_setup table that references the users table.

Change values in the same table before or after update trigger (oracle)

I have table with 3 fields:
--------------------------------------------------
| ID | FILE | STATUS |
--------------------------------------------------
| 1 | my.exe | valid |
--------------------------------------------------
| 2 | my.exe | invalid |
--------------------------------------------------
| 3 | my.exe | invalid |
--------------------------------------------------
| 4 | my.exe | invalid |
--------------------------------------------------
This are some versions of one file. When I update status to "Valid" for any of them, I need all files with this name to change their status to "Invalid",
except one updated:
--------------------------------------------------
| ID | FILE | STATUS |
--------------------------------------------------
| 1 | my.exe | invalid |
--------------------------------------------------
| 2 | my.exe | invalid |
--------------------------------------------------
| 3 | my.exe | valid |
--------------------------------------------------
| 4 | my.exe | invalid |
--------------------------------------------------
I thought that it can be done in before update trigger:
create or replace trigger ChangeValid
before update
on mytable
for each row
declare
begin
update mytable t set t.status = 'ivalid' where t.status = 'valid' and t.file = :new.file;
end ChangeValid;
But I kindly get ORA-04091. Is the way to change values in this table with trigger?
This is the mutating table problem. The reason it happens is because you have written an UPDATE trigger which tries to execute an UPDATE statement on the same table. What do you think happens when you update those other rows? The trigger tries to fire, which means it executes the update statement, and so on recursively. Oracle short-circuits the nonsense by forbidding row level triggers which act on their own table and hurling ORA-04091.
You can solve this with a compound trigger:
create or replace trigger ChangeValid
for update on mytable compound trigger
type rec_nt is table of mytable%rowtype;
recs rec_nt;
before statement is
begin
recs := rec_nt();
end before statement;
before each row is
begin
null;
end before each row;
after each row is
begin
recs.extend();
recs(recs.count()).id := :new.id;
recs(recs.count()).file_name := :new.file_name;
recs(recs.count()).status := :new.status;
end after each row;
after statement is
begin
for idx in 1 .. recs.count() loop
if recs(idx).status = 'valid' then
update mytable t
set t.status = 'invalid'
where t.file_name = recs(idx).file_name
and t.status = 'valid'
and t.id != recs(idx).id;
end if;
end loop;
end after statement;
end;
/
Note that this trigger will run twice: once for the row you updated and once for all the rows updated when the trigger runs. This is why we need the IF statement around the UPDATE in the after statement section. You also need to be careful that the UPDATE doesn't trigger recursion.
So if you have lots of records for my.exe this could be an expensive way of implementing such logic. A better approach would be to have a PL/SQL API which updates the status of the current valid record and then applies the update to the target row.

Is it possible to update an "order" column from within a trigger in MySQL?

We have a table in our system that would benefit from a numeric column so we can easily grab the 1st, 2nd, 3rd records for a job. We could, of course, update this column from the application itself, but I was hoping to do it in the database.
The final method must handle cases where users insert data that belongs in the "middle" of the results, as they may receive information out of order. They may also edit or delete records, so there will be corresponding update and delete triggers.
The table:
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`seq` int(11) unsigned NOT NULL,
`job_no` varchar(20) NOT NULL,
`date` date NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=latin1
And some example data:
mysql> SELECT * FROM test ORDER BY job_no, seq;
+----+-----+--------+------------+
| id | seq | job_no | date |
+----+-----+--------+------------+
| 5 | 1 | 123 | 2009-10-05 |
| 6 | 2 | 123 | 2009-10-01 |
| 4 | 1 | 123456 | 2009-11-02 |
| 3 | 2 | 123456 | 2009-11-10 |
| 2 | 3 | 123456 | 2009-11-19 |
+----+-----+--------+------------+
I was hoping to update the "seq" column from a t rigger, but this isn't allowed by MySQL, with an error "Can't update table 'test' in stored function/trigger because it is already used by statement which invoked this stored function/trigger".
My test trigger is as follows:
CREATE TRIGGER `test_after_ins_tr` AFTER INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = 0;
UPDATE
`test` t
SET
t.`seq` = #seq := (SELECT #seq + 1)
WHERE
t.`job_no` = NEW.`job_no`
ORDER BY
t.`date`;
END;
Is there any way to achieve what I'm after other than remembering to call a function after each update to this table?
What about this?
CREATE TRIGGER `test_after_ins_tr` BEFORE INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = (SELECT COALESCE(MAX(seq),0) + 1 FROM test t WHERE t.job_no = NEW.job_no);
SET NEW.seq = #seq;
END;
From Sergi's comment above:
http://dev.mysql.com/doc/refman/5.1/en/stored-program-restrictions.html - "Within a stored function or trigger, it is not permitted to modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger."