Showing posts with label PL/SQL. Show all posts
Showing posts with label PL/SQL. Show all posts

Wednesday, March 12, 2025

Errors returned by expressions in SQL queries are not necessarily the same as the errors returned by equivalent PL/SQL expressions.

Have you ever noticed that error codes change depending on whether the context is SQL or PL/SQL?
DECLARE
x number := 0;
y number;
BEGIN
select log(10,x)
into y
from dual;
END;
/

The PL/SQL block above returns following error:

ORA-01428: argument '0' is out of range
ORA-06512: at line 5

But if I change the way I assign the value to y, the error will be much more generic.

DECLARE
x number := 0;
y number;
BEGIN
y := log(10,x);
END;
/
ORA-06502: PL/SQL: numeric or value error
ORA-06512: at line 5 

Now, this is a trivial case, but imagine a situation where you initially wrote the code in a certain way and then it turns out you have to change completely the approach for some reason, a business request, code refactoring, whatever.
If there is an EXCEPTION block catching a specific error, ORA-01428 for instance, after the change it won't catch that error any longer, presumably with some consequences for the final outcome of the procedure or function.

Wednesday, February 26, 2025

Enhanced simple CASE statement syntax in Oracle 23ai

Oracle 23ai extends the syntax of the so-called "simple" CASE statement in PL/SQL, that is the version of CASE where an expression appears right after the CASE keyword.
The list of WHEN blocks now allows dangling expressions (in red below), that is conditional expressions where the left operand is missing.
DECLARE
    x number;
BEGIN
    case x
    when is null THEN
      dbms_output.put_line('null');
    when 1 then 
        dbms_output.put_line('1');
        return;
    when > 1 then 
        dbms_output.put_line('greater than 1');
        return;
    else 
        dbms_output.put_line('else');
        return;
    end case;
END;
/

The PL/SQL block above would raise PL/SQL parsing errors in earlier versions (PLS-00103 on lines 5 and 10).

It is worth noting that in earlier versions the only way to catch WHEN x IS NULL, would be to replace NULL with some other value or, better, use the "searched" CASE statement instead.

For more information, please refer to the official Oracle 23ai documentation.

Tuesday, November 12, 2024

End loop statement can raise ORA-06502 too

I was puzzled when I got an error message allegedly occurring at a line containing an "end loop" statement and it took me a while to figure out that this occurs when either bound of the loop is NULL.

In my case both the initial and final bounds are variables and they were supposed to be not null or so I thought...

Here is a code snippet reproducing the error:

begin
  for i in null..100
  loop
    null;
  end loop;
end;
Error report -
ORA-06502: PL/SQL: numeric or value error
ORA-06512: at line 5
06502. 00000 -  "PL/SQL: numeric or value error%s"
*Cause:    An arithmetic, numeric, string, conversion, or constraint error
           occurred. For example, this error occurs if an attempt is made to
           assign the value NULL to a variable declared NOT NULL, or if an
           attempt is made to assign an integer larger than 99 to a variable
           declared NUMBER(2).
*Action:   Change the data, how it is manipulated, or how it is declared so
           that values do not violate constraints.

So, if you see this error reported at this unusual location, you know what you are up against.

Wednesday, July 24, 2024

DBA_ERRORS and error lines reported for TRIGGERS

As you probably know, the lines shown in views DBA_ERRORS/ALL_ERRORS/USER_ERRORS in the case of triggers are wrong.

But they are not totally wrong, they are just shifted by the amount of lines between the line containing the keyword TRIGGER and either DECLARE or BEGIN, whichever occurs first.

See the example below for an error reported by the dba_errors view on line 2 caused by missing grants on the table used in the variable declaration:

TRIGGER GAGREA2.TR_ASSEGNAZIONE_AI
AFTER INSERT ON ASSEGNAZIONE
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
LI_ID_ASSEGNAZIONE_DOM_RICGEN GAGREA2.DOMANDA_ASG_SPECIFICHE.ID_ASSEGNAZIONE_DOM_RICGEN%TYPE;
BEGIN

GAGREA2.PKG_TRIGGER.SET_ID_ASSEGNAZIONE_DOM_RICGEN ( :NEW.ID_DOMANDA, LI_ID_ASSEGNAZIONE_DOM_RICGEN );

END TR_ASSEGNAZIONE_AI;

So, the real line number can be obtained adding the number of the line containing DECLARE (or BEGIN if DECLARE is missing) minus 1, that is 2 + 5 - 1 = 6.


Tuesday, July 04, 2023

Weird behavior of UPDATE when a SDO_GEOMETRY column needs to be updated

A few days ago I posted a comment in the SQL & PL/SQL forum regarding a strange situation that occurs when updating a SDO_GEOMETRY column with a value resulting from the execution of a user-defined function, i.e.:

UPDATE TEST_GEOM
   SET SHAPE = MY_FUNCTION(SHAPE)
 WHERE ID = 1;

It turns out that in the case of a SDO_GEOMETRY column MY_FUNCTION is executed 8 times for each row being updated.

This phenomenon is further enhanced by adding a "sleep" of 1 second inside the function, which causes the update to take 8 seconds. 

This has been verified on Oracle 12.2.0.1.0 and Oracle Version 19.18.0.0.0.

Oddly enough, if you update another non-geometry column with a second user-defined function at the same time, the latter is executed only once.

UPDATE TEST_GEOM
   SET SHAPE = MY_FUNCTION(SHAPE),
       NUM   = ANOTHER_FUNCTION(10)
 WHERE ID = 1;

You can find below the code to reproduce the problem.

