14. August 2013

PL/SQL Code für mehrere DB-Versionen schreiben: Bedingtes Kompilieren

PL/SQL for multiple database version: Conditional Compilation
Der PL/SQL Präprozessor oder Conditional Compilation steht nun schon seit einiger Zeit zur Verfügung, dennoch stelle ich in Gespächen meist fest, dass dies kaum bekannt ist. Man kann also mit PL/SQL (und das schon seit Version 10), ähnlich wie mit C oder C++, bedingtes Kompilieren durchführen. Wozu das gut ist? Nun, man kann bspw. Code für verschiedene Editionen oder Datenbankversionen erstellen. Ein Beispiel könnte Debugging-Code sein sein. Der folgende Code veranschaulicht, wie man bedingtes Kompilieren nutzt:
set serveroutput on 

alter session set PLSQL_CCFLAGS = 'DEBUG_TABLE:false'
/

begin
  for i in 1..10 loop
    $if $$DEBUG_TABLE $then
      insert into debug_table (datum, message) values (sysdate, 'Durchlauf ' ||i);
    $else
      dbms_output.put_line('Durchlauf '||i);
    $end
  end loop;
end;
/
Lasst den Code laufen, ohne die Tabelle DEBUG_TABLE anzulegen. Normalerweise würde man einen Fehler erwarten ... aber der Code läuft durch und die Ausgaben erscheinen auf dem Bildschirm.
Durchlauf 1
Durchlauf 2
Durchlauf 3
Durchlauf 4
Durchlauf 5
:

PL/SQL-Prozedur erfolgreich abgeschlossen.
Ändert man allerdings den ALTER SESSION-Befehl zu Beginn, dann sieht das Ergebnis anders aus:
set serveroutput on 

alter session set  PLSQL_CCFLAGS = 'DEBUG_TABLE:true'
/

begin
  for i in 1..10 loop
    $if $$DEBUG_TABLE $then
      insert into debug_table (datum, message) values (sysdate, 'Durchlauf ' ||i);
    $else
      dbms_output.put_line('Durchlauf '||i);
    $end
  end loop;
end;
/

FEHLER in Zeile 4:
ORA-06550: Zeile 4, Spalte 19:
PL/SQL: ORA-00942: Tabelle oder View nicht vorhanden
ORA-06550: Zeile 4, Spalte 7:
Grund für den Erfolg im ersten Fall ist, dass, dass die INSERT-Anweisung erst gar nicht kompiliert wird - der Präprozessor filtert sie vorher aus. Dazu dienen die Präprozessor-Direktiven $IF $THEN $ELSE $END. Diese entscheiden, welcher Code überhaupt zum Compiler gelangt - hier ausgeschlossener Code wird in keinster Weise validiert, kann also durchaus ungültig sein oder nicht vorhandene Objekte referenzieren. Das kann man auch selbst überprüfen - dazu gibt es das Paket DBMS_PREPROCESSOR. Die darin enthaltene Prozedur PRINT_POST_PROCESSED_SOURCE zeigt den Code unmittelbar nach Anwendung der Präprozessor-Direktiven und umittelbar vor dem Kompilieren.
alter session set  PLSQL_CCFLAGS = 'DEBUG_TABLE:false'
/

begin
  dbms_preprocessor.print_post_processed_source(q'#
   begin
    for i in 1..10 loop
     $if $$DEBUG $then
      insert into debug_table (datum, message) values (sysdate, 'Durchlauf ' ||i);
     $else
      dbms_output.put_line('Durchlauf '||i);
     $end
    end loop;
   end;#'
  );
end;
/

PRINT_POST_PROCESSED_SOURCE
------------------------------------------------------------
begin
 for i in 1..10 loop
  dbms_output.put_line('Durchlauf '||i);
 end loop;
end;
Der PL/SQL Präprozessor ist allerdings nicht so mächtig, wie PL/SQL selbst. So können die Compiler-Variablen (PLSQL_CCFLAGS) nur numerische oder boolean-Literale aufnehmen; Stringliterale sind nicht erlaubt. Auch kann in der Select-Direktive $IF $THEN $ELSE $END nur eine static boolean expression verwendet werden - was das genau bedeutet, ist in dieser Tabelle in der Dokumentation näher erläutert. Man kann also Boolean-Literale (wie in obigem Beispiel) verwenden und man kann numerische Werte vergleichen, nicht jedoch Strings. Das folgende PL/SQL-Skript ist also nicht erlaubt, und das aus zwei Gründen: Erstens kann der PL/SQL Compiler-Parameter keinen String aufnehmen ("BETA") und zweitens kann die $IF-Direktive keinen Stringvergleich durchführen.
alter session set  PLSQL_CCFLAGS = 'CODE_VERSION:BETA'
/

begin
  $IF $$CODE_VERSION = 'BETA' $THEN
    -- Betacode hier
  $END
end;
/
      *
FEHLER in Zeile 2:
ORA-06550: Zeile 2, Spalte 7:
PLS-00174: Ein statischer Boolescher Ausdruck muss benutzt werden
Der wichtigste Nutzen des bedingten Kompilierens (und auch der Grund, warum es eingeführt wurde), ist, dass ein- und dieselbe Version des Quellcodes nun in verschiedenen Datenbankversionen laufen kann. Zum Feststellen der Datenbankversion kann das PL/SQL Paket DBMS_DB_VERSION genutzt werden; dieses enthält fertige Boolean-Konstanten zum Ermitteln der Datenbankversion und kann entsprechend in den Präprozessor-Direktiven genutzt werden.
$IF DBMS_DB_VERSION.VER_LE_11_1 $THEN
   -- Code für Versionen kleiner oder gleich 11.1
