All About PL/SQL Compilation Settings

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.

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):

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:
  1. CREATE OR REPLACE DDL statement
  2. ALTER-COMPILE statement
  3. DBMS_DDL.ALTER_COMPILE
The third option is really just an API to the ALTER-COMPILE statement.

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.

Comments

  1. Hello Steven, All,

    I 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

    ReplyDelete
    Replies
    1. Iudith, please submit this as an idea on the community site: https://community.oracle.com/community/technology_network_community/database/database-ideas/content

      Thanks 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? :-)

      Delete
  2. Hello Steven,

    Thanks 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

    ReplyDelete
  3. "Yes ... maybe another idea for the community site :)"

    I agree!

    ReplyDelete
  4. Hello Steven, All,

    I 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 )

    ReplyDelete
  5. 2. As a second test, I tried to use the whole range of the codes, and found the following:

    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 ) );

    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 )

    ReplyDelete
  6. 3. The 3rd test, exiting when the maximum length is exceeded:

    CREATE 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 )

    ReplyDelete
  7. START: DISABLE:ALL
    LENGTH=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

    ReplyDelete
    Replies
    1. 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.

      Delete

Post a Comment

Popular posts from this blog

Table Functions, Part 1: Introduction and Exploration

Quick Guide to User-Defined Types in Oracle PL/SQL

Recommendations for unit testing PL/SQL programs