Run DELETE query in trigger function - postgresql

I'm trying to run a DELETE and UPDATE query inside a trigger function in PL/pgSQL.
CREATE OR REPLACE FUNCTION trg_delete_order_layer()
RETURNS trigger AS
$BODY$
DECLARE index_ INTEGER;
DECLARE a RECORD;
BEGIN
IF EXISTS (SELECT * FROM map_layers_order WHERE id_map = OLD.id_map)
THEN index_ := position FROM map_layers_order WHERE id_map = OLD.id_map AND id_layer = OLD.id_layer AND layer_type = 'layer';
ELSE index_ := 0;
END IF;
RAISE NOTICE 'max_index % % %', index_, OLD.id_map, OLD.id_layer;
EXECUTE 'DELETE FROM map_layers_order WHERE id_map = $1 AND id_layer = $2 AND layer_type = $3' USING OLD.id_map, OLD.id_layer, 'layer';
EXECUTE 'UPDATE map_layers_order SET position = position -1 WHERE id_map = $1 AND position > $2' USING OLD.id_map, index_;
VALUES (OLD.id_map, OLD.id_layer, 'layer', index_);
RETURN OLD;
END;
$BODY$
LANGUAGE plpgsql;
I don't know why i'm getting this error in the line where it runs the DELETE query:
Query has no destination for result data
Why is this error happen and how can I solve this?
Obviously if I use EXECUTE...INTO, I get a more reasonable error saying that the query has no result.

Related

Postgresql: fetch all not showing any data

Data is not showing when I tried to fetch data from cursor. dbo.show_cities_multiple2 is procedure which returns two cursors.
DO $$
<<first_block>>
DECLARE
counter refcursor := 0;
ca_cur refcursor:=null;
tx_cur refcursor:=null;
BEGIN
call dbo.show_cities_multiple2(ca_cur, tx_cur);
EXECUTE 'FETCH ALL from "' || tx_cur || '"';
RAISE NOTICE 'The current value of counter is %', ca_cur;
END first_block $$;
Your problem here is that you aren't actually doing anything with the results of your EXECUTE FETCH ... EXECUTE merely runs the statement that comes after it. A plpgsql function does not return anything unless you explicitly specify RETURN ...,RETURN NEXT ... or RETURN QUERY .... Furthermore, an anonymous DO $$ block cannot return any results. So if you want to actually retrieve rows from the cursor, you need to name your cursors and execute your fetch outside of the do block:
DO $$
<<first_block>>
DECLARE
counter integer := 0;
ca_cur refcursor:='ca_cur'; --name your cursors first, otherwise they will be anonymous!
tx_cur refcursor:='tx_cur';
BEGIN
call dbo.show_cities_multiple2(ca_cur, tx_cur);
...
END first_block $$;
--Fetch
FETCH ALL FROM tx_cur;
Alternatively, you can define a standard function and use RETURN NEXT OR RETURN QUERY to return the result sets from the cursors:
CREATE FUNCTION return_city_data() RETURNS SETOF cities AS $$
DECLARE
counter integer = 0;
tx_cur refcursor = 'tx_cur';
ca_cur refcursor = 'ca_cur';
rec RECORD;
BEGIN
CALL dbo.show_cities_multiple(tx_cur,ca_cur);
-- Iterate over each record in the cursor
FOR rec in EXECUTE 'FETCH ALL FROM ' || tx_cur LOOP
counter = counter + 1;
-- RETURN NEXT means "Append each value to the function's result set".
RETURN NEXT rec;
END LOOP;
FOR rec in EXECUTE 'FETCH ALL FROM ' || ca_cur LOOP
counter = counter + 1;
RETURN NEXT rec;
END LOOP;
--All records have been appended to the result set. the cursors can now be closed.
CLOSE tx_cur;
CLOSE ca_cur;
RAISE NOTICE 'The current value of counter is %', "counter";
END
$$ LANGUAGE plpgsql;
OR
CREATE FUNCTION return_city_data() RETURNS SETOF cities AS $$
DECLARE
counter integer = 0;
tx_cur refcursor = 'tx_cur';
ca_cur refcursor = 'ca_cur';
rec RECORD;
BEGIN
CALL dbo.show_cities_multiple(tx_cur,ca_cur);
-- RETURN QUERY means "append all rows from the query to the function's result set"
RETURN QUERY EXECUTE 'FETCH ALL FROM ' || tx_cur;
RETURN QUERY EXECUTE 'FETCH ALL FROM ' || ca_cur;
CLOSE tx_cur;
CLOSE ca_cur;
END
$$ LANGUAGE plpgsql;
You can use either of these forms with a simple SELECT statement:
SELECT * FROM return_city_data();
Note that unlike RETURN, RETURN NEXT and RETURN QUERY do not terminate your function and you can use them multiple times in your blocks. The function will then naturally terminate at the end of the last block.

