Triggers on Views in PostgreSQL - sql

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.

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

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.

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();

PostgreSQL: Trigger Function bypasses trigger of another table

There are two tables: gc_res and anchor. Anchor table has a unique key - anchor_id.
When Data arrives in gc_res the res_eval trigger is fired and the data is partially evaluated by gc_res_eval().
Some of the entries remain, others are pushed with an insert into anchor table. The anchor table has a trigger before insert, which also pre-evaluates the data and either accepts the insert or updates an old entry. In latter case it might additionally write to a third (log) table.
Both triggers look the same:
CREATE TRIGGER res_eval BEFORE INSERT ON gc_res
FOR EACH ROW EXECUTE PROCEDURE gc_res_eval();
CREATE TRIGGER anchor_smart_insert BEFORE INSERT ON anchor
FOR EACH ROW EXECUTE PROCEDURE anchor_smart_insert();
When I directly try to write to anchor table, the anchor_smart_insert trigger fires properly and works fine.
The gc_res_eval part which tries to insert the data works with a CTE:
WITH anchor_insert AS (
INSERT INTO cbm.anchor (anchor_id, pin, geo, rwo, rwo_value, addr) (
SELECT osm_id, pin, geo, rwo, rwo_value, addr FROM gc_eval
WHERE similarity > accept_polygon AND geo_type = 'Polygon'
) RETURNING anchor_id
) SELECT array_agg(anchor_id) FROM anchor_insert INTO anchors
;
When gc_res_eval tries to insert into the anchor table and there are no conflicts on anchor_id - everything works fine. If there is a conflict, the insert fails on the unique constraint. Thus I assume - that the second trigger does not fire.
CREATE OR REPLACE FUNCTION anchor_smart_insert()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
geog geography;
acceptable_intersect int := 10;
acceptable_overlap_index NUMERIC := 0.56;
BEGIN
SELECT INTO geog geo FROM cbm.anchor a
WHERE a.anchor_id=NEW.anchor_id;
RAISE NOTICE 'anchor_smart_insert is running';
IF ST_Area(ST_Intersection(geog, NEW.geo))>acceptable_intersect THEN
UPDATE cbm.anchor a SET
pin=COALESCE(NEW.pin, ST_Centroid(NEW.geo)),
geo=NEW.geo,
rwo=NEW.rwo,
rwo_value=NEW.rwo_value,
addr=NEW.addr
WHERE a.anchor_id=NEW.anchor_id AND (
a.geo IS DISTINCT FROM NEW.geo
OR a.rwo IS DISTINCT FROM NEW.rwo
OR a.rwo_value IS DISTINCT FROM NEW.rwo_value
OR a.addr IS DISTINCT FROM NEW.addr);
INSERT INTO cbm.anchor_history (anchor_id, geo) VALUES (NEW.anchor_id, geog);
IF ST_Area(ST_Intersection(geog, NEW.geo))^2/(ST_Area(geog)*ST_Area(NEW.geo))<acceptable_overlap_index THEN
INSERT INTO cbm.anchor_review(anchor_id, geo, rwo, rwo_value, addr) VALUES
(NEW.anchor_id, NEW.geo, NEW.rwo, NEW.rwo_value, NEW.addr);
END IF;
RETURN NULL;
END IF;
RETURN NEW;
END;
$function$
;
The DBMS is a pg11 AWS RDS.
Blaming myself:
The anchor_smart_insert() function was working fine as long as the polygons which have been compared were greater than 10sqm. There was the assumption, that there should be only two options: greater than 10sqm, or null.
With such logical reasoning the part, which captures all exemptions was deleted since it seemed unnecessary.
This is also why the tests via direct insert worked well: the polygons were >10sqm and resulted in an update.
Thanks #Adrian Klaver - your suggestions helped!
The restored/improved function properly handles polygons <10sqm in a way, that it pre-compares it to initial and final sizes - if they are smaller, it takes a safer value.
Additionally it takes explicitly care of possible inserts (existing anchor_id ISNULL).
CREATE OR REPLACE FUNCTION cbm.anchor_smart_insert()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
geog geography;
acceptable_intersect NUMERIC:= 10;
acceptable_overlap_index NUMERIC := 0.56;
BEGIN
SELECT INTO geog geo FROM cbm.anchor a
WHERE a.anchor_id=NEW.anchor_id;
IF geog ISNULL THEN RETURN NEW; END IF;
IF ST_Area(ST_Intersection(geog, NEW.geo))>LEAST(acceptable_intersect, GREATEST(ST_Area(geog), ST_Area(NEW.geo))) THEN
UPDATE cbm.anchor a SET
pin=COALESCE(NEW.pin, ST_Centroid(NEW.geo)),
geo=NEW.geo,
rwo=NEW.rwo,
rwo_value=NEW.rwo_value,
addr=NEW.addr
WHERE a.anchor_id=NEW.anchor_id AND (
a.geo IS DISTINCT FROM NEW.geo
OR a.rwo IS DISTINCT FROM NEW.rwo
OR a.rwo_value IS DISTINCT FROM NEW.rwo_value
OR a.addr IS DISTINCT FROM NEW.addr);
IF ST_Area(ST_Intersection(geog, NEW.geo))^2/(ST_Area(geog)*ST_Area(NEW.geo))<acceptable_overlap_index THEN
INSERT INTO cbm.anchor_history (anchor_id, geo) VALUES (NEW.anchor_id, geog);
END IF;
END IF;
-- bad intersection:
INSERT INTO cbm.anchor_review(anchor_id, geo, rwo, rwo_value, addr) VALUES
(NEW.anchor_id, NEW.geo, NEW.rwo, NEW.rwo_value, NEW.addr);
RETURN NULL;
END;
$function$
;

