Check if a `RECORD` variable is initialized - postgresql

I'm attempting to declare a variable with the data type record but am having troubles when there are no results to populate it with. Below is a simplified model of the function:
CREATE OR REPLACE FUNCTION do_something()
RETURNS TRIGGER AS $$
DECLARE
engagement record;
BEGIN
SELECT code
INTO engagement
FROM "mainEngagement" me
WHERE me.value = NEW.id;
IF TG_OP = 'INSERT'
THEN
INSERT INTO engagement_details (code)
values (
case
when engagement is not null
then exists(select code from engagement where "value" = 'expected')
else false
end
);
ELSE
UPDATE engagement_details
SET
code = case
when engagement is not null
then exists(select 1 from engagement where "value" = 'expected')
else false
end
WHERE engagement_details.assessment_id = NEW.id;
END IF;
RAISE NOTICE 'Updated engagement_details: [%]', NEW;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
When the trigger runs and there is no data in "mainEngagement" I'm getting an error in the INSERT:
ERROR: relation "engagement" does not exist
I cannot find a way to test if engagement is populated or not. I've searched through many stack overflow questions but most people just recommend doing engagement := row(null) but then when engagement is not null still doesn't work for me.

Use IF NOT FOUND:
SELECT code
INTO engagement
FROM "mainEngagement" me
WHERE me.value = NEW.id;
IF NOT FOUND
THEN
RAISE WARNING '|W| do_something(): engagement undefined'; -- report warning
RETURN; -- exit early, function cannot proceed
END IF;

The issue I was having here was that a RECORD entry can only have one row as the result. I was attempting to return a value with multiple rows and then run the CASE statement over the record, which will never work. Instead do the following:
CREATE OR REPLACE FUNCTION do_something()
RETURNS TRIGGER AS $$
DECLARE
engagement boolean;
BEGIN
SELECT EXISTS (
SELECT 1 FROM engagement WHERE "value" = 'expected'
) INTO engagement;
IF TG_OP = 'INSERT'
THEN
INSERT INTO engagement_details (code)
values (engagement);
ELSE
UPDATE engagement_details
SET
code = engagement
WHERE engagement_details.assessment_id = NEW.id;
END IF;
RAISE NOTICE 'Updated engagement_details: [%]', NEW;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Much simpler, cleaner, and works.

Related

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 trigger operating on both NEW and OLD records

This is primarily a style question.
I have an AFTER INSERT OR UPDATE trigger. I want to run the same query against whichever of the the NEW/OLD records are available. Rather than have conditionals checking TG_OP and duplicating the query, what is the cleanest way to determine which are available and to loop over them?
Example code:
CREATE FUNCTION myfunc() RETURNS TRIGGER AS $$
DECLARE
id int;
ids int[];
BEGIN
IF TG_OP IN ('INSERT', 'UPDATE') THEN
ids := ids || NEW.id;
END IF;
IF TG_OP IN ('UPDATE', 'DELETE') THEN
ids := ids || OLD.id;
END IF;
FOREACH id IN ARRAY ARRAY(SELECT DISTINCT UNNEST(ids))
LOOP
RAISE NOTICE 'myfunc called for % on id %', TG_OP, id;
/* RUN QUERY REFERENCING id */
END LOOP;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Is there a shorter/simpler/more idiomatic way to achieve that?
Array handling and a separate SELECT DISTINCT seem too expensive for the job. This should be cheaper:
CREATE FUNCTION myfunc()
RETURNS TRIGGER AS
$func$
DECLARE
_id int;
BEGIN
CASE TG_OP
WHEN 'INSERT', 'UPDATE' THEN
_id := NEW.id;
WHEN 'DELETE' THEN
_id := OLD.id;
END CASE;
FOR i IN 1..2 -- max 2 iterations
LOOP
RAISE NOTICE 'myfunc called for % on id %', TG_OP, _id;
/* RUN QUERY REFERENCING _id */
EXIT WHEN TG_OP <> 'UPDATE' OR i = 2; -- only continue in 1st round for UPDATE
EXIT WHEN _id = OLD.id; -- only continue for different value
_id := OLD.id;
END LOOP;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
Related:
BREAK statement in PL/pgSQL
But I would probably just write a separate trigger function & trigger for each DML statement. Rather three very simple and faster functions than one generic but complex and slower one.

Postgres trigger function: substitute value before insert or update

