Update another table through a trigger where new value is the result of a SELECT query - sql

I have these tables:
Users
Skills (name - string, count - integer)
Has_skills (skill_id - skills.id, user_id users.id)
Has_skills is a many to many table between the first two through these FK:
user_id (users.id) and skill_id (skills.id).
What I want to do is update the count column inside skills when a new row is inserted into has_skills. I want to do this through an update trigger on table has_skills. The new value for count I will get through a select query:
SELECT COUNT(*) AS cnt FROM skills
JOIN has_skills hs ON skills.id = hs.skill_id
WHERE hs.skill_id = 1;
The ID above is hardcoded (1), but it works.
I also tested this code in isolation, and it works (although hardcoded, as well):
UPDATE skills
SET count = subquery.cnt
FROM (
SELECT COUNT(*) AS cnt FROM skills
JOIN has_skills hs ON skills.id = hs.skill_id
WHERE hs.skill_id = 1
) AS subquery
WHERE skills.id = 1;
RETURN NEW;
Alright, so here's probably where the problem is. Below is the trigger function and also the trigger itself.
Function:
CREATE OR REPLACE FUNCTION update_skill_count() RETURNS trigger AS
$func$
BEGIN
UPDATE skills
SET count = subquery.cnt
FROM (
SELECT COUNT(*) AS cnt FROM skills
JOIN has_skills hs ON skills.id = hs.skill_id
WHERE hs.skill_id = NEW.skill_id
) AS subquery
WHERE skills.id = NEW.skill_id;
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER on_has_skills_insert
AFTER INSERT ON has_skills
FOR EACH ROW
EXECUTE PROCEDURE update_skill_count();
I successfully create the function and trigger, but when I insert new data into has_skills, it doesn't change the count column inside skills. What could be the problem?

There's no need for a select in the trigger function at all. The key for the skill table is directly available in new.skill_id so just use it directly:
-- trigger function and trigger
create or replace function update_skill_count()
returns trigger
as $func$
begin
update skills sk
set count = count+1
where sk.skill_id = new.skill_id;
return new;
end;
$func$ language plpgsql;
create trigger on_has_skills_insert
after insert on has_skills
for each row
execute procedure update_skill_count();

I'm not much familiar with postgresql, but having understanding of Oracle and SQL Server, this looks to be a mutating trigger problem, which is: Trying to read from or write into the same table within a row level trigger on which the trigger is placed.
One of the ways to get rid of mutating trigger/table problem can be changing the row level trigger to a statement level trigger and changing the function accordingly. Here is a psudo code you can try out (not providing the exact tested code as I do not have Postgresql installed):
Function:
CREATE OR REPLACE FUNCTION update_skill_count() RETURNS trigger AS
$func$
BEGIN
UPDATE skills
SET count = subquery.cnt
FROM (
SELECT hs.skill_id, COUNT(*) AS cnt
FROM new_table hs
GROUP BY hs.skill_id
) AS subquery
WHERE skills.id = subquery.skill_id;
RETURN NULL;
END;
$func$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER on_has_skills_insert
AFTER INSERT ON has_skills
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE PROCEDURE update_skill_count();

Related

Dynamic query that uses CTE gets "syntax error at end of input"