Why does function i PostgreSQL not raise exception?

I have a before insert or update trigger which is supposed to validate values of two columns in the inserted/updated data, raise an exception if the data are not valid or - if valid - add values to two other columns in the insert/update.
However, when I test with invalid data I get "ERROR: query has no destination for result data. HINT: If you want to discard the results of a SELECT, user PERFORM instead. CONTEXT: PL/pgSQL function before_insert_update() line 3 at SQL statement."
I have tried to read the PostgreSQL documentation and I have googled for explanations, but I just have to admin that my understanding of SQL is not sufficient to make it work.
My function is
CREATE or replace FUNCTION before_insert_update()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
begin
select * from addresses
where addresses.roadname = NEW.roadname and
addresses.housenumber = NEW.housenumber;
if ##ROWCOUNT > 0 then
begin
select addresses.roadcode,
addresses.geom
into NEW.roadcode, NEW.geom
from addresses
where addresses.roadname = NEW.roadname and
addresses.housenumber = NEW.housenumber;
return NEW;
end;
else
declare _adresse text;
begin
_address := concat(NEW.roadname, ' ', NEW.housenumber);
raise exception 'The address does not exist: %', _address
using hint = 'Check the address here: https://danmarksadresser.dk/adresser-i-danmark/';
end;
end if;
END
$BODY$;
The trigger is pretty straight forward
BEFORE INSERT OR UPDATE
ON table
FOR EACH ROW
EXECUTE PROCEDURE before_insert_update();
What is it that I am doing wrong?
There is no need to run a test if the row is present. Just select it, and check the status afterwards:
CREATE or replace FUNCTION before_insert_update()
RETURNS trigger
LANGUAGE plpgsql
COST 100
VOLATILE NOT LEAKPROOF
AS
$BODY$
begin
select addresses.roadcode, addresses.geom
into NEW.roadcode, NEW.geom
from addresses
where addresses.roadname = NEW.roadname and
addresses.housenumber = NEW.housenumber;
if found then
return new;
end if;
raise exception 'The address does not exist: %', concat(NEW.roadname, ' ', NEW.housenumber)
using hint = 'Check the address here: https://danmarksadresser.dk/adresser-i-danmark/';
END
$BODY$;
Looks like you're trying to use SQL Server specific code in Postgres, which isn't going to work of course.
PL/pgSQL doesn't allow SELECTs just anywhere, when the result isn't passed to somehwere, like variables, etc.. And there is no ##ROWCOUNT. From what I can guess what you want to do, you can use EXISTS and your query for the condition of the IF.
CREATE
OR REPLACE FUNCTION before_insert_update()
RETURNS trigger
AS
$$
BEGIN
IF EXISTS(SELECT *
FROM addresses
WHERE addresses.roadname = new.roadname
AND addresses.housenumber = new.housenumber) THEN
SELECT addresses.roadcode,
addresses.geom
INTO new.roadcode,
new.geom
FROM addresses
WHERE addresses.roadname = new.roadname
AND addresses.housenumber = new.housenumber;
RETURN new;
ELSE
RAISE EXCEPTION 'The address does not exist: %', concat(new.roadname, ' ', new.housenumber)
USING HINT = 'Check the address here: https://danmarksadresser.dk/adresser-i-danmark/';
END IF;
END
$$
LANGUAGE plpgsql;