Anton Scheffer, who replied to my comment, pointed out that adding DETERMINISTIC to the definition of the function would fix the problem, which is an excellent workaround, but it is still a workaround, I mean, why should the function gets executed 8 times in the first place?

And why a second numeric function doesn't need the DETERMINISTIC clause to be added but still works as expected?

These questions remain unanswered.

 

CREATE SEQUENCE DUMMY_SEQ MINVALUE 0 ORDER;
CREATE SEQUENCE DUMMY2_SEQ MINVALUE 0 ORDER;

SELECT DUMMY_SEQ.NEXTVAL FROM DUAL;
SELECT DUMMY2_SEQ.NEXTVAL FROM DUAL;


CREATE TABLE "TEST_GEOM" 
( "ID"    INTEGER, 
  "NUM"   NUMBER,
  "SHAPE" SDO_GEOMETRY
 );

SET DEFINE OFF;
Insert into TEST_GEOM (ID, SHAPE) 
values (1, MDSYS.SDO_GEOMETRY(2003, NULL, NULL, MDSYS.SDO_ELEM_INFO_ARRAY(1, 1003, 1, 53, 2003, 1), MDSYS.SDO_ORDINATE_ARRAY(1617857.39992003, 4927454.61993996, 1617859.95997274, 4927451.20990379, 1617869.76990222, 4927448.42992377, 1617871.36989997, 4927394.54999967, 1617870.37998793, 4927351.50990073, 1617883.78998438, 4927349.87990116, 1617900.64988697, 4927346.58992017, 1617900.31990137, 4927344.38001657, 1617893.83995069, 4927309.679913, 1617915.1700947, 4927297.58996787, 1617928.12005408, 4927335.75984816, 1617940.09995791, 4927328.25990929, 1617945.94009625, 4927325.55002712, 1617943.32575782, 4927334.82798004, 1617943.28014197, 4927334.99066672, 1617944.51009718, 4927354.62002357, 1617936.85009823, 4927386.20001927, 1617932.40883821, 4927415.70410291, 1617932.61397489, 4927415.84461214, 1617938.54007733, 4927412.46993659, 1617944.46009097, 4927419.68995848, 1617951.05009863, 4927434.1299835, 1617957.74998072, 4927474.19009812, 1617887.96998141, 4927460.48009826, 1617866.40997905, 4927456.55009778, 1617857.39992003, 4927454.61993996, 1617887.6001005, 4927375.78008369, 1617887.72009935, 4927395.83989044, 1617905.43989081, 4927394.20990962, 1617903.38991087, 4927372.97011743, 1617887.6001005, 4927375.78008369)));

COMMIT;

CREATE OR REPLACE FUNCTION DUMMY2(
  P_NUM IN NUMBER
)
RETURN NUMBER
AS
 I INTEGER;
BEGIN
  I := DUMMY2_SEQ.NEXTVAL;
  RETURN P_NUM;
END;
/

CREATE OR REPLACE FUNCTION DUMMY(
  P_SHAPE IN SDO_GEOMETRY
)
RETURN SDO_GEOMETRY
AS
 I INTEGER;
BEGIN
  DBMS_SESSION.SLEEP(1);
  I := DUMMY_SEQ.NEXTVAL;
  RETURN P_SHAPE;
END;
/

SET TIMING ON
UPDATE TEST_GEOM
   SET SHAPE = DUMMY(SHAPE),
       NUM   = DUMMY2(10)
 WHERE ID = 1;

COMMIT;

SET TIMING OFF

SELECT DUMMY_SEQ.CURRVAL FROM DUAL;
SELECT DUMMY2_SEQ.CURRVAL FROM DUAL;

DROP SEQUENCE DUMMY_SEQ;
DROP SEQUENCE DUMMY2_SEQ;
DROP TABLE TEST_GEOM PURGE;

Wednesday, October 26, 2022

The strange case of the REVERSE function

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Goal: reverse the order of the characters in a string:

select reverse('hello') s from dual;

exec dbms_output.put_line(reverse('hello'));
 
S    
-----
olleh


Error starting at line : 11 in command -
BEGIN dbms_output.put_line(reverse('hello')); END;
Error report -
ORA-06550: line 1, column 28:
PLS-00201: identifier 'REVERSE' must be declared
ORA-06550: line 1, column 7:
 

The REVERSE function exists only in the context of a SQL statement, therefore in order to execute it in a PL/SQL block you need to put it inside a SELECT ... FROM.

Interestingly, REVERSE is nowhere to be found in the SQL Reference but the function  seems to be available since at least Oracle 11g.

Another possibility is to write your own PL/SQL REVERSE function (but with a different name).

See message translations for PLS-00201 and search additional resources.

Thursday, June 03, 2021

JSON_QUERY behavior in SQL vs PL/SQL (Oracle 12.2 only?)

Frankly speaking I can't say if this is expected behavior for JSON_QUERY but I find it somewhat inconsistent:

set serveroutput on
begin 
 dbms_output.put_line(json_query('X','$.test'));
end;
/
[] 
select json_query('X', '$.test') v from dual;
====== 
(null)

So, on Oracle 12.2 in PL/SQL the string returned is not null, whilst in SQL is null.

From a test I made later on Oracle 18c the anonymous PL/SQL block above returns null, so I am inclined to consider this a bug of Oracle 12.2.

It's also worth noting that adding NULL ON ERROR clause in Oracle 12.2 doesn't change the result, which is also another possible bug (or a variation of the same problem).

Eventually I chose to use ERROR ON ERROR then catch the exception and return null, because that should avoid the risk of inconsistent results in case the db is upgraded some time in the future.

Thursday, March 28, 2013