sql query inside if stage with exists

I want to check if the id I want to insert into tableA exists in tableB into an if statement
Can I do something like this
if new.id exists (select id from tableB where stat = '0' ) then
some code here
end if;
When I try this I get an error message, any thoughts?
Why not do it like this? I'm not very knowledgeable about PostgreSQL but this would work in T-SQL.
INSERT INTO TargetTable(ID)
SELECT ID
FROM TableB
WHERE ID NOT IN (SELECT DISTINCT ID FROM TargetTable)
This is usually done with a trigger. A trigger function does the trick:
CREATE FUNCTION "trf_insert_tableA"() RETURNS trigger AS $$
BEGIN
PERFORM * FROM "tableB" WHERE id = NEW.id AND stat = '0';
IF FOUND THEN
-- Any additional code to go here, optional
RETURN NEW;
ELSE
RETURN NULL;
END IF;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER "tr_insert_tableA"
BEFORE INSERT ON "tableA"
FOR EACH ROW EXECUTE PROCEDURE "trf_insert_tableA"();
A few notes:
Identifiers in PostgreSQL are case-insensitive. PostgreSQL by default makes them lower-case. To maintain the case, use double-quotes. To make your life easy, use lower-case only.
A trigger needs a trigger function, this is always a two-step affair.
In an INSERT trigger, you can use the NEW implicit parameter to access the column values that are attempted to be inserted. In the trigger function you can modify these values and those values are then inserted. This only works in a BEFORE INSERT trigger, obviously; AFTER INSERT triggers are used for side effects such as logging, auditing or cascading inserts to other tables.
The PERFORM statement is a special form of a SELECT statement to test for the presence of data; it does not return any data, but it does set the FOUND implicit parameter that you can use in a conditional statement.
Depending on your logic, you may want the insert to succeed or to fail. RETURN NEW to make the insert succeed, RETURN NULL to make it fail.
After you defined the trigger, you can simply issue an INSERT statement: the trigger function is invoked automatically.
Presumably, you want something like this:
if exists (select 1 from tableB b where stat = '0' and b.id = new.id) then
some code here
end if;