$ELSIF DBMS_DB_VERSION.VER_LE_11_2 $THEN
   -- Code für Version 11.2
$ELSE
   -- Code für 12 und später
$END
Der PL/SQL-Präprozessor bietet zusätzlich eine Reihe vordefinierter Compiler-Parameter an: So gibt $$PLSQL_LINE wieder, in welcher Zeile des Quellcodes sich der Compiler gerade befindet; analog dazu gibt $$PLSQL_UNIT den Namen der Prozedur, der Funktion oder des Packages zurück. Auch die komplette "Compiler-Umgebung" steht für Präprozessor-Direktiven bereit - die Dokumentation enthält eine Übersicht. Mehr zum Thema Bedingtes Kompilieren von PL/SQL-Code findet sich in der PL/SQL Dokumentation.
PL/SQL Conditional Compilation is available for some years now - but in talks I frequently recognize, that it seems to be a rather unknown one. So, although I already had a blog posting about this in 2007, I take the chance to post again about the PL/SQL Preprocessor.
It allows, like in C or C++, to pre-process the PL/SQL sourcecode before actually compiling it. And this opens several opportunities: It allows easy including or excluding of debugging code or having support für multiple database versions within the same sourcecode files. The following example illustrates, how PL/SQL conditional compilation is being used.
set serveroutput on 

alter session set PLSQL_CCFLAGS = 'DEBUG_TABLE:false'
/

begin
  for i in 1..10 loop
    $if $$DEBUG_TABLE $then
      insert into debug_table (datum, message) values (sysdate, 'Iteration ' ||i);
    $else
      dbms_output.put_line('Iteration '||i);
    $end
  end loop;
end;
/
Now run this script, but don't create the table DEBUG_TABLE. We'd expect an immediate error message, but the anonymous block completes successfully.
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
:

PL/SQL procedure successfully completed.
However, after changing the DEBUG_TABLE parameter (ALTER SESSION statement) to TRUE, the code will fail ...
set serveroutput on 

alter session set PLSQL_CCFLAGS = 'DEBUG_TABLE:true'
/

begin
  for i in 1..10 loop
    $if $$DEBUG_TABLE $then
      insert into debug_table (datum, message) values (sysdate, 'Iteration ' ||i);
    $else
      dbms_output.put_line('Iteration '||i);
    $end
  end loop;
end;
/

ERROR at line 4:
ORA-06550: line 4, column 19:
PL/SQL: ORA-00942: table or view does not exist
ORA-06550: line 4, column 7:
As the name Conditional Compilation indicates: Depending on the DEBUG_TABLE parameter, some code sections are not being considered for compilation. This is done with the preprocessor directives $IF $THEN $ELSE $END. When the condition in the $IF directive is not met, the embedded PL/SQL code is not being compiled. So, in the first case, the SQL INSERT statement is just not considered as part of the code. The Package DBMS_PREPROCESSOR helps understanding this - its PRINT_POST_PROCESSED_SOURCE allows to review the PL/SQL code - after the preprocessor directives have been applied - and before compilation.
alter session set PLSQL_CCFLAGS = 'DEBUG_TABLE:false'
/

begin
  dbms_preprocessor.print_post_processed_source(q'#
   begin
    for i in 1..10 loop
     $if $$DEBUG $then
      insert into debug_table (datum, message) values (sysdate, 'Iteration ' ||i);
     $else
      dbms_output.put_line('Iteration '||i);
     $end
    end loop;
   end;#'
  );
end;
/

PRINT_POST_PROCESSED_SOURCE
------------------------------------------------------------
begin
 for i in 1..10 loop
  dbms_output.put_line('Iteration '||i);
 end loop;
end;
The PL/SQL processor is not as powerful as the PL/SQL compiler itself: So we cannot use all data types for the compilation parameters - only boolean and PLS_INTEGER literals are allowed. The selection directive $IF $THEN $ELSE $END only allows to test for static boolean expressions - a detailed explanation is contained in a table in the documentation. So we can use simple boolean literals or numeric comparisions in the $IF branch - and we can not use string comparisons. So the following script is invalid for two reasons: First, we cannot assign a String value to a compiler parameter. Second, we cannot perform a string comparison in the preprocessor $IF branch.
alter session set  PLSQL_CCFLAGS = 'CODE_VERSION:BETA'
/

begin
  $IF $$CODE_VERSION = 'BETA' $THEN
    -- Betacode hier
  $END
end;
/
      *
ERROR at line 2:
ORA-06550: line 2, column 7:
PLS-00174: a static boolean expression must be used
Conditional Compilation is most useful when it's about authoring code for multiple database versions, since we can add PL/SQL calls available only in recent versions, embed these into preprocessor directives and run the scripts in older versions as well. To determine the version of the database, in which a PL/SQL script runs, the DBMS_DB_VERSION package can be used. It contains boolean constants like VER_LE_11_1 (this database is of version 11.1 or lower), VER_LE_11_2 and so on. Using these constants, together with conditional compilation, might look like this.
$IF DBMS_DB_VERSION.VER_LE_11_1 $THEN
   -- Code for version 11.1 and lower
$ELSIF DBMS_DB_VERSION.VER_LE_11_2 $THEN
   -- Code for version 11.2
$ELSE
   -- Code for version 12.1 and higher
$END
Beyond this, the preprocessor offers a set of predefined compiler parameters. $$PLSQL_LINE contains the current source code line and $$PLSQL_UNIT returns the name of the PL/SQL unit (procedure, function, package). In addition, also the complete compiler environment is available: The docementation) contains an overview. More information about conditional compilation is available in the PL/SQL Dokumentation.

Beliebte Postings