Continue loop when dblink SQL statement timeout occurs - sql

I'm writing a procedure that loops over several remote Databases using dblink, I want to include statement timeout to prevent queries hanging too long. However if such timeout occurs, whole procedure fails with:
ERROR: canceling statement due to statement timeout
SQL state: 57014
Context: while executing query on dblink connection named xxx
I want to ignore it and continue the loop.
Normally such code allows to skip exception throwing notice only, but not with dblink query canceled.
do $$
declare
exceptionMsg text;
BEGIN
select * from foo;
exception when others then get stacked diagnostics exceptionMsg = message_text;
raise notice ' ******EXCEPTION*******
%
**************', exceptionMsg;
END;
$$
It's too long to include whole procedure here, but it loops over database and commits results after each database. Everything works fine, except handling these timeouts, part of the code looks like that:
for rec in (select dbc.db_name, dbc.con_string || ' options = ''-c statement_timeout='||_queryTimeout*1000||''' ' as con_string
from db_connections dbc
)
LOOP
PERFORM dblink_connect(rec.db_name, rec.con_string);
raise notice '% start', rec.db_name ;
BEGIN
insert into results_tbl (db_name, value, query_text)
select rec.db_name, value, _queryText
from dblink(rec.db_name, format($query$
select json_agg(x.*)::text from (%1$s)x -- it's like this to avoid declaring every column used in the query
$query$, _queryText
) ) r (value text);
exception when others then get stacked diagnostics exceptionMsg = message_text;
raise notice ' ******EXCEPTION*******
%
**************', exceptionMsg;
END;
PERFORM dblink_disconnect( rec.db_name );
COMMIT;
raise notice '% done', rec.db_name ;
END LOOP;

As documented,
The special condition name OTHERS matches every error type except QUERY_CANCELED and ASSERT_FAILURE.
So you need to capture QUERY_CANCELED explicitly.
Capturing OTHERS is bad style. Only capture the exceptions you expect.

Related

Oracle SQL: Trigger to add grants after creating view

I am working with vendor application that uses Oracle database. To save the content it uses database tables, which are queried by using views. I do not have any control over that code.
Because of security I gave access to those views to special reporting user, which can only select entries from it.
Whenever some major change is made in the application it drops the appropriate view and creates it anew. Of course all grants are lost and since the changes are made rarely it is easy to forget to backup up and restore them afterwards.
I consulted DBA and he suggested to write a trigger to save grants in temporary table, after which the entries can be used to restore grants. Saving part works fine as expected:
create or replace TRIGGER RECORD_GRANTS_ONDROP
BEFORE DROP ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_name not like 'TEMP_PRIV%' and ora_dict_obj_type='VIEW' then
EXECUTE IMMEDIATE 'CREATE TABLE TEMP_PRIV AS SELECT ''GRANT '' || PRIVILEGE || '' ON MYUSER.'' || TABLE_NAME || '' TO '' || GRANTEE PRIVILEGE_x FROM USER_TAB_PRIVS WHERE GRANTEE not in (''MYUSER'',''PUBLIC'') AND TABLE_NAME=''' || ora_dict_obj_name || '''';
ELSE null;
END IF;
END;
As a result I get a table with all grants assigned to the said view.
For restoring I wanted to run similar trigger:
create or replace TRIGGER RESTORE_GRANTS_AFTERCREATE
AFTER CREATE ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_type='VIEW' then
FOR loop_counter IN (select '''' || privilege_x || '''' AS privilege_x from temp_priv)
LOOP
EXECUTE IMMEDIATE loop_counter.privilege_x;
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
END LOOP;
ELSE null;
END IF;
NULL;
END;
I will note here that is just basic test of concept without any proper checks, so just focus on the big issue here.
When I try to create a view now I get an error:
Error report -
ORA-00604: error occurred at recursive SQL level 1
ORA-00900: invalid SQL statement
ORA-06512: at line 5
00604. 00000 - "error occurred at recursive SQL level %s"
*Cause: An error occurred while processing a recursive SQL statement
(a statement applying to internal dictionary tables).
*Action: If the situation described in the next error on the stack
can be corrected, do so; otherwise contact Oracle Support.
This can only mean that the view is not created at the time when trigger tries to add grants. Syntax wise I successfully ran the command:
BEGIN
EXECUTE IMMEDIATE 'GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER';
END
But when I try to run it using for, or just by itself in 'after create' trigger I get the same error.
Does anyone knows how to approach this, and I would like to avoid jobs at all costs if possible.
Ignoring whether this is a good idea... you're getting that error because you're overthinking your string manipulation. What you're putting into your table looks OK. The problem is when you get it back out. The value in the table is already a string, so you don't need to then enclose it in another set of quotes.
What you're actually running is the equivalent of:
EXECUTE IMMEDIATE '''GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER''';
which will also throw "ORA-00900: invalid SQL statement", rather than your standalone, working, version:
EXECUTE IMMEDIATE 'GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER';
If you swapped the order of your EXECUTE IMMEDIATE and DBMS_OUTPUT calls you'd see the problem statement before it ran, which would be more helpful - you'd see those quotes as part of the string.
So in your second trigger, instead of doing:
FOR loop_counter IN (select '''' || privilege_x || '''' AS privilege_x from temp_priv)
LOOP
EXECUTE IMMEDIATE loop_counter.privilege_x;
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
END LOOP;
just do:
FOR loop_counter IN (select privilege_x from temp_priv)
LOOP
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
EXECUTE IMMEDIATE loop_counter.privilege_x;
END LOOP;
However, this still won't work; it will now get ORA-30511: invalid DDL operation in system triggers. This is presumably because of the restrictions shown in the documentation:
Trigger cannot do DDL operations on object that caused event to be generated.
DDL on other objects is limited to compiling an object, creating a trigger, and creating, altering, and dropping a table.
You said "it is easy to forget to backup up and restore them afterwards" but you're going to have to put a robust process around your upgrades to make sure that does happen.
You can change your process to have a separate step at the end of every upgrade that is always run, which executes all of those stored statements for all objects - maybe skipping or ignoring errors from anything that wasn't recreated - and then drops the temp_priv table.
But you don't really want to (try to) create that temporary table in a trigger anyway - if two views are dropped, the first creates it, the second fails because it already exists. A perhaps more realistic approach might be to create that table once now:
create table TEMP_PRIV (PRIVILEGE_X VARCHAR2(4000));
and then utilise it for all subsequent upgrades, either by populating it with all grants for all views as a single step before the upgrade starts:
INSERT INTO TEMP_PRIVS (PRIVILEGE_X)
SELECT 'GRANT ' || PRIVILEGE || ' ON MYUSER.' || TABLE_NAME || ' TO ' || GRANTEE
FROM USER_VIEWS UV
JOIN USER_TAB_PRIVS UTP ON UTP.TABLE_NAME = UV.VIEW_NAME
WHERE UTP.GRANTEE not in ('MYUSER','PUBLIC');
or if you're still worried about possibly forgetting that step then with a trigger to do it one view at a time as they are dropped:
create or replace TRIGGER RECORD_GRANTS_ONDROP
BEFORE DROP ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_name not like 'TEMP_PRIV%' and ora_dict_obj_type='VIEW' then
INSERT INTO TEMP_PRIV
SELECT 'GRANT ' || PRIVILEGE || ' ON MYUSER.' || TABLE_NAME || ' TO ' || GRANTEE
FROM USER_TAB_PRIVS
WHERE GRANTEE not in ('MYUSER','PUBLIC')
AND TABLE_NAME = ora_dict_obj_name;
END IF;
END;
/
Then at the end of the upgrade process reissue all the statements in the table and clear it down ready for next time:
DECLARE
missing_view EXCEPTION;
PRAGMA EXCEPTION_INIT(missing_view, -942);
BEGIN
FOR loop_counter IN (select privilege_x from temp_priv)
LOOP
BEGIN
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
EXECUTE IMMEDIATE loop_counter.privilege_x;
EXCEPTION
WHEN missing_view THEN
-- report but otherwise ignore
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
END LOOP;
END;
/
TRUNCATE TABLE temp_priv;
If you go with the simpler non-trigger approach then it will re-grant existing privileges, but that's OK. And the exception handler means it'll report but skip any views that were dropped and not recreated, if that ever happens. (You'll still have to deal with any new view of course; your after-create trigger wouldn't have helped with that anyway.) And note that I've truncated the table, rather than dropped it - so it's still there, empty, when the next upgrade comes around and wants to populate it.

Oracle stored procedure not working PLS-00306

i have this questions i am trying to solve and find below what i have solved so far. although the stored procedure haveno error but calling it i get this error :
ERROR at line 2: ORA-06550: line 2, column 3: PLS-00306: wrong
number or types of arguments in call to 'PUB_JOB_COUNT' ORA-06550:
line 2, column 3: PL/SQL: Statement ignored
Requirement:
Create a stored PL/SQL procedure object in the database. The procedure
should insert the publisher’s name, city, telephone number and the
number (count) of jobs he/she requested in the table PublisherDetails
for each Publisher who requested less than three print jobs otherwise
the procedure should display on the screen the publisher name followed
by job number, job start date and job completion date for each job
he/she requested. Screen output (hint: use the concatenation operator
‘||’) should be in the following format:
Please someone help me out please?
Publisher Name: Addison-Wesley
JobNo Start Date Completion Date
12 17-JAN-14 25-JAN-14
14 28-FEB-14 01-APR-14
Finally, a NO-DATA-FOUND exception should be catered for in the
EXCEPTION section and a message displayed on the screen (hint: use
DBMS_OUTPUT.put_line procedure provided by Oracle) informing the user
if such an error arises. Note that in order for DBMS_OUTPUT.put_line
to work in SQL*Plus, you should set SERVEROUTPUT on first. You should
check if the procedure executes properly by invoking the procedure and
checking the content of the PublisherDetails table. Do the following:
a) Create a script file with the necessary code to create the table
PublisherDetails and the PL/SQL procedure in the database; b) Create a
second script file with the following: • An SQL statement that clears
the content of the table PublisherDetails; • A PL/SQL anonymous block
statement to invoke (execute) the PL/SQL procedure; • A SELECT
statement to select all the records in PublisherDetails table.
my tables
publisher(publisherName, publisherCity, phoneNo)
pk
printJob(JobNo, startDate, complitionDate, publisherName)
pk fk(publisher)
publisherdetails(publisherName, publisherCity, phoneNo, JobNo)
pk
Code:
CREATE OR REPLACE PROCEDURE PUB_JOB_COUNT (
JOBNO IN NUMBER
) AS
PUBLISHERNAME PRINTJOB.PUBLISHERNAME%TYPE;
NOTFOUND EXCEPTION;
CURSOR PUBCURSOR IS
SELECT PUBLISHER.PUBLISHERNAME,
PUBLISHER.PUBLISHERCITY,
PUBLISHER.PHONENO,
PRINTJOB.STARTDATE,
PRINTJOB.COMPLETIONDATE,
SUM(JOBNO) AS NUMOFJOBS
FROM PUBLISHER
INNER JOIN PRINTJOB ON PUBLISHER.PUBLISHERNAME = PRINTJOB.PUBLISHERNAME
GROUP BY PUBLISHER.PUBLISHERNAME,
PUBLISHER.PUBLISHERCITY,
PUBLISHER.PHONENO,
PRINTJOB.STARTDATE,
PRINTJOB.COMPLETIONDATE;
PUBREC PUBCURSOR%ROWTYPE;
BEGIN
OPEN PUBCURSOR;
FOR PRINTJOB IN PUBCURSOR LOOP
PUBLISHERNAME := PRINTJOB.PUBLISHERNAME;
DBMS_OUTPUT.PUT_LINE('Publisher Name : ' || PRINTJOB.PUBLISHERNAME);
LOOP
FETCH PUBCURSOR INTO PUBREC;
EXIT WHEN PUBCURSOR%NOTFOUND;
IF PUBREC.NUMOFJOBS <= 3 THEN INSERT INTO PUBLISHERDETAILS VALUES (
PUBREC.PUBLISHERNAME,
PUBREC.PUBLISHERCITY,
PUBREC.PHONENO,
PUBREC.NUMOFJOBS
);
ELSE DBMS_OUTPUT.PUT_LINE(PUBREC.NUMOFJOBS
|| ' '
|| PUBREC.STARTDATE
|| ' '
|| PUBREC.COMPLETIONDATE);
END IF;
END LOOP;
END LOOP;
CLOSE PUBCURSOR;
COMMIT;
EXCEPTION
WHEN NOTFOUND THEN DBMS_OUTPUT.PUT_LINE('Record Not Found');
END;
Gleaned from comments below, the code being used to execute the procedure:
BEGIN
pub_Job_Count;
End;
Your program is expecting an input, a number.
But when you call it, you're not providing said number.
So, the database gets upset and issues this:
PLS-00306: wrong number or types of arguments in call to 'PUB_JOB_COUNT'
To fix it,
BEGIN
pub_Job_Count(1); -- your number is added here, either explicitley or via a variable you add in a DECLARE
END;
/
A basic example
clear screen
create or replace procedure so_missing_inputs (x in integer, y in date) is
begin
dbms_output.put_line('X is: ' || x);
dbms_output.put_line('Y is: ' || to_char(y, 'MM-DD-YYYY HH24:MI:SS'));
end;
/
set serveroutput on
declare
a integer := 2;
b date := sysdate;
begin
-- so_missing_inputs(); -- missing 2 inputes
-- so_missing_inputs(1); -- missing 1 inputs
-- so_missing_inputs(sysdate, 2); -- right number of inputs, but not right data types
so_missing_inputs(x => a, y => b); -- let's be explicit about inputs vs coutning on right order
end;
/
If I run this -
If you were to uncomment one of the previous lines, you'd see the PLS-00306 creep back in.
One final note, on DBMS_OUTPUT. It's a good way to see what things are happening while 'debugging' your code, but it's not a good way to communicate things outside the PL/SQL program.

Postgres Function not working when I have a large result

I'm copying information from table 1(tmp_subtype) to table 2(subtype_user). I have a test table 1 with 15 registers. I run this function into postgres:
CREATE OR REPLACE FUNCTION VERIFY_AND_INSERT_SUPTYPE()
RETURNS text AS $$
DECLARE
register_subtype RECORD;
existe INT DEFAULT 0;
MESSAGE_EXCEPTION TEXT;
cursor_subtype CURSOR
FOR
SELECT tsd.subtype,tsd.type_id_client,tsd.id_client,tsd.email
FROM tmp_subtype tsd;
BEGIN
OPEN cursor_subtype;
LOOP
FETCH cursor_subtype INTO register_subtype;
EXIT WHEN NOT FOUND;
SELECT COUNT(*) INTO existe FROM (
SELECT sdu.id_client FROM subtype_user sdu
WHERE sdu.id_client = register_subtype.id_client AND sdu.type_id_client = register_subtype.type_id_client
LIMIT 1
) SUB0;
IF existe = 0 THEN
INSERT INTO subtype_user(subtype,type_id_client,id_client,email)
VALUES (register_subtype.subtype,register_subtype.type_id_client,register_subtype.id_client,register_subtype.email);
ELSE
UPDATE subtype_user sdu2 SET subtype=register_subtype.subtype,email=register_subtype.email
WHERE sdu2.id_client = register_subtype.id_client AND sdu2.type_id_client = register_subtype.type_id_client;
END IF;
END LOOP;
CLOSE cursor_subtype;
RETURN 'OK';
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS MESSAGE_EXCEPTION = MESSAGE_TEXT;
RETURN MESSAGE_EXCEPTION;
END; $$
LANGUAGE plpgsql;
It works, but When I run this function with the real table 1, it is not working. The function finishes but nothing happend. The real table 1 has 1 million of registers.
Row-by-row processing with embedded counting is a recipe for slow and inefficient processing. Additionally your check for existence won't work if the function is invoked from concurrent transactions. As far as I can tell you can replace the whole loop and cursor with a single INSERT statement:
CREATE OR REPLACE FUNCTION VERIFY_AND_INSERT_SUPTYPE()
RETURNS text
AS $$
DECLARE
MESSAGE_EXCEPTION TEXT;
BEGIN
INSERT INTO subtype_user(subtype, type_id_client, id_client, email)
SELECT tsd.subtype, tsd.type_id_client, tsd.id_client, tsd.email
FROM tmp_subtype tsd
ON conflict (id_client, type_id_client) DO UPDATE
SET subtype = excluded.register_subtype,
email = excluded.email;
RETURN 'OK';
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS MESSAGE_EXCEPTION = MESSAGE_TEXT;
RETURN MESSAGE_EXCEPTION;
END; $$
LANGUAGE plpgsql;
I probable would not add an exception handler to begin with, so that the caller sees the complete exception.
It is hard to say, what is wrong on this code - in this situation RAISE NOTICE is your best friend. I see some issues in your code, but these issues are related to performance. Table with 1 mil rows is nothing.
the code in ISAM programming style can be really slow - instead cycle over cursor use INSERT ON CONFLICT .. statement.
SELECT COUNT(*) ... can be rewritten to little bit faster, but surely more readable form:
IF EXISTS(SELECT ... FROM subtype_user) THEN
UPDATE ...
ELSE
INSERT ...
END IF;
Handling errors from your example is little bit obsolete - catch only exception that you can really solve. Your type of exception handling doesn't solve any, and more, you lose details info about the exception (position, line, ...). Just don't do it.

Elegant way of handling PostgreSQL exceptions?

In PostgreSQL, I would like to create a safe-wrapping mechanism which returns empty result if an exception occurs. Consider the following:
SELECT * FROM myschema.mytable;
I could do the safe-wrapping in the client application:
try {
result = execute_query('SELECT value FROM myschema.mytable').fetchall();
}
catch(pg_exception) {
result = []
}
But could I do such a thing in SQL directly? I would like to make the following code work, but it seems like it should by put into DO $$ ... $$ block and here I'm getting lost.
BEGIN
SELECT * FROM myschema.mytable;
EXCEPTION WHEN others THEN
SELECT unnest(ARRAY[]::TEXT[])
END
Exception handling in PL/pgSQL
PL/pgSQL code is always wrapped into a BEGIN ... END block. That can be inside the body of a DO statement or a function. Blocks can be nested inside - but they cannot exist outside, don't confuse it with plain SQL.
Each block can optionally contain an EXCEPTION clause for handling exceptions, but functions that need to trap exceptions are more expensive, so it's best to avoid exceptions a priori. Postgres needs to prepare for the possibility of rolling back to a point in the transaction before the exception happened, similar to an SQL SAVEPOINT. The manual:
A block containing an EXCEPTION clause is significantly more
expensive to enter and exit than a block without one. Therefore, don't
use EXCEPTION without need.
Example:
Is SELECT or INSERT in a function prone to race conditions?
How to avoid an exception in the example
A DO statement can't return anything. Create a function that takes table and schema name as parameters and returns whatever you want:
CREATE OR REPLACE FUNCTION f_tbl_value(_tbl text, _schema text = 'public')
RETURNS TABLE (value text)
LANGUAGE plpgsql AS
$func$
DECLARE
_t regclass := to_regclass(_schema || '.' || _tbl);
BEGIN
IF _t IS NULL THEN
value := ''; RETURN NEXT; -- return single empty string
ELSE
RETURN QUERY EXECUTE
'SELECT value FROM ' || _t; -- return set of values
END IF;
END
$func$;
Call:
SELECT * FROM f_tbl_value('my_table');
Or:
SELECT * FROM f_tbl_value('my_table', 'my_schema');
Assuming you want a set of rows with a single text column or an empty string if the table does not exist.
Also assuming that a column value exists if the given table exists. You could test for that, too, but you didn't ask for that.
Both input parameters are only case sensitive if double-quoted. Just like identifiers are handled in SQL statements.
The schema name defaults to 'public' in my example. Adapt to your needs. You could even ignore the schema completely and default to the current search_path.
to_regclass() is new in Postgres 9.4. For older versions substitute:
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = _schema
AND table_name = _tbl
) THEN ...
This is actually more accurate, because it tests exactly what you need. More options and detailed explanation:
Table name as a PostgreSQL function parameter
Always defend against SQL injection when working with dynamic SQL! The cast to regclass does the trick here. More details:
How to check if a table exists in a given schema
If you are selecting only one column then the COALESCE() function should be able to do the trick for you
SELECT COALESCE( value, '{}'::text[] ) FROM myschema.mytable
If you require more rows you may require to create a function with types.

Raise notice to print a table's data

I'd like to be able to print some debug information from sql script / function. Of course, I can do this by
RAISE NOTICE 'hello!'
But I also need to print a whole table's data. This doesn't work:
RAISE NOTICE '%' (SELECT * FROM table1)
Is it possible and how?
The most straightforward way would be to iterate over the rows in a for loop and use RAISE NOTICE containing each column you're interested in interpolated in it.
i.e. something like:
FOR items IN SELECT * FROM table1 LOOP
RAISE NOTICE 'col1: %, col2: %', quote_ident(items.col1), quote_ident(items.col2);
END LOOP;
where items is declared as RECORD.
Since postgres 9.3 you can use to_json() to convert record into text suitable for notice,
RAISE NOTICE '%', to_json(record1);
RAISE NOTICE will print table data without alignment, so it will be hard to read. More flexible way is to use refcursor:
DECLARE
_temp_cur1 refcursor = 'unique_name_of_temp_cursor_1';
...
BEGIN
...
OPEN _temp_cur1 FOR
SELECT *
FROM table1;
...
END
Then run function in transaction:
BEGIN;
SELECT my_func();
FETCH ALL FROM "unique_name_of_temp_cursor_1"; --here is double-quotes ""!
ROLLBACK; --do not save any changes to DB during tests (or use COMMIT;)
Such refcursor will be available for reading during the same transaction. If you do not wrap your test with BEGIN and ROLLBACK (or COMMIT), PostgreSQL will not be able to find this one.