Say we have a PostgreSQL table like so:
CREATE TABLE master (
id INT PRIMARY KEY,
...
);
and many other tables referencing it with foreign keys:
CREATE TABLE other (
id INT PRIMARY KEY,
id_master INT NOT NULL,
...
CONSTRAINT other_id_master_fkey FOREIGN KEY (id_master)
REFERENCES master (id) ON DELETE RESTRICT
);
Is there a way to check (from within trigger function) if a master row is deletable without actually trying to delete it? The obvious way is to do a SELECT on all referencing tables one by one, but I would like to know if there is an easier way.
The reason I need this is that I have a table with hierarchical data in which any row can have child rows, and only child rows that are lowest in hierarchy can be referenced by other tables. So when a row is about to become a parent row, I need to check whether it is already referenced anywhere. If it is, it cannot become a parent row, and insertion of new child row is denied.
You can try to delete the row and roll back the effects. You wouldn't want to do that in a trigger function because any exception cancels all persisted changes to the database. The manual:
When an error is caught by an EXCEPTION clause, the local variables of
the PL/pgSQL function remain as they were when the error occurred, but
all changes to persistent database state within the block are rolled back.
Bold emphasis mine.
But you can wrap this into a separate block or a separate plpgsql function and catch the exception there to prevent the effect on the main (trigger) function.
CREATE OR REPLACE FUNCTION f_can_del(_id int)
RETURNS boolean AS
$func$
BEGIN
DELETE FROM master WHERE master_id = _id; -- DELETE is always rolled back
IF NOT FOUND THEN
RETURN NULL; -- ID not found, return NULL
END IF;
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
This returns TRUE / FALSE / NULL indicating that the row can be deleted / not be deleted / does not exist.
db<>fiddle here
Old sqlfiddle
One could easily make this function dynamic to test any table / column / value.
Since PostgreSQL 9.2 you can also report back which table was blocking.
PostgreSQL 9.3 or later offer more detailed information, yet.
Generic function for arbitrary table, column and type
Why did the attempt on a dynamic function that you posted in the comments fail? This quote from the manual should give a clue:
Note in particular that EXECUTE changes the output of GET DIAGNOSTICS, but does not change FOUND.
It works with GET DIAGNOSTICS:
CREATE OR REPLACE FUNCTION f_can_del(_tbl regclass, _col text, _id int)
RETURNS boolean AS
$func$
DECLARE
_ct int; -- to receive count of deleted rows
BEGIN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RETURN NULL; -- ID not found, return NULL
END IF;
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Old sqlfiddle
While being at it, I made it completely dynamic, including the data type of the column (it has to match the given column, of course). I am using the polymorphic type anyelement for that purpose. See:
How to write a function that returns text or integer values?
I also use format() and a parameter of type regclass to safeguard against SQLi. See:
SQL injection in Postgres functions vs prepared queries
You can do that also with Procedure.
CREATE OR REPLACE procedure p_delable(_tbl text, _col text, _id int)
AS $$
DECLARE
_ct bigint;
_exists boolean; -- to receive count of deleted rows
BEGIN
_exists := (SELECT EXISTS ( SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1 ));
IF _exists THEN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RAISE NOTICE 'no records found. no records will be deleted';
END IF;
ELSE
raise notice 'Input text is invalid table name.';
END IF;
EXCEPTION
WHEN undefined_column then
raise notice 'Input text is invalid column name.';
WHEN undefined_table then
raise notice 'Input text is invalid table name.';
WHEN FOREIGN_KEY_VIOLATION THEN
RAISE NOTICE 'foreign key violation, cannot be deleted.';
WHEN SQLSTATE 'MYERR' THEN
RAISE NOTICE 'rows % found and can be deleted.', _ct;
END
$$ LANGUAGE plpgsql;
You can call it, also can validate your input.
call p_delable('parent_tree', 'parent_id',30);
Will get:
NOTICE: no records found. no records will be deleted
Lets try an actual exist row.
call p_delable('parent_tree', 'parent_id',3);
It will return
NOTICE: rows 1 found and can be deleted.
It can also check your input table name exists in public schema or not.
call p_delable('parent_tre', 'parent_id',3);
It will give you notice:
NOTICE: Input text is invalid table name.
Related
I need to add a trigger that raises a warning message when a certain out of bounds (not 2, 3, 5-7) numeric value is inserted into or altered in an EXISTING row in a "grade" column in the sql table. This code example raises such a message ONLY when a NEW row is created.
How to raise the message when a value in the EXISTING row is altered?
Values in the "grade" column are tied via key to a column in another table "grade_salary" where they are stored. How to write the insertion/alteration check in such a way that raises the message without specifying the concrete correct values (2, 3, 5-7), but stating only that "IF changed value lies outside of the values specified in column "grade" of the table "grade_salary" THEN raise the error message" (and not let the value be modified)?
CREATE TRIGGER person
BEFORE INSERT ON hr."position"
FOR EACH ROW EXECUTE PROCEDURE person();
CREATE OR REPLACE FUNCTION person()
RETURNS TRIGGER
SET SCHEMA 'hr'
LANGUAGE plpgsql
AS $$
BEGIN
IF ((NEW.grade < 2) or (NEW.grade > 3 and NEW.grade < 5)
or (NEW.grade > 7)) THEÎ
RAISE EXCEPTION 'Incorrect value';
END IF;
RETURN NEW;
END;
$$;
One checks whether the new value corresponds to an existing one in a column with:
IF new_value not in (SELECT DISTINCT grade FROM grade_salary)
THEN RAISE EXCEPTION 'Inadmissible value.'
In full:
CREATE OR REPLACE FUNCTION person()
RETURNS TRIGGER
SET SCHEMA 'hr'
LANGUAGE plpgsql
AS $$
declare
new_value numeric;
begin
select new.grade from "position" into new_value;
IF new_value not in (SELECT DISTINCT grade FROM grade_salary)
THEN RAISE EXCEPTION 'Inadmissible value.';
END IF;
RETURN NEW;
END;
$$;
I have an SQL file that contains the schema of the database (creating of tables and some functions to check the constraints). One of the functions has different actions to do for the first entered value and for the rest. What I mean is, that in the function I am firstly counting the number of elements in a table:
CREATE TABLE table_name(
column1 INT,
column2 INT CHECK(func())
);
CREATE OR REPLACE FUNCTION func1()
returns BOOLEAN
LANGUAGE plpgsql
AS
$$
BEGIN
SELECT COUNT(*) INTO var1 FROM table_name;
RAISE NOTICE 'count: %', var1;
IF var1 = 0 THEN
...
return true;
ELSE
...
return true;
END IF;
return false;
END;
$$;
...
and if it is 0, then it does some checks for the first element, and else it does a bit different checks. Then to insert the data I am using COPY(in another SQL file):
COPY table_name(column1, column2) FROM stdin;
5 2
8 7
\.
but I am getting an error:
ERROR: new row for relation "table_name" violates check constraint "table_name_check"
for the second row. So, I have added RAISE NOTICE to print the count in my schema(as shown in the first block of code above):
RAISE NOTICE 'count: %', var1;
and for the first insert, it gives me count = 0, but for the second I also get count = 0. That is why the function treats the second insert as if it is first and it causes errors. What could be the reason for this problem? It looks like even if the first row has been inserted, there are still no rows on the table after the first insertion.
I have the following table that describes which chemical elements each planet is composed of using percentage.
CREATE TABLE elem_in_planet
(
id_planet INTEGER,
element_symbol CHAR(2),
percent_representation NUMBER CONSTRAINT NN_elem_in_planet NOT NULL,
CONSTRAINT PK_elem_in_planet PRIMARY KEY (id_planet, element_symbol),
CONSTRAINT FK_planet_has_elem FOREIGN KEY (id_planet) REFERENCES planet (id_planet),
CONSTRAINT FK_elem_in_planet FOREIGN KEY (element_symbol) REFERENCES chemical_element (element_symbol)
);
I'm trying to make a trigger that warns users when they add a new element to a planet and the sum of elements in that planet exceeds 100%. I came up with this.
CREATE OR REPLACE TRIGGER elem_in_planet_check
AFTER INSERT OR UPDATE ON elem_in_planet
FOR EACH ROW
DECLARE
sum_var NUMBER;
PRAGMA autonomous_transaction;
BEGIN
SELECT SUM(percent_representation)
INTO sum_var
FROM elem_in_planet
WHERE id_planet = :NEW.id_planet
GROUP BY id_planet;
EXCEPTION
WHEN NO_DATA_FOUND THEN
sum_var := 0;
IF sum_var > 100 THEN
DBMS_OUTPUT.put_line('WARNING: Blah blah.');
END IF;
END;
/
This code seems to throw the NO_DATA_FOUND exception every single time, even though I have inserted test data and when I run the SQL query alone, it works as expected.
I'm new to this and don't understand what I'm doing wrong.
Thank you for any advice.
You have NOT inserted the row into the table, 2 reasons.
The trigger runs as part of the insert statement which has not
completed. So the row does not exist.
You specified "PRAGMA autonomous_transaction" (AKA the create
untraceable bug here statement), did you previously get a mutating
table exception. So you cannot see any data inserted/updated/deleted
by the current transaction.Further if an error did occur the row would still be inserted as you did not raise an error or re-raise the existing error. I suggest you familiarize yourself with the PLSQL block structure. For now you may want to try:
You could use an after statement trigger or after statement section of compound trigger to make this test, do raise_application_error if sum > 100;
BTW as it stands your "if on sum_var > 100" runs only when an error occurs. Anything after the "EXCEPTION" and before END for that block runs only when a error occurs.
create or replace trigger elem_in_planet_check
after insert or update on elem_in_planet
declare
error_detected boolean := False;
begin
for planet in
(
select id_planet, sum_var
from (select id_planet, sum(percent_representation) sum_var
from elem_in_planet
group by id_planet
)
where sum_var > 100
)
loop
dbms_output.put_line('Planet ' || planet.id_planet || ' at '|| planet.sum_var || '% resources exceed 100%');
error_detected:= True;
end loop;
if error_detected then
Raise_application_error('-20001', 'Planet resources cannot exceed 100%');
end if;
end elem_in_planet_check;
I want to check during a request if the constraint are respected, if they are not instead of sending an ERROR message, simply return FALSE. How would I do that?
Example of TABLE I'm using:
CREATE TABLE tree (
name VARCHAR(64) UNIQUE PRIMARY KEY,
leaf INT CHECK (leaf > 0)
);
Example of Functions I'd use:
CREATE FUNCTION add_tree(name, nb_leaf) RETURNS BOOLEAN;
CREATE FUNCTION remove_leaf(tree_name, leaf_to_remove) RETURNS BOOLEAN;
In my function it would be too repetitive to check for the name
IF EXISTS (SELECT name FROM tree WHERE name=tree_name) THEN...
Since I already have a UNIQUE constraint, but if I don't check I get the error message of course, how do I not use the check (IF..) and not get the error message but a return false instead when the input is wrong?
PS: I'm using postgresql if that changes anything
When exception is raised, return false from your function:
EXCEPTION
WHEN OTHERS THEN
return false;
END;
http://www.postgresql.org/docs/9.3/static/plpgsql-control-structures.html
At the end of the page you will see an example:
DECLARE
text_var1 text;
text_var2 text;
text_var3 text;
BEGIN
-- some processing which might cause an exception
...
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
text_var2 = PG_EXCEPTION_DETAIL,
text_var3 = PG_EXCEPTION_HINT;
END;
I want to implement a not-null constraint on an attribute using a trigger.
Here's my code:
create table mytable2(id int);
create or replace function p_fn() returns trigger as $prim_key$
begin
if (tg_op='insert') then
if (id is null) then
raise notice 'ID cannot be null';
return null;
end if;
return new;
end if;
end;
$prim_key$ language plpgsql;
create trigger prim_key
before insert on mytable2
for each row execute procedure p_fn();
But I get an error saying "control reached end of trigger procedure without RETURN" whenever I try to insert a null value. I tried placing the "return new" statement in the inner IF, but it still gave me the same error. What am I doing wrong?
The immediate cause of the problem is that PostgreSQL string comparisons are case sensitive. INSERT is not the same as insert. Try:
IF tg_op = 'INSERT' THEN
Advice
You're only raising a notice. This allows flow of control to continue to the next line in the procedure. You should generally RAISE EXCEPTION to abort execution and roll the transaction back. See RAISE. As it stands, the trigger will cause inserts that do not satisfy the requirement to silently fail, which is generally not what you want.
Additionally, your triggers should usually end with a RAISE EXCEPTION if they're always supposed to return before end of function. That would've helped you see what was going wrong sooner.
I'd add just a couple of things to the good suggestions already made:
make sure you handle the UPDATE case as well
RAISE using the proper error condition. I've given a basic example below - for more formatting options see the docs here.
for clarity, I like to include at least the table name in my trigger name
CREATE OR REPLACE FUNCTION fn_validate_id_trigger() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (TG_OP IN ('INSERT', 'UPDATE')) THEN
IF (NEW.id IS NULL) THEN
RAISE not_null_violation;
END IF;
END IF;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER tr_mytable_validate_id
AFTER INSERT OR UPDATE
ON mytable2
FOR EACH ROW EXECUTE PROCEDURE fn_validate_id_trigger();
Update: this is now a CONSTRAINT trigger and fires AFTER insert or update. In first edit, I presented a column-specific trigger (UPDATE OF id). This was problematic as it would not fire if another trigger executed on the table changed column 'id' to null.
Again, this isn't the most efficient way to handle constraints but it's good to know.