I have a table that looks like this:
CREATE TABLE label (
hid UUID PRIMARY KEY DEFAULT UUID_GENERATE_V4(),
name TEXT NOT NULL UNIQUE
);
I want to create a function that takes a list of names and inserts multiple rows into the table, ignoring duplicate names, and returns an array of the IDs generated for the rows it inserted.
This works:
CREATE OR REPLACE FUNCTION insert_label(nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
WITH new_names AS (
INSERT INTO label(name)
SELECT tn.name
FROM tmp_names tn
WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name)
RETURNING hid
)
SELECT ARRAY_AGG(hid) INTO ids
FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
I have many tables with the exact same columns as the label table, so I would like to have a function that can insert into any of them. I'd like to create a dynamic query to do that. I tried that, but this does not work:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('WITH new_names AS ( INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid)', h_tbl);
EXECUTE query_str;
SELECT ARRAY_AGG(hid) INTO ids FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
This is the output I get when I run that function:
psql=# select insert_label('label', array['how', 'now', 'brown', 'cow']);
ERROR: syntax error at end of input
LINE 1: ...SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
^
QUERY: WITH new_names AS ( INSERT INTO label(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
CONTEXT: PL/pgSQL function insert_label(regclass,text[]) line 19 at EXECUTE
The query generated by the dynamic SQL looks like it should be exactly the same as the query from static SQL.
I got the function to work by changing the return value from an array of UUIDs to a table of UUIDs and not using CTE:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS TABLE (hid UUID)
AS $$
DECLARE
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid', h_tbl);
RETURN QUERY EXECUTE query_str;
DROP TABLE tmp_names;
RETURN;
END;
$$ LANGUAGE PLPGSQL;
I don't know if one way is better than the other, returning an array of UUIDs or a table of UUIDs, but at least I got it to work one of those ways. Plus, possibly not using a CTE is more efficient, so it may be better to stick with the version that returns a table of UUIDs.
What I would like to know is why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
If anyone can let me know what I did wrong, I would appreciate it.
... why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
No, it was only the CTE without (required) outer query. (You had SELECT ARRAY_AGG(hid) INTO ids FROM new_names in the static version.)
There are more problems, but just use this query instead:
INSERT INTO label(name)
SELECT unnest(nms)
ON CONFLICT DO NOTHING
RETURNING hid;
label.name is defined UNIQUE NOT NULL, so this simple UPSERT can replace your function insert_label() completely.
It's much simpler and faster. It also defends against possible duplicates from within your input array that you didn't cover, yet. And it's safe under concurrent write load - as opposed to your original, which might run into race conditions. Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
I would just use the simple query and replace the table name.
But if you still want a dynamic function:
CREATE OR REPLACE FUNCTION insert_label(_tbl regclass, _nms text[])
RETURNS TABLE (hid uuid)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE format(
$$
INSERT INTO %s(name)
SELECT unnest($1)
ON CONFLICT DO NOTHING
RETURNING hid
$$, _tbl)
USING _nms;
END
$func$;
If you don't need an array as result, stick with the set (RETURNS TABLE ...). Simpler.
Pass values (_nms) to EXECUTE in a USING clause.
The tablename (_tbl) is type regclass, so the format specifier %I for format() would be wrong. Use %s instead. See:
Table name as a PostgreSQL function parameter

Trigger for conditional insert into table

I have subscription data that is being appended to a table in real-time (via kafka). i have set up a trigger such that once the data is added it is checked for consistency. If checks pass some of the data should be added to other tables (that have master information on the customer profile etc.). The checks function i wrote works fine but i keep getting errors on the function used in the trigger. The function for the trigger is:
CREATE OR REPLACE FUNCTION update_tables()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
BEGIN
CASE (SELECT check_incoming_data()) WHEN 0
THEN INSERT INTO
sub_master(sub_id, sub_date, customer_id, product_id)
VALUES(
(SELECT sub_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT sub_date::date FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT customer_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime)),
(SELECT product_id::int FROM sub_realtime WHERE CTID = (SELECT MAX(CTID) FROM sub_realtime))
);
RETURN sub_master;
END CASE;
RETURN sub_master;
END;
$$
The trigger is then:
CREATE TRIGGER incoming_data
AFTER INSERT
ON claims_realtime_3
FOR EACH ROW
EXECUTE PROCEDURE update_tables();
What I am saying is 'if checks pass then select data from the last added row and add them to the master table'. What is the best way to structure this query?
Thanks a lot!
The trigger functions are executed for each row and you must use a record type variable called "NEW" which is automatically created by the database in the trigger functions. "NEW" gets only inserted records. For example, I want to insert data to users_log table when inserting records to users table.
CREATE OR REPLACE FUNCTION users_insert()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
insert into users_log
(
username,
first_name,
last_name
)
select
new.username,
new.first_name,
new.last_name;
return new;
END;
$function$;
create trigger store_data_to_history_insert
before insert
on users for each row execute function users_insert();

Using a procedure to update Column 2 in a Table A, by checking if column 1 value is present in a column in Table B [PostgreSQL]

I have two tables as follows:
Table A: Name, ID, value
Table B: ID, title
TableA.value is dependent on whether TableA.ID is present in TableB.ID.
I am trying to create a procedure and trigger so that whenever TableB is modified, the procedure is triggered to check if TableA.ID is in TableB.ID and sets TableA.value to 10.
I am using the following code and getting an error:
CREATE PROCEDURE update1
LANGUAGE SQL
AS $$
UPDATE tablA as a
SET a.value = 10
WHERE a.ID EXISTS ( SELECT b.ID
FROM tableB as b
WHERE a.ID = b.ID)
$$;
I am an SQL noob, and this is the first time I am trying to use a procedure.
UPDATE
I was able to create a procedure which does the job when I manually run it using CALL
However, it does not have a RETURNS TRIGGER block within it. Adding that returns the error Procedure cannot return triggers.
If I create a trigger as follows
CREATE TRIGGER b_trigger
AFTER UPDATE OR INSERT ON b.ID
FOR EACH STATEMENT
EXECUTE PROCEDURE update1();
However, this returns the following
ERROR: function columbia_deli.manager_discount must return type trigger
SQL state: 42P17
Try this using plpgsql Language:
Trigger Function:
CREATE OR REPLACE FUNCTION update1()
RETURNS trigger AS
$BODY$
begin
update "TableA" set value= 10 where id =new.id;
return NEW;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Then create trigger on TableB for after update event
CREATE TRIGGER trig_tableb
AFTER UPDATE
ON "TableB"
FOR EACH ROW
EXECUTE PROCEDURE update1();

Update error in postgres SQL

CREATE TRIGGER INSERT_salesorderdetail
before insert on salesorderdetail
for each row
UPDATE customer
set number_of_items=IFNULL(number_of_items,0)+1
where new.customerid=customer.customerid;
I have 2 tables salesorderdetail and customer and i want each time i inserts a new item in salesorderdetail to update my columne in customer the number_of_items but for some reason i get a syntax error in update.
This might help you:
CREATE OR REPLACE FUNCTION update_customer()
RETURNS trigger AS
$BODY$
-- update ur table here
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION update_customer()
OWNER TO db_name;
Now create trigger as:
CREATE TRIGGER INSERT_salesorderdetail
before insert on salesorderdetail
for each row execute procedure update_customer();

Triggers on Views in PostgreSQL

I want to create a trigger for my view in PostgreSQL. The idea is that all new data must fulfill a condition to be inserted. But something is wrong here and I can't find the answer in manuals.
CREATE OR REPLACE VIEW My_View AS
SELECT name, adress, count FROM club, band, country;
CREATE OR REPLACE FUNCTION insert() RETURNS TRIGGER AS $$
BEGIN
IF(NEW.count > 10) THEN
INSERT INTO My_View VALUES (NEW.name, NEW.adress, NEW.count);
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER insert INSTEAD OF INSERT ON My_View
FOR EACH ROW EXECUTE PROCEDURE insert();
There is a semicolon ( ; ) missing at the end of the INSERT statement in the function.
insert is a reserved word in the SQL standard and should not be used as trigger or function name. Even if it's allowed in PostgreSQL, it's a very bad idea.
There are no join conditions for the three tables club, band, country in your view definition. This leads to a CROSS JOIN, which can be extremely expensive. If there are 1000 rows in each table, you get 1,000,000,000 combinations. You most definitely do not want that.
Also, you should table-qualify the columns in your view definition to avoid ambiguities.
CREATE OR REPLACE VIEW my_view AS
SELECT ??.name, ??.address, ??.mycount
FROM club cl
JOIN band ba ON ?? = ??
JOIN country co ON ?? = ??;
You need to fill in where I left question marks.
And always add a column definition list to your INSERT statements.
And finally, you do not want to INSERT into the same view again, which would create an endless loop and may be the primary cause of your error.
CREATE OR REPLACE FUNCTION f_insert()
RETURNS TRIGGER AS
$func$
BEGIN
IF NEW.mycount > 10 THEN
INSERT INTO my_view ???? (col1?, col2?, col3?)
VALUES (NEW.name, NEW.address, NEW.mycount);
END IF;
END
$func$ LANGUAGE plpgsql;
BTW, you shouldn't use count as identifier either. I use mycountinstead.