26Jan
By: Steven Feuerstein On: January 26, 2022 In: APEX Developer Solutions, Feuertips Comments: 0

Conditional compilation is a very cool but rarely used feature of PL/SQL that allows you to tell the compiler to include or exclude (ignore) chunks of code based on the values of conditional compilation flags or package-based static expressions.

OK, sure, that’s a mouthful. And it’s a tricky feature to implement. That’s why I wrote a five-part series on conditional compilation back in 2019. So no need to repeat all of that here, right? I did, however, copy/paste the resources I listed in the first article to the end of this post. Because I am that nice a guy.

🙂

Instead, here I want to focus on what is probably the most important use case of conditional compilation: compile and run your PL/SQL code on different versions of Oracle, taking full advantage of features specific to those versions.

In other words, you write code that has to be able to run on more than one version of Oracle Database. You don’t want to take the least common denominator approach (can’t take advantage of newer features), and you don’t want to maintain multiple copies of your code, one for each supported version.

With conditional compilation, you can write code like this:

create or replace package body pkg
is
   procedure insert_rows ( rows_in in ibt )
   is
   begin
$IF DBMS_DB_VERSION.VER_LE_10_1
$THEN
   /* Remove gaps in the collection */
   declare
      l_dense t;
      l_index pls_integer := rows_in.first;
   begin
      while (l_index is not null)
      loop
         l_dense (l_dense.count + 1) := rows_in (l_index);
         l_index := rows_in.next (l_index);
      end loop;
      
      forall indx in l_dense.first .. l_dense.last
         insert into t values l_dense (indx);
   end;
$ELSE
      /* Use the very cool INDICES OF feature to 
         skip over gaps. */
      FORALL indx IN INDICES OF rows_in
         INSERT INTO t VALUES rows_in (indx);
$END
   end insert_rows;
end;

This code recognizes that as of 10.2, the indices of feature allows us to declaratively “skip over” undefined index values, so no need to compress the collection, which is what all that code does inside the “less than or equal to 10.1” branch.

This is possible because each version of Oracle Database comes with its own particular version of the dbms_db_version package, with all the boolean static expressions (constants) set appropriately.

I recently ran into the need to do something similar with Oracle APEX. I am building a new utility for Insum consultants called Insum Mail. It adds all sorts of email-related features to APEX applications by building a layer of code (and APEX apps) around apex_mail.

One of the features of Insum Mail is 100% compatibility with the apex_mail package: do a global search and replace of “apex_mail” with “insum_mail” and voila! All your code compiles and continues to work, and you can take advantage of all the other wonderful stuff packed into Insum Mail.

But! The apex_mail API changes over time. In fact, while I was developing the utility on 20.2, Oracle APEX 21.1 was released – and it added a new overloading of add_attachment (for clobs) and some other changes to parameter lists.

No problem, right? Just use some conditional compilation statements like this:

   $if oracle_apex_version.ver_le_20 $then  
   $elsif oracle_apex_version.ver_le_21_1 $then 
   $else
   procedure add_attachment (
      p_mail_id    in number,
      p_attachment in clob,
      p_filename   in varchar2, 
      p_mime_type  in varchar2
   );
   $end

Great idea, Steven! Just one problem: (boo hoo!) APEX does not come with its own versioning package.

Fortunately, this turned out to be one of those problems that could be solved by moi (aka, me).

Here it is:

declare
   vr  number; v   number; r   number;
   blk varchar(4000);

   procedure add_line ( p_line in varchar2 ) is
   begin
      blk := blk  || case when blk is not null then chr(10) end || p_line;
   end;

   function add_bool ( p_current_version in number, p_a_version in number ) return varchar2 is
   begin
      return case when p_current_version <= p_a_version then 'true' else 'false' end;
   end;
begin
   select to_number(substr(version_no, 1, instr(version_no, '.', 1, 2) - 1)),
          to_number(substr(version_no, 1, instr(version_no, '.', 1, 1) - 1)),
          to_number(substr(version_no, instr(version_no, '.', 1, 1) + 1, instr(version_no, '.', 1, 1) - 2))
     into vr,
          v,
          r
     from apex_release;
   add_line(
      'create or replace package oracle_apex_version authid definer is 
   version constant pls_integer := '
      || v
      || '; release constant pls_integer := '
      || r
      || '; '
   );
   for yr in 18..v loop
      add_line('ver_le_' || yr || ' constant boolean := ' || add_bool(trunc(vr), yr) || ';');
      add_line('ver_le_' || yr || '_1 constant boolean := ' || add_bool(vr, yr +.1) || ';');
      add_line('ver_le_' || yr || '_2 constant boolean := ' || add_bool(vr, yr +.2) || ';');
   end loop;
   add_line(' end;');
   execute immediate blk;
end;
/

I run this block of code as a supporting object installation script. It generates and compiles a package that looks like this (for 20.2 anyway):

create or replace package oracle_apex_version authid definer is 
   version constant pls_integer := 1; 
   release constant pls_integer := 2; 
   ver_le_18_1  constant boolean := false;
   ver_le_18_2  constant boolean := false;
   ver_le_19    constant boolean := false;
   ver_le_19_1  constant boolean := false;
   ver_le_19_2  constant boolean := false;
   ver_le_20_1  constant boolean := false;
   ver_le_20    constant boolean := true;
   ver_le_20_2  constant boolean := true;
end;

And now I can do versioning of my APEX-based code using conditional compilation!

But wait, there’s more!

I realize that most of you will never need to build database or APEX code that is installed on multiple versions of those products.

But what about your own products? Do you have APIs that change over time? Do you need to be able to support your own product across installations of various versions of that product?

Then I’ve got a treat for you! I’ve generalized the above block into some code you can use to generate a versioning package for any product that has a function which returns its version in the form V.R (version.release, as in 21.2). Check it out on LiveSQL!

Resources

Comprehensive white paper: a great starting place – and required reading – for anyone planning on using conditional compilation in production code

Conditional compilation scripts on LiveSQL

Tim Hall (Oracle-BASE) coverage of conditional compilation

Conditional compilation documentation

My Oracle Magazine article on this topic

Watch the full Feuertips episode here

Feuertips

Every other Wednesday at 11 am ET, I go live on YouTube and Facebook with Michelle Skamene to provide you with a not-to-be-missed PL/SQL tip and fun conversation. Sure, we record it so you can always watch later, but live is so much more rewarding!
One participant will be selected at random to choose from three organizations dedicated to the preservation of our planet and its biodiversity.
Insum will then make a donation of $25 to that group in your name.
What could be better than levelling up your PL/SQL tips and helping one of these worthy organizations? Join us every other Wednesday at 11!
Share this:
Share

Leave reply:

Your email address will not be published.