invalid LOB locator specified: ORA-22275

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Just a simple reminder (I tend to forget things over the centuries) when using procedure DBMS_LOB.CONVERTTOCLOB with persistent lob locators:
  1. don't forget to include the FOR UPDATE clause in the SELECT statement fetching the lob locators. 
  2. don't forget to initialize the destination lob (in this case the clob column DOC)  with EMPTY_CLOB().
otherwise ORA-22275 may occur (see also another cause of this error).

...
 select doc, bdoc, charset
   into l_clob, l_blob, l_cset
   from file_imports
  where id = p_fileid
    for update;

...
 dbms_lob.converttoclob(
  l_clob,
  l_blob,
  dbms_lob.lobmaxsize,
  l_coff,
  l_boff, 
  l_cset,
  l_context,
  l_warn);
...
 
ORA-06502: PL/SQL: numeric or value error: invalid LOB locator specified: ORA-22275 

In the PL/SQL fragment above, column DOC was null, which is a different value from EMPTY_CLOB().
As this column is always null when the record is created, I modified the default value of the column as follows:

alter table file_imports modify doc default empty_clob();

This avoids the annoyance of having to update the row and set DOC to EMPTY_CLOB() before calling DBMS_LOB.CONVERTTOCLOB.


See message translations for ORA-22275 and search additional resources.

Friday, February 24, 2012

Something I didn't know up TO_DATE

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Look at this (my session date format is DD/MM/YYYY):

select to_date('2011','YYYY') d from dual;
 
D                   
---------------------
01-02-2011 00:00:00
 
 
select trunc(to_date('2011','YYYY'),'Y') d from dual;
D                   
---------------------
01-01-2011 00:00:00 

I didn't know that when converting a year from characters to a date, Oracle would return the value using the current month (in the SQL reference I can't find anything about this behavior).

I found this out because while working on some queries spanning over a 12 month period supposedly starting from the first month of the year, I could not understand why I was seeing a missing January 2011 and an unwanted January 2012 in the result set.

Initially I feared that this was something introduced with Oracle 11gR2, but I later verified that this happens on Oracle 10gR2 as well, so it must have always been like this.

Fortunately in my applications I never store date information as strings, so I don't really expect this to affect my software, but finding this at a customer's site, makes me think that I'll need to search each and every occurrence of this and replace it with the TRUNCated expression in order to make their queries consistent. 

You never stop learning.

Tuesday, January 18, 2011

How fast can we replace multiple strings at once?

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

One of my favorite techniques for sending emails from Apex is to put the text of the message in a page region and then retrieve it through the apex dictionary. In most cases this text contains some substitution strings that i need to replace with a dynamic value, pretty much in the same way Apex does itself when rendering a page.
When i need to process a large number of emails, i usually "cache" the mail message body in a global variable initialized inside the package body initialization section, such that the apex repository is queried only when a new oracle session is created or the package body is invalidated for some reason.

Whether it is better to store all these email texts in a separate page or inside the relevant page with a display condition of "never" is a matter of taste, the most important aspect is in the fact that i am keeping these "user interface" components together with my application and when i export it, I can be sure that my email bodies will follow without the need of any additional operation. Moreover they are located in a place where they can be easily edited.

While i am using the aforementioned approach since years, this morning i decided to investigate a little further the performance aspects of replacing my custom substitution strings with their dynamic values, so i set up some search and replace benchmarking code in order to understand if there was a method better than others.
When it comes to measure optimizations like these, you must consider that most likely you won't perceive any visible difference unless you iterate the operation on a large scale. This is the reason why in the code below i had to repeat the same operation 100000 times to begin seeing some variation.

Also, when running such benchmarks, you should perform the operation in a controlled environment (no users kicking in while doing the job) and repeat the test cases several times to ensure that you are comparing consistent samples, excluding the first run where the parse time will adversely impact the measure.
That's why i repeated each test case five times recording the best and worst performance, excluding the first run of each test case.
-- first method: explicit IFs
declare
l_body1 varchar2(4000);
l_body2 varchar2(4000);
p_s0 varchar2(50) := '#CODICE#';
p_v0 varchar2(50) := 'SI0000001';
p_s1 varchar2(50) := '#DENOMINAZIONE#';
p_v1 varchar2(50) := 'prova';
p_s2 varchar2(50) := '#USERNAME#';
p_v2 varchar2(50) := 'U0000001';
p_s3 varchar2(50) := '#PASSWORD#';
p_v3 varchar2(50) := 'test';
p_s4 varchar2(50);
p_v4 varchar2(50);
p_s5 varchar2(50);
p_v5 varchar2(50);
p_s6 varchar2(50);
p_v6 varchar2(50);
p_s7 varchar2(50);
p_v7 varchar2(50);
p_s8 varchar2(50);
p_v8 varchar2(50);
p_s9 varchar2(50);
p_v9 varchar2(50);
begin
select region_source
into l_body1
from apex_application_page_regions
where application_id = 55001 and
page_id = 9999 and
static_id = 'mail_message';

for i in 1..100000 loop
if p_s0 is not null then
l_body2 := replace(l_body1, p_s0, p_v0);
else
l_body2 := l_body1;
end if;
if p_s1 is not null then
l_body2 := replace(l_body2, p_s1, p_v1);
end if;
if p_s2 is not null then
l_body2 := replace(l_body2, p_s2, p_v2);
end if;
if p_s3 is not null then
l_body2 := replace(l_body2, p_s3, p_v3);
end if;
if p_s4 is not null then
l_body2 := replace(l_body2, p_s4, p_v4);
end if;
if p_s5 is not null then
l_body2 := replace(l_body2, p_s5, p_v5);
end if;
if p_s6 is not null then
l_body2 := replace(l_body2, p_s6, p_v6);
end if;
if p_s7 is not null then
l_body2 := replace(l_body2, p_s7, p_v7);
end if;
if p_s8 is not null then
l_body2 := replace(l_body2, p_s8, p_v8);
end if;
if p_s9 is not null then
l_body2 := replace(l_body2, p_s9, p_v9);
end if;
end loop;
end;

