Thursday, August 28, 2014

ODI Debugging error

I tried to start the debugger in ODI but kept getting an error message which looked like this:


If I used Local (No Agent) the debug session would work fine. This confused me for a while since I thought everything was configured correctly. Searching the documentation I found out that the JMXPort which is used for the debugging session is set to the ODI agent port + 1000 by default. And then the penny dropped, but before I can explain what went wrong I have to explain a little bit about my test setup.

My laptop runs Windows 7 Enterprise, the database containing the ODI repository and most working schema's is installed on a VirtualBox virtual machine running Oracle Enterprise Linux 6.4. The VM is configured to use Network Address Translation (NAT). This means the VM box can be accessed on IP adress 127.0.0.1 or localhost. To make sure that the database connections are actually sent to the VM, instead of the windows machine, we need to configure Port Forwarding. This had been setup for the database listener (port 1521) and the ODI agent running on port 15101. What I had omitted to do was add port forwarding for the JMXPort used for debugging. Adding port 16101 solved the problem.

I wonder....

I wonder if anybody at Oracle ever looked up the different meanings of the word odi. In Latin it means hate or hatred. I hope I will not learn to hate ODI.




Friday, August 22, 2014

How to access a package variable in a SQL-statement.

Recently I tried to use a public package variable in a select statement, but ran into an error. 
ORA-06553: PLS-221: 'A_VAR' is not a procedure or is undefined

Unfortunately it is not possible to access such a variable directly in SQL. The solution would be to create a getter function which returns value of the variable.

CREATE OR REPLACE PACKAGE onevar
IS
   a_var   VARCHAR2(10) DEFAULT 'Hello';
   a_date  DATE DEFAULT SYSDATE;
   a_nr    NUMBER DEFAULT 100;

   FUNCTION get_a_var
      RETURN VARCHAR2;
END;
/

CREATE OR REPLACE PACKAGE BODY onevar
IS
   FUNCTION get_a_var
      RETURN VARCHAR2
   IS
   BEGIN
      RETURN (a_var);
   END;
END;
/


We could then use this function in a select statement


The advantage of this approach is that we can define the variable as private, which is a good idea from a security point of view, but we would have to define a getter function for each variable which could be quite tedious. If it is a third party package we might not be able to change the source code because of copyright reasons. So I got to think about another, more generic, way to achieve the same effect. 

The best alternative solution I could think of, which could be made universal, is by using an application context. I created a package which accepts a package variable name (which has to be public for obvious reasons) and then use dynamic PL/SQL to define a context variable and then return it from a function.

First the context needs to be created:  
CREATE OR REPLACE CONTEXT get_any_var_ctx USING get_any_var;

The package get_any_var is authorized to set the context. After that I created the package that sets the context, fetches it and returns it from a function:

CREATE OR REPLACE PACKAGE get_any_var
IS
   PROCEDURE set_context(p_name    VARCHAR2
                        ,p_value   VARCHAR2);
   FUNCTION get_str(p_var_name VARCHAR2)
      RETURN VARCHAR2;
END;
/

CREATE OR REPLACE PACKAGE BODY get_any_var
IS
   PROCEDURE set_context(p_name    VARCHAR2
                        ,p_value   VARCHAR2)
   IS
   BEGIN
      -- Create a session with a previously defined context.
      dbms_session.set_context('GET_ANY_VAR_CTX'
                              ,p_name
                              ,p_value);
   END;

   FUNCTION get_str(p_var_name VARCHAR2)
      RETURN VARCHAR2
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'begin get_any_var.set_context(''l_temp_var'','
         || p_var_name
         || '); end;';

      RETURN (SYS_CONTEXT('GET_ANY_VAR_CTX'
                         ,'l_temp_var'));
   END;
END;
/

Now it is possible to use any public package variable or constant in a select statement:  

The package can be extended with multiple functions for each data type, eg. NUMBER, DATE etc.

Of course there are some caveats:

  • The schema under which the package is created must have CREATE ANY CONTEXT privilege, and probably also DELETE ANY CONTEXT. 
  • Because the variables are public they can be set by anyone who has execute rights on the package.
  • There could be a negative impact on performance.
  • Because of the dynamic SQL typo’s will not be noticed at compile time. 
  • Using the wrong function could lead to implicit type conversions unless extensive error handling is added to the package.
 A full version of the package could look like this (no error handling however):
 

CREATE OR REPLACE PACKAGE get_any_var
IS
   PROCEDURE set_context(p_name    VARCHAR2
                        ,p_value   VARCHAR2);
   FUNCTION get_nr(p_var_name VARCHAR2)
      RETURN NUMBER;
   FUNCTION get_str(p_var_name VARCHAR2)
      RETURN VARCHAR2;
   FUNCTION get_date(p_var_name      VARCHAR2
                    ,p_date_format   VARCHAR2 DEFAULT 'dd-mm-yyyy hh24:mi:ss')
      RETURN DATE;
END;
/

CREATE OR REPLACE PACKAGE BODY get_any_var
IS
   PROCEDURE set_context(p_name    VARCHAR2
                        ,p_value   VARCHAR2)
   IS
   BEGIN
      -- Create a session with a previously defined context.
      dbms_session.set_context('GET_ANY_VAR_CTX'
                              ,p_name
                              ,p_value);
   END;

   FUNCTION get_nr(p_var_name VARCHAR2)
      RETURN NUMBER
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'begin get_any_var.set_context(''l_temp_var'','
         || p_var_name
         || '); end;';

      RETURN (TO_NUMBER(SYS_CONTEXT('GET_ANY_VAR_CTX'
                                   ,'l_temp_var')));
   END;

   FUNCTION get_str(p_var_name VARCHAR2)
      RETURN VARCHAR2
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'begin get_any_var.set_context(''l_temp_var'','
         || p_var_name
         || '); end;';

      RETURN (SYS_CONTEXT('GET_ANY_VAR_CTX'
                         ,'l_temp_var'));
   END;

   FUNCTION get_date(p_var_name      VARCHAR2
                    ,p_date_format   VARCHAR2 DEFAULT 'dd-mm-yyyy hh24:mi:ss')
      RETURN DATE
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'begin get_any_var.set_context(''l_temp_var'', to_char('
         || p_var_name
         || ','''
         || p_date_format
         || ''')); end;';

      RETURN (TO_DATE(SYS_CONTEXT('GET_ANY_VAR_CTX'
                                 ,'l_temp_var')
                     ,p_date_format));

   END;
END;
/