A recent Twitter thread delved into the topic of the best way to enable PL/SQL warnings for program units, including this recommendation from Bryn Llewellyn, Distinguished Product Manager for PL/SQL:
which then led to Bryn volunteering me to delve into the details of PL/SQL compiler settings in an AskTOM PL/SQL Office Hours session.
Which I will do. But I figured I could start right off by writing this post. So let's explore how to set and modify PL/SQL compiler settings.
First, you might wonder what those settings are or could be. The best way to check is by examining the USER_PLSQL_OBJECT_SETTINGS view (and of course the ALL* version to examine attributes of code you do not own but can execute):
The values that are stored for a PL/SQL unit are set every time it is compiled—in response to "create", "create or replace", "alter", invoking a utility like Utl_Recomp, or implicitly as a side effect of trying to execute an invalid PL/SQL unit.
A special case of "set" is to set the exact same values that were already stored for the unit. There's no way to ask for this outcome explicitly as part of the "create" or "create or replace" DDLs. It's the programmer's responsibility to ensure that the set of required values obtains in the session at the moment of compilation. There is a way to ask for this outcome with "alter". It's to say "reuse settings" (and not to mention any settings explicitly). More on this below. Notice that Utl_Recomp and its cousins, and implicit recompilation use the plain "reuse settings" mode of "alter".
I will explore how to set these attributes at both session and program unit level below, how to override, and how to preserve them. You can run all of this code yourself on LiveSQL.
which then led to Bryn volunteering me to delve into the details of PL/SQL compiler settings in an AskTOM PL/SQL Office Hours session.
Which I will do. But I figured I could start right off by writing this post. So let's explore how to set and modify PL/SQL compiler settings.
First, you might wonder what those settings are or could be. The best way to check is by examining the USER_PLSQL_OBJECT_SETTINGS view (and of course the ALL* version to examine attributes of code you do not own but can execute):
The values that are stored for a PL/SQL unit are set every time it is compiled—in response to "create", "create or replace", "alter", invoking a utility like Utl_Recomp, or implicitly as a side effect of trying to execute an invalid PL/SQL unit.
A special case of "set" is to set the exact same values that were already stored for the unit. There's no way to ask for this outcome explicitly as part of the "create" or "create or replace" DDLs. It's the programmer's responsibility to ensure that the set of required values obtains in the session at the moment of compilation. There is a way to ask for this outcome with "alter". It's to say "reuse settings" (and not to mention any settings explicitly). More on this below. Notice that Utl_Recomp and its cousins, and implicit recompilation use the plain "reuse settings" mode of "alter".
I will explore how to set these attributes at both session and program unit level below, how to override, and how to preserve them. You can run all of this code yourself on LiveSQL.
When I first connect to a schema, and until I issue any ALTER statements, compilations of code will rely on the system defaults. You can see what they are by running the following script (thanks to Bryn Llewellyn for providing it):
You can also use this query, as offered up by Connor McDonald:
Here's the LiveSQL script that performs these two steps.
Note: I hope to update this post soon with a query that does not require you to create a database object first. :-)
Now let's take a look at how you can change the compilation settings for a program unit, at the session level and also for specifics.
There are three ways you can compile an individual program unit:
When you compile your code via CREATE OR REPLACE that program unit will inherit all the current settings in your session.
With both ALTER-COMPILE and DBMS_DDL.ALTER_COMPILE, you can either inherit those settings or reuse (re-apply) the settings that are currently associated with the program unit.
All right, then, let's get going. I connect to my schema and immediately change the setting for PL/SQL compile-time warnings. I then compile my procedure and confirm that it used the session setting.
Now I am going to recompile that procedure with an ALTER statement and specify a value different from the session's for PL/SQL warnings, but re-use all other settings. ALTER takes all the settings values from the environment. If you mention only some, and don't say "reuse settings", then it takes what you don't mention from the environment. As you can see in the query, the procedure now has PL/SQL warnings set to treat any warning as a compile error.
Now I CREATE OR REPLACE to demonstrate that the session setting is now applied to the procedure:
But what if you want to recompile your program unit and you do not want to pick up the current session settings? You want to keep all settings intact. We offer you the REUSE SETTINGS clause.
From the last sequence of statements we can see that the session setting for PL/SQL warnings is "ENABLE:ALL". Below, I recompile my procedure, specifying "ERROR:ALL". Then I recompile again, but this time I do not specify a value for PL/SQL warnings. Instead I ask to reuse settings for the procedure.
As you can see, the setting for my procedure did not change.
OK, I think that covers this territory pretty well (until my readers point out what I missed!).
Here are some links you might find helpful.
The COMPILE Clause
Conditional Compilation
Many thanks for the close reading and numerous suggestions for improvement from Bryn Llewellyn.
declare
Plsql_Debug constant varchar2(5) not null :=
case
when $$Plsql_Debug then 'TRUE'
when not $$Plsql_Debug then 'FALSE'
else 'illegal'
end;
Plsql_CCflags constant varchar2(4000) not null :=
case
when $$Plsql_CCflags is null then '[Not Set]'
else $$Plsql_CCflags
end;
begin
if Plsql_CCflags = 'illegal' then
raise Program_Error;
end if;
Sys.DBMS_Output.Put_Line('Plsql_Optimize_Level:' ||To_Char($$Plsql_Optimize_Level, '9'));
Sys.DBMS_Output.Put_Line('Plsql_Code_Type: '||$$Plsql_Code_Type);
Sys.DBMS_Output.Put_Line('Plsql_Debug: '||Plsql_Debug);
Sys.DBMS_Output.Put_Line('Plsql_Warnings: '||$$Plsql_Warnings);
Sys.DBMS_Output.Put_Line('NLS_Length_Semantics: '||$$NLS_Length_Semantics);
Sys.DBMS_Output.Put_Line('Plsql_CCflags: '||Plsql_CCflags);
Sys.DBMS_Output.Put_Line('Plscope_Settings: '||$$Plscope_Settings);
end;
You can also use this query, as offered up by Connor McDonald:
select name, value
from v$parameter
where name like 'plsql%'
or name like 'plscope%'
or name like 'nls_length_semantics%';
Here's the LiveSQL script that performs these two steps.
Note: I hope to update this post soon with a query that does not require you to create a database object first. :-)
Now let's take a look at how you can change the compilation settings for a program unit, at the session level and also for specifics.
There are three ways you can compile an individual program unit:
- CREATE OR REPLACE DDL statement
- ALTER-COMPILE statement
- DBMS_DDL.ALTER_COMPILE
When you compile your code via CREATE OR REPLACE that program unit will inherit all the current settings in your session.
With both ALTER-COMPILE and DBMS_DDL.ALTER_COMPILE, you can either inherit those settings or reuse (re-apply) the settings that are currently associated with the program unit.
All right, then, let's get going. I connect to my schema and immediately change the setting for PL/SQL compile-time warnings. I then compile my procedure and confirm that it used the session setting.
ALTER SESSION SET plsql_warnings = 'ENABLE:ALL'
/
CREATE OR REPLACE PROCEDURE aftercompile
AUTHID DEFINER
IS
BEGIN
NULL;
END;
/
SELECT plsql_warnings "From Session"
FROM user_plsql_object_settings
WHERE name = 'AFTERCOMPILE'
/
From Session
------------
ENABLE:ALL
Now I am going to recompile that procedure with an ALTER statement and specify a value different from the session's for PL/SQL warnings, but re-use all other settings. ALTER takes all the settings values from the environment. If you mention only some, and don't say "reuse settings", then it takes what you don't mention from the environment. As you can see in the query, the procedure now has PL/SQL warnings set to treat any warning as a compile error.
ALTER PROCEDURE aftercompile COMPILE plsql_warnings = 'ERROR:ALL' REUSE SETTINGS
/
SELECT plsql_warnings "From Procedure Override"
FROM user_plsql_object_settings
WHERE name = 'AFTERCOMPILE'
/
From Procedure Override
-------------------------
ERROR:ALL
Now I CREATE OR REPLACE to demonstrate that the session setting is now applied to the procedure:
CREATE OR REPLACE PROCEDURE aftercompile
AUTHID DEFINER
IS
BEGIN
NULL;
END;
/
SELECT plsql_warnings "Compile From Source"
FROM user_plsql_object_settings
WHERE name = 'AFTERCOMPILE'
/
Compile From Source
-------------------
ENABLE:ALL
But what if you want to recompile your program unit and you do not want to pick up the current session settings? You want to keep all settings intact. We offer you the REUSE SETTINGS clause.
From the last sequence of statements we can see that the session setting for PL/SQL warnings is "ENABLE:ALL". Below, I recompile my procedure, specifying "ERROR:ALL". Then I recompile again, but this time I do not specify a value for PL/SQL warnings. Instead I ask to reuse settings for the procedure.
ALTER PROCEDURE aftercompile COMPILE plsql_warnings = 'ERROR:ALL'
/
SELECT plsql_warnings "Back to Procedure Override"
FROM user_plsql_object_settings
WHERE name = 'AFTERCOMPILE'
/
Back to Procedure Override
--------------------------
ERROR:ALL
ALTER SESSION SET plsql_warnings = 'ENABLE:ALL'
/
ALTER PROCEDURE aftercompile COMPILE REUSE SETTINGS
/
SELECT plsql_warnings "Session Change No Impact with REUSE SETTINGS"
FROM user_plsql_object_settings
WHERE name = 'AFTERCOMPILE'
/
Session Change No Impact with REUSE SETTINGS
--------------------------------------------
ERROR:ALL
As you can see, the setting for my procedure did not change.
OK, I think that covers this territory pretty well (until my readers point out what I missed!).
Here are some links you might find helpful.
The COMPILE Clause
Conditional Compilation
Many thanks for the close reading and numerous suggestions for improvement from Bryn Llewellyn.
Hello Steven, All,
ReplyDeleteI think that it could be nice if the CREATE OR REPLACE statement itself will allow for using
REUSE SETTINGS, so that, after performing changes in the code rather than just recompiling,
the settings of the last (previous) compilation could be preserved, without having to repeat
them in a new ALTER SESSION.
Otherwise, the last stored settings will be lost, and an additional ALTER ... REUSE SETTINGS
could not help to restore them.
Maybe it would also be nice if a new database object type could be created that can store a full set of compilation settings, and then just specify that object in a COMPILE USING clause,
instead of a long list of individual settings.
I would just like to add that, in addition to USER_PLSQL_OBJECT_SETTINGS, there exists
another view named USER_WARNING_SETTINGS, that contains a separate row for each
PL/SQL warning setting.
The column USER_PLSQL_OBJECT_SETTINGS.PLSQL_WARNINGS is "limited" to VARCHAR2(4000), so I wonder what could happen if a very "pedant" developer would like to specify explicit settings for many individual warning codes, and could exceed this limit.
This can be done using the DBMS_WARNING package, and I guess that,
if the total length of all the settings will exceed 4000 bytes, then the value in USER_PLSQL_OBJECT_SETTINGS.PLSQL_WARNINGS will probably be truncated,
while USER_WARNING_SETTINGS could store the complete information.
Thanks a lot & Best Regards,
Iudith Mentzel
Iudith, please submit this as an idea on the community site: https://community.oracle.com/community/technology_network_community/database/database-ideas/content
DeleteThanks for the heads up about USER_WARNING_SETTINGS. Didn't know about that. Gee, I wonder where we might see something about that soon....
Finally, as to your "I wonder what could happen...": surely you will try this out for us and let us know? :-)
Hello Steven,
ReplyDeleteThanks for the ideas ... maybe indeed I will try this out :)
This brought me to realize something trivial enough, namely,
that, as far as I can see, there is no dictionary table (ex. a V$ or X$ table) that can be queried
to retrieve ALL the available Oracle error and warning codes, their types, messages, a.s.o.
I think that Oracle could definitely have supplied such a possibility, for example by defining
an external table that accesses the OS file which stores the messages definitions,
similarly to how this was done for giving easy access to the Oracle alert file.
Yes ... maybe another idea for the community site :)
Thanks a lot & Best Regards,
Iudith
"Yes ... maybe another idea for the community site :)"
ReplyDeleteI agree!
Hello Steven, All,
ReplyDeleteI took the challenge to test the behavior of "long" warnings settings, and the findings are interesting enough.
Since I don't have a table with the existing warning codes, I took the approach of trying to add the entire range of documented codes, aka from 5000 to 7207, using an exception handler to bypass the possible errors caused by trying to set a non-existing code.
1. As a first check, I tried with codes from 5000 to 5020 only, and it looks like it worked:
ALTER SESSION SET PLSQL_WARNINGS='DISABLE:ALL'
/
DECLARE
l_count PLS_INTEGER := 0;
BEGIN
DBMS_OUTPUT.put_line( 'START: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
DBMS_OUTPUT.PUT_LINE( '$$PLSQL_WARNINGS = ' || $$PLSQL_WARNINGS);
FOR num IN 5000 .. 5020
LOOP
BEGIN
DBMS_WARNING.ADD_WARNING_SETTING_NUM ( num,
CASE MOD(l_count, 3)
WHEN 0 THEN 'ENABLE'
WHEN 1 THEN 'DISABLE'
WHEN 2 THEN 'ERROR'
END,
'SESSION' );
l_count := l_count + 1;
DBMS_OUTPUT.put_line('Added '|| l_count || ' - ' || num);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line('Error adding '||num||': '||SQLERRM);
END;
END LOOP;
DBMS_OUTPUT.put_line( 'END: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
END;
/
START: DISABLE:ALL
LENGTH=11
$$PLSQL_WARNINGS = DISABLE:ALL
Added 1 - 5000
Added 2 - 5001
Added 3 - 5002
Added 4 - 5003
Added 5 - 5004
Added 6 - 5005
Added 7 - 5006
Added 8 - 5007
Added 9 - 5008
Added 10 - 5009
Added 11 - 5010
Added 12 - 5011
Added 13 - 5012
Added 14 - 5013
Added 15 - 5014
Added 16 - 5015
Added 17 - 5016
Added 18 - 5017
Added 19 - 5018
Added 20 - 5019
Added 21 - 5020
END: DISABLE:ALL,ENABLE: 5000,ERROR: 5002,ENABLE: 5003,ERROR: 5005,ENABLE: 5006,ERROR: 5008,ENABLE: 5009,ERROR: 5011,ENABLE: 5012,ERROR: 5014,ENABLE: 5015,ERROR: 5017,ENABLE: 5018,ERROR: 5020
LENGTH=200
-- an additional block, to check whether the settings are preserved
BEGIN
DBMS_OUTPUT.put_line( 'AFTER: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.PUT_LINE( '$$PLSQL_WARNINGS = ' || $$PLSQL_WARNINGS);
END;
/
AFTER: DISABLE:ALL,ENABLE: 5000,ERROR: 5002,ENABLE: 5003,ERROR: 5005,ENABLE: 5006,ERROR: 5008,ENABLE: 5009,ERROR: 5011,ENABLE: 5012,ERROR: 5014,ENABLE: 5015,ERROR: 5017,ENABLE: 5018,ERROR: 5020
$$PLSQL_WARNINGS = DISABLE:ALL,ENABLE: 5000,ERROR: 5002
A few remarks from this test:
- There was no exception raised when trying to add a non-existent code, for example 5002.
Or, maybe this code is however legal, but not documented ?!?
- The codes used with DISABLE were not added explicitly, as they are already included in the starting setting DISABLE:ALL.
- The value of $$PLSQL_WARNINGS was truncated, in comparison with the value returned by DBMS_WARNING in the second block above
I also made a few other checks, using ALTER SESSION SET PLSQL_WARNINGS, and found that $$PLSQL_WARNINGS does NOT follow always the ALTER SESSION settings ...
Here is one example, showing an incorrect value for $$PLSQL_WARNINGS after ALTER SESSION:
ALTER SESSION SET PLSQL_WARNINGS = 'DISABLE:ALL,ENABLE: 5000,ERROR: 5002'
/
-- Statement processed.
BEGIN
DBMS_OUTPUT.put_line( 'AFTER: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.PUT_LINE( '$$PLSQL_WARNINGS = ' || $$PLSQL_WARNINGS);
END;
/
AFTER: DISABLE:ALL,ENABLE: 5000,ERROR: 5002
$$PLSQL_WARNINGS = DISABLE:ALL
( to be continued )
2. As a second test, I tried to use the whole range of the codes, and found the following:
ReplyDeleteALTER SESSION SET PLSQL_WARNINGS='DISABLE:ALL'
/
DECLARE
l_count PLS_INTEGER := 0;
BEGIN
DBMS_OUTPUT.put_line( 'START: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
FOR num IN 5000 .. 7207
LOOP
BEGIN
DBMS_WARNING.ADD_WARNING_SETTING_NUM ( num,
CASE MOD(l_count, 3)
WHEN 0 THEN 'ENABLE'
WHEN 1 THEN 'DISABLE'
WHEN 2 THEN 'ERROR'
END,
'SESSION' );
l_count := l_count + 1;
DBMS_OUTPUT.put_line('Added '|| l_count || ' - ' || num);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line('Error adding '||num||': '||SQLERRM);
END;
END LOOP;
DBMS_OUTPUT.put_line( 'END: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
END;
/
START: DISABLE:ALL
LENGTH=11
Added 1 - 5000
Added 2 - 5001
Added 3 - 5002
...
Added 373 - 5372
Added 374 - 5373
Added 375 - 5374
Error adding 5375: ORA-39950: invalid parameter for PLSQL warnings flag
Error adding 5376: ORA-39950: invalid parameter for PLSQL warnings flag
Error adding 5377: ORA-39950: invalid parameter for PLSQL warnings flag
...
Error adding 7205: ORA-39950: invalid parameter for PLSQL warnings flag
Error adding 7206: ORA-39950: invalid parameter for PLSQL warnings flag
Error adding 7207: ORA-39950: invalid parameter for PLSQL warnings flag
END: Value too big for this column
LENGTH=29
So, in this test, after adding successfully the first 375 codes, I got the ORA-39950 for ALL the subsequent codes, regardless of whether they are legal documented codes or not.
And, finally, the last value for the warning string became "Value too big for this column"
and this has a length of 29.
The error ORA-39950 says the following:
ORA-39950: invalid parameter for PLSQL warnings flag
Cause: The user either entered invalid value for the PLSQL_WARNINGS flag or the
value of the flag conflicts with other values.
Action: Enter correct values for the switch.
So, this error might occur not only for non-existing codes, but also for conflicting codes,
which is a new discovery, for me anyway.
Anyway, I cannot exactly see how can all the codes above 7000 , which are performance-related,
to conflict with codes below 6000, which are NOT performance-related ... very strange ...
I suspected that this is the case because the 4000 limit was exceeded.
So, to catch exactly the point where this happened and to preserve the last valid value
BEFORE this happened, I tried the following third test, with various attempts to exit the loop before the problem happened:
( to be continued )
3. The 3rd test, exiting when the maximum length is exceeded:
ReplyDeleteCREATE TABLE mytab (x CLOB)
/
ALTER SESSION SET PLSQL_WARNINGS='DISABLE:ALL'
/
DECLARE
l_count PLS_INTEGER := 0;
l_setting VARCHAR2(32767);
BEGIN
DBMS_OUTPUT.put_line( 'START: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
FOR num IN 5000 .. 7207
LOOP
BEGIN
l_setting := DBMS_WARNING.get_warning_setting_string;
DBMS_WARNING.ADD_WARNING_SETTING_NUM ( num,
CASE MOD(l_count, 3)
WHEN 0 THEN 'ENABLE'
WHEN 1 THEN 'DISABLE'
WHEN 2 THEN 'ERROR'
END,
'SESSION' );
l_count := l_count + 1;
DBMS_OUTPUT.put_line('Added '|| l_count || ' - ' || num);
--
EXIT WHEN DBMS_WARNING.get_warning_setting_string = 'Value too big for this column';
--
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line('Error adding '||num||': '||SQLERRM);
--
-- EXIT;
--
END;
END LOOP;
DELETE FROM mytab;
INSERT INTO mytab (x)
VALUES ( l_setting );
COMMIT;
DBMS_OUTPUT.put_line( 'LAST: '||l_setting);
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH(l_setting) );
DBMS_OUTPUT.put_line( 'END: '|| DBMS_WARNING.get_warning_setting_string );
DBMS_OUTPUT.put_line( 'LENGTH='|| LENGTH( DBMS_WARNING.get_warning_setting_string ) );
END;
/
( to be continued )
START: DISABLE:ALL
ReplyDeleteLENGTH=11
...
Added 373 - 5372
Added 374 - 5373
Added 375 - 5374
...
LAST: DISABLE:ALL,ENABLE: 5000,ERROR: 5002,ENABLE: 5003,ERROR: 5005,ENABLE: 5006,ERROR: 5008,ENABLE: 5009,ERROR: 5011,ENABLE: 5012,ERROR: 5014,ENABLE: 5015,ERROR: 5017,ENABLE: 5018,ERROR: 5020,ENABLE: 5021,ERROR: 5023,ENABLE: 5024,ERROR: 5026,ENABLE: 5027,ERROR: 5029,ENABLE: 5030,ERROR: 5032,ENABLE:
..... ( some values omitted here )
5357,ERROR: 5359,ENABLE: 5360,ERROR: 5362,ENABLE: 5363,ERROR: 5365,ENABLE: 5366,ERROR: 5368,ENABLE: 5369,ERROR: 5371,ENABLE: 5372
LENGTH=3373
END: Value too big for this column
LENGTH=29
So, the final finding is that, when the length of the current warning setting was 3373,
that is, still much lower than 4000, the next addition to that setting was not executed, and the value of the whole string was silently changed to "Value too big for this column".
If you look at the last setting string before this happened, the last code that appears in the string is ENABLE: 5372.
The next code was DISABLE:5373 (not seen explicitly in the string), and the last code that produced the setting string replacement was 5374.
The documentation of package DBMS_WARNING does not specify anything about the maximum length allowed for a warnings settings string, but, anyway, it looks like it is much lower
than 4000 bytes.
So, I cannot currently reach the point of answering my initial question regarding the USER_PLSQL_OBJECT_SETTINGS.PLSQL_WARNINGS
column size limitation ... but, anyway, these tests were very instructive :)
Cheers & Best Regards,
Iudith
Thanks so much for your detailed analysis and stress testing of this feature, Iudith. On the one hand, it is easy to say "Well, no one is ever going to encounter this issue." On the other hand, we should document clearly current behavior and/or remove edge case issues. I will report this.
DeleteWhere the 'upvote' button for these questions and comments? :)
ReplyDeleteThis comment is just because I forgot to tick the 'notify me' button earlier.
ReplyDelete