-- best 1.635s, worst 1.716s

-- second method, brute force replace
declare
l_body1 varchar2(4000);
l_body2 varchar2(4000);
p_s0 varchar2(50) := '#CODICE#';
p_v0 varchar2(50) := 'SI0000001';
p_s1 varchar2(50) := '#DENOMINAZIONE#';
p_v1 varchar2(50) := 'prova';
p_s2 varchar2(50) := '#USERNAME#';
p_v2 varchar2(50) := 'U0000001';
p_s3 varchar2(50) := '#PASSWORD#';
p_v3 varchar2(50) := 'test';
p_s4 varchar2(50);
p_v4 varchar2(50);
p_s5 varchar2(50);
p_v5 varchar2(50);
p_s6 varchar2(50);
p_v6 varchar2(50);
p_s7 varchar2(50);
p_v7 varchar2(50);
p_s8 varchar2(50);
p_v8 varchar2(50);
p_s9 varchar2(50);
p_v9 varchar2(50);
begin
select region_source
into l_body1
from apex_application_page_regions
where application_id = 55001 and
page_id = 9999 and
static_id = 'mail_message';

for i in 1..100000 loop
l_body2 := replace(l_body1, p_s0, p_v0);
l_body2 := replace(l_body2, p_s1, p_v1);
l_body2 := replace(l_body2, p_s2, p_v2);
l_body2 := replace(l_body2, p_s3, p_v3);
l_body2 := replace(l_body2, p_s4, p_v4);
l_body2 := replace(l_body2, p_s5, p_v5);
l_body2 := replace(l_body2, p_s6, p_v6);
l_body2 := replace(l_body2, p_s7, p_v7);
l_body2 := replace(l_body2, p_s8, p_v8);
l_body2 := replace(l_body2, p_s9, p_v9);
end loop;
end;

-- best 1.766s, worst 1.867s

--third method, using collections as parameter for increased flexibility
create or replace
function multi_replace(
p_str in varchar2,
p_search_arr in apex_application_global.vc_arr2,
p_replace_arr in apex_application_global.vc_arr2)
return varchar2 as
l_str varchar2(32767);
begin

if p_search_arr.count != p_replace_arr.count then
raise_application_error(-20001, 'arrays count do not match');
end if;

l_str := p_str;
for i in 1..p_search_arr.count loop
l_str := replace(l_str, p_search_arr(i), p_replace_arr(i));
end loop;
return l_str;
end;
/
declare
l_body1 varchar2(4000);
l_body2 varchar2(4000);
l_s_tab apex_application_global.vc_arr2;
l_v_tab apex_application_global.vc_arr2;
begin
select region_source
into l_body1
from apex_application_page_regions
where application_id = 55001 and
page_id = 9999 and
static_id = 'mail_message';

l_s_tab(1) := '#CODICE#';
l_v_tab(1) := 'SI0000001';
l_s_tab(2) := '#DENOMINAZION#';
l_v_tab(2) := 'prova';
l_s_tab(3) := '#USERNAME#';
l_v_tab(3) := 'U0000001';
l_s_tab(4) := '#PASSWORD#';
l_v_tab(4) := 'test';

for i in 1..100000 loop
l_body2 := multi_replace(l_body1, l_s_tab, l_v_tab);
end loop;
end;

-- best 1.679s, worst 1.748s

So, the first method seems slightly faster, albeit the code is longer. The first technique could become a little bit faster if we assume that substitution strings are "dense", that is if no susbstitution string is given for p_s1, then we assume that subsequent strings must be all NULLs and replace the distinct IFs with a series of ELSIFs.
The third method is slightly slower but it adds some flexibility as there are no restrictions in terms of the total number of substitution strings to be processed and their "density".

Unless someone else finds a better way of doing this (please let me know!), the conclusion is that all the three methods perform in comparable times and you won't notice any practical difference regardless of the method, therefore I'd go for the third method because it gives maximum flexibility to the programmer.

Monday, December 20, 2010

The strange old case of ORA-01403 no data found exception

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Even if I found out later that i am only three years late in discovering the situation I am going to describe, when i saw the results i was really puzzled.

I don't know if i was more puzzled by the curious handling of the NO_DATA_FOUND exception in SQL or by the fact that i stumbled on it only today.
At any rate, it's something that can lead to unexpected results if you make the mistake to mix SQL and PL/SQL without taking the appropriate countermeasures.
create or replace
function strange(p in number)
return number
as
n number;
begin
if p = 0 then
select p
into n
from dual;
elsif p = 1 then
select p
into n
from dual
where 1=0;
else
begin
select p
into n
from dual
where 1=0;
exception
when no_data_found then
raise_application_error(-20001, 'no_data_found');
end;
end if;
return n;
end;

select strange(0) v from dual;

V
----
0

select strange(1) v from dual;

V
----
(null)

select strange(2) v from dual;

ORA-20001: no_data_found

declare
n number;
begin
n := strange(1);
end;
/

ORA-1403: no data found

