I have a table that defines users with administrative rights. The table has a column labeled ADMINISTRATOR. The apex tabular form is set so the column is displayed as check boxes. when a check is present, the value is 1. When the check is empty, the value is null.
The problem I have is any admin accessing this page can add or revoke admin privileges to anyone. This means if an admin accidentally or intentionally revokes permissions from all the other admins including himself, no one can access that tool on the front end.
I want to establish a validation that requires the system to have at least one administrator and throw an error if someone tried to update the table with no 1 in the ADMINISTRATOR column.
I have been trying to determine what validation would work best.
My most recent attempts have been:
Type: Function returning a boolean
Expression:
declare
admincount number(8);
begin
select count(administrator) into admincount from supervisor;
if admincount < 1 then
return false;
else
return true;
end if;
end;
When I try to run this script on Oracle SQL Developer, I get:
Error report -
ORA-06550: line 8, column 4:
PLS-00372: In a procedure, RETURN statement cannot contain an expression
ORA-06550: line 8, column 4:
PL/SQL: Statement ignored
ORA-06550: line 10, column 4:
PLS-00372: In a procedure, RETURN statement cannot contain an expression
ORA-06550: line 10, column 4:
PL/SQL: Statement ignored
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:
What is wrong with this code? Am I applying this expression to the wrong location?
What happens in apex is that your block is actually converted into a function.
declare
admincount number(8);
begin
select count(administrator) into admincount from supervisor;
if admincount < 1 then
return false;
else
return true;
end if;
end;
Is internally changed to be this function with a return value. Then the engine calls this. An anonymous block in isolation can not have a return statement or you will get the error you are seeing.
declare
ret boolean;
function x return boolean is
begin
declare
admincount number(8);
begin
select count(administrator) into admincount from supervisor;
if admincount < 1 then
return false;
else
return true;
end if;
end;
end;
begin
ret := x;
end;
What would be best is to move your logic into an actual function
create or replace function is_admin return boolean
as
admincount number(8);
begin
select count(administrator) into admincount from supervisor;
if admincount < 1 then
return false;
else
return true;
end if;
end;
Then you can test this function in sqldev / sqlcl or any tool.
The resulting expression then used in APEX would simply be
return is_admin;
That won't work in SQL Developer; you must return BOOLEAN into something (another PL/SQL procedure) and then decide what to do. Here's an example:
SQL> select * From ts65_supervisor order by id;
ID ADMINISTRATOR
---------- -------------
1 1
2 1
3
4
5 1
SQL> create or replace function f_super return boolean as
2 admincount number;
3 begin
4 select count(administrator) into admincount from ts65_supervisor;
5
6 return admincount > 1;
7 end;
8 /
Function created.
Is it OK? (3 admins - should be):
SQL> begin
2 if f_super then
3 dbms_output.put_line('OK, more than 1 admin');
4 else
5 dbms_output.put_line('The last one');
6 end if;
7 end;
8 /
OK, more than 1 admin
PL/SQL procedure successfully completed.
After update?
SQL> update ts65_supervisor set administrator = null where id < 5;
4 rows updated.
SQL> begin
2 if f_super then
3 dbms_output.put_line('OK, more than 1 admin');
4 else
5 dbms_output.put_line('The last one');
6 end if;
7 end;
8 /
The last one
PL/SQL procedure successfully completed.
SQL>
I'd suggest you NOT to test it in SQL Developer, but directly in Apex - it'll wrap your code into its own BEGIN - END block(s) and that function might work just fine (note the difference between your code and mine - you don't actually need IF-THEN-ELSE - a simple RETURN with a condition is enough for Oracle to know which one of TRUE/FALSE to return).
Related
I have a Stored Procedure that in turns calls several other Stored Procedures; each of them returns true or false and internally deal with errors by store them into a table.
Something like this:
-- (MAIN STORED PROCEDURE)
BEGIN
CALL STORED_PROC_1('WW','TT','FF',result);
IF result = TRUE then
CALL STORED_PROC_2('a','b','c',result);
...
END IF;
END;
IF result != TRUE THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
-- (END MAIN STORED PROCEDURE)
-------
--Example of Stored Procedure 1
CREATE OR REPLACE PROCEDURE STORED_PROC_1 (IN a TEXT, IN b TEXT, IN c TEXT, INOUT result boolean)
AS $BODY$
BEGIN
-- DO SOME STUFF HERE
IF ERROR_FOUND THEN
INSERT INTO ERROR_LOG VALUES ('Error','Type of Error',CURRENT_DATE);
--COMMIT; (I cannot do this commit here but I would like to save the information going into the ERROR_LOG table)
result := FALSE;
ELSE
result := TRUE;
END IF;
END;
$BODY$;
This is actually what I want; only commit if all return TRUE;
The problem is that inside the STORED_PROC_1 or _2 there are error handlings that write into a Error Log table and ... if there are errors, they will return FALSE in the result and, that in turn will call a rollback and I will loose my Error Log.
Is there a way to create a sort of a memory table that I can load with the error info and write it after the ROLLBACK? Or is there a better way to achieve this?
Thanks a lot.
I would suggest using FUNCTIONs rather than PROCEDUREs if you can for all of this because they have transaction control built-in in a sense. If the FUNCTION fails you automatically rollback its trasaction.
That being said, instead of doing the insert directly I would make a function like the following:
CREATE OR REPLACE FUNCTION func_logerror(error TEXT, errortype TEXT, date DATE DEFAULT CURRENT_DATE)
RETURNS SETOF ERROR_LOG
AS $$
DECLARE
BEGIN
RETURN QUERY
INSERT INTO ERROR_LOG VALUES (error, errortype, date) RETURNING *;
END;
$$;
Call it using either SELECT or PERFORM depending on whether or not you want the results in the calling function.
PERFORM * FROM func_logerror('Error', 'Type of Error', CURRENT_DATE);
After reading your answer response I've come up with this to help clarify. I was suggesting not using stored procedures at all. Instead handle your error cases in a different manner:
-- (MAIN STORED PROCEDURE)
CREATE OR REPLACE FUNCTION FUNC_MAIN()
RETURNS boolean
AS $$
DECLARE
res boolean;
BEGIN
SELECT * INTO res FROM FUNC_1('WW','TT','FF');
IF res = TRUE
THEN
SELECT * INTO res FROM FUNC_2('a','b','c');
...
ELSE
RETURN FALSE;
END IF;
RETURN res;
END;
$$;
-- (END MAIN STORED PROCEDURE)
-------
--Example of Stored Procedure 1
CREATE OR REPLACE FUNCTION FUNC_1 (a TEXT, b TEXT, c TEXT)
RETURNS boolean
AS $$
DECLARE
BEGIN
-- ****complete data validation before writing to tables****
-- If you hit any invalid data return out of the function before writing any persistent data
-- Ex:
IF COALESCE(a, '') = '' -- could be anything that may produce ERROR_FOUND from your example
THEN
RAISE WARNING 'FUNC_1 | param: a: must have a value';
PERFORM * FROM "Schema".func_errlog('Error','Type of Error',CURRENT_DATE);
RETURN FALSE;
END IF;
RETURN TRUE; -- If you've made it to the end of the function there should be no errors
END;
$$;
I have 10 functions, all 10 return '1' if the function has no errors and '0' if function has errors. I want to create another function witch calls all this functions and which checks it out if the functions return 0 or 1. After that, I want to run this function in linux crontab and the function's output (some text from if conditions) to go in a log file.
I'm not sure if I can check this functions like this. Thanks for your time!
CREATE OR REPLACE FUNCTION public.test_al1()
RETURNS text
LANGUAGE 'plpgsql'
COST 100
AS $BODY$
DECLARE
BEGIN
select public.test();
if (select public.test()) = 1 then
RAISE NOTICE 'No errors'
else
RAISE NOTICE 'Errors'
end if;
END
$BODY$;
You were missing a return point for your query and there were also a few ; missing. I'm not really sure what you want to achieve with this function, since you declared the function would return TEXT and there is no RETURN statement.
One option would be to not return anything and use RAISE as you've been doing - keep in mind that the intention of RAISE (without INFO, EXCEPTION, etc.) alone is rather to report error messages:
CREATE OR REPLACE FUNCTION public.test_al1() RETURNS VOID LANGUAGE plpgsql
AS $BODY$
BEGIN
IF public.test() = 1 THEN
RAISE 'Errors';
ELSE
RAISE 'No errors';
END IF;
END
$BODY$;
.. or alternatively you can simplify it a bit by returning the message as TEXT in the RETURN clause.
CREATE OR REPLACE FUNCTION public.test_al1() RETURNS TEXT LANGUAGE plpgsql
AS $BODY$
DECLARE res TEXT DEFAULT 'No errors';
BEGIN
IF public.test() = 1 THEN
res := 'Errors';
END IF;
RETURN res;
END
$BODY$;
Further reading: CREATE FUNCTION
i have this error:
---------------------------
pgAdmin III
---------------------------
An error has occurred:
ERROR: relation "vettura_tariffa" does not exist
LINE 2: from vettura_tariffa
^
QUERY: SELECT ( select valore
from vettura_tariffa
where new.targa=vettura.targa)
CONTEXT: PL/pgSQL function "progettoBD".costout() line 14 at assignment
---------------------------
OK
---------------------------
the code is the following
create or replace function costout()
returns trigger AS
$$
DECLARE
giorno integer;
gg1 integer;
gg2 integer;
tariffa numeric(2,0) ;
costo integer;
BEGIN
gg1=extract (days from new.dataconsegna);
gg2=extract (days from new.dataritiro);
giorno=gg1-gg2;
tariffa=( select valore
from vettura join tariffa on vettura.tipotariffa=tariffa.tipo
where new.targa=vettura.targa);
costo=tariffa * giorni;
new.costoutilizzo=costo;
END;
$$ language plpgsql;
and this one is the trigger's code
create trigger costo_utilizzo
after insert on utilizzo
for each row
execute procedure costout();
It seems like it doesn't find the table vettura, it's the same with other tables, and if I try a simply "select * from vettura" it works fine.
I tried swapping the from with a view but the problem persists.
This error appears after the insert on "utilizzo".
Sorry for my bad english, thank you in advance.
Solution for JAVA problem.
We need to engage in double quotes and bars so that the command is accepted by postgresql.
sql = "INSERT INTO \"SCHEMA\".\"TABLE\"() values(?,?,?)
Ciao Atz34,
The error that is generated indicates that you are calling a function that makes a reference to vettura_tariffa while the code you post refers to vettura JOIN tariffa. Probably a simple mistake somewhere in your code. However, you have a few more issues with your trigger and trigger function.
First of all, you should call a BEFORE INSERT trigger when you modify any fields of NEW; on an AFTER INSERT trigger the changes will not be saved in the table (you can only do side effects like auditing or making changes to other tables).
Second, extract(date from ...) gives problems between months. Assuming your datacosegna and dataritiro are date columns, you can simply subtract them and add 1: NEW.dataritiro - NEW.dataconsegna + 1 (so same-day returns are not gratis).
Third, always RETURN NEW from an insert trigger.
You can then fold all statements into:
CREATE OR REPLACE FUNCTION costout() RETURNS trigger AS $$
BEGIN
SELECT valore * (NEW.dataritiro - NEW.dataconsegna + 1) INTO NEW.costoutilizzo
FROM vettura
JOIN tariffa ON vettura.tipotariffa = tariffa.tipo
WHERE NEW.targa = vettura.targa;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
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;
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.