PostgreSQL update trigger Comparing Hstore values

I am creating trigger in PostgresSQL. On update I would like to compare all of the values in a Hstore column and update changes in my mirror table. I managed to get names of my columns in variable k but I am not able to get values using it from NEW and OLD.
CREATE OR REPLACE FUNCTION function_replication() RETURNS TRIGGER AS
$BODY$
DECLARE
k text;
BEGIN
FOR k IN SELECT key FROM EACH(hstore(NEW)) LOOP
IF NEW.k != OLD.k THEN
EXECUTE 'UPDATE ' || TG_TABLE_NAME || '_2' || 'SET ' || k || '=' || new.k || ' WHERE ID=$1.ID;' USING OLD;
END IF;
END LOOP;
RETURN NEW;
END;
$BODY$
language plpgsql;
You should operate on hstore representations of the records new and old. Also, use the format() function for better control and readibility.
create or replace function function_replication()
returns trigger as
$body$
declare
newh hstore = hstore(new);
oldh hstore = hstore(old);
key text;
begin
foreach key in array akeys(newh) loop
if newh->key != oldh->key then
execute format(
'update %s_2 set %s = %L where id = %s',
tg_table_name, key, newh->key, oldh->'id');
end if;
end loop;
return new;
end;
$body$
language plpgsql;
Another version - with minimalistic numbers of updates - in partially functional design (where it is possible).
This trigger should be AFTER trigger, to be ensured correct behave.
CREATE OR REPLACE FUNCTION function_replication()
RETURNS trigger AS $$
DECLARE
newh hstore;
oldh hstore;
update_vec text[];
pair text[];
BEGIN
IF new IS DISTINCT FROM old THEN
IF new.id <> old.id THEN
RAISE EXCEPTION 'id should be immutable';
END IF;
newh := hstore(new); oldh := hstore(old); update_vec := '{}';
FOREACH pair SLICE 1 IN ARRAY hstore_to_matrix(newh - oldh)
LOOP
update_vec := update_vec || format('%I = %L', pair[1], pair[2]);
END LOOP;
EXECUTE
format('UPDATE %I SET %s WHERE id = $1',
tg_table_name || '_2',
array_to_string(update_vec, ', '))
USING old.id;
END IF;
RETURN NEW; -- the value is not important in AFTER trg
END;
$$ LANGUAGE plpgsql;
CREATE TABLE foo(id int PRIMARY KEY, a int, b int);
CREATE TABLE foo_2(LIKE foo INCLUDING ALL);
CREATE TRIGGER xxx AFTER UPDATE ON foo
FOR EACH ROW EXECUTE PROCEDURE function_replication();
INSERT INTO foo VALUES(1, NULL, NULL);
INSERT INTO foo VALUES(2, 1,1);
INSERT INTO foo_2 VALUES(1, NULL, NULL);
INSERT INTO foo_2 VALUES(2, 1,1);
UPDATE foo SET a = 20, b = 30 WHERE id = 1;
UPDATE foo SET a = NULL WHERE id = 1;
This code is little bit more complex, but all what should be escaped is escaped and reduce number of executed UPDATE commands. UPDATE is full SQL command and the overhead of full SQL commands should be significantly higher than code that reduce number of full SQL commands.

Using variable passed in from psql command line, in function