If you wonder why SQL handles differently the same function call to STRANGE(1), you may want to have a look at the following thread on AskTom.
Until we fiddle with SELECT function FROM DUAL statements in SQLPlus for quick testing purposes we may be surprised to see that it returns NULL instead of an error, but the potential for more subtle problems comes when you invoke this function as a SQL cursor inside a PL/SQL program:

set serveroutput on
declare
n integer;
begin
select strange(1)
into n
from dual;
dbms_output.put_line(nvl(n,-1));
end;
/

-1
The lesson here is to always trap the NO_DATA_FOUND error inside the function if we want to propagate the error to the caller, because we cannot predict if the caller will invoke the function from within a SQL cursor or as a PL/SQL function.

As a last note of folklore, in the PL/SQL Reference Manual for 10gR2 there is a little note warning the user about this eccentric exception, but for some reason it seems it has been removed from the corresponding manual of 11gR2, however the behavior remained "consistent" across the oracle versions.

See message translations for ORA-01403 and search additional resources.

Friday, September 24, 2010

ORA-04091: table string.string is mutating, trigger/function may not see it

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Today i learned something new about the mutating trigger error and in particular i learned that under certain conditions, without implementing any workaround, the trigger will work without issuing such exception.

I was reviewing some code written by other people and i stumbled upon the following trigger definition:
CREATE OR REPLACE
TRIGGER "BIU_MYTABLE"
BEFORE INSERT OR UPDATE
ON "MYTABLE"
FOR EACH ROW
DECLARE
v_newval NUMBER := 0;
v_incval NUMBER := 0;
BEGIN
IF :new."ID" IS NULL
THEN
SELECT "MYTABLE_SEQ".NEXTVAL INTO v_newval FROM DUAL;

IF v_newval = 1
THEN
SELECT NVL (MAX ("ID"), 0) INTO v_newval FROM "MYTABLE";

v_newval := v_newval + 1;

LOOP
EXIT WHEN v_incval >= v_newval;

SELECT "MYTABLE_SEQ".NEXTVAL INTO v_incval FROM DUAL;

END LOOP;
END IF;

:new."ID" := v_newval;
END IF;

END;
My first reaction was to raise my flag and say: hey, watch out, this code is working by pure chance, simply because the sequence is greater than 1 and the inner code is never run (besides the fact that this method of updating the sequence value is certainly questionable for tables containing very large values for ID).
My gut feeling was that ORA-04091 would be returned after dropping and recreating the sequence starting with 1 and attempting to insert a new row into the table.
After talking with the people who are in charge of this program, they assured me that they never came across any problems, even when they migrated the data initially, so i set out to make a little test on my own.

With some surprise i saw that running a statement like
insert into mytable (col_1, col_2) values('test 1','test 2');
did not return any errors.

How can it be ?!?
I knew i cannot SELECT from the table being updated, unless i adopt a much more elaborated approach.

After some research on the web, i came across the following OTN thread, where Justin Cave explains how he found out about before insert triggers selecting from the same table. As Justin noted, this curious case is explained by Tom Kyte in an older thread on AskTom, and in the end the rule of thumb can be summarized as follows:

a before insert trigger may select from the same table being inserted if the original statement is in the form:
insert into mytable (col_1, col_2) values('test 1','test 2');
the same trigger will fail if the original statement is in the form:
insert into mytable (col_1, col_2) select 'test 1','test 2' from dual;

SQL Error: ORA-04091: table TEST.MYTABLE is mutating, trigger/function may not see it
ORA-06512: at "TEST.BIU_MYTABLE", line 11
ORA-04088: error during execution of trigger 'TEST.BIU_MYTABLE'
In my opinion this isn't good news, because having some code that works depending on the method of inserting rows is not going to make your application very robust.

And as a final touch, my customer claims that the code of this trigger has been generated by the migration assistant of SQL Developer.

See message translations for ORA-04091 and search additional resources.

Thursday, April 08, 2010

When APEX_UTIL.STRING_TO_TABLE is not enough

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Yesterday i needed to find a way to ensure that a list values entered manually by a user were unique.
If these values came from a table, it would be fairly easy to build a query returning DISTINCT values, but in this case values are pulled in as comma separated values.

This type of problems are suitable for set operations using PL/SQL collections because we are dealing with a limited, although unspecified amount of values that are not permanently stored as individual rows in a table.
PL/SQL comes with an array of operators for manipulating collections as sets and you can find a few basic examples in PL/SQL Language Reference manual. The functional description of each set operator can be found in the SQL Reference instead. The links refer to Oracle version 11gR2 but these functions are available at least since version 10gR1. There is also a nice tutorial by Steven Feuerstein discussing these functions in an old Oracle Magazine issue, so you may want to check it out.

It is worth noting that such set operators work only on PL/SQL nested tables, but not on associative arrays or varying arrays and this makes a difference as we shall see.

One of the most common operations involving collections in Oracle Application Express is to take some delimited string (by commas, semicolons, colons or spaces typically) and convert into a collection or viceversa using function APEX_UTIL.STRING_TO_TABLE (or its counterpart APEX_UTIL.TABLE_TO_STRING). Unfortunately the result of function STRING_TO_TABLE is APEX_APPLICATION_GLOBAL.VC_ARR2, that is an associative array, not a PL/SQL table, which means that an attempt to use the SET function on it will lead to the following run time error:
PLS-00306: wrong number or types of arguments in call to 'SET'
This error can be puzzling at first because the difference between a PL/SQL table and an associative array indexed by positive integers is very subtle and the only way to say what kind of collection type is APEX_APPLICATION_GLOBAL.VC_ARR2 is to look at its definition, which can be not so straightforward for an unprivileged user. So, once we are aware of the fact that APEX_APPLICATION_GLOBAL.VC_ARR2 is an associative array, we are basically stuck at the original problem.
How do we move forward?

