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

Friday, 20 June 2025

Oracle Database - Conditional Compiling based on the Table/View Accessibility

The Scenario

Recently I've got a request to write a PL/SQL function and include it in a package, which counts a number of active sessions in the database. The following code was working well.

CREATE OR REPLACE PACKAGE BODY tica_demo AS

FUNCTION count_active_sessions RETURN number IS
    l_count number;
BEGIN
    SELECT count(*)
    INTO l_count
    FROM v$session
    WHERE status = 'ACTIVE';

    RETURN l_count;
END count_active_sessions;

END tica_demo;
/

But then additional request came and this was a problematic one. 

"This package is going to be a part of the product, which we are going to install on various environments. And on some environments a user/schema, where the package is installed does not have access to V$SESSION view. 
But nevertheless we want this package to be compiled and valid after the installation.
And if the user/schema, where is the package installed, does not have the access to the V$SESSION view we want the function to return a value -1".

What happened. They tried to install / compile the package on one environment and got an error ORA-00942: table or view does not exist. Like on the pic below.

And this is where a conditional compiling of PL/SQL code comes handy. In next 2 chapters I'm going to present You a Conditional Compiling Solution and, as alternative, a dynamic SQL solution.

A Conditional Compiling Solution

Conditional compiling is providing an option to include or exclude a piece of code from Your program units during the compilation, based on the Boolean condition. With directives like $IF boolean condition $THEN ... $ELSE ... $END You can control it. Read more on this topic here:

https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/conditional-compilation1.html#GUID-75F8A716-B2BE-494F-B150-1B4AB802DF25

And my (optimistic) idea for the solution was: 

  • to create a function named OBJECT_ACCESSIBLE returning Boolean value, which is checking if the current user has the access to the certain object like table, view, package
  • to implement conditional compiling in my package by using OBJECT_ACCESSIBLE function as a boolean condition; like this
    $IF object_accessible('v$session') $THEN ... $ELSE 

OBJECT_ACCESSIBLE function:

CREATE OR REPLACE FUNCTION object_accessible (
    p_name varchar2,
    p_type varchar2 --TABLE, VIEW, PLSQL, SEQUENCE, TYPE
) RETURN boolean IS

    l_schema VARCHAR2(4000);
    l_part1 VARCHAR2(4000);
    l_part2 VARCHAR2(4000);
    l_dblink VARCHAR2(4000);
    l_part1_type NUMBER;
    l_object_number NUMBER;

    l_no_object exception;
    PRAGMA EXCEPTION_INIT(l_no_object, -6564);

BEGIN
    dbms_utility.name_resolve (
        name => p_name,
        context =>
            CASE p_type
                WHEN 'TABLE' THEN 0
                WHEN 'VIEW' THEN 0
                WHEN 'PLSQL' THEN 1
                WHEN 'SEQUENCE' THEN 2
                WHEN 'TYPE' THEN 7
            END,
        schema => l_schema,
        part1 => l_part1,
        part2 => l_part2,
        dblink => l_dblink,
        part1_type => l_part1_type,
        object_number => l_object_number
    );

    RETURN true;

EXCEPTION WHEN l_no_object THEN
    RETURN false;

END object_accessible;
/

If the user / schema, where the package is installed, has proper SELECT privileges on the view V$SESSION then include the code which counts a number of sessions and returns it's number. Otherwise exclude this piece of code and include RETURN -1. Simple as that.

But I've got a following error when I implemented this function in the conditional compiling
PLS-00174: a static boolean expression must be used

Seems that boolean conditions used in conditional compiling are pretty limited and You can not use functions or other more complex conditions, only static boolean expressions like comparing 2 numbers.

But then I got an idea to introduce Inquiry Directives in the solution. Inquiry Directives resemble to boolean variables, whose true or false values are stored in Your current database session and they can be used as static boolean expressions.

And it worked! So:

  • First, I created a procedure, which sets a true/false value to a named Inquiry Directive based on the object accessibility, like v$session view in my example
  • Second, I executed this procedure before the main package is compiled; the procedure sets the appropriate true/false value to Inquiry Directive named "v$sessionAccessible"
  • Third, I used Inquiry Directive "v$sessionAccessible" in the conditional compiling within the package body source and compiled the package

First - The procedure code:

CREATE OR REPLACE PROCEDURE set_obj_exists_cnd_cmp_flag (
    p_inquiry_directive varchar2,
    p_object varchar2,
    p_type varchar2
) IS
BEGIN
    EXECUTE IMMEDIATE
        'ALTER SESSION SET PLSQL_CCFLAGS = ''' ||
        p_inquiry_directive || ':' ||
        CASE WHEN object_accessible(p_object, p_type) THEN 'TRUE' ELSE 'FALSE' END ||
        ''''
    ;
END set_obj_exists_cnd_cmp_flag;
/

Second - the code to execute a procedure before the package compilation:

BEGIN
    set_obj_exists_cnd_cmp_flag (
        p_inquiry_directive => 'v$sessionAccessible',
        p_object => 'V$SESSION',
        p_type => 'VIEW'
    );
END;
/

Third - The package source with the conditional compiling included:

CREATE OR REPLACE PACKAGE BODY tica_demo AS

FUNCTION count_active_sessions RETURN number IS
    l_count number;
BEGIN
    $IF $$v$sessionAccessible $THEN
        SELECT count(*)
        INTO l_count
        FROM v$session
        WHERE status = 'ACTIVE';

        RETURN l_count;
    $ELSE
        RETURN -1;
    $END

END count_active_sessions;

END tica_demo;
/

Using DBMS_PREPROCESSOR package You may check how the source code of the package should look based on the conditional compiling. Execute the following code and the source is going to be printed as DBMS output.

BEGIN
  DBMS_PREPROCESSOR.print_post_processed_source (
    object_type => 'PACKAGE BODY',
    schema_name => 'ZORANDBA',
    object_name => 'TICA_DEMO');
END;
/

In case that the user has no access to v$session view the package body source is going to look like this (notice empty space where lines of code are skipped):

And this approach can be used for other checks and boolean conditions too, not only the object accessibility. 

Some pros and cons of this solution:

Pros:

  • Compiled code is natively compiled and executed and not wrapped in dynamic SQL or treated in some other unusual way.
  • You may use any kind of conditions and not only static boolean expressions for conditional compiling
  • It is much easy to handle a longer code with conditional compiling than with dynamic SQL

Cons:

  • If the PL/SQL code is re-compiled without Inquiry Directive values properly set, the resulted compiled code might not execute as expected. In our example the function might return -1 even if the user has access to the v$session view.

Alternative - a Dynamic SQL Solution

Dynamic SQL is constructing and executing SQL and PL/SQL statements during the runtime. This means that even if the database user/schema, where the PL/SQL code is compiled, does not have access to certain object (like in my scenario V$SESSION view), the code is going to be compiled and valid.

Actual errors are going to happen during the runtime.

The following package code can be compiled and be valid even if the user does not have a select grant to the V$SESSION view, and the request from the scenario was fulfilled:

CREATE OR REPLACE PACKAGE BODY tica_demo AS

FUNCTION count_active_sessions RETURN number IS
    l_count number;

    l_no_grant exception;
    PRAGMA EXCEPTION_INIT (l_no_grant, -00942);

BEGIN
    EXECUTE IMMEDIATE q'[
        SELECT count(*)
        FROM v$session
        WHERE status = 'ACTIVE'
    ]'
    INTO l_count;

    RETURN l_count;

EXCEPTION WHEN l_no_grant THEN
    RETURN -1;

END count_active_sessions;

END tica_demo;
/

Some pros and cons of this solution:

Pros:

  • A pretty simple solution, which does not require any additional pre-compiling or program logic.

Cons: 

  • This implementation can be especially tricky and complex if longer SQL or PL/SQL code is in play. I had an example of another package, where we needed to handle around 2500 lines of code.
  • It is a dynamic SQL and not a natively compiled and executed SQL or PL/SQL, which might have an impact on performances.
  • Dynamic SQL is not sensitive to DDL changes and the package is not going to become invalid if for example some tables, which are used in SELECT statement are dropped. The undesired behavior or error is going to occur only during the runtime. As consequence, it might happen that the this is noticed on the very production and not caught and handled during the development.

Tuesday, 9 April 2024

Oracle APEX ZIP API - an Upgrade which is not officially documented

Recently I had a request from a customer to unzip files in the database. Of course, I used one of the most popular Oracle APEX APIs - APEX_ZIP - which is implemented in the APEX long time ago... I think in version 4 or 5.

At the moment I'm writing this blog I've been using the latest Oracle APEX 23.2. and I noticed some new functionality in this API... which is not yet officially documented in the Oracle API documentation.

https://docs.oracle.com/en/database/oracle/apex/23.2/aeapi/APEX_ZIP.html#GUID-270BFF3A-5FB1-4089-894E-978608F9BD87

So, what's new?

It goes for a new function named "get_dir_entries" and a new function "get_file_content" adapted for a new dir_entries approach.

Warning! Old function get_files is getting deprecated! This is stated in the package comments:

In which APEX version is this new functionality implemented?

I checked all APEX versions going from 23.2 toward previous versions and figured out that those new functionalities are implemented in version 21.2

Version 21.1:

Version 21.2:


But in the official documentation there is no mention of those new functions:


New approach vs old approach - benchmark

In the package comment it is stated that this new function is much faster and much more efficient.

So I tested performances on extracting 10112 files from a ZIP archive. There are 2 steps in the process: 

  1. get a list of all files from the ZIP file with get_dir_entries or ger_files function
  2. extract all files from the ZIP file with get_file_content function

Results are:

  • new approach with get_dir_entries took cca 30 seconds to finish
  • old approach with get_files - after more than 1 hour I stopped the script execution

Looking in details for a new approach:

  • the first step of preparing a file list took most of the time... around 29 seconds
  • to extract 10112 files - it took only a second.

Benchmark details (dbms output):


The code I used for testing:

DECLARE
    l_zip blob;
    l_files apex_zip.t_dir_entries;
    l_file blob;
    l_counter pls_integer := 1;
    l_index varchar2(32767);
    
    PROCEDURE p_out (p_text varchar2) IS
    BEGIN
        dbms_output.put_line(to_char(systimestamp, 'hh24:mi:ssXff') || ' - ' || p_text);
    END p_out;
    
BEGIN
    SELECT blob_content 
    INTO l_zip
    FROM import_zip_xml 
    WHERE id = 2;
    
    dbms_output.put_line('ZIP file size: ' || dbms_lob.getLength(l_zip) );
    p_out('Getting dir entries list...');
    
    l_files := apex_zip.get_dir_entries (
        p_zipped_blob => l_zip,
        p_only_files => true
    );
    
    p_out('Total files in the list: ' || l_files.count);
    p_out('Start extracting files...');

    --unzip files
    l_index := l_files.first;
    
    LOOP
        EXIT WHEN l_index is null;
        
        --p_out('Processing file ' || to_char(l_counter, 'fm00000') || ' ' || l_index || ': ');
        
        l_file := apex_zip.get_file_content (
            p_zipped_blob => l_zip,
            p_dir_entry => l_files( l_index ) 
        );
        
        l_index := l_files.next(l_index);
        l_counter := l_counter + 1;
    END LOOP;
    p_out('Finish.');
END;


One more detail I noticed - a function get_dir_entries is returning not only a collection of filenames found in the ZIP file but for every file it returns an uncompressed file size too.

This is the record type and the collection it returns:


A Conclusion

If You have a newer version of APEX, >= 21.2, this new approach is definitely worth using.

Performances are blazing fast.

Plus, there are additional metadata provided for files, like an uncompressed file size for example.

Wednesday, 27 March 2024

Oracle Database - When is the data in the user_errors view populated

So, today I've been playing with visible and invisible columns in Oracle database tables and noticed one interesting fact related to USER_ERRORS view data population.

The data is populated AFTER the first explicit or implicit compilation of the invalid object.


The example

I created a table named TICA_TEST

CREATE TABLE tica_test (
    id number primary key not null,
    c1 varchar2(100), 
    c2 varchar2(100), 
    c3 varchar2(100), 
    c4 varchar2(100), 
    c5 varchar2(100) 
);

Also created a procedure P_TICA_TEST, which is reading data from the tica_test table:

CREATE OR REPLACE PROCEDURE p_tica_test IS
    
    l_id number;
    l_c1 varchar2(100);
    l_c2 varchar2(100);
    l_c3 varchar2(100);
    l_c4 varchar2(100);
    l_c5 varchar2(100);

BEGIN
    SELECT *
    INTO l_id, l_c1, l_c2, l_c3, l_c4, l_c5
    FROM tica_test
    WHERE rownum = 1
    ;

END;

If I drop the column named C5 or make it invisible I expect that the procedure gets invalidated and that errors are stated in the view USER_ERRORS... 

But expectations are reality are not always aligned 😁


The column is dropped:

ALTER TABLE tica_test MODIFY (c5 invisible);

The status of the procedure is INVALID, as expected :

SELECT status
FROM user_objects
WHERE object_name = 'P_TICA_TEST';



But the USER_ERRORS view does not return any data?!

SELECT *
FROM user_errors
WHERE name = 'P_TICA_TEST';








And only when I compile the procedure (explicit compile) or execute the procedure (implicit compile) errors are shown and the view is populated:

ALTER PROCEDURE p_tica_test COMPILE;

or

BEGIN
    p_tica_test;
END;







Data from the view:










The conclusion

Do not be surprised if the object is invalid and there are no errors stated. 

Maybe indeed there are no errors... and the object is just marked as invalid... but the errors are going to be revealed only after the object compilation.

Monday, 11 December 2023

Deferrable constraint in Oracle database - be aware of them!

Recently I bumped into an error during Oracle APEX application import and I started digging in the depths of the APEX trying to figure out what the problem was. The error message stated that one foreign key constraint failed and I hoped to find a procedure with DML statement, where this error happened.

The error back trace was pretty long... and it ended at the COMMIT statement 😐At the COMMIT statement? And not at the very DML operation? 😮 I was puzzled... staring blankly at the screen... checking three times if I traced the error correctly... but I did. 😵

And suddenly I remembered that there is a parameter related to table constraints, the parameter I never used in my whole life, which determines if the constraint is validated at the moment of DML statement execution OR later on during the transaction ending COMMIT statement. This parameter is called "deferrable". And indeed, it was a case here. In the problematic APEX table all constraints were initially deferrable.