I can't figure out how to access variables inside my plpgsql function. I'm using postgres 9.5 under Cygwin.
functions.sql
-- this works fine
\echo Recreate = :oktodrop
CREATE OR REPLACE FUNCTION drop_table(TEXT) RETURNS VOID AS
$$
BEGIN
IF EXISTS ( SELECT * FROM information_schema.tables WHERE table_name = $1 ) THEN
-- syntax error here:
IF (:oktodrop == 1 ) THEN
DROP TABLE $1;
END IF;
END IF;
END;
$$
language 'plpgsql';
psql.exe -v oktodrop=1 -f functions.sql
Password:
Recreate = 1
psql:functions.sql:13: ERROR: syntax error at or near ":"
LINE 5: IF (:oktodrop == 1 ) THEN
^
Perhaps I've oversimplified your task (feel free to tell me if that's the case), but why not create the function like this:
CREATE OR REPLACE FUNCTION drop_table(tablename TEXT, oktodrop integer)
RETURNS text AS
$$
DECLARE
result text;
BEGIN
IF EXISTS ( SELECT * FROM information_schema.tables WHERE table_name = tablename ) THEN
IF (oktodrop = 1 ) THEN
execute 'DROP TABLE ' || tablename;
result := 'Dropped';
ELSE
result := 'Not Okay';
END IF;
ELSE
result := 'No such table';
END IF;
return result;
END;
$$
language 'plpgsql';
Then the implementation would be:
select drop_table('foo', 1);
I should also caution that because you have not specified the table_schema field, it's conceivable that your target table exists in another schema, and the actual drop command will fail because it doesn't exist in the default schema.

How to pass stored procedure parameter into EXECUTE statement

CREATE OR REPLACE FUNCTION "Test"(character varying[],character varying[])
RETURNS refcursor AS
$BODY$
DECLARE
curr refcursor;
filter text;
counter integer;
BEGIN
counter = 1;
filter = '';
IF array_length($1,1) > 0 THEN
filter = 'AND ';
WHILE ($1[counter] <> '') LOOP
filter = filter||'LOWER('||$1[counter]||'::character varying) LIKE ''%''||LOWER($2['||counter||'])||''%'' AND ';
counter = counter + 1;
END LOOP;
filter = substring(filter FROM 1 FOR (char_length(filter)-4));
OPEN curr FOR
EXECUTE 'SELECT "Reservation".* FROM "Reservation" WHERE "Reservation"."id" > 0 '||filter;
return curr;
END IF;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
SELECT "Test"(ARRAY['"Reservation"."status"'],'{"waiting"}');
FETCH ALL IN "<unnamed portal 1>";
I tried to print out the query:
"SELECT "Reservation".* FROM "Reservation" WHERE "Reservation"."id" > 0 AND LOWER("Reservation"."status"::character varying) LIKE '%'||LOWER($2[1])||'%' "
But when it's executed it said that there was no parameter $2. So I realize that it can't access that stored procedure's parameter.
I don't have to worry about the first parameter of sql injection since it's hard coded. But the second param has to be passed into the execution. How do I do that?
I've found out that I could pass the parameter into EXECUTE using the "USING" statement.
Here's the final working code:
CREATE OR REPLACE FUNCTION "Test"(character varying[],character varying[])
RETURNS refcursor AS
$BODY$
DECLARE
curr refcursor;
filter text;
counter integer;
BEGIN
counter = 1;
filter = '';
IF array_length($1,1) > 0 THEN
filter = 'AND ';
WHILE ($1[counter] <> '') LOOP
filter = filter||'LOWER('||$1[counter]||'::character varying) LIKE ''%''||LOWER($1['||counter||'])||''%'' AND ';
counter = counter + 1;
END LOOP;
filter = substring(filter FROM 1 FOR (char_length(filter)-4));
OPEN curr FOR
EXECUTE 'SELECT "Reservation".* FROM "Reservation" WHERE "Reservation"."id" > 0 '||filter USING $2;
return curr;
END IF;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
SELECT "Test"(ARRAY['"Reservation"."status"'],ARRAY['no-show']);
FETCH ALL IN "<unnamed portal 1>";
Note that I have $1 as the value in the EXECUTE statement, because it accepts $2 as its first parameter.