The options are the following:
  1. stick to the apex STRING_TO_TABLE function and copy the values one by one into a PL/SQL nested table, using a FOR...LOOP structure.
  2. write you own STRING_TO_TABLE function and return a PL/SQL table instead.
  3. find another way of quickly transforming the associative array into a PL/SQL table other than #1.
  4. normalize the values yourself instead of using SET.
  5. ?
I opted for option #2 because from a performance point of view is the most effective probably and it also gave me some additional flexibility, but let's review each point first.

Option #1 saves me from rewriting a function but it forces me to perform a not so efficient extra loop that it's also requiring extra memory allocation.
Option #2 is probably comparable in terms of memory and speed although i hate to "reinvent the wheel", unless this is absolutely necessary. It gives me also the advantage of further customization, like space trimming and case insensitive or sensitive comparison.
Option #3 cannot be performed with CAST, so i guess there is no easy way of doing it unless we fall back to option #1, but i do not exclude it could be done in another fashion.
Option #4 is probably the worst choice because i need to replace an efficient piece of C code with my own PL/SQL logic based on a search loop.
Option #5 is an open question, if you know a better way of doing this, please let me know.

So, after rewriting my custom STR2TAB function, i could finally leverage the power of SET function and simply return the result in the following manner:
DECLARE
str varchar2(1000) := 'A,B,C,A,D';
TYPE nested_typ IS TABLE OF VARCHAR2(255);
nt1 nested_typ;
nt2 nested_typ;
PROCEDURE print_nested_table(p_nt nested_typ) IS
output VARCHAR2(128);
BEGIN
IF p_nt IS NULL THEN
DBMS_OUTPUT.PUT_LINE('Results: ');
RETURN;
END IF;
IF p_nt.COUNT = 0 THEN
DBMS_OUTPUT.PUT_LINE('Results: empty set');
RETURN;
END IF;
FOR i IN p_nt.FIRST .. p_nt.LAST
LOOP
output := output || p_nt(i) || ' ';
END LOOP;
DBMS_OUTPUT.PUT_LINE('Results: ' || output);
END;

FUNCTION str2tab(
p_text in varchar2,
p_separator in varchar2 default ',')
return nested_typ
as
l_string varchar2(32767) := p_text || p_separator;
l_data nested_typ := nested_typ();
n binary_integer;
begin
loop
exit when l_string is null;
n := instr( l_string, p_separator );
l_data.extend;
l_data(l_data.count) := substr(l_string, 1, n-1);
l_string := substr(l_string, n + length(p_separator));
end loop;

return l_data;
END;
BEGIN
nt1 := str2tab(str);
nt2 := set(nt1);
print_nested_table(nt2);
END;
/

Results: A B C D
In conclusion, apex function APEX_UTIL.STRING_TO_TABLE does an excellent job when you are taking the values as they have been typed in, but if you need to work on the values as a set, then you'll need to come up with your own custom function.

See message translations for PLS-00306 and search additional resources.

Thursday, July 16, 2009

ORA-22908: reference to NULL table value

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

This exception is raised when an attempt of updating the rows of an atomically null nested table column is made.
For instance:

create type complex_number as (real_part number, imaginary_part number);

create or replace type complex_tab as table of complex_number;

create table complex_sets (
set_id number,
set_name varchar2(50),
set_values complex_tab)
nested table set_values store as nested_complex_set_values;

insert into COMPLEX_SETS (SET_ID,SET_NAME,SET_VALUES)
values (1,'first set',null);

update table(select set_values
from complex_sets
where set_id = 1)
set real_part = real_part + 1
where real_part = 0;

ORA-22908: reference to NULL table value
The nested table underlying the column set_values for row having set_id = 1 has not been initialized, in other words it is said to be atomically null.
Let's repeat the last steps in order to initialize the nested table, ORA-22908 will disappear:

delete from complex_sets where set_id = 1;

insert into COMPLEX_SETS (SET_ID,SET_NAME,SET_VALUES)
values (1,'first set',complex_tab(complex_number(0,0), complex_number(0,1)));

update table(select set_values
from complex_sets
where set_id = 1)
set real_part = real_part + 1
where real_part = 0;
--

2 rows updated

select a.set_id, rownum, b.real_part, b.imaginary_part
from complex_sets a, table(a.set_values) b
where set_id = 1;

SET_ID ROWNUM REAL_PART IMAGINARY_PART
------ ------ --------- --------------
1 1 1 0
1 2 1 1
I initialized the nested table with two rows, each containing a complex_number (user-defined) type, note however that it is possible to initialize the nested table to an empty set by specifying the nested table type constructor:

delete from complex_sets where set_id = 1;

insert into COMPLEX_SETS (SET_ID,SET_NAME,SET_VALUES)
values (1,'first set',complex_tab());

update table(select set_values
from complex_sets
where set_id = 1)
set real_part = real_part + 1
where real_part = 0;

--
0 rows updated
Whether it is preferable to have atomically null nested tables or nested tables initialized to an empty set depends on your application. If most nested table columns are going to remain null, then it can make sense to keep them atomically null, otherwise you might consider initializing the nested table to an empty set either using the default clause for the column or inside a before insert trigger or handle ORA-22908 at the application code level, depending on performance considerations.
-- initialize the nested table to an empty set using the DEFAULT column clause

drop table complex_sets cascade constraints;

create table complex_sets (
set_id number,
set_name varchar2(50),
set_values complex_tab default complex_tab())
nested table set_values store as nested_complex_set_values;

