Trigger in postgres not raising exception - postgresql

I have a trigger working in postgres, it allows a view to become updateable.
CREATE OR REPLACE FUNCTION actualizaIDConductor() RETURNS
TRIGGER AS '
BEGIN
RAISE NOTICE ''Working'';
SELECT COUNT(*) INTO bool
FROM Infotrenes
WHERE cod_empleado = OLD.cod_empleado;
RAISE NOTICE ''bool = %'', bool;
--The id belongs to the view
IF (bool > 0) THEN
UPDATE ...; --This works fine
ELSE
RAISE EXCEPTION ''ERROR: ...'';
END IF;
RETURN NEW;
END;
' LANGUAGE 'plpgsql';
The "problem" comes when I want to update a row that doesn't belong to the view. Instead of printing my "ERROR:..." message it just displays UPDATE 0. It doesn't even display the "Working" notice.
Is this some sort of the manager optimization or am I doing something wrong? Can I somehow print the message?

This code should not to work - there are undeclared variable bool (secondary - it's wrong name due same named data type). So this code should not to work ever, and should not to raise a notice 'working'.

Related

PostgreSQL Stored Procedures with text as parameters

I have searched and could not find an open post regarding this topic:
I'm working with stored procedures in PostgreSQL and I'm currently being asked to create one that will collect certain information from a publisher based on the only parameter passed, their publishercode.
This code is in the existing table as a "character" type and as such I'm passing it the same and calling the function with that same type input, but I'm only obtaining a "NULL" value from my input and I'm not sure why.
This is my code:
CREATE OR REPLACE FUNCTION uspGetPubStatsMAC (pubcode publisher.publishercode%TYPE) -- Publisher Code
RETURNS text AS
$$
DECLARE PubCode publisher.publishercode%TYPE;
PubName text;
NumOfAuth int;
NumOfPubBooks int;
HiValBook text;
NumHiValBook int;
TotNumHiValBook int;
BEGIN
-- Get the publisher's name from the given code
RAISE NOTICE 'Code: (%)', pubcode;
SELECT publishername INTO PubName
FROM publisher
WHERE publishercode = pubcode;
RAISE NOTICE 'Name: (%)', PubName;
-- check if a publisher exists with the given code
IF ( PubName is null )
THEN
RAISE NOTICE 'No publisher exists with the given code (%)', pubcode;
RETURN (-1);
END IF;
RAISE NOTICE 'Current Statistics of the publisher (%)', pubcode;
RAISE NOTICE ' The Name of the Publisher: (%)', trim (trailing ' ' from PubName);
SELECT count(distinct a.authornum) INTO NumOfAuth
FROM publisher p, book b, wrote w, author a
WHERE p.publishercode = b.publishercode
and b.bookcode = w.bookcode;
RAISE NOTICE ' The number of distinct authors who have written book(s) for this publisher: (%)', NumOfAuth;
END;
$$ language plpgsql;
SELECT uspGetPubStatsMAC('AH');
Where 'AH' is the code corresponding to publisher Arkham House in my table. Any idea of what I'm doing wrong?
You have at least two errors in your code:
pubcode is the parameter passed in to the function. Then you declare a local PubCode variable. Remove that declaration.
Your function should RETURN some text value representing PUBstatsMAC when it succeeds. Currently you only return -1 if there is an error. At that point you should RAISE an error with an appropriate message and forget about a return value, otherwise a bad return value may get cascaded into other code.
Furthermore, after a SELECT ... INTO ... statement you should test for the FOUND internal variable rather than a NULL value in a local variable:
IF NOT FOUND THEN
RAISE 'No publisher exists with the given code (%)', pubcode;
END IF;
And your query on number of distinct authors will give a bad result because you are missing a join clause on table author.
Then, you have a bunch of unused local variables declared.
Lastly, it is bad practice to return your information from a function in a bunch of RAISE NOTICE statements.
Thanks a lot for your feedback! I will test some of these fixes.
With respect to the return of -1 that is a requirement by the teacher. He wants a -1 if an error occurred at run time and a 0 if not. Hence why the output is formatted as raise notices. Also a requirement.
The extra local variables are there because that is an unfinished portion of the entire procedure code.
I tried what you suggested though and it worked like a charm. The extra declaration of PubCode was causing the problem.
Thanks so much!!!

a postgres update trigger performs everything else except the actual update

Let's use a test table :
CREATE TABLE labs.date_test
(
pkey int NOT NULL,
val integer,
date timestamp without time zone,
CONSTRAINT date_test_pkey PRIMARY KEY (pkey)
);
I have a trigger function defined as below. It is a function to insert a date into a specified column in the table. Its arguments are the primary key, the name of the date field, and the date to be inserted:
CREATE OR REPLACE FUNCTION tf_set_date()
RETURNS trigger AS
$BODY$
DECLARE
table_name text;
pkey_col text := TG_ARGV[0];
date_col text := TG_ARGV[1];
date_val text := TG_ARGV[2];
BEGIN
table_name := format('%I.%I', TG_TABLE_SCHEMA, TG_TABLE_NAME);
IF TG_NARGS != 3 THEN
RAISE 'Wrong number of args for tf_set_date()'
USING HINT='Check triggers for table ' || table_name;
END IF;
EXECUTE format('UPDATE %s SET %I = %s' ||
' WHERE %I = ($1::text::%s).%I',
table_name, date_col, date_val,
pkey_col, table_name, pkey_col )
USING NEW;
RAISE NOTICE '%', NEW;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
The actual trigger definition is as follows:
CREATE TRIGGER t_set_ready_date
BEFORE UPDATE OF val
ON labs.date_test
FOR EACH ROW
EXECUTE PROCEDURE tf_set_date('pkey', 'date', 'localtimestamp(0)');
Now say I do: INSERT INTO TABLEdate_test(pkey) values(1);`
Then I perform an update as follows:
UPDATE labs.date_test SET val = 1 WHERE pkey = 1;
Now the date gets inserted as expected. But the val field is still NULL. It does not have 1 as one would expect (or rather as I expected).
What am I doing wrong? The RAISE NOTICE in the trigger shows that NEW is still what I expect it to be. Aren't UPDATEs allowed in BEFORE UPDATE triggers? One comment about postgres triggers seems to indicate that original the UPDATE gets overwritten if there is an UPDATE statement in a BEFORE UPDATE trigger. Can someone help me out?
EDIT
I am trying to update the same table that invoked the trigger, and that too the same row which is to be modified by the UPDATE statement that invoked the trigger. I am running Postgresql 9.2
Given all the dynamic table names it isn't entirely clear if this trigger issues an update on the same table that invoked the trigger.
If so: That won't work. You can't UPDATE some_table in a BEFORE trigger on some_table. Or, more strictly, you can, but if you update any row that is affected by the statement that's invoking the trigger results will be unpredictable so it isn't generally a good idea.
Instead, alter the values in NEW directly. You can't do this with dynamic column names, unfortunately; you'll just have to customise the trigger or use an AFTER trigger to do the update after the rows have already been changed.
I am not sure, but your triggers can do recursion calls - it does UPDATE same table from UPDATE trigger. This is usually bad practice, and usually is not good idea to write too generic triggers. But I don't know what you are doing, maybe you need it, but you have to be sure, so you are protected against recursion.
For debugging of triggers is good push to start and to end of function body debug messages. Probably you use GET DIAGNOSTICS statement after EXECUTE statement for information about impact of dynamic SQL
DECLARE
_updated_rows int;
_query text;
BEGIN
RAISE NOTICE 'Start trigger function xxx';
...
_query := format('UPDATE ....);
RAISE NOTICE 'dynamic sql %, %', _query, new;
EXECUTE _query USING new;
GET DIAGNOSICS _updated_rows = ROW_COUNT;
RAISE NOTICE 'Updated rows %', _updated_rows;
...

Raising error in postgreSQL

CREATE OR REPLACE FUNCTION msgfailerror() RETURNS trigger AS
' BEGIN
IF NEW.noces< new.first_column THEN
RAISE EXCEPTION 'cannot have a negative salary';
END IF;
return new;
END' LANGUAGE plpgsql
Trigger
create trigger msgfail before insert on first for each row
execute procedure msgfailerror()
Giving error:
syntax error at or near "cannot" LINE 5: RAISE
EXCEPTION 'cannot have a negative ...
I have almost one validation for each field of row. I want trigger to check all validations while insertion is being done and, raise error log afterwards once for all. Should I use raise exception on raise notice ?
For example:
Insert into first (first_column, noces,dob) values ('4545','75','545')
I am checking noces is less than first_column, for the same row i want to check if dob > 80 and if first_column is integer and raise error for all validations. Thanks in advance
The quoting is wrong. It's easier to use dollar quotes $$:
CREATE OR REPLACE FUNCTION msgfailerror()
RETURNS trigger AS
$$
BEGIN
IF NEW.noces< new.first_column THEN
RAISE EXCEPTION 'cannot have a negative salary';
END IF;
return new;
END;
$$
LANGUAGE plpgsql;
But on the other hand, what's wrong with a check constraint?
there is nothing wrong with you the only thing is using quotes
please change :
RAISE EXCEPTION 'cannot have a negative salary';
to:
RAISE EXCEPTION ''cannot have a negative salary'';
'' is different from "
'' = two single quotes
I agree with Frank that you could better use constraints, but you call it validation. Validation is typically done before insertion takes place. If you would like to validate insertions, you could use functions instead of triggers or constraints.
When you would write functions is the answer to your question to raise exceptions or notices that as long as there has been no write action a notice would suffice (together with leaving the function). Once there has been a write to the database, do you have to use exceptions as they perform a rollback.
Like this:
CREATE OR REPLACE FUNCTION field_validate(p_int int) RETURNS boolean AS $$
DECLARE
i_id int;
BEGIN
if p_int > 10 then
raise notice 'should be smaller then 10';
return false;
end if;
insert into tbl_numbers(firstfield) values(p_int) returning id in i_id;
insert into tbl_fake(nofield) values(i_id);
return true;
EXCEPTION
WHEN raise exception THEN
return false;
END;
$$ LANGUAGE plpgsql;

Variable containing the number of rows affected by previous DELETE? (in a function)

I have a function that is used as an INSERT trigger. This function deletes rows that would conflict with [the serial number in] the row being inserted. It works beautifully, so I'd really rather not debate the merits of the concept.
DECLARE
re1 feeds_item.shareurl%TYPE;
BEGIN
SELECT regexp_replace(NEW.shareurl, '/[^/]+(-[0-9]+\.html)$','/[^/]+\\1') INTO re1;
RAISE NOTICE 'DELETEing rows from feeds_item where shareurl ~ ''%''', re1;
DELETE FROM feeds_item where shareurl ~ re1;
RETURN NEW;
END;
I would like to add to the NOTICE an indication of how many rows are affected (aka: deleted). How can I do that (using LANGUAGE 'plpgsql')?
UPDATE:
Base on some excellent guidance from "Chicken in the kitchen", I have changed it to this:
DECLARE
re1 feeds_item.shareurl%TYPE;
num_rows int;
BEGIN
SELECT regexp_replace(NEW.shareurl, '/[^/]+(-[0-9]+\.html)$','/[^/]+\\1') INTO re1;
DELETE FROM feeds_item where shareurl ~ re1;
IF FOUND THEN
GET DIAGNOSTICS num_rows = ROW_COUNT;
RAISE NOTICE 'DELETEd % row(s) from feeds_item where shareurl ~ ''%''', num_rows, re1;
END IF;
RETURN NEW;
END;
For a very robust solution, that is part of PostgreSQL SQL and not just plpgsql you could also do the following:
with a as (DELETE FROM feeds_item WHERE shareurl ~ re1 returning 1)
select count(*) from a;
You can actually get lots more information such as:
with a as (delete from sales returning amount)
select sum(amount) from a;
to see totals, in this way you could get any aggregate and even group and filter it.
In Oracle PL/SQL, the system variable to store the number of deleted / inserted / updated rows is:
SQL%ROWCOUNT
After a DELETE / INSERT / UPDATE statement, and BEFORE COMMITTING, you can store SQL%ROWCOUNT in a variable of type NUMBER. Remember that COMMIT or ROLLBACK reset to ZERO the value of SQL%ROWCOUNT, so you have to copy the SQL%ROWCOUNT value in a variable BEFORE COMMIT or ROLLBACK.
Example:
BEGIN
DECLARE
affected_rows NUMBER DEFAULT 0;
BEGIN
DELETE FROM feeds_item
WHERE shareurl = re1;
affected_rows := SQL%ROWCOUNT;
DBMS_OUTPUT.
put_line (
'This DELETE would affect '
|| affected_rows
|| ' records in FEEDS_ITEM table.');
ROLLBACK;
END;
END;
I have found also this interesting SOLUTION (source: http://markmail.org/message/grqap2pncqd6w3sp )
On 4/7/07, Karthikeyan Sundaram wrote:
Hi,
I am using 8.1.0 postgres and trying to write a plpgsql block. In that I am inserting a row. I want to check to see if the row has been
inserted or not.
In oracle we can say like this
begin
insert into table_a values (1);
if sql%rowcount > 0
then
dbms.output.put_line('rows inserted');
else
dbms.output.put_line('rows not inserted');
end if; end;
Is there something equal to sql%rowcount in postgres? Please help.
Regards skarthi
Maybe:
http://www.postgresql.org/docs/8.2/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-SQL-ONEROW
Click on the link above, you'll see this content:
37.6.6. Obtaining the Result Status There are several ways to determine the effect of a command. The first method is to use the GET
DIAGNOSTICS command, which has the form:
GET DIAGNOSTICS variable = item [ , ... ];This command allows
retrieval of system status indicators. Each item is a key word
identifying a state value to be assigned to the specified variable
(which should be of the right data type to receive it). The currently
available status items are ROW_COUNT, the number of rows processed by
the last SQL command sent down to the SQL engine, and RESULT_OID, the
OID of the last row inserted by the most recent SQL command. Note that
RESULT_OID is only useful after an INSERT command into a table
containing OIDs.
An example:
GET DIAGNOSTICS integer_var = ROW_COUNT; The second method to
determine the effects of a command is to check the special variable
named FOUND, which is of type boolean. FOUND starts out false within
each PL/pgSQL function call. It is set by each of the following types
of statements:
A SELECT INTO statement sets FOUND true if a row is assigned, false if
no row is returned.
A PERFORM statement sets FOUND true if it produces (and discards) a
row, false if no row is produced.
UPDATE, INSERT, and DELETE statements set FOUND true if at least one
row is affected, false if no row is affected.
A FETCH statement sets FOUND true if it returns a row, false if no row
is returned.
A FOR statement sets FOUND true if it iterates one or more times, else
false. This applies to all three variants of the FOR statement
(integer FOR loops, record-set FOR loops, and dynamic record-set FOR
loops). FOUND is set this way when the FOR loop exits; inside the
execution of the loop, FOUND is not modified by the FOR statement,
although it may be changed by the execution of other statements within
the loop body.
FOUND is a local variable within each PL/pgSQL function; any changes
to it affect only the current function.
I would to share my code (I had this idea from Roelof Rossouw):
CREATE OR REPLACE FUNCTION my_schema.sp_delete_mytable(_id integer)
RETURNS integer AS
$BODY$
DECLARE
AFFECTEDROWS integer;
BEGIN
WITH a AS (DELETE FROM mytable WHERE id = _id RETURNING 1)
SELECT count(*) INTO AFFECTEDROWS FROM a;
IF AFFECTEDROWS = 1 THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
EXCEPTION WHEN OTHERS THEN
RETURN 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

count number of rows to be affected before update in trigger

I want to know number of rows that will be affected by UPDATE query in BEFORE per statement trigger . Is that possible?
The problem is that i want to allow only queries that will update up to 4 rows. If affected rows count is 5 or more i want to raise error.
I don't want to do this in code because i need this check on db level.
Is this at all possible?
Thanks in advance for any clues on that
Write a function that updates the rows for you or performs a rollback. Sorry for poor style formatting.
create function update_max(varchar, int)
RETURNS void AS
$BODY$
DECLARE
sql ALIAS FOR $1;
max ALIAS FOR $2;
rcount INT;
BEGIN
EXECUTE sql;
GET DIAGNOSTICS rcount = ROW_COUNT;
IF rcount > max THEN
--ROLLBACK;
RAISE EXCEPTION 'Too much rows affected (%).', rcount;
END IF;
--COMMIT;
END;
$BODY$ LANGUAGE plpgsql
Then call it like
select update_max('update t1 set id=id+10 where id < 4', 3);
where the first param ist your sql-Statement and the 2nd your max rows.
Simon had a good idea but his implementation is unnecessarily complicated. This is my proposition:
create or replace function trg_check_max_4()
returns trigger as $$
begin
perform true from pg_class
where relname='check_max_4' and relnamespace=pg_my_temp_schema();
if not FOUND then
create temporary table check_max_4
(value int check (value<=4))
on commit drop;
insert into check_max_4 values (0);
end if;
update check_max_4 set value=value+1;
return new;
end; $$ language plpgsql;
I've created something like this:
begin;
create table test (
id integer
);
insert into test(id) select generate_series(1,100);
create or replace function trg_check_max_4_updated_records()
returns trigger as $$
declare
counter_ integer := 0;
tablename_ text := 'temptable';
begin
raise notice 'trigger fired';
select count(42) into counter_
from pg_catalog.pg_tables where tablename = tablename_;
if counter_ = 0 then
raise notice 'Creating table %', tablename_;
execute 'create temporary table ' || tablename_ || ' (counter integer) on commit drop';
execute 'insert into ' || tablename_ || ' (counter) values(1)';
execute 'select counter from ' || tablename_ into counter_;
raise notice 'Actual value for counter= [%]', counter_;
else
execute 'select counter from ' || tablename_ into counter_;
execute 'update ' || tablename_ || ' set counter = counter + 1';
raise notice 'updating';
execute 'select counter from ' || tablename_ into counter_;
raise notice 'Actual value for counter= [%]', counter_;
if counter_ > 4 then
raise exception 'Cannot change more than 4 rows in one trancation';
end if;
end if;
return new;
end; $$ language plpgsql;
create trigger trg_bu_test before
update on test
for each row
execute procedure trg_check_max_4_updated_records();
update test set id = 10 where id <= 1;
update test set id = 10 where id <= 2;
update test set id = 10 where id <= 3;
update test set id = 10 where id <= 4;
update test set id = 10 where id <= 5;
rollback;
The main idea is to have a trigger on 'before update for each row' that creates (if necessary) a temporary table (that is dropped at the end of transaction). In this table there is just one row with one value, that is the number of updated rows in current transaction. For each update the value is incremented. If the value is bigger than 4, the transaction is stopped.
But I think that this is a wrong solution for your problem. What's a problem to run such wrong query that you've written about, twice, so you'll have 8 rows changed. What about deletion rows or truncating them?
PostgreSQL has two types of triggers: row and statement triggers. Row triggers only work within the context of a row so you can't use those. Unfortunately, "before" statement triggers don't see what kind of change is about to take place so I don't believe you can use those, either.
Based on that, I would say it's unlikely you'll be able to build that kind of protection into the database using triggers, not unless you don't mind using an "after" trigger and rolling back the transaction if the condition isn't satisfied. Wouldn't mind being proved wrong. :)
Have a look at using Serializable Isolation Level. I believe this will give you a consistent view of the database data within your transaction. Then you can use option #1 that MusiGenesis mentioned, without the timing vulnerability. Test it of course to validate.
I've never worked with postgresql, so my answer may not apply. In SQL Server, your trigger can call a stored procedure which would do one of two things:
Perform a SELECT COUNT(*) to determine the number of records that will be affected by the UPDATE, and then only execute the UPDATE if the count is 4 or less
Perform the UPDATE within a transaction, and only commit the transaction if the returned number of rows affected is 4 or less
No. 1 is timing vulnerable (the number of records affected by the UPDATE may change between the COUNT(*) check and the actual UPDATE. No. 2 is pretty inefficient, if there are many cases where the number of rows updated is greater than 4.