I've looked up pretty much everything I could find regarding this issue, but I still don't understand what is wrong with this trigger:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
raise notice 'SELECT';
SELECT place.geom_center, place.city_guid
INTO NEW.post_latlng, NEW.city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
IF NEW.post_latlng IS NOT NULL THEN
NEW.post_geoaccuracy = 'place';
IF NEW.city_guid IS NOT NULL THEN
SELECT country_guid INTO NEW.country_guid
FROM public.city WHERE (origin_id, city_guid) IN (VALUES (NEW.origin_id,NEW.city_guid));
END IF;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
DROP TRIGGER IF EXISTS trig_SubstitutePostLatLng_Upd on public.post;
CREATE TRIGGER trig_SubstitutePostLatLng_Upd
BEFORE UPDATE
ON public.post
FOR EACH ROW
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE func_SubstitutePostLatLng_Upt()
(I have a second similar trigger for insert)
The code is supposed to do the following:
On Update on table "post", check if no post_latlng is submitted (=NULL), and if yes, substitute post_latlng from table place (geom_center), if available.
However, no matter what I do, I get the following when updating an entry in table "post" (=triggering the above trigger):
NOTICE: SELECT
NOTICE: Value db_geom: <NULL>
NOTICE: Value db_city_guid: <NULL>
INSERT 0 1
Query returned successfully in 47 msec.
The test-data for place_guid, geom_center etc. is definitely available and both
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
should not output NULL.
There were several smaller issues, it now works. Here is a more cleaner code that uses variables in between:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
DECLARE
db_geom_center text;
db_city_guid text;
db_country_guid text;
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
SELECT place.geom_center, place.city_guid
INTO db_geom_center, db_city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
IF db_geom_center IS NOT NULL THEN
NEW.post_latlng = db_geom_center;
NEW.post_geoaccuracy = 'place';
END IF;
IF db_city_guid IS NOT NULL THEN
NEW.city_guid = db_city_guid;
SELECT city.country_guid
INTO db_country_guid
FROM public.city
WHERE (city.origin_id, city.city_guid) IN (VALUES (NEW.origin_id,db_city_guid));
NEW.country_guid = db_country_guid;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

How to store values in trigger PostgreSQL

I have a trigger that looks something like this:
CREATE OR REPLACE FUNCTION CHECK_SCHEDULE()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS(
SELECT DAY, TIME FROM MEETING
WHERE NEW.DAY = MEETING.DAY AND NEW.TIME > MEETING.TIME
) THEN
RAISE EXCEPTION 'THERE IS A MEETING HAPPENING ON % % ', NEW.DAY, NEW.TIME;
ELSE
RETURN NEW;
END IF;
END;
$BODY$ LANGUAGE PLPGSQL;
This works fine except I want the message to be the time it's conflicting with: There is a meeting happening on MEETING.DAY and MEETING.TIME.
However I cannot do this because it doesn't know what these variables are. Is it possible to store the values in my select clause so I can use them later?
You can move the day and time into a declared variable (e.g. a RECORD) for reference later.
CREATE OR REPLACE FUNCTION CHECK_SCHEDULE()
RETURNS TRIGGER AS
$BODY$
DECLARE
meetinginfo RECORD;
BEGIN
SELECT meeting.day, meeting.time
INTO meetinginfo
FROM meeting
WHERE new.day = meeting.day
AND new.time > meeting.time
ORDER BY new.time
LIMIT 1;
IF FOUND THEN
RAISE EXCEPTION 'THERE IS A MEETING HAPPENING ON % %', meetinginfo.day, meetinginfo.time;
END IF;
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;

How to create a trigger that checks values?

I need help with my trigger. For example i have Query like this
INSERT INTO test(id_users,id_type,id_color) VALUES(1,3,4);
in my data base i have table with name: test
ghost, id, id_users, id_type, id_color
and i need before insert or update to check
Create trigger
Begin
select id from test where ghost = false and id_users = 26 and id_type = 3
if NO execute INSERT
if YEST exit with no action
END
How can i creat this trigger ?
There are two ways, depending on how you want to manage the problem.
If you wish to silence it, use a before trigger and return null:
create function ignore_invalid_row() returns trigger as $$
begin
if not exists(
select 1
from test
where not ghost
and id_users = new.id_users
and id_type = new.id_type
)
then
return null;
end if;
return new;
end;
$$ language plpgsql;
create trigger ignore_invalid_row_tg before insert on test
for each row execute procedure ignore_invalid_row();
If you wish to raise it, use a constraint trigger and raise an exception:
create function reject_invalid_row() returns trigger as $$
begin
if not exists(
select 1
from test
where not ghost
and id_users = new.id_users
and id_type = new.id_type
)
then
raise exception 'message';
end if;
return null;
end;
$$ language plpgsql;
create constraint trigger reject_invalid_row_tg after insert on test
for each row execute procedure reject_invalid_row();
http://www.postgresql.org/docs/current/static/sql-createtrigger.html
First, you need to create a trigger function and then the trigger, based on it:
CREATE OR REPLACE FUNCTION my_trigger_function() RETURNS TRIGGER AS $BODY$ BEGIN
IF EXISTS (SELECT id FROM test WHERE ghost = false AND id_users = 26 AND id_type = 3) THEN
return NEW;
ELSE
return NULL;
END IF; END; $BODY$ LANGUAGE 'plpgsql';
Then, you create trigger based on this function:
CREATE TRIGGER t_my_trigger BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE my_trigger_function();
For more on triggers, see postgres docs.