insert into COMPLEX_SETS (SET_ID,SET_NAME)
values (1,'first set');

update table(select set_values
from complex_sets
where set_id = 1)
set real_part = real_part + 1
where real_part = 0;
--

0 rows updated

-- handling ORA-22908 inside the application
declare
l_set number := 1;
null_table exception;
pragma exception_init(null_table, -22908);
begin
update table(select set_values
from complex_sets
where set_id = l_set)
set real_part = real_part + 1
where real_part = 0;
exception
when null_table then
update complex_sets
set set_values = complex_tab()
where set_id = l_set;
end;
/

See message translations for ORA-22908 and search additional resources.

Tuesday, May 19, 2009

ORA-06553: PLS-221: 'name' is not a procedure or is undefined

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

This type of parsing error is quickly explained:
select dbms_db_version.version from dual;

SQL Error: ORA-06553: PLS-221: 'VERSION' is not a procedure or is undefined
In the example above dbms_db_version.version is not a packaged function, but a packaged constant.
Packaged constants can be used inside PL/SQL procedures but not inside ordinary SQL statements, however you can specify such constants in SQL DML statements inside programs:
declare
v number;
begin
select dbms_db_version.version into v
from dual;
dbms_output.put_line(v);
end;
/

If you need to specify constants inside DML statements such as SELECT/DELETE/INSERT/UPDATE statements, then you can create wrapping functions as follows:

create or replace function db_version
return integer deterministic
is
begin
return dbms_db_version.version;
end;

select db_version from dual;

DB_VERSION
-------------
10

See message translations for PLS-00221 and search additional resources.

Monday, March 30, 2009

LENGTHB and SUBSTRB: when smaller is bigger

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Lately I'm working a lot with UTF-8 files and while doing some test i came across the following situation, which sounds rather odd at a first sight but it's perfectly legitimate in the way Oracle treats multi-byte strings, so i thought it could be an interesting story for those who rarely deal with this class of problems.

In short, given a multibyte string (i.e. AL32UTF8), when you take a byte-wise substring, a bigger chunk of bytes can result in a shorter string character-wise.

Standard functions SUBSTR and LENGTH have their byte-wise counterparts called SUBSTRB and LENGHTB respectively.
Now, let's take a multibyte string made up of two alternating Unicode characters, "A" = ascii 41h and the ellipsis "..." = Unicode 2026h.
As i explained some time ago, in order to convert back and forth Unicode character codes, you need to call dedicated string functions like UNISTR and ASCIISTR. For instance, the internal code for Unicode character 2026h can be obtained with ASCII(UNISTR('\2026')) which returns the magic number 14844070, that you see in the source code below.

The following three blocks of PL/SQL show this strange phenomenon, where the standard LENGTH function initially returns 6 for a substring of 10 bytes, then 5 for a substring of 11 bytes, then again 6 for a substring of 12 bytes. The relevant parameters and values are highligthed in color.

declare
s varchar2(50) := rpad(chr(14844070),10, 'A'||chr(14844070));
t varchar2(16) := substrb(s,1,10);
begin
for i in 1..length(s) loop
dbms_output.put(ascii(substr(s,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
for i in 1..length(t) loop
dbms_output.put(ascii(substr(t,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
dbms_output.put_line('length:'||length(t));
dbms_output.put_line('bytes:'||lengthb(t));
end;
/

14844070|65|14844070|65|14844070|65|14844070|65|14844070|65|
14844070|65|14844070|65|32|32|
length:6
bytes:10

declare
s varchar2(50) := rpad(chr(14844070),10, 'A'||chr(14844070));
t varchar2(16) := substrb(s,1,11);
begin
for i in 1..length(s) loop
dbms_output.put(ascii(substr(s,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
for i in 1..length(t) loop
dbms_output.put(ascii(substr(t,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
dbms_output.put_line('length:'||length(t));
dbms_output.put_line('bytes:'||lengthb(t));
end;
/

14844070|65|14844070|65|14844070|65|14844070|65|14844070|65|
14844070|65|14844070|65|14844070|
length:5
bytes:11

declare
s varchar2(50) := rpad(chr(14844070),10, 'A'||chr(14844070));
t varchar2(16) := substrb(s,1,12);
begin
for i in 1..length(s) loop
dbms_output.put(ascii(substr(s,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
for i in 1..length(t) loop
dbms_output.put(ascii(substr(t,i)));
dbms_output.put('|');
end loop;
dbms_output.new_line;
dbms_output.put_line('length:'||length(t));
dbms_output.put_line('bytes:'||lengthb(t));
end;
/

14844070|65|14844070|65|14844070|65|14844070|65|14844070|65|
14844070|65|14844070|65|14844070|65|
length:6
bytes:12
These somewhat bizarre values are easily explained. When Oracle cuts a multi-byte string at some point falling inside a multibyte character, it replaces the meaningless fractional character portion with blanks. You can see this occurring in the first block, where the last two characters are blanks replacing 2/3 of the bytes originally belonging to the ellipsis character taking 3-bytes.

In conclusion when cutting multi-byte strings:
  1. you may get spare blanks that were not part of the original string
  2. you will get a varying length character-wise, depending on the cutting place.
Not a big deal, but something to be aware of.

Monday, November 24, 2008

The DBMS_PROFILER companion for Oracle Apex is (finally) available

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

The DBMS_PROFILER companion for Oracle Application Express that i presented at Oracle Open World 2008 is finally available for download.

Since i came back from OOW, i spent most of my spare time testing and checking the installation and deinstallation scripts, which was a non trivial task for an application that can work in a variety of possible configurations, although it is supposed to install and uninstall itself in an almost point and click fashion.
I've also invested some time improving the look and feel of the application compared to the initial version and writing the on line help for each page. There are also a few new features compared to the preliminary version i presented in San Francisco, most of them deal with automatic installation checks, auto-testing and migrating existing data.

Most of the working details are covered by the on-line help inside the application, whilst the installation details are given in the download page, so i am not going to write this stuff twice here, but let me recap the main goals of this application:
  1. to provide a human understandable format for the output of Oracle's built-in PL/SQL profiler;
  2. to minimize the cost/benefit ratio of using the DBMS_PROFILER for optimizing your PL/SQL code. That's the easy bit as this tool costs zero to you ;-)
  3. to take charge of some annoying operations like checking if the repository user was granted the debug privilege, checking if all the required objects are properly installed, providing a sample procedure to perform a test and so on.
  4. to show you that Apex is cool for developing productivity tools like this one.
When i presented the application at OOW i remarked the fact that it took very little time to develop a working application with Oracle Apex: that statement still holds today for an application created for internal use without all the bells and whistles that are necessary when you want to distribute the software to the general public. As you can easily guess, writing the help and designing the installation/deinstallation scripts plus all the auto-check functionality is a time consuming task that is usually not necessary when the application is only used by selected individuals or within a small development team.

Hopefully i'll find the time in the next days to come up with some video about installing and using this tool, but for now, this is it.

Although i spent quite a lot of time checking, testing, changing, amending and so on, the current version should be considered a beta release.

See more articles about Oracle Application Express or download tools and utilities.

Wednesday, October 08, 2008

PLS-00431: bulk SQL attributes must use a single index

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

PLS-00431: bulk SQL attributes must use a single index
ORA-06550: line 15, column 37:
PL/SQL: ORA-00904: : invalid identifier
If you got a message like this, perhaps you tried to execute or compile a
PL/SQL program containing a statement like the following:
DECLARE
TYPE NUM_TAB IS TABLE OF INTEGER INDEX BY PLS_INTEGER;

COLL_OF_FLAGGED NUM_TAB;
COLL_OF_ORDERS NUM_TAB;
COLL_OF_PARTS NUM_TAB;
BEGIN
...
FORALL x IN 1..COLL_OF_FLAGGED.COUNT
UPDATE order_parts
SET is_damaged = 'Y'
WHERE order_id = COLL_OF_ORDERS(COLL_OF_FLAGGED(x))
AND part_id = COLL_OF_PARTS(COLL_OF_FLAGGED(x));
...
END;
I highlighted in red color the cause of problem: nested collection indexes.
FORALL cannot cope with nested collection indexes on 10GR2 and earlier (on 11G i didn't tried yet), which means that you will need to rewrite the statement as a non-bulk UPDATE loop structure:
...
FOR x IN 1..COLL_OF_FLAGGED.COUNT
LOOP
UPDATE order_parts
SET is_damaged = 'Y'
WHERE order_id = COLL_OF_ORDERS(COLL_OF_FLAGGED(x))
AND part_id = COLL_OF_PARTS(COLL_OF_FLAGGED(x));
END LOOP;
...
or alternatively you might want to modify the table structure and "demote" the pair (order_id, part_id) to a simple unique constraint, while creating a single column primary key based on a sequence number, which would probably allow you to rewrite the statement above in the following way (assuming that the collection COLL_OF_FLAGGED contains the new primary key values):
...
FORALL x IN 1..COLL_OF_FLAGGED.COUNT
UPDATE order_parts
SET is_damaged = 'Y'
WHERE order_part_id = COLL_OF_FLAGGED(x);
...

See message translations for PLS-00431 and search additional resources.

Thursday, April 10, 2008

ORA-01006: bind variable does not exist

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

Here is a common scenario for this error:
i specified two variables in the USING clause of the EXECUTE IMMEDIATE statement, however i didn't specify two bind variables in the PL/SQL string to be executed, there is only one variable (:1).

declare
a varchar2(50) := 'TEST PROCEDURE';
b varchar2(50) := 'DYNAMIC SQL';
c varchar2(50);
d varchar2(50);
begin
execute immediate 'begin dbms_application_info.set_module(:1); end;' using a, b;
dbms_application_info.read_module(c,d);
dbms_output.put_line(c);
dbms_output.put_line(d);
end;

ORA-01006: bind variable does not exist
After adding the bind variable as a second parameter for SET_MODULE, the program works correctly:

declare
a varchar2(50) := 'TEST PROCEDURE';
b varchar2(50) := 'DYNAMIC SQL';
c varchar2(50);
d varchar2(50);
begin
execute immediate 'begin dbms_application_info.set_module(:1,:2); end;' using a, b;
dbms_application_info.read_module(c,d);
dbms_output.put_line(c);
dbms_output.put_line(d);
end;

TEST PROCEDURE
DYNAMIC SQL


You can easily try out the code inside Apex SQL Workshop.

Another situation where you might get this error is when you attempt to execute this unsupported single-row native dynamic SQL statement:

declare
a varchar2(10);
b varchar2(10) := 'Y';
begin
execute immediate 'select dummy into :a from dual where dummy != :b' using out a, in b;
dbms_output.put_line(' a:'||a);
end;

ORA-01006: bind variable does not exist

however in this case the problem is with the syntax, not with the missing bind variable, because single-row dynamic SELECTs must be written with the special INTO clause as already explained in a previous posting (see ORA-01745).

See message translations for ORA-01006 and search additional resources

yes you can!

Two great ways to help us out with a minimal effort. Click on the Google Plus +1 button above or...
We appreciate your support!

latest articles