<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:media="http://search.yahoo.com/mrss/"
	>

<channel>
	<title>Finn Ellebaek Nielsen&#039;s Blog</title>
	<atom:link href="http://ellebaek.wordpress.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://ellebaek.wordpress.com</link>
	<description>Sophisticated Tips for Oracle Developers</description>
	<lastBuildDate>Mon, 13 Feb 2012 14:34:03 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.com/</generator>
<cloud domain='ellebaek.wordpress.com' port='80' path='/?rsscloud=notify' registerProcedure='' protocol='http-post' />
<image>
		<url>http://0.gravatar.com/blavatar/0a36f139059475030d7178f681a4ee8e?s=96&#038;d=http%3A%2F%2Fs2.wp.com%2Fi%2Fbuttonw-com.png</url>
		<title>Finn Ellebaek Nielsen&#039;s Blog</title>
		<link>http://ellebaek.wordpress.com</link>
	</image>
	<atom:link rel="search" type="application/opensearchdescription+xml" href="http://ellebaek.wordpress.com/osd.xml" title="Finn Ellebaek Nielsen&#039;s Blog" />
	<atom:link rel='hub' href='http://ellebaek.wordpress.com/?pushpress=hub'/>
		<item>
		<title>Copying/Transforming a REF CURSOR in Oracle 10g+</title>
		<link>http://ellebaek.wordpress.com/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g/</link>
		<comments>http://ellebaek.wordpress.com/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g/#comments</comments>
		<pubDate>Tue, 29 Mar 2011 14:52:25 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[copy]]></category>
		<category><![CDATA[oracle]]></category>
		<category><![CDATA[ref cursor]]></category>
		<category><![CDATA[transform]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=211</guid>
		<description><![CDATA[Introduction Use case: You need to be able to copy and optionally transform any PL/SQL REF CURSOR in a uniform way across all editions of Oracle Database from 10g Release 1 and newer. You need to fetch the REF CURSOR into transient (memory) or persistent storage (normal or global temporary table). Performance is not crucial. [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=211&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<h1>Introduction</h1>
<p>Use case: You need to be able to copy and optionally transform any PL/SQL <code>REF CURSOR</code> in a uniform way across all editions of Oracle Database from 10<em>g</em> Release 1 and newer. You need to fetch the <code>REF CURSOR</code> into transient (memory) or persistent storage (normal or global temporary table). Performance is not crucial.</p>
<p>Challenges: <code>REF CURSOR</code>s are very limited and in some Oracle releases quite fragile to work with.</p>
<p>Solution: I&#8217;ve developed PL/SQL code that implements the use case (except for <code>LONG</code> and <code>LONG RAW</code> columns) . This code leverages the <code>REF CURSOR</code> description technique described in my blog post <a href="/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c" target="_blank">Describing a REF CURSOR in Oracle 10<em>g</em>+ Using PL/SQL, Java and C</a>. This blog post describes the code, which has taken a significant amount of time to develop.</p>
<h1>Algorithm</h1>
<p>This is the basic algorithm of the solution, implemented in a PL/SQL package <code>REF_CURSOR_COPY</code>:</p>
<ul>
<li>Describe given <code>REF CURSOR</code>.</li>
<li>Create/reuse object types and table compatible with given <code>REF CURSOR</code>.</li>
<li>Generate dynamic PL/SQL code that fetches all rows from given <code>REF CURSOR</code> into either memory or table, returning a new <code>REF CURSOR</code> selecting from this copy, optionally calling:
<ul>
<li>Given PL/SQL function for each row that was fetched.</li>
<li>Given PL/SQL block before the new copy is opened.</li>
<li>Given PL/SQL block after the new copy is opened.</li>
</ul>
</li>
<li>Call dynamic PL/SQL code, returning a new <code>REF CURSOR</code> opened for selecting from the copy<code>.</code></li>
</ul>
<p>The code generated would have the following form:<br />
<pre class="brush: sql; pad-line-numbers: false;">
declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  fetched_row ref_cur_copy_&lt;seq&gt;_t := ref_cur_copy_&lt;seq&gt;_t(null, ..., null);
  fetched_rows ref_cur_copy_&lt;seq&gt;_c := ref_cur_copy_&lt;seq&gt;_c();
  n pls_integer := 0;
  ...
begin
  rc1 := :rc1;
  loop
    exit when not rc1%isopen;
    fetch rc1 into fetched_row.&lt;col1&gt;, ..., fetched_row.&lt;coln&gt;;
    exit when rc1%notfound;
    n := n + 1;

    -- Optionally manipulate each row.
    &lt;plsql_block_for_each&gt;

    fetched_rows.extend(1);
    fetched_rows(n) := fetched_row;
  end loop;
  begin
    if rc1%isopen then
      close rc1;
    end if;
  exception
    when others then
      null;
  end;
  :row_count := n;

  -- Optionally call PL/SQL block before copy is opened.
  &lt;plsql_block_before&gt;

  open rc2 for
  select *
  from   table(cast(fetched_rows as ref_cur_copy_&lt;seq&gt;_c));

  -- Optionally call PL/SQL block after copy is opened.
  &lt;plsql_block_after&gt;

  :rc2 := rc2;
end;
</pre></p>
<p>Where we&#8217;re supplying the following bind variables:</p>
<ul>
<li><code>:RC1</code>: Input <code>REF CURSOR</code>.</li>
<li><code>:ROW_COUNT</code>: Output number of rows fetched from <code>:RC1</code>.</li>
<li><code>:RC2</code>: Output <code>REF CURSOR</code>.</li>
</ul>
<p>It would have been nice if you could write code like the following, but unfortunately Oracle doesn&#8217;t allow that:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
fetch rc into fetched_row;
</pre><br />
where <code>FETCHED_ROW</code> is an instance of an object type compatible with the row type of <code>RC</code>. Instead, we need to nominate a variable per column.</p>
<p>If copying to a table instead the code generated is slightly different:</p>
<p><pre class="brush: sql; collapse: false; pad-line-numbers: false;">
declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  fetched_row ref_cur_copy_&lt;seq&gt;_t := ref_cur_copy_&lt;seq&gt;_t(null, ..., null);
  fetched_rows ref_cur_copy_&lt;seq&gt;_c := ref_cur_copy_&lt;seq&gt;_c();
  n pls_integer := 0;
  ...
begin
  rc1 := :rc1;
  loop
    exit when not rc1%isopen;
    fetch rc1 into fetched_row.&lt;col1&gt;, ..., fetched_row.&lt;coln&gt;;
    exit when rc1%notfound;
    n := n + 1;

    -- Optionally manipulate each row.
    &lt;plsql_block_for_each&gt;

    insert into ref_cur_copy_&lt;seq&gt; (
      -- Session ID if not a global temporary table.
      &quot;sessionid&quot;,
      &quot;rownum&quot;,
      &lt;col1&gt;,
      ...
      &lt;coln&gt;
    )
    values (
      userenv('sessionid'),
      n,
      fetched_row.&lt;col1&gt;,
      ...
      fetched_row.&lt;coln&gt;
    );
  end loop;
  begin
    if rc1%isopen then
      close rc1;
    end if;
  exception
    when others then
      null;
  end;
  :row_count := n;
  commit;

  -- Optionally call PL/SQL block before copy is opened.
  &lt;plsql_block_before&gt;

  open rc2 for
  select t.&lt;col1&gt;, ..., t.&lt;coln&gt;
  from   ref_cur_copy_&lt;seq&gt; t
  where  &quot;sessionid&quot; = userenv('sessionid')
  order  by &quot;rownum&quot;;

  -- Optionally call PL/SQL block after copy is opened.
  &lt;plsql_block_after&gt;

  :rc2 := rc2;
end;
</pre></p>
<p>If any of the columns in the <code>REF CURSOR</code> is a <code>REF CURSOR</code> itself (weak, strong or <code>CURSOR</code> expression, called a sub or an embedded <code>REF CURSOR</code> in the following), it gets a little more complicated. Unfortunately, we cannot use the <code>SYS_REFCURSOR</code> datatype for an object type attribute and we cannot use it in an associative array either, which is the reason why I didn&#039;t bother using <code>BULK COLLECT</code> when fetching to memory. Instead, we must declare a local variable of datatype <code>SYS_REFCURSOR </code>for each sub <code>REF CURSOR</code>, &quot;sub fetch&quot; this into an appropriate &quot;sub&quot; object type and convert this to an <code>ANYDATA</code> instance, such that we can hold the value in the &quot;top&quot; object type. Since we in Oracle cannot describe a <code>REF CURSOR</code> before it has been executed, this is the only solution possible as we cannot describe the sub <code>REF CURSOR</code> until it has been fetched for each row in the top <code>REF CURSOR</code>.</p>
<p>Here is the code generated when we have embedded <code>REF CURSOR</code>s: (here we assume that the sub <code>REF CURSOR</code> columns are the last columns of the top <code>REF CURSOR</code>. This is just in order to simplify the code shown below, <code>REF_CURSOR_COPY</code> doesn&#8217;t assume that):</p>
<p><pre class="brush: sql; collapse: false; pad-line-numbers: false;">
declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  fetched_row ref_cur_copy_&lt;seq&gt;_t := ref_cur_copy_&lt;seq&gt;_t(null, ..., null);
  fetched_rows ref_cur_copy_&lt;seq&gt;_c := ref_cur_copy_&lt;seq&gt;_c();
  n pls_integer := 0;
  rce1 sys_refcursor;
  type_name_e1 user_types.type_name%type;
  ...
  rce&lt;m&gt; sys_refcursor;
  type_name_e&lt;m&gt; user_types.type_name%type;
  ...
begin
  rc1 := :rc1;
  loop
    exit when not rc1%isopen;
    fetch rc1 into fetched_row.&lt;col1&gt;, ..., fetched_row.&lt;coln-m&gt;, rce1, ..., rce&lt;m&gt;;
    exit when rc1%notfound;
    fetched_row.&lt;coln-m+1&gt; := ref_cursor_copy.fetch_data(rce1, type_name_e1);
    type_name_e1 := ref_cursor_copy.get_collection_type_name(type_name_e1);
    ...
    fetched_row.&lt;colm&gt; := ref_cursor_copy.fetch_data(rce&lt;m&gt;, type_name_e&lt;m&gt;);
    type_name_e&lt;m&gt; := ref_cursor_copy.get_collection_type_name(type_name_e&lt;m&gt;);
    n := n + 1;

    -- Optionally manipulate each row.
    &lt;plsql_block_for_each&gt;

    fetched_rows.extend(1);
    fetched_rows(n) := fetched_row;
  end loop;
  begin
    if rc1%isopen then
      close rc1;
    end if;
  exception
    when others then
      null;
  end;
  :row_count := n;

  -- Optionally call PL/SQL block before copy is opened.
  &lt;plsql_block_before&gt;

  open rc2 for q'[
  select &lt;col1&gt;,
         ...,
         &lt;coln-m&gt;,
         cursor(select * from table(&lt;coln-m+1&gt;)) &lt;coln-m+1&gt;,
         ...,
         cursor(select * from table(&lt;coln&gt;)) &lt;coln&gt;
  from   (
           select &lt;col1&gt;,
                  ...,
                  &lt;coln-m&gt;,
                  cast(&lt;coln-m+1&gt; as ]' || type_name_e1 || q'[) &lt;coln-m+1&gt;,
                  ...,
                  cast(&lt;coln&gt; as ]' || type_name_e&lt;m&gt; || q'[) &lt;coln&gt;
           from   table(cast(:fetched_rows as ref_cur_copy_&lt;seq&gt;_c))
         )]'
  using in fetched_rows;

  -- Optionally call PL/SQL block after copy is opened.
  &lt;plsql_block_after&gt;

  :rc2 := rc2;
end;
</pre><br />
Please note that now we need to dynamically select from the collection holding the fetched data. This is in order to cast each sub <code>REF CURSOR</code> to the appropriate object type and then select all the columns from this. Also note that the <code>SELECT</code> statement seems overly complex with the subquery, but unfortunately this is necessary as a workaround to an Oracle bug through which an <code>ORA-00600 [kocgpn129]</code> error is raised if we cast the<code> ANYDATA</code> to the appropriate collection and select from it in the same <code>SELECT</code> statement (seems to be <code>Bug 9836806: ORA-600 AND ORA-7445 ERRORS WHEN UNNESTING SYS.ANYDATA</code>). This is illustrated by the following example:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
drop type ref_cur_copy_0_c;
drop type ref_cur_copy_0_t;
drop type ref_cur_copy_0e_c;
drop type ref_cur_copy_0e_t;

create type ref_cur_copy_0_t as object (
  deptno     number(2),
  dname      varchar2(14),
  emp_cursor sys.anydata
);
/

show err

create type ref_cur_copy_0_c as
table of ref_cur_copy_0_t;
/

show err

create type ref_cur_copy_0e_t as object (
  empno     number(4),
  ename     varchar2(10)
);
/

show err

create type ref_cur_copy_0e_c as
table of ref_cur_copy_0e_t;
/

show err

variable rc2 refcursor

prompt Expected to succeed

declare
  rc1 sys_refcursor;

  fetched_row ref_cur_copy_0_t := ref_cur_copy_0_t(null, null, null);
  fetched_rows ref_cur_copy_0_c := ref_cur_copy_0_c();

  fetched_row_e ref_cur_copy_0e_t := ref_cur_copy_0e_t(null, null);
  fetched_rows_e ref_cur_copy_0e_c := ref_cur_copy_0e_c();

  rce1 sys_refcursor;
  type_name_e1 user_types.type_name%type := 'REF_CUR_COPY_0E_C';
begin
  open rc1 for
  select deptno,
         dname,
         cursor(
           select empno,
                  ename
           from   emp e
           where  e.deptno = d.deptno and
                  rownum &lt;= 2
         ) emp_cursor
  from   dept d
  where  deptno &lt;= 20;

  loop
    -- Fetch one top REF CURSOR row.
    fetch rc1 into fetched_row.deptno, fetched_row.dname, rce1;
    exit when rc1%notfound;

    -- Fetch all sub REF CURSOR rows for the top row.
    fetched_rows_e := ref_cur_copy_0e_c();
    loop
      fetch rce1 into fetched_row_e.empno, fetched_row_e.ename;
      exit when rce1%notfound;
      fetched_rows_e.extend(1);
      fetched_rows_e(fetched_rows_e.count) := fetched_row_e;
    end loop;
    --if portable.get_major_minor_version != 10.1 then
      close rce1;
    --end if;
    fetched_row.emp_cursor := anydata.convertcollection(fetched_rows_e);
    fetched_rows.extend(1);
    fetched_rows(fetched_rows.count) := fetched_row;
  end loop;
  --if portable.get_major_minor_version != 10.1 then
    close rc1;
  --end if;

  open :rc2 for
  select deptno,
         dname,
         cursor(
           select *
           from   table(emp_cursor)
         ) emp_cursor
  from   (
           select deptno,
                  dname,
                  cast(emp_cursor as ref_cur_copy_0e_c) emp_cursor
           from   table(cast(fetched_rows as ref_cur_copy_0_c))
         );
end;
/

print :rc2

prompt Expected to fail

declare
  rc1 sys_refcursor;

  fetched_row ref_cur_copy_0_t := ref_cur_copy_0_t(null, null, null);
  fetched_rows ref_cur_copy_0_c := ref_cur_copy_0_c();

  fetched_row_e ref_cur_copy_0e_t := ref_cur_copy_0e_t(null, null);
  fetched_rows_e ref_cur_copy_0e_c := ref_cur_copy_0e_c();

  rce1 sys_refcursor;
  type_name_e1 user_types.type_name%type := 'REF_CUR_COPY_0E_C';
begin
  open rc1 for
  select deptno,
         dname,
         cursor(
           select empno,
                  ename
           from   emp e
           where  e.deptno = d.deptno and
                  rownum &lt;= 2
         ) emp_cursor
  from   dept d
  where  deptno &lt;= 20;

  loop
    -- Fetch one top REF CURSOR row.
    fetch rc1 into fetched_row.deptno, fetched_row.dname, rce1;
    exit when rc1%notfound;

    -- Fetch all sub REF CURSOR rows for the top row.
    fetched_rows_e := ref_cur_copy_0e_c();
    loop
      fetch rce1 into fetched_row_e.empno, fetched_row_e.ename;
      exit when rce1%notfound;
      fetched_rows_e.extend(1);
      fetched_rows_e(fetched_rows_e.count) := fetched_row_e;
    end loop;
    --if portable.get_major_minor_version != 10.1 then
      close rce1;
    --end if;
    fetched_row.emp_cursor := anydata.convertcollection(fetched_rows_e);
    fetched_rows.extend(1);
    fetched_rows(fetched_rows.count) := fetched_row;
  end loop;
  --if portable.get_major_minor_version != 10.1 then
    close rc1;
  --end if;

  open :rc2 for
  select deptno,
         dname,
         cursor(
           select empno,
                  ename
           from   table(cast(emp_cursor as ref_cur_copy_0e_c))
         ) emp_cursor
  from   table(cast(fetched_rows as ref_cur_copy_0_c));
end;
/

print :rc2

drop type ref_cur_copy_0_c;
drop type ref_cur_copy_0_t;
drop type ref_cur_copy_0e_c;
drop type ref_cur_copy_0e_t;
</pre><br />
which produces the following output (abbreviated):<br />
<pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
Expected to succeed

PL/SQL procedure successfully completed.


    DEPTNO DNAME          EMP_CURSOR
---------- -------------- --------------------
        10 ACCOUNTING     CURSOR STATEMENT : 3

CURSOR STATEMENT : 3

     EMPNO ENAME
---------- ----------
      7782 CLARK
      7839 KING

        20 RESEARCH       CURSOR STATEMENT : 3

CURSOR STATEMENT : 3

     EMPNO ENAME
---------- ----------
      7369 SMITH
      7566 JONES


Expected to fail

PL/SQL procedure successfully completed.

ERROR:
ORA-00600: internal error code, arguments: [kocgpn129], [2], [], [], [], [], [], []
</pre></p>
<p>We could have simply selected all columns from the <code>ANYDATA</code> columns, letting Oracle unnest appropriately. However, this would mean that the structure of the copy wouldn&#8217;t be the same as the original <code>REF CURSOR</code> (we would suddenly have a collection instead of a <code>REF CURSOR</code> for the embedded <code>REF CURSOR</code>), so this is not a viable solution.</p>
<p>I&#039;m assuming that the <code>REF CURSOR</code> columns are compatible across the rows &#8211; ie, my implementation doesn&#039;t cater for a <code>REF CURSOR</code> returned in given column for row 1 that has 3 columns <code>COLA</code>, <code>COLB</code>, <code>COLC</code> and for row 2 it has two columns <code>COLB</code> and <code>COLD</code>. </p>
<h2>Object Type</h2>
<p>Because object types cannot have attributes of datatypes <code>LONG</code> and <code>LONG RAW</code>, because it&#8217;s not possible to fetch more than 32KB from these in PL/SQL unless resorting to dynamic PL/SQL and because in general their use has been deprecated for many years, I won&#8217;t be supporting them with my code. Please refer to my blog post <a href="/2010/12/06/converting-a-long-column-to-a-clob-on-the-fly/" target="_blank">Converting a LONG Column to a CLOB on the fly</a> on how to convert a <code>LONG</code> column to a <code>CLOB</code> value on the fly if you need this.</p>
<p>For obvious performance reasons it&#8217;s important that we reuse the object type instead of creating a new for each use. This is controlled through a table <code>REF_CURSOR_COPY_TYPES</code> that has the following definition:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
create table ref_cursor_copy_types (
  description_md5_hash   varchar2(32),
  description_length     number(5),
  ref_cursor_description xmltype,
  type_name              varchar2(30),
  type_name_collection   varchar2(30),
  table_name             varchar2(30),
  table_impl             varchar2(2)
);
</pre></p>
<h2>Table</h2>
<p>The table generated for each <code>REF CURSOR</code>has the following structure:</p>
<p><pre class="brush: sql; collapse: false; pad-line-numbers: false;">
create table ref_cur_copy_&lt;seq&gt; (
  &quot;sessionid&quot; integer,
  &quot;rownum&quot;    integer,
  &lt;col1&gt;      &lt;col1_datatype&gt;,
  ...
  &lt;coln&gt;      &lt;coln_datatype&gt;
);
</pre><br />
where</p>
<ul>
<li><code>"sessionid"</code> is used to ensure session-specific data.</li>
<li><code>"rownum"</code> is used to ensure ordering of the data.</li>
</ul>
<p>The code can be configured to create a global temporary table instead (with either deletion or preservation of data upon commit) as well, which means that the <code>&quot;sessionid&quot;</code> column is not used, as Oracle guarantees that the table data is session-specific. If you would like to use autonomous transactions in the copy code you need to be aware that this is not compatible with <code>&quot;delete on commit&quot;</code> as this will remove the data from the temporary table such that the copy <code>REF CURSOR</code> will return no rows.</p>
<h2>VARRAY vs Nested Table</h2>
<p>I would prefer to keep data in the copy in the same order as the original so the default implementation of the collection object types uses <code>VARRAY</code>. However, this creates problems on non-Windows platforms such as Linux, SPARC Solaris and Intel Solaris, as Oracle throws the error <code>ORA-22909: exceeded maximum VARRAY limit</code> even though this obviously isn&#039;t true. For this reason, I&#039;ve made it configurable whether to use <code>VARRAY</code> or a nested table. It must be noted that the ordering is undefined when using the nested table.</p>
<h1>REF_CURSOR_COPY</h1>
<p>Here&#8217;s the implementation of the <code>REF_CURSOR_COPY</code> package specification:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace package ref_cursor_copy
authid current_user as
/**
 * Utility to copy a REF CURSOR with the possibility to inject code before, for
 * each row and after the copy is fetched.
 * Depends on REF_CURSOR_DESCRIBE.
 * Feel free to use at your own risk.
 * @version   $Revision: 3 $
 * @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.
 */

  /**
   * Controls implementation details:
   * 'O': Use ODCI.SAVE-/RESTORECURSOR for top-level.
   * 'o': Use ODCI.SAVE-/RESTORECURSOR for non top-level.
   * 'C': Close original REF CURSOR again, after fetched and close.
   * 'c': Close sub REF CURSORs outside sub fetch code, not inside.
   * 'R': Use DBMS_ODCI.RESTOREREFCURSOR where it shouldn't be necessary.
   * 'B': Don't fetch using BULK COLLECT, fetch one row at a time.
   */
  copy_impl varchar2(5) := '';
  /**
   * Controls close implementation details:
   * '':  Close both top and sub REF CURSORs once in the appropriate place.
   * 'A': Close REF CURSOR again, after fetched and close.
   * 'N': Set REF CURSOR to NULL, after fetched and close.
   * 'S': Close sub REF CURSORs outside sub fetch code, not inside.
   * 'd': Don't close sub REF CURSORs at all.
   * 'D': Don't close top REF CURSORs at all.
   */
  close_impl varchar2(5) := '';

  /**
   * Controls collection type creation details:
   * 'V': Use VARRAY for collections (default).
   * 'T': Use TABLE for collections.
   */
  collection_impl varchar2(2) := 'V';
  /**
   * Controls table creation details:
   * 'T':  Use normal table with &quot;sessionid&quot; column.
   * 'td': Use global temporary table, delete rows upon commit.
   * 'tp': Use global temporary table, preserve rows upon commit (default).
   */
  table_impl varchar2(2) := 'tp';

  function to_ref_cursor(
    rc in out sys_refcursor
  )
  return sys_refcursor;
  function to_ref_cursor(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := '',
    plsql_block_before_copy_open in varchar2 := '',
    plsql_block_after_copy_open in varchar2 := ''
  )
  return sys_refcursor;
  function to_anydata(
    rc in out sys_refcursor,
    plsql_block_after_each_fetch in varchar2 := ''
  )
  return anydata;

  function fetch_data(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := ''
  )
  return anydata;
  function get_fetch_data_plsql(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    name_list in out sys.odcivarchar2list,
    type_code_list in out sys.odcinumberlist,
    ref_cur_count in out pls_integer,
    top in boolean,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := '',
    expose_rows in boolean := true,
    xml out xmltype
  )
  return varchar2;

  function get_row_count
  return pls_integer;
  function get_sub_row_count
  return pls_integer;

  function get_type_name(
    ref_cursor_desc in xmltype
  )
  return user_types.type_name%type;
  function get_collection_type_name(
    type_name in user_types.type_name%type
  )
  return user_types.type_name%type;
  function get_table_name(
    type_name in user_types.type_name%type
  )
  return user_tables.table_name%type;
  function get_table_impl(
    table_name in user_types.type_name%type,
    destination in varchar2
  )
  return ref_cursor_copy_types.table_impl%type;

  function rc_from_anydata(
    ad in anydata,
    type_name_collection in varchar2
  )
  return sys_refcursor;

  function purge_tables
  return integer;
  function drop_types_and_tables
  return integer;

  function use_odci(top in boolean)
  return boolean;
end ref_cursor_copy;
/
</pre><br />
Please note the package parameters <code>COPY_IMPL</code>, <code>CLOSE_IMPL</code> and <code>COLLECTION_IMPL</code> that can be used to manipulate the generated code in various ways.<br />
Here&#8217;s the implementation of the <code>REF_CURSOR_COPY</code> package specification:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace package body ref_cursor_copy as
  --- @version   $Revision: 3 $

  --- Number of rows fetched in latest COPY call.
  row_count pls_integer;
  --- Number of rows fetched in latest FETCH_DATA call.
  sub_row_count pls_integer;
  --- Error info.
  error_info varchar2(32767);

  procedure safely_close(
    rc in out sys_refcursor,
    force in boolean := false
  );

/**
 * Copies a REF CURSOR to memory.
 * @param   rc      REF CURSOR.
 * @return  A new REF CURSOR opened for the copy in memory.
 * @throws  ORA-20000 if the REF CURSOR contains LONG or LONG RAW columns.
 */

  function to_ref_cursor(
    rc in out sys_refcursor
  )
  return sys_refcursor as

    type_name user_types.type_name%type;

  begin
    return to_ref_cursor(rc, type_name);
  end to_ref_cursor;

/**
 * Copies a REF CURSOR to either memory or a table. Provides the ability to
 * execute PL/SQL blocks before the copy is fetched, after each row is fetched,
 * and after the entire copy has been fetched, before opening the new REF
 * CURSOR.
 * @param   rc      REF CURSOR.
 * @param   type_name
 *                  Object type name of object type representing a row on the
 *                  given REF CURSOR. Has the name of the form
 *                  REF_CURSOR_COPY_&lt;sequence&gt;_T.
 * @param   destination
 *                  Whether the copy should be made to memory ('M') or to a
 *                  table ('T').
 * @param   plsql_block_after_each_fetch
 *                  PL/SQL block to run for each row after it has been
 *                  fetched, working on the FETCHED_ROW object type instance
 *                  that holds the column values fetched. This can be used to
 *                  transform the data fetched.
 * @param   plsql_block_before_copy_open
 *                  PL/SQL block to run before the REF CURSOR copy is opened.
 * @param   plsql_block_after_copy_open
 *                  PL/SQL block to run after the REF CURSOR copy is opened.
 * @return  A new REF CURSOR opened for the copy in memory or table.
 * @throws  ORA-20000 if the REF CURSOR contains LONG or LONG RAW columns.
 */

  function to_ref_cursor(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := '',
    plsql_block_before_copy_open in varchar2 := '',
    plsql_block_after_copy_open in varchar2 := ''
  )
  return sys_refcursor as
  /*pragma autonomous_transaction;*/

    plsql          varchar2(32767);
    plsql2         varchar2(32767);
    plsql3         varchar2(32767);
    plsql4         varchar2(32767);
    rcn            number;
    rc2n           number;
    rc2            sys_refcursor;
    type_name_c    user_types.type_name%type;
    table_name     user_tables.table_name%type;
    name_list      sys.odcivarchar2list := sys.odcivarchar2list();
    type_code_list sys.odcinumberlist := sys.odcinumberlist();
    ref_cur_count  pls_integer;
    j              pls_integer := 0;
    xml            xmltype;
    table_impl     ref_cursor_copy_types.table_impl%type;

  begin
    -- This is necessary to make it work on various Oracle versions. If
    -- omitted, Oracle throws various &quot;invalid cursor&quot; errors.
    if use_odci(true) then
      dbms_odci.saverefcursor(rc, rcn);
    end if;

    plsql := get_fetch_data_plsql(
      rc,
      type_name,
      name_list,
      type_code_list,
      ref_cur_count,
      true,
      upper(destination),
      plsql_block_after_each_fetch,
      false,
      xml
    ) || chr(10);
    type_name_c := get_collection_type_name(type_name);
    table_name := get_table_name(type_name);
    table_impl := get_table_impl(table_name, upper(destination));

    if plsql_block_before_copy_open is not null then
      plsql := plsql ||
        '' || chr(10) ||
        '  ' || plsql_block_before_copy_open || chr(10) ||
        '' || chr(10);
    end if;

    plsql := plsql ||
        '' || chr(10);

    if ref_cur_count &gt; 0 then
      -- Embedded REF CURSORs. More complex SELECT statement with CAST and
      -- CURSOR.
      if upper(destination) = 'M' then
        plsql4 :=
            '           from   table(cast(:fetched_rows as ' ||
                type_name_c || '))' || chr(10);
      elsif upper(destination) = 'T' then
        plsql4 :=
            '           from   ' || table_name || ' t' || chr(10) ||
            case when table_impl = 'T' then
              '           where  &quot;sessionid&quot; = userenv(''sessionid'')' || chr(10)
            end ||
            '           order  by &quot;rownum&quot;' || chr(10);
      end if;

      plsql := plsql ||
          '  open rc2 for q''[' || chr(10);
      for i in nvl(name_list.first, 0) .. nvl(name_list.last, -1) loop
        if i &gt; name_list.first then
          plsql2 := plsql2 || '         ';
          plsql3 := plsql3 || '                  ';
        end if;
        if type_code_list(i) = type_codes.tc_ref_cursor then
          j := j + 1;
          plsql2 := plsql2 ||
              'cursor(select * from table(' || name_list(i) || ')) ' ||
              name_list(i);
          plsql3 := plsql3 ||
              'cast(' || name_list(i) || ' as ]'' || type_name_e' || j ||
                  ' || q''[) ' || name_list(i);
        else
          plsql2 := plsql2 ||
              name_list(i);
          plsql3 := plsql3 ||
              name_list(i);
        end if;
        if i &lt; name_list.last then
          plsql2 := plsql2 || ',';
          plsql3 := plsql3 || ',';
        end if;
        plsql2 := plsql2 || chr(10);
        plsql3 := plsql3 || chr(10);
      end loop;
      plsql := plsql ||
          '  select ' ||
          plsql2 ||
          '  from   (' || chr(10) ||
          '           select ' ||
          plsql3 ||
          plsql4 ||
          '         )]''';
      if upper(destination) = 'M' then
        plsql := plsql || chr(10) ||
          '  using in fetched_rows;' || chr(10);
      else
        plsql := plsql || ';' || chr(10);
      end if;
    else
      -- No embedded REF CURSORs. Simple SELECT statement.
      if upper(destination) = 'M' then
        plsql := plsql ||
            '  open rc2 for' || chr(10) ||
            '  select *' || chr(10) ||
            '  from   table(cast(fetched_rows as ' || type_name_c || '));' ||
                chr(10);
      elsif upper(destination) = 'T' then
        plsql := plsql ||
            '  open rc2 for' || chr(10) ||
            '  select ';
        for c in 1 .. name_list.count loop
          plsql := plsql || case when c &gt; 1 then ', ' end || 't.' || name_list(c);
        end loop;
        plsql := plsql || chr(10) ||
            '  from   ' || table_name || ' t' || chr(10) ||
            case when table_impl = 'T' then
              '  where  &quot;sessionid&quot; = userenv(''sessionid'')' || chr(10)
            end ||
            '  order  by &quot;rownum&quot;;' || chr(10);
      end if;
    end if;

    if plsql_block_after_copy_open is not null then
      plsql := plsql ||
          '' || chr(10) ||
          '  ' || plsql_block_after_copy_open || chr(10) ||
          '' || chr(10);
    end if;

    if use_odci(true) then
      plsql := plsql ||
          '' || chr(10) ||
          '  dbms_odci.saverefcursor(rc2, :rc2n);' || chr(10);
    else
      plsql := plsql ||
          '' || chr(10) ||
          '  :rc2 := rc2;' || chr(10);
    end if;
    plsql := plsql ||
        'end;';

    if use_odci(true) then
      execute immediate plsql
      using in rcn, out row_count, out rc2n;
      if rcn is not null and copy_impl like '%R%' then
        dbms_odci.restorerefcursor(rc, rcn);
      end if;
      if rc2n is not null then
        dbms_odci.restorerefcursor(rc2, rc2n);
      end if;
    else
      execute immediate plsql
      using in out rc, out row_count, in out rc2;
    end if;
    safely_close(rc);

    return rc2;
  exception
    when others then
      error_info :=
          'ref_cursor_copy.to_ref_cursor: ' || chr(10) ||
          'plsql = ''' || plsql || '''' || chr(10) ||
          rtrim(sqlerrm, chr(10)) || chr(10) ||
          rtrim(dbms_utility.format_error_backtrace, chr(10));
      dbms_output_put_line(error_info);
      raise;
  end to_ref_cursor;

/**
 * Copies a REF CURSOR to memory. Provides the ability to execute a PL/SQL block
 * after each row is fetched.
 * @param   rc      REF CURSOR.
 * @param   plsql_block_after_each_fetch
 *                  PL/SQL block to run for each row after it has been
 *                  fetched, working on the FETCHED_ROW object type instance
 *                  that holds the column values fetched. This can be used to
 *                  transform the data fetched.
 * @return  An ANYDATA instance for the copy in memory.
 */

  function to_anydata(
    rc in out sys_refcursor,
    plsql_block_after_each_fetch in varchar2 := ''
  )
  return anydata as

    type_name user_types.type_name%type;

  begin
    return fetch_data(rc, type_name, 'M', plsql_block_after_each_fetch);
  end to_anydata;

/**
 * Fetches a REF CURSOR to either memory or a table. Provides the ability to
 * execute PL/SQL function for each row fetched.
 * @param   rc      REF CURSOR.
 * @param   type_name
 *                  Object type name of object type representing a row on the
 *                  given REF CURSOR. Has the name of the form
 *                  REF_CURSOR_COPY_&lt;sequence&gt;_T.
 * @param   destination
 *                  Whether the copy should be made to memory ('M') or to a
 *                  table ('T').
 * @param   plsql_block_after_each_fetch
 *                  PL/SQL block to run for each row after it has been
 *                  fetched, working on the FETCHED_ROW object type instance
 *                  that holds the column values fetched. This can be used to
 *                  transform the data fetched.
 * @return  A collection of the data fetched if DESTINATION = 'M'. NULL
 *          otherwise.
 * @throws  ORA-20000 if the REF CURSOR contains LONG or LONG RAW columns.
 */

  function fetch_data(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := ''
  )
  return anydata as
  /*pragma autonomous_transaction;*/

    plsql          varchar2(32767);
    plsql2         varchar2(32767);
    rc1            sys_refcursor;
    rcn            number;
    rc2n           number;
    rc2            sys_refcursor;
    rcd            xmltype;
    type_name_c    user_types.type_name%type;
    table_name     user_tables.table_name%type;
    n              pls_integer;
    name_list      sys.odcivarchar2list := sys.odcivarchar2list();
    type_code_list sys.odcinumberlist := sys.odcinumberlist();
    fetched_rows   anydata;
    j              pls_integer;
    ref_cur_count  pls_integer;
    xml            xmltype;

  begin
    -- This is necessary to make it work on Oracle 10.1. If omitted, Oracle
    -- throws various &quot;invalid cursor&quot; errors.
    if use_odci(false) then
      dbms_odci.saverefcursor(rc, rcn);
    end if;

    plsql := get_fetch_data_plsql(
      rc,
      type_name,
      name_list,
      type_code_list,
      ref_cur_count,
      false,
      destination,
      plsql_block_after_each_fetch,
      true,
      xml
    );

    if plsql is not null then
      plsql := plsql || chr(10) ||
          'end;';
      if ref_cursor_descriptor.describe_impl = 'xC' then
        execute immediate plsql
        using in xml;
      else
        if use_odci(false) then
          execute immediate plsql
          using in rcn, out sub_row_count, out fetched_rows;
          if copy_impl like '%R%' then
            dbms_odci.restorerefcursor(rc, rcn);
          end if;
        else
          execute immediate plsql
          using in out rc, out sub_row_count, out fetched_rows;
        end if;
      end if;
    end if;
    safely_close(rc);

    return fetched_rows;
  exception
    when others then
      error_info :=
          'ref_cursor_copy.fetch_data: ' || chr(10) ||
          'plsql = ''' || plsql || '''' || chr(10) ||
          rtrim(sqlerrm, chr(10)) || chr(10) ||
          rtrim(dbms_utility.format_error_backtrace, chr(10));
      dbms_output_put_line(error_info);
      safely_close(rc, true);
      raise;
  end fetch_data;

/**
 * Builds the PL/SQL that can be used to fetch a REF CURSOR to either memory or
 * a table. Provides the ability to execute PL/SQL function for each row
 * fetched.
 * @param   rc      REF CURSOR.
 * @param   type_name
 *                  Object type name of object type representing a row on the
 *                  given REF CURSOR. Has the name of the form
 *                  REF_CURSOR_COPY_&lt;sequence&gt;_T.
 * @param   name_list
 *                  Output: A collection of the column names of RC.
 * @param   type_code_list
 *                  Output: A collection of the column type codes of RC.
 * @param   ref_cur_count
 *                  Output: Number of REF CURSOR columns in RC.
 * @param   top     Is this a top-level REF CURSOR (TRUE) or an embedded REF
 *                  CURSOR (FALSE)?
 * @param   destination
 *                  Whether the copy should be made to memory ('M') or to a
 *                  table ('T').
 * @param   plsql_block_after_each_fetch
 *                  PL/SQL block to run for each row after it has been
 *                  fetched, working on the FETCHED_ROW object type instance
 *                  that holds the column values fetched. This can be used to
 *                  transform the data fetched.
 * @param   expose_rows
 *                  Should
 * @return  A collection of the data fetched if DESTINATION = 'M'. NULL
 *          otherwise.
 * @throws  ORA-20000 if the REF CURSOR contains LONG or LONG RAW columns.
 */

  function get_fetch_data_plsql(
    rc in out sys_refcursor,
    type_name in out user_types.type_name%type,
    name_list in out sys.odcivarchar2list,
    type_code_list in out sys.odcinumberlist,
    ref_cur_count in out pls_integer,
    top in boolean,
    destination in char := 'M',
    plsql_block_after_each_fetch in varchar2 := '',
    expose_rows in boolean := true,
    xml out xmltype
  )
  return varchar2 as

    plsql         varchar2(32767);
    plsql2        varchar2(32767);
    rcn           integer;
    rc2n          integer;
    rc1           sys_refcursor;
    rc2           sys_refcursor;
    rcd           xmltype;
    type_name_c   user_types.type_name%type;
    table_name    user_tables.table_name%type;
    n             pls_integer;
    fetched_rows  anydata;
    j             pls_integer;
    context       dbms_xmlgen.ctxtype;
    table_impl    ref_cursor_copy_types.table_impl%type;

  begin
    if not top and ref_cursor_descriptor.describe_impl = 'xC' then
      if rc is not null and rc%isopen then
        context := dbms_xmlgen.newcontext(rc);
        xml := dbms_xmlgen.getxmltype(context);
        rcd := ref_cursor_descriptor.describe(rc);
        dbms_xmlgen.closecontext(context);
        safely_close(rc, true);
      end if;
    else
      rcd := ref_cursor_descriptor.describe(rc);
    end if;
    type_name := get_type_name(rcd);
    type_name_c := get_collection_type_name(type_name);
    table_name := get_table_name(type_name);
    table_impl := get_table_impl(table_name, destination);

    -- This is necessary to make it work on Oracle 10.1. If omitted, Oracle
    -- throws various &quot;invalid cursor&quot; errors.
    if use_odci(top) then
      dbms_odci.saverefcursor(rc, rcn);
    end if;

    select count(*)
    into   ref_cur_count
    from   table(
             xmlsequence(
               extract(rcd, '/ROWSET/ROW')
             )
           ) c
    where  extractvalue(value(c), '//TYPE_CODE') = type_codes.tc_ref_cursor;

    if not top and ref_cursor_descriptor.describe_impl = 'xC' then
      plsql :=
          'declare' || chr(10) ||
          '  fetched_rows ' || type_name_c || ' := ' || type_name_c || '();' || chr(10) ||
          '  fetched_row ' || type_name || ';' || chr(10) ||
          '  n pls_integer := 0;' || chr(10) ||
          '  ad_fetched_rows anydata;' || chr(10) ||
          'begin' || chr(10);
    else
      plsql :=
          'declare' || chr(10) ||
          '  rc1 sys_refcursor;' || chr(10) ||
          '  rc2 sys_refcursor;' || chr(10) ||
          '  fetched_rows ' || type_name_c || ' := ' || type_name_c || '();' || chr(10) ||
          '  n pls_integer := 0;' || chr(10) ||
          '  ' || chr(10) ||
          '  type_name user_types.type_name%type;' || chr(10) ||
          '  ad_fetched_rows anydata;' || chr(10);
    end if;
    if not (not top and nvl(ref_cursor_descriptor.describe_impl, '{}') = 'xC') then
      for i in 1 .. ref_cur_count loop
        plsql := plsql ||
            '  rce' || i || ' sys_refcursor;' || chr(10) ||
            '  type_name_e' || i || ' user_types.type_name%type;' || chr(10);
      end loop;
      plsql := plsql ||
          'begin' || chr(10) ||
          case
            when use_odci(top) then
              '  dbms_odci.restorerefcursor(rc1, :rcn);' || chr(10)
            else
              '  rc1 := :rc;' || chr(10)
          end;
    end if;
    if upper(destination) = 'T' then
      -- Clear table.
      plsql := plsql ||
          '  delete' || chr(10) ||
          '  from ' || table_name ||
          case when table_impl = 'T' then
            ' t ' || chr(10) ||
                '  where t.&quot;sessionid&quot; = userenv(''sessionid'')'
          end ||
          ';' || chr(10);
    end if;
    if not top and ref_cursor_descriptor.describe_impl = 'xC' then
      plsql := plsql ||
          '  for r in (' || chr(10) ||
          '        select value(x) xml' || chr(10) ||
          '        from   table(xmlsequence(extract(:xml, ''/ROWSET/ROW''))) x' || chr(10) ||
          '      ) loop' || chr(10) ||
          '    r.xml.toobject(fetched_row);' || chr(10) ||
          '    fetched_rows.extend(1);' || chr(10) ||
          '    fetched_rows(fetched_rows.count) := fetched_row;' || chr(10) ||
          '    n := n + 1;' || chr(10) ||
          '  end loop;' || chr(10) ||
          '  :row_count := n;' || chr(10);
    else
      plsql := plsql ||
        '  loop' || chr(10) ||
        '    exit when not rc1%isopen;' || chr(10) ||
        '    fetch rc1 into ';

      n := 0;
      j := 1;
      for c in (
            select extractvalue(value(c), '//ID') id,
                   extractvalue(value(c), '//NAME') name,
                   extractvalue(value(c), '//TYPE_CODE') type_code
            from   table(
                     xmlsequence(
                       extract(rcd, '/ROWSET/ROW')
                     )
                   ) c
          ) loop
        plsql := plsql ||
            case when c.id &gt; 1 then ', ' end;
        if c.type_code = type_codes.tc_ref_cursor then
          -- Embedded REF CURSOR.
          plsql := plsql || 'rce' || j;
          plsql2 := plsql2 ||
              '    fetched_row.' || ref_cursor_descriptor.quote_identifier(c.name) ||
                     ' := ref_cursor_copy.fetch_data(' ||
                       'rce' || j || ', ' ||
                       'type_name_e' || j ||
                    ');' || chr(10) ||
              case when close_impl like '%S%' and close_impl not like '%d%' then
                '    begin' || chr(10) ||
                '      if rce' || j || '%isopen then' || chr(10) ||
                '        close rce' || j || ';' || chr(10) ||
                '      end if;' || chr(10) ||
                '    exception' || chr(10) ||
                '      when others then' || chr(10) ||
                '        null;' || chr(10) ||
                '    end;' || chr(10)
              end ||
              '    type_name_e' || j || ' := ' ||
                  'ref_cursor_copy.get_collection_type_name(type_name_e' || j ||
                  ');' || chr(10);
          j := j + 1;
        else
          plsql := plsql || 'fetched_row.' || ref_cursor_descriptor.quote_identifier(c.name);
        end if;
        n := n + 1;
        name_list.extend(1);
        name_list(n) := ref_cursor_descriptor.quote_identifier(c.name);
        type_code_list.extend(1);
        type_code_list(n) := c.type_code;
      end loop;

      plsql :=
          plsql || ';' || chr(10) ||
          '    exit when rc1%notfound;' || chr(10) ||
          plsql2 ||
          '    n := n + 1;' || chr(10);
      if plsql_block_after_each_fetch is not null then
        plsql := plsql ||
            '' || chr(10) ||
            plsql_block_after_each_fetch || chr(10) ||
            '' || chr(10);
      end if;
      if upper(destination) = 'M' then
        plsql := plsql ||
            '    fetched_rows.extend(1);' || chr(10) ||
            '    fetched_rows(n) := fetched_row;' || chr(10);
      elsif upper(destination) = 'T' then
        plsql := plsql ||
            '    insert into ' || table_name || ' (' || chr(10) ||
            case when table_impl = 'T' then
              '      &quot;sessionid&quot;,' || chr(10)
            end ||
            '      &quot;rownum&quot;,' || chr(10);
        for c in 1 .. name_list.count loop
          plsql := plsql ||
              '      ' || name_list(c) ||
              case when c &lt; name_list.count then ', ' end || chr(10);
        end loop;
        plsql := plsql ||
            '    )' || chr(10) ||
            '    values (' || chr(10) ||
            case when table_impl = 'T' then
              '      userenv(''sessionid''),' || chr(10)
            end ||
            '      n,' || chr(10);
        for c in 1 .. name_list.count loop
          plsql := plsql ||
              '      fetched_row.' || name_list(c) ||
              case when c &lt; name_list.count then ', ' end || chr(10);
        end loop;
        plsql := plsql ||
            '    );' || chr(10);
      end if;

      plsql := plsql ||
          '  end loop;' || chr(10) ||
          case when close_impl is null or
              top and
                close_impl not like '%D%'
              or not top and
                close_impl not like '%S%' and
                close_impl not like '%d%' then
            '  begin' || chr(10) ||
            '    if rc1%isopen then' || chr(10) ||
            '      close rc1;' || chr(10) ||
            '    end if;' || chr(10) ||
            '  exception' || chr(10) ||
            '    when others then' || chr(10) ||
            '      null;' || chr(10) ||
            '  end;' || chr(10)
          end ||
          '  :row_count := n;' || chr(10);

      -- Build constructor call to FETCHED_ROW variable.
      plsql := replace(
        plsql,
        '  fetched_rows ' || type_name_c || ' := ' || type_name_c || '();',
        '  fetched_rows ' || type_name_c || ' := ' || type_name_c || '();' || chr(10) ||
           '  fetched_row ' || type_name || ' := ' || type_name || '(' ||
                rtrim(rpad('null, ', 6 * n, 'null, '), ', ') || ');'
      );
    end if;

    if expose_rows then
      if destination = 'M' then
        plsql := plsql ||
          '  ad_fetched_rows := anydata.convertcollection(fetched_rows);' || chr(10);
      elsif destination = 'T' then
        if not table_impl like '%td' then
          plsql := plsql ||
              '  commit;' || chr(10);
        end if;
        plsql := plsql ||
            '  ad_fetched_rows := null;' || chr(10);
      end if;
      plsql := plsql ||
          '  :rows2 := ad_fetched_rows;';
    else
      if destination = 'T' and not table_impl like '%td' then
        plsql := plsql ||
            '  commit;';
      end if;
    end if;

    return plsql;
  end get_fetch_data_plsql;

/**
 * Gets the number of rows fetched in the latest COPY call.
 */

  function get_row_count
  return pls_integer as

  begin
    return row_count;
  end get_row_count;

/**
 * Gets the number of rows fetched in the latest FETCH_DATA call.
 */

  function get_sub_row_count
  return pls_integer as

  begin
    return sub_row_count;
  end get_sub_row_count;

/**
 * Creates two object types and a table matching a description of a REF CURSOR
 * obtained from REF_CURSOR_DESCRIBE.DESCRIBE. The object type has the the name
 * of the form REF_CURSOR_COPY_&lt;sequence&gt;_T. The VARRAY/TABLE collection has a
 * name of the form REF_CURSOR_COPY_&lt;sequence&gt;_C. The table has a name of the
 * form REF_CURSOR_COPY_&lt;sequence&gt;. If such object types and table already exist
 * they are reused.
 * @param   ref_cursor_desc
 *                  REF CURSOR description.
 * @return  Object type name.
 */

  function get_type_name(
    ref_cursor_desc in xmltype
  )
  return user_types.type_name%type as
  pragma autonomous_transaction;

    plsql                varchar2(32767);
    plsql2               varchar2(32767);
    type_name            user_types.type_name%type;
    type_name_c          user_types.type_name%type;
    table_name           user_tables.table_name%type;
    xml                  varchar2(32767);
    description_md5_hash ref_cursor_copy_types.description_md5_hash%type;
    description_length   pls_integer;
    n                    pls_integer;
    nt                   pls_integer := 0;
    m                    pls_integer;
    name_list            sys.odcivarchar2list := sys.odcivarchar2list();
    decl_list            sys.odcivarchar2list := sys.odcivarchar2list();

  begin
    xml := ref_cursor_desc.getstringval;
    description_md5_hash := rawtohex(utl_raw.cast_to_raw(
      dbms_obfuscation_toolkit.md5(input_string =&gt; xml))
    );
    description_length := length(xml);

    begin
      -- Do we have a matching object type already?
      select type_name
      into   type_name
      from   ref_cursor_copy_types rcct
      where  rcct.description_md5_hash = get_type_name.description_md5_hash and
             rcct.description_length = get_type_name.description_length;

      type_name_c := get_collection_type_name(type_name);
      table_name := get_table_name(type_name);

      -- Do both still exist?
      select count(ut1.type_name) + count(ut2.table_name)
      into   n
      from   user_types ut1,
             user_tables ut2
      where  ut1.type_name in (
               get_type_name.type_name,
               get_type_name.type_name_c
             ) and
             ut2.table_name = get_type_name.table_name;

      if n &lt; 3 then
        -- We had them, but not anymore. Drop what may have remained and remove
        -- from mapping.
        delete
        from   ref_cursor_copy_types rcct
        where  rcct.type_name = get_type_name.type_name;

        commit;

        begin
          execute immediate 'truncate table ' || table_name;
        exception
          when others then
            null;
        end;
        begin
          execute immediate 'drop table ' || table_name;
        exception
          when others then
            null;
        end;
        begin
          execute immediate 'drop type ' || type_name_c;
        exception
          when others then
            null;
        end;
        begin
          execute immediate 'drop type ' || type_name;
        exception
          when others then
            null;
        end;

        -- Create new ones.
        raise no_data_found;
      end if;
    exception
      when no_data_found then
        -- We don't have a matching object type -- create one.
        select 'REF_CUR_COPY_' || ref_cursor_copy_seq.nextval || '_T'
        into   type_name
        from   dual;

        type_name_c := get_collection_type_name(type_name);
        table_name := get_table_name(type_name);

        plsql :=
            'create type ' || type_name || ' as object (' || chr(10);

        for c in (
              select extractvalue(value(c), '//ID') id,
                     extractvalue(value(c), '//NAME') name,
                     extractvalue(value(c), '//TYPE_CODE') type_code,
                     extractvalue(value(c), '//DECLARATION') declaration,
                     extractvalue(value(c), '//OWNER') owner,
                     extractvalue(value(c), '//TYPE_NAME') type_name
              from   table(
                       xmlsequence(
                         extract(ref_cursor_desc, '/ROWSET/ROW')
                       )
                     ) c
            ) loop
          if c.type_code in (type_codes.tc_long, type_codes.tc_long_raw) then
            raise_application_error(
              -20000,
              'ref_cursor_copy: LONG [RAW] is not supported: Column ' ||
                  c.id || ', name &quot;' || c.name || '&quot;'
            );
          end if;

          plsql := plsql || c.declaration || ',' || chr(10);
          name_list.extend(1);
          name_list(c.id) := ref_cursor_descriptor.quote_identifier(c.name);
          decl_list.extend(1);
          decl_list(c.id) := c.declaration;

          if c.type_code = type_codes.tc_object then
            -- Check whether the type is a nested table.
            select count(text)
            into   m
            from   all_type_versions atv
            where  atv.owner = c.owner and
                   atv.type_name = c.type_name and
                   atv.typecode = 'COLLECTION' and
                   lower(text) like '%table%';

            if m &gt; 0 then
              -- Nested table.
              nt := nt + 1;
              plsql2 := plsql2 || chr(10) ||
                'nested table ' || c.name || ' store as ' || table_name || nt;
            end if;
          end if;
        end loop;

        plsql := rtrim(plsql, ',' || chr(10)) || chr(10) ||
            ');';

        begin
          -- Create object type.
          execute immediate plsql;

          -- Create object type collection.
          plsql :=
            'create type ' || type_name_c || ' as' || chr(10);
          if collection_impl = 'V' then
            plsql := plsql ||
                'varray(2147483647) of ' || type_name || ';';
          elsif collection_impl = 'T' then
            plsql := plsql ||
                'table of ' || type_name || ';';
          end if;

          execute immediate plsql;

          -- Create table.
          plsql :=
            'create ' ||
                case when table_impl in ('td', 'tp') then
                  'global temporary '
                end ||
                'table ' || table_name || ' (' || chr(10) ||
                case when table_impl = 'T' then
                  '  &quot;sessionid&quot; integer,' || chr(10)
                end ||
            '  &quot;rownum&quot; integer,' || chr(10);
          for c in 1 .. decl_list.count loop
            plsql := plsql ||
            '  ' || decl_list(c) ||
                case when c &lt; decl_list.count then ',' else '' end || chr(10);
          end loop;
          plsql := plsql ||
              ')';
          if table_impl = 'td' then
            plsql := plsql || chr(10) ||
                'on commit delete rows';
          elsif table_impl = 'tp' then
            plsql := plsql || chr(10) ||
                'on commit preserve rows';
          end if;

          if plsql2 is not null then
            -- We have nested table attributes. Specify table for these.
            plsql := plsql || chr(10) ||
                plsql2;
          end if;

          execute immediate plsql;

          insert into ref_cursor_copy_types (
            description_md5_hash,
            description_length,
            type_name,
            type_name_collection,
            table_name,
            table_impl
          )
          values (
            description_md5_hash,
            description_length,
            type_name,
            type_name_c,
            table_name,
            table_impl
          );

          commit;
        exception
          when others then
            dbms_output.put_line('ref_cursor_copy.get_type_name: plsql =');
            dbms_output_put_line(plsql);
            dbms_output_put_line(sqlerrm);
            dbms_output_put_line(dbms_utility.format_error_backtrace);
            raise;
        end;
    end;

    return type_name;
  end get_type_name;

/**
 * Returns collection type name for given object type name.
 * @param   type_name
 *                  Object type name.
 * @return  Collection type name.
 */

  function get_collection_type_name(
    type_name in user_types.type_name%type
  )
  return user_types.type_name%type as

  begin
    return substr(type_name, 1, length(type_name) - 1) || 'C';
  end;

/**
 * Returns table name for given object type name.
 * @param   type_name
 *                  Object type name.
 * @return  Table name.
 */

  function get_table_name(
    type_name in user_types.type_name%type
  )
  return user_tables.table_name%type as

  begin
    return substr(type_name, 1, length(type_name) - 2);
  end;

/**
 * Returns table implementation details for given table name.
 * @param   table_name
 *                  Table name.
 * @return  Table implementation used (refer to TABLE_IMPL).
 */

  function get_table_impl(
    table_name in user_types.type_name%type,
    destination in varchar2
  )
  return ref_cursor_copy_types.table_impl%type as

    table_impl ref_cursor_copy_types.table_impl%type;
  begin
    select table_impl
    into   table_impl
    from   ref_cursor_copy_types rcct
    where  rcct.table_name = get_table_impl.table_name;

    return table_impl;
  exception
    when no_data_found then
      if destination = 'M' then
        -- Ignore -- we don't need the table as we're copying to memory.
        null;
      else
        raise;
      end if;
  end get_table_impl;

/**
 * Opens a REF CURSOR for a collection.
 * @param   ad      Collection converted to ANYDATA.
 * @return  REF CURSOR on the collection.
 */

  function rc_from_anydata(
    ad in anydata,
    type_name_collection in varchar2
  )
  return sys_refcursor as

    rc sys_refcursor;

  begin
    open rc for '
    select *
    from   table(cast(:ad as ' || type_name_collection || '))'
    using in ad;

    return rc;
  end rc_from_anydata;

/**
 * Purges data in tables for this session and commits. In general, this is not
 * necessary if global temporary tables are used as data will be purged either
 * when the transaction is committed or at the very latest when the session
 * ends.
 * @return    Number of rows deleted.
 */

  function purge_tables
  return integer as

    dml varchar2(1000);
    n integer := 0;

  begin
    for t in (
          select table_name,
                 table_impl
          from   ref_cursor_copy_types
        ) loop
      dml :=
          'delete ' || chr(10) ||
          'from   ' || t.table_name || ' t' || chr(10);

      if t.table_impl = 'T' then
        -- Not a global temporary table.
        dml := dml ||
            'where  t.&quot;sessionid&quot; = userenv(''sessionid'')';
      end if;

      begin
        execute immediate dml;
        commit;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.purge_tables: Purging table &quot;' ||
                  t.table_name || '&quot;: ' || sqlerrm
          );
      end;

      n := n + sql%rowcount;
    end loop;

    return n;
  end purge_tables;

/**
 * Drops object types and tables.
 * @return    Number of objects dropped.
 */

  function drop_types_and_tables
  return integer as

    dxl varchar2(1000);
    n integer := 0;

  begin
    for t in (
          select type_name,
                 type_name_collection,
                 table_name
          from   ref_cursor_copy_types
        ) loop
      begin
        dxl := 'drop type ' || t.type_name_collection;
        execute immediate dxl;
        n := n + 1;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.drop_types_and_tables: Dropping type &quot;' ||
                  t.type_name_collection || '&quot;: ' || sqlerrm
          );
      end;

      begin
        dxl := 'drop type ' || t.type_name;
        execute immediate dxl;
        n := n + 1;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.drop_types_and_tables: Dropping type &quot;' ||
                  t.type_name || '&quot;: ' || sqlerrm
          );
      end;

      -- Empty table first. Necessary if temporary table in order to avoid
      -- ORA-14452: attempt to create, alter or drop an index on temporary table
      -- already in use
      begin
        dxl := 'delete from ' || t.table_name;
        execute immediate dxl;
        n := n + 1;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.drop_types_and_tables: Deleting from table &quot;' ||
                  t.table_name || '&quot;: ' || sqlerrm
          );
      end;

      commit;

      -- Truncate table first. Necessary if temporary table in order to avoid
      -- ORA-14452: attempt to create, alter or drop an index on temporary table
      -- already in use
      begin
        dxl := 'truncate table ' || t.table_name;
        execute immediate dxl;
        n := n + 1;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.drop_types_and_tables: Truncating table &quot;' ||
                  t.table_name || '&quot;: ' || sqlerrm
          );
      end;

      begin
        dxl := 'drop table ' || t.table_name;
        execute immediate dxl;
        n := n + 1;
      exception
        when others then
          dbms_output.put_line(
              'ref_cursor_copy.drop_types_and_tables: Dropping table &quot;' ||
                  t.table_name || '&quot;: ' || sqlerrm
          );
      end;
    end loop;

    delete
    from   ref_cursor_copy_types;

    commit;

    return n;
  end drop_types_and_tables;

/**
 * Determines whether to use DBMS_ODCI.SAVE-/RESTOREREFCURSOR for given context.
 * This can be controlled through the configuration of COPY_IMPL.
 * @param   top     TRUE if top-level, FALSE if embedded REF CURSOR.
 * @return  TRUE if DBMS_ODCI must be used.
 */

  function use_odci(top in boolean)
  return boolean as

  begin
    return top and copy_impl like '%O%' or
        not top and copy_impl like '%o%';
  end use_odci;

/**
 * Safely closes a REF CURSOR. This can be controlled by the value of COPY_IMPL
 * or forced. If FORCE is FALSE this really shouldn't be necessary, but it could
 * help prevent ORA-00600 [17281] [1001].
 * @param   rc      REF CURSOR.
 * @param   force   If TRUE configuration of COPY_IMPL is overridden and RC
 *                  is attempted closed and set to NULL. If FALSE, the value of
 *                  COPY_IMPL controls what is done.
 */

  procedure safely_close(
    rc in out sys_refcursor,
    force in boolean := false
  ) as

  begin
    if rc%isopen then
      if force or close_impl like '%A%' then
        close rc;
      end if;
      if force or close_impl like '%N%' then
        rc := null;
      end if;
    end if;
  exception
    when others then
      null;
  end safely_close;

begin
  copy_impl := '';
  if portable.get_major_minor_version = 10.1 then
    close_impl := 'Dd';
    copy_impl := 'OoR';
  end if;

  if portable.get_major_version = 10 or
      portable.get_full_version like '11.1.0.6%' then
    if portable.is_option_enabled('Java') and
        portable.get_major_minor_version != 10.1 then
      /*
       * Force Java implementation of REF CURSOR description as the PL/SQL
       * implementation doesn't correctly restore the REF CURSOR state, leading
       * to one of the following errors:
       * ORA-00600 [psdmsc: psdinvdef#1]
       * ORA-01001: invalid cursor
       */
      ref_cursor_descriptor.describe_impl := 'J2';
    else
      ref_cursor_descriptor.describe_impl := 'C2';
    end if;
  end if;

  if portable.get_platform != 'Windows' then
    -- Use TABLE for collections instead of VARRAY in order to avoid
    -- ORA-22909: exceeded maximum VARRAY limit.
    collection_impl := 'T' ;
  end if;
end ref_cursor_copy;
/
</pre><br />
The PL/SQL code shown above depends on the PL/SQL, Java and C code developed in my blog post <a href="/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c" target="_blank">Describing a REF CURSOR in Oracle 10<em>g</em>+ Using PL/SQL, Java and C</a>.</p>
<h1>Examples</h1>
<h2>Increase EMP.SAL by 10%</h2>
<p>In this example we use the <code>PLSQL_BLOCK_AFTER_EACH_FETCH</code> parameter to call a PL/SQL block for each row fetched in order to increase the <code>SAL</code> column by 10%:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
set feedback 1

prompt From EMP

select *
from   (
         select empno,
                sal
         from   emp
         order  by empno
       )
where  rownum &lt;= 3;

variable rc2 refcursor

prompt Transformed REF CURSOR copy of EMP

declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  type_name user_types.type_name%type;
begin
  open rc1 for
  select *
  from   (
           select empno,
                  sal
           from   emp
           order  by empno
         )
  where  rownum &lt;= 3;

  :rc2 := ref_cursor_copy.to_ref_cursor(
    rc1,
    type_name,
    destination =&gt; 'M',
    plsql_block_after_each_fetch =&gt; 'fetched_row.sal := fetched_row.sal * 1.10;'
  );
end;
/

print rc2
</pre><br />
which shows<br />
<pre class="brush: plain; collapse: false; pad-line-numbers: false;">
From EMP

     EMPNO        SAL
---------- ----------
      7369        800
      7499       1600
      7521       1250

3 rows selected.

Transformed REF CURSOR copy of EMP

PL/SQL procedure successfully completed.


     EMPNO        SAL
---------- ----------
      7369        880
      7499       1760
      7521       1375

3 rows selected.
</pre></p>
<h2>Forcing DBMS_XMLGEN to Generate XML Schema-compliant Date/Time Values Without Side Effects</h2>
<p>In this example we use the <code>PLSQL_BLOCK_BEFORE_COPY_OPEN</code> parameter to change various NLS date/time formats in order to make <code>DBMS_XMLGEN</code> generate XML Schema-compliant date/time values. Since we&#8217;re doing this after the original <code>REF CURSOR</code> has been fetched, this does not have any side effects on other calls to <code>TO_CHAR</code> in the original query:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
set serveroutput on format truncated

alter session set nls_date_format = 'dd-mon-yy';
alter session set nls_timestamp_format = 'dd-mon-rr hh.mi.ssxff am';
alter session set nls_timestamp_tz_format = 'dd-mon-rr hh.mi.ssxff am tzr';

declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  type_name user_types.type_name%type;
  context dbms_xmlgen.ctxtype;
  xml xmltype;
begin
  open rc1 for
  select 1 id,
         123.456 n,
         'abcDEF' vc,
         cast(timestamp'2011-02-01 14:25:30' as date) d,
         timestamp'2011-02-01 14:25:30.123456789' t1,
         timestamp'2011-02-01 14:25:30.123456789 +02:00' t2,
         to_char(cast(timestamp'2011-02-01 14:25:30' as date)) d_tc,
         to_char(timestamp'2011-02-01 14:25:30.123456789') t1_tc,
         to_char(timestamp'2011-02-01 14:25:30.123456789 +02:00') t2_tc
  from   dual;

  rc2 := ref_cursor_copy.to_ref_cursor(
    rc1,
    type_name,
    destination =&gt; 'M',
    plsql_block_before_copy_open =&gt;
      q'[
        dbms_session.set_nls(
          'nls_date_format', '''yyyy-mm-dd&quot;T&quot;hh24:mi:ss'''
        );
        dbms_session.set_nls(
          'nls_timestamp_format', '''yyyy-mm-dd&quot;T&quot;hh24:mi:ss.ff9'''
        );
        dbms_session.set_nls(
          'nls_timestamp_tz_format', '''yyyy-mm-dd&quot;T&quot;hh24:mi:ss.ff9tzh:tzm'''
        );
      ]'
  );

  context := dbms_xmlgen.newcontext(rc2);
  xml := dbms_xmlgen.getxmltype(context);
  dbms_xmlgen.closecontext(context);
  dbms_output_put_line(xml.getclobval);

  dbms_session.set_nls(
    'nls_date_format', '''dd-mon-yy'''
  );
  dbms_session.set_nls(
    'nls_timestamp_format', '''dd-mon-rr hh.mi.ssxff am'''
  );
  dbms_session.set_nls(
    'nls_timestamp_tz_format', '''dd-mon-rr hh.mi.ssxff am tzr'''
  );
end;
/
</pre><br />
which produces the following output:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
Session altered.


Session altered.


Session altered.

&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;ID&gt;1&lt;/ID&gt;
  &lt;N&gt;123.456&lt;/N&gt;
  &lt;VC&gt;abcDEF&lt;/VC&gt;
  &lt;D&gt;2011-02-01T14:25:30&lt;/D&gt;
  &lt;T1&gt;2011-02-01T14:25:30.123456789&lt;/T1&gt;
  &lt;T2&gt;2011-02-01T14:25:30.123456789+02:00&lt;/T2&gt;
  &lt;D_TC&gt;01-feb-11&lt;/D_TC&gt;
  &lt;T1_TC&gt;01-feb-11 02.25.30.123456789 pm&lt;/T1_TC&gt;
  &lt;T2_TC&gt;01-feb-11 02.25.30.123456789 pm +02:00&lt;/T2_TC&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;

PL/SQL procedure successfully completed.
</pre><br />
Please note how the values for columns <code>D</code>, <code>T1</code> and <code>T2</code> are generated with XML Schema-compliant date/time values, whereas the values for columns <code>D_TC</code>, <code>T1_TC</code> and <code>T2_TC</code> are generated according to the NLS settings active when the original <code>REF CURSOR</code> was opened.</p>
<h1>Known Issues</h1>
<h2>Fragile Cursor Management in Oracle Database 10<em>g</em> Release 1</h2>
<p>Cursor management in Oracle Database 10<em>g</em> Release 1 seems extremely fragile. Often when the <code>REF CURSOR</code>s are closed Oracle throws <code>ORA-00600 [17281] [1001]</code> when or before you terminate your session. Also when they are closed, Oracle might reuse them in an incorrect manner, leading to errors like <code>ORA-01001: invalid cursor</code>, <code>ORA-01007: variable not in select list</code> etc. The only solution I&#8217;ve found on 10.1 is to configure the implementation to not close any of the <code>REF CURSOR</code>s, which obviously you need to be careful about. Oracle will close them correctly when your session ends but if you&#8217;ve got code that runs for a long time, calling <code>REF_CURSOR_COPY</code>, you might need to increase the <code>init.ora</code> parameter <code>open_cursors</code> or migrate to Oracle 10.2 or newer.<br />
Another issue is that transferring <code>REF CURSOR</code>s into and out from dynamic PL/SQL often leads to <code>ORA-01001: invalid cursor</code>. The solution here is to use <code>DBMNS_ODCI.SAVE-/RESTOREREFCURSOR</code> and transfer the obtained cursor number instead.<br />
These issues have been verified on 10.1.0.2.0, 10.1.0.3.0, 10.1.0.4.0 and 10.1.0.5.0.</p>
<h2>ORA-01001: invalid cursor in DBMS_SQL</h2>
<p>If Oracle throws <code>ORA-01001: invalid cursor</code> in <code>DBMS_SQL</code> try to use either the direct Java or the direct C description implementation as this is a bug in <code>DBMS_SQL</code>. This seems to be related to <code>DBMS_SQL</code> that cannot handle <code>CURSOR</code> expressions in the call to <code>DBMS_SQL.TO_CURSOR_NUMBER</code>.</p>
<p>Example:</p>
<p><pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
set serveroutput on format truncated

declare
  rc1 sys_refcursor;
  rc2 sys_refcursor;
  type_name user_types.type_name%type;
  describe_impls dbms_utility.name_array;
  xml xmltype;
begin
  describe_impls(1) := 'P';
  describe_impls(2) := 'J2';
  describe_impls(3) := 'C2';

  for i in nvl(describe_impls.first, 0) .. nvl(describe_impls.last, -1) loop
    ref_cursor_descriptor.describe_impl := describe_impls(i);

    dbms_output.put_line('ref_cursor_descriptor.describe_impl = ''' ||
        ref_cursor_descriptor.describe_impl || '''');

    open rc1 for
    select d.deptno,
           d.dname,
           cursor (
             select empno,
                    hiredate,
                    ename
             from   emp e
             where  e.deptno = d.deptno and
                    rownum &lt;= 2
           ) emp_cursor
    from   dept d
    where  deptno &lt; 40;

    begin
      rc2 := ref_cursor_copy.to_ref_cursor(
        rc1,
        type_name,
        destination =&gt; 'M',
        plsql_block_before_copy_open =&gt; 'xml_gen.set_xml_nls_formats;'
      );

      xml := xml_gen.from_ref_cursor(rc2);
      dbms_output_put_line(xml.getclobval);

      xml_gen.set_session_nls_formats;
    exception
      when others then
        dbms_output.put_line(sqlerrm);
        dbms_output.put_line(dbms_utility.format_error_backtrace);
    end;
  end loop;
end;
/
</pre><br />
which shows the following on Oracle 11.2.0.1.0 (abbreviated):<br />
<pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
ref_cursor_descriptor.describe_impl = 'P'
ORA-01001: invalid cursor
ORA-06512: at &quot;SYS.DBMS_SQL&quot;, line 2610
ORA-06512: at &quot;SCOTT.REF_CURSOR_DESCRIPTOR_PLSQL&quot;, line 123
ORA-06512: at &quot;SCOTT.REF_CURSOR_DESCRIPTOR&quot;, line 132
ORA-06512: at &quot;SCOTT.REF_CURSOR_DESCRIPTOR&quot;, line 83
ORA-06512: at &quot;SCOTT.REF_CURSOR_COPY&quot;, line 425
ORA-06512: at &quot;SCOTT.REF_CURSOR_COPY&quot;, line 305
ref_cursor_descriptor.describe_impl = 'J2'
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7782&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-06-09T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;CLARK&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7839&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-11-17T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;KING&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
  &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7369&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1980-12-17T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;SMITH&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7566&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-04-02T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;JONES&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
  &lt;DNAME&gt;SALES&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7499&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-02-20T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;ALLEN&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7521&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-02-22T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;WARD&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
ref_cursor_descriptor.describe_impl = 'C2'
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7782&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-06-09T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;CLARK&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7839&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-11-17T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;KING&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
  &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7369&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1980-12-17T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;SMITH&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7566&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-04-02T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;JONES&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
  &lt;DNAME&gt;SALES&lt;/DNAME&gt;
  &lt;EMP_CURSOR&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7499&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-02-20T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;ALLEN&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
   &lt;EMP_CURSOR_ROW&gt;
    &lt;EMPNO&gt;7521&lt;/EMPNO&gt;
    &lt;HIREDATE&gt;1981-02-22T00:00:00&lt;/HIREDATE&gt;
    &lt;ENAME&gt;WARD&lt;/ENAME&gt;
   &lt;/EMP_CURSOR_ROW&gt;
  &lt;/EMP_CURSOR&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre></p>
<h1>Supporting Oracle Database 9<i>i</i> Release 2</h1>
<p>The techniques in <code>REF_CURSOR_COPY</code> and the related blog can be made to work for Oracle 9.2 as well but you would need to remove calls to <code>DBMS_ODCI.SAVE-/RESTOREREFCURSOR</code> and <code>DBMS_UTILITY.FORMAT_ERROR_BACKTRACE</code> as these procedures are not available in Oracle 9.2.<br />
I haven&#8217;t used Conditional Compilation techniques for this as it would exclude Oracle versions 10.1.0.2.0 and 10.1.0.3.0.</p>
<h1>Improvements</h1>
<p>The code can be improved in the following ways:</p>
<ul>
<li>Usage of <code>BULK COLLECT</code> in the fetch process. I didn&#039;t bother implementing this because it&#039;s not possible to use <code>BULK COLLECT</code> with embedded <code>REF CURSOR</code>s because we cannot declare an associative array type that holds <code>REF CURSOR</code>s (you get <code>PLS-00990: Index Tables of Cursor Variables are disallowed</code>). So implementing <code>BULK COLLECT</code> would only be used if the incoming <code>REF CURSOR</code> doesn&#039;t have embedded <code>REF CURSOR</code>s.</li>
<li>Commit interval in the code generated when <code>DESTINATION</code> is <code>'T'</code> (table), preventing errors in case the rollback segment is not large enouogh to hold all rows.</li>
<li>The <code>GET_TYPE_NAME</code> function that creates object types and table for handling a given <code>REF CURSOR</code> could be improved such that it&#8217;s guarded by a critical section/region, preventing that multiple sessions try to create identical object types (with different names through the sequence) with the same MD5 hash, all but the first failing with a <code>DUP_VALUE_ON_INDEX</code> exception.</li>
</ul>
<h1>Source Code</h1>
<p>You can download the source code <a href="http://www.ellebaek-consulting.com/files/blog/refcurcopy.zip">here</a>.<br />
The PL/SQL code depends on the PL/SQL, Java and C code developed in my blog post <a href="/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c" target="_blank">Describing a REF CURSOR in Oracle 10<em>g</em>+ Using PL/SQL, Java and C</a>.</p>
<h1>Installation</h1>
<p>Install the code for the other blog post first. Then run <code>grant.sql</code> with SQL*Plus logged on as a DBA, specifying the schema you would like to install in as a parameter and then run <code>install.sql</code> with SQL*Plus, logged on to the schema you want the code to be installed in.</p>
<h1>Conclusion</h1>
<p>The code shown here has been fairly well tested with the following Oracle versions and editions:</p>
<ul>
<li>Oracle Database 10<em>g</em> Release 1 10.1.0.2.0 Personal Edition on Windows XP SP3.</li>
<li>Oracle Database 10<em>g</em> Release 1 10.1.0.3.0 Enterprise Edition on Intel Solaris 10.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.1.0 Express Edition on Windows XP SP3.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.1.0 Express Edition on Red Hat Enterprise Linux 5.3.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.2.0 Enterprise Edition on Intel Solaris 10.</li>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.6.0 Personal Edition on Windows XP SP3.</li>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.6.0 Enterprise Edition on SPARC Solaris 10.</li>
<li>Oracle Database 11<em>g</em> Release 2 11.2.0.1.0 Personal Edition on Windows 7.</li>
</ul>
<p>Feel free to use the code at your own risk. I welcome your feedback and suggestions for improvements but the code as such is not supported.</p>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/211/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/211/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/211/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=211&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
		<item>
		<title>Describing a REF CURSOR in Oracle 10g+ Using PL/SQL, Java and C</title>
		<link>http://ellebaek.wordpress.com/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c/</link>
		<comments>http://ellebaek.wordpress.com/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c/#comments</comments>
		<pubDate>Fri, 11 Mar 2011 16:38:41 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[Oracle Call Interface]]></category>
		<category><![CDATA[JDBC]]></category>
		<category><![CDATA[oracle]]></category>
		<category><![CDATA[ref cursor]]></category>
		<category><![CDATA[describe]]></category>
		<category><![CDATA[oci]]></category>
		<category><![CDATA[c external procedure]]></category>
		<category><![CDATA[java stored procedure]]></category>
		<category><![CDATA[java resultset]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=99</guid>
		<description><![CDATA[Introduction Use case: You need to be able to describe any PL/SQL REF CURSOR in a uniform way across all editions of Oracle Database from 10g Release 1 and newer. Performance of the solution is not important. Potentially, you need to fetch the REF CURSOR into transient or persistent storage &#8212; I&#8217;ll come back to [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=99&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<h1>Introduction</h1>
<p>Use case: You need to be able to describe any PL/SQL <code>REF CURSOR</code> in a uniform way across all editions of Oracle Database from 10<em>g</em> Release 1 and newer. Performance of the solution is not important. Potentially, you need to fetch the <code>REF CURSOR</code> into transient or persistent storage &#8212; I&#8217;ll come back to this in a future blog post.</p>
<p>Challenges: Description of a <code>REF CURSOR</code> directly in PL/SQL requires Oracle 11<em>g</em> Release 1 or newer. Java stored procedures are not supported by Oracle Database 10<em>g</em> Release 2 Express Edition.</p>
<p>Solution: I&#8217;ve developed code that uses a combination of Java, C and PL/SQL in order to implement the use case. This blog post describes the code, which has taken a significant amount of time to develop.</p>
<h1>Alternatives</h1>
<p>Basically, we have the following alternatives:</p>
<ul>
<li>On Oracle Database 11<em>g</em> Release 1 and newer you can use <code>DBMS_SQL.TO_CURSOR_NUMBER</code> to convert the <code>REF CURSOR</code> to a <code>DBMS_SQL</code> cursor and then use <code>DBMS_SQL.DESCRIBE_COLUMNS3</code> to get a descriptions of the columns.</li>
<li>On Oracle Database 10<em>g</em> Release 1 and 2 you can write a Java stored procedure that receives the <code>REF CURSOR</code> as a parameter, which Oracle converts to a <code>java.sql.ResultSet</code>, that you can describe through <code>java.sql.ResultSetMetaData</code>.</li>
<li>On Oracle Database 10<em>g</em> Release 2 Express Edition (in fact with Oracle Database 9<em>i</em> Release 2 as well) you can write an external procedure in C and use Oracle Call Interface (OCI) to describe the <code>REF CURSOR</code>.</li>
</ul>
<p>We&#8217;ll look at the alternatives and implement each of them in the following. I&#8217;ve chosen to implement the functionality such that the <code>REF CURSOR</code> description returned from each of the 3 functions is a <code>VARCHAR2</code> with XML on the form:<br />
<pre class="brush: xml; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
&lt;ROWSET generator=&quot;PL/SQL|Java|C&quot;&gt;
  &lt;ROW&gt;&lt;!-- Column 1. --&gt;
    &lt;ID&gt;column_id_1&lt;/ID&gt;
    &lt;NAME&gt;column_name_1&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
    &lt;!-- Character set form: 1: Database. 2: National. --&gt;
    &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
    &lt;NAME&gt;column_name_1&lt;/NAME&gt;
    &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
    &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
    &lt;!-- For CHAR, VARCHAR2. --&gt;
    &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
    &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
    &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
    &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
    &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
    &lt;!-- For object types, including collections. --&gt;
    &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
    &lt;!-- For object types, including collections. --&gt;
    &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
    &lt;DECLARATION&gt;declaration_1&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;!-- Columns 2 through n-1. --&gt;
  &lt;ROW&gt;&lt;!-- Column n. --&gt;
    &lt;ID&gt;column_id_n&lt;/ID&gt;
    &lt;NAME&gt;column_name_n&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
    &lt;!-- Character set form: 1: Database. 2: National. --&gt;
    &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
    &lt;NAME&gt;column_name_n&lt;/NAME&gt;
    &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
    &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
    &lt;!-- For CHAR, VARCHAR2. --&gt;
    &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
    &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
    &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
    &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
    &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
    &lt;!-- For object types, including collections. --&gt;
    &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
    &lt;!-- For object types, including collections. --&gt;
    &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
    &lt;DECLARATION&gt;declaration_n&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
I could have chosen to create an object type and collection for the description but I&#8217;ve chosen the XML form in order not to overcomplicate the code.</p>
<h1>Type Code Mappings</h1>
<p>Oracle uses different type codes in different contexts so we need to be able to map them from the native type codes used in PL/SQL, Java and C to a &#8220;uniform&#8221; type code, in order to make usages of the code we develop here portable across the various Oracle Database editions and versions. I&#8217;ve described such a &#8220;uniform&#8221; type code mapping in my blog post <a href="/2011/02/25/oracle-type-code-mappings/" target="_blank">Oracle Type Code Mappings</a>.</p>
<h1>Implementations</h1>
<p>We&#8217;ll have a look at the various implementations in the following sections. I&#8217;ve chosen not to use Conditional Compilation for separation of the language-specific variants of the describe functionality. This is in order to be able to support Oracle 10.1.0.2.0 and 10.1.0.3.0 as Conditional Compilation wasn&#8217;t introduced for Oracle 10<i>g</i> until 10.1.0.4.0.</p>
<p>We use a trick to be able to transfer <code>REF CURSOR</code> to a Java stored procedure and C external procedure: We obtain a cursor number for the <code>REF CURSOR</code> through a call to <code>DBMS_ODCI.SAVEREFCURSOR</code>, transfer the cursor number to Java/C and there obtain the <code>REF CURSOR</code> based on the number with <code>DBMS_ODCI.RESTOREREFCURSOR</code>. This shouldn&#8217;t be necessary for Java but experience shows that transferring the <code>REF CURSOR</code> directly fails with Oracle Database 10<em>g</em> Release 1 (Oracle sometimes throws various &#8220;invalid cursor&#8221; errors or provides a <code>null java.sql.ResultSet</code>) and furthermore, it&#8217;s not possible to transfer a <code>REF CURSOR</code> directly from PL/SQL to a C external procedure. Well, the latter turns out not to be true but more about that later.</p>
<h2>PL/SQL: REF_CURSOR_DESCRIPTOR_PLSQL</h2>
<p>This is the pure PL/SQL implementation (<code>ref_cursor_descriptor_plsql.fnc</code>):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace function ref_cursor_descriptor_plsql(rc in out sys_refcursor)
return varchar2 as

/**
 * PL/SQL function that describes a REF CURSOR. Requires Oracle 11g Release 1 or
 * newer. Description is returned in the following XML format (without the XML
 * comments):
 * &lt;ROWSET generator=&quot;PL/SQL&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *     &lt;DECLARATION&gt;declaration_1&lt;/DECLARATION&gt;
 *   &lt;/ROW&gt;
 *   ...
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *     &lt;DECLARATION&gt;declaration_n&lt;/DECLARATION&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Feel free to use at your own risk.
 * @param   rc      REF CURSOR.
 * @return  XML describing the REF CURSOR.
 * @version   $Revision: 1 $
 * @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.
 */

  rcc binary_integer;
  result varchar2(32767);
  column_count number;
  column_metadata dbms_sql.desc_tab3;
  type_code pls_integer;
  native_type_code pls_integer;
  xml varchar2(32767);

/**
 * Maps type code from native type code returned by DBMS_SQL.DESCRIBE_COLUMNS3
 * to &quot;uniform&quot; type code as described in blog article
 * http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/
 * @param   type_code
 *                  Type code returned by DBMS_SQL.DESCRIBE_COLUMNS3.
 * @return  &quot;Uniform&quot; type code.
 */

  function map_type_code(type_code in pls_integer)
  return pls_integer as

  begin
    case type_code
      when 11 then
        -- ROWID.
        return type_codes.tc_rowid;
      when 100 then
        -- BINARY_FLOAT.
        return type_codes.tc_binary_float;
      when 101 then
        -- BINARY_DOUBLE.
        return type_codes.tc_binary_double;
      when 111 then
        -- Object REF.
        return type_codes.tc_ref;
      else
        return type_code;
    end case;
  end map_type_code;

/**
 * Append XML element, indented to given level (2 spaces per level, starting
 * from 1).
 * @param   level   Level for indentation.
 * @param   name    Element name.
 * @param   value   Element numeric value.
 */

  procedure append(
    level in pls_integer,
    name in varchar2,
    value in varchar2
  ) as

  begin
    xml := xml ||
      rpad(' ', (level - 1) * 2) ||
      '&lt;' || name || '&gt;' ||
      value ||
      '&lt;/' || name || '&gt;' || chr(10);
  end append;

begin
  -- Convert the REF CURSOR to a DBMS_SQL cursor. Only possible with Oracle
  -- Database 11g Release 1 and newer.
  rcc := dbms_sql.to_cursor_number(rc);

  -- Describe the columns.
  dbms_sql.describe_columns3(
    c =&gt; rcc,
    col_cnt =&gt; column_count,
    desc_t =&gt; column_metadata
  );

  if column_count &gt; 0 then
    xml := '&lt;ROWSET generator=&quot;PL/SQL&quot;&gt;' || chr(10);

    for i in 1 .. column_count loop
      native_type_code := column_metadata(i).col_type;
      type_code := map_type_code(native_type_code);
      xml := xml ||
          '  &lt;ROW&gt;' || chr(10);
      append(3, 'ID', i);
      append(3, 'NAME', column_metadata(i).col_name);
      append(3, 'TYPE_CODE', type_code);
      append(3, 'NATIVE_TYPE_CODE', native_type_code);
      if type_code in (
          type_codes.tc_char,
          type_codes.tc_varchar2,
          type_codes.tc_clob
        ) then
        -- Text.
        append(3, 'CHARSET_FORM', column_metadata(i).col_charsetform);
      end if;
      if type_code in (
          type_codes.tc_char,
          type_codes.tc_varchar2,
          type_codes.tc_raw,
          type_codes.tc_urowid
        ) then
        -- Text (not CLOB), RAW, UROWID.
        append(3, 'LENGTH', column_metadata(i).col_max_len);
      end if;
      if type_code in (
            type_codes.tc_char,
            type_codes.tc_varchar2
          )
          and column_metadata(i).col_charsetform = 1 then
        -- Database character set.
        append(3, 'LENGTH_SEMANTICS', 'BYTE');
      end if;
      if type_code in (
          type_codes.tc_number,
          type_codes.tc_interval_ym,
          type_codes.tc_interval_ds
        ) then
        -- NUMBER/INTERVAL YEAR TO MONTH/INTERVAL DAY TO SECOND.
        append(3, 'PRECISION', column_metadata(i).col_precision);
      end if;
      if type_code in (
          type_codes.tc_timestamp,
          type_codes.tc_timestamp_tz,
          type_codes.tc_timestamp_ltz
        ) then
        -- TIMESTAMP%.
        append(3, 'PRECISION', column_metadata(i).col_scale);
      end if;
      if type_code in (
          type_codes.tc_number,
          type_codes.tc_interval_ds
        ) then
        -- NUMBER/INTERVAL DAY TO SECOND.
        append(3, 'SCALE', column_metadata(i).col_scale);
      end if;
      if type_code = type_codes.tc_object then
        -- Object type/collection.
        append(3, 'OWNER', column_metadata(i).col_schema_name);
        append(3, 'TYPE_NAME', column_metadata(i).col_type_name);
      end if;
      xml := xml || '  &lt;/ROW&gt;' || chr(10);
    end loop;

    xml := xml || '&lt;/ROWSET&gt;';
  end if;

  -- Convert the DBMS_SQL cursor back to a REF CURSOR. Only possible with Oracle
  -- Database 11g Release 1 and newer. This is necessary in order to continue
  -- to use the REF CURSOR.
  rc := dbms_sql.to_refcursor(rcc);

  return xml;
end ref_cursor_descriptor_plsql;
/
</pre></p>
<h2>Java Stored Procedure: com.appatra.blog.RefCursorDescriptor + REF_CURSOR_DESCRIPTOR_JAVA</h2>
<p>This is the Java stored procedure <code>com.appatra.blog.RefCursorDescriptor</code> (<code>RefCursorDescriptor.sql</code>):<br />
<pre class="brush: java; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace and resolve java source named
    &quot;com/appatra/blog/RefCursorDescriptor&quot; as
package com.appatra.blog;

/**
 * Java class useful for describing REF CURSORs in PL/SQL. Requires Oracle 10g
 * Release 1 or newer. Does not work with Oracle 10g Release 2 Express Edition
 * as this doesn't support Java stored procedures.
 * Feel free to use at your own risk.
 * When installing, substitution must be switched off (&quot;set scan off&quot;) or the
 * define character must be set to something different than &amp; (&quot;set define ^&quot;).
 * @version   $Revision: 1 $
 * @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.
 */

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.Types;

import java.lang.StringBuffer;

import java.sql.ResultSetMetaData;
import java.sql.SQLException;

import oracle.jdbc.OracleTypes;
import oracle.jdbc.OracleResultSetMetaData;

public class RefCursorDescriptor {
  /// Buffer.
  private static StringBuffer result;

/**
 * Java method that describes a REF CURSOR. Description is returned in the
 * following XML format (without the XML comments):
 * &lt;ROWSET generator=&quot;Java&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 *   &lt;!-- Columns 2 through n-1. --&gt;
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * @param   rS      Result set. Oracle translates a PL/SQL REF CURSOR to a Java
 *                  result set when this method is called. However, this only
 *                  works from Oracle 10g Release 2. For 10g Release 1, you need
 *                  to use the overloaded method that takes a REF CURSOR number
 *                  as input.
 * @return  XML describing the REF CURSOR.
 */

  public static String describe(java.sql.ResultSet rS)
  throws SQLException {
    result = new StringBuffer(10000);

    int typeCode, nativeTypeCode;
    String typeName;
    int precision;
    int scale;

    if (rS != null) {
      OracleResultSetMetaData rSMD = (OracleResultSetMetaData)rS.getMetaData();

      result.append(&quot;&lt;ROWSET generator=\&quot;Java\&quot;&gt;\n&quot;);
      for (int i = 1; i &lt;= rSMD.getColumnCount(); i++) {
        result.append(&quot;  &lt;ROW&gt;\n&quot;);
        append(&quot;ID&quot;, i);
        append(&quot;NAME&quot;, rSMD.getColumnName(i));
        typeName = rSMD.getColumnTypeName(i);
        nativeTypeCode = rSMD.getColumnType(i);
        typeCode = mapTypeCode(nativeTypeCode, typeName);
        append(&quot;TYPE_CODE&quot;, typeCode);
        append(&quot;NATIVE_TYPE_CODE&quot;, nativeTypeCode);
        if (typeCode == 1 ||
            typeCode == 96 ||
            typeCode == 112) {
          append(&quot;CHARSET_FORM&quot;, rSMD.isNCHAR(i) ? &quot;2&quot; : &quot;1&quot;);
        }
        if (typeCode == 109 || typeCode == 111) {
          append(&quot;OWNER&quot;, typeName.substring(0, typeName.indexOf('.')));
          append(&quot;TYPE_NAME&quot;, typeName.substring(typeName.indexOf('.') + 1));
        }
        if (typeCode == 1 ||
            typeCode == 96 ||
            typeCode == 23 ||
            typeCode == 208) {
          append(&quot;LENGTH&quot;, rSMD.getColumnDisplaySize(i));
          if (typeCode != 23 &amp;&amp; typeCode != 208 &amp;&amp; ! rSMD.isNCHAR(i))
            append(&quot;LENGTH_SEMANTICS&quot;, &quot;CHAR&quot;);
        }

        precision = 0;
        if (typeCode == 2 ||
            typeCode == 182 ||
            typeCode == 183)
          precision = rSMD.getPrecision(i);

        scale = 0;
        if (typeCode == 2 ||
            typeCode == 180 ||
            typeCode == 181 ||
            typeCode == 183 ||
            typeCode == 231)
          scale = rSMD.getScale(i);

        if (typeCode == 2 ||
            typeCode == 183) {
          // Number, INTERVAL DS
          append(&quot;PRECISION&quot;, precision);
          append(&quot;SCALE&quot;, scale);
        }
        if (typeCode == 182) {
          // INTERVAL YM.
          append(&quot;PRECISION&quot;, precision);
        }
        if (typeCode == 180 ||
            typeCode == 181 ||
            typeCode == 231) {
          // TIMESTAMP%,
          append(&quot;PRECISION&quot;, scale);
        }
        result.append(&quot;  &lt;/ROW&gt;\n&quot;);
      }
      result.append(&quot;&lt;/ROWSET&gt;&quot;);

      return result.toString();
    }
    else result.append(&quot;&lt;ROWSET/&gt;&quot;);

    return null;
  }

/**
 * Java method that describes a REF CURSOR. Description is returned in the
 * following XML format (without the XML comments):
 * &lt;ROWSET generator=&quot;Java&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 *   &lt;!-- Columns 2 through n-1. --&gt;
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * @param   rCN     REF CURSOR number for the REF CURSOR that is to be
 *                  described. This must have been obtained through a call to
 *                  DBMS_ODCI.SAVEREFCURSOR. Result set. This &quot;trick&quot; is
 *                  required for Oracle 10g Release 1. For For 10g Release 2 and
 *                  newer, you can use the overloaded method that takes a Java
 *                  result set as input.
 * @return  XML describing the REF CURSOR.
 */

  public static String describe(int rCN)
  throws SQLException {
    Connection connection =
        DriverManager.getConnection(&quot;jdbc:default:connection:&quot;);

    CallableStatement cS =
        connection.prepareCall(&quot;begin dbms_odci.restorerefcursor(:rc, :rcn); end;&quot;);

    ResultSet rS;

    cS.registerOutParameter(1, OracleTypes.CURSOR);
    cS.setInt(2, rCN);
    cS.execute();
    rS = (ResultSet)cS.getObject(1);

    return describe(rS);
  }

/**
 * Append XML element, indented to given level 3 (2 spaces per level).
 * @param   name    Element name.
 * @param   value   Element string value.
 */

  private static void append(String name, String value) {
    result.append(&quot;    &lt;&quot; + name + &quot;&gt;&quot; + value + &quot;&lt;/&quot; + name + &quot;&gt;\n&quot;);
  }

/**
 * Append XML element, indented to given level 3 (2 spaces per level).
 * @param   name    Element name.
 * @param   value   Element integer value.
 */

  private static void append(String name, int value) {
    append(name, &quot;&quot; + value);
  }

/**
 * Maps from JDBC type code to &quot;uniform&quot; type code, which differs in many
 * cases. Refer to blog post
 * http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/
 * @param   typeCode
 *                  JDBC type code.
 * @param   typeName
 *                  Type name, required to distinguish DATE and TIMESTAMP.
 * @return  &quot;Uniform&quot; type code.
 */

  private static int mapTypeCode(int typeCode, String typeName) {
    switch (typeCode) {
      case Types.VARCHAR:
        // VARCHAR2.
        return 1;
      case Types.NUMERIC:
        // NUMBER.
        return 2;
      case Types.LONGVARCHAR:
        // LONG.
        return 8;
      case Types.DATE:
        // DATE.
        return 12;
      case 100:
        // BINARY_FLOAT.
        return 21;
      case 101:
        // BINARY_DOUBLE.
        return 22;
      case Types.TIMESTAMP:
        // TIMESTAMP/DATE.
        if (typeName.equals(&quot;DATE&quot;))
          return 12;
        return 180;
      case -101:
        // TIMESTAMP WITH TIME ZONE.
        return 181;
      case -102:
        // TIMESTAMP WITH LOCAL TIME ZONE.
        return 231;
      case -103:
        // INTERVAL YEAR TO MONTH.
        return 182;
      case -104:
        // INTERVAL DAY TO SECOND.
        return 183;
      case Types.VARBINARY:
        // RAW.
        return 23;
      case Types.LONGVARBINARY:
        // LONG RAW.
        return 24;
      case -8:
        // ROWID/UROWID, map to UROWID.
        return 208;
      case Types.CHAR:
        // CHAR
        return 96;
      case Types.CLOB:
        // CLOB.
        return 112;
      case Types.BLOB:
        // BLOB.
        return 113;
      case -13:
        // BFILE.
        return 114;
      case Types.STRUCT:
      case Types.ARRAY:
      case 2007:
        // Object type, collection etc.
        return 109;
      case 2006:
        // Object REF.
        return 111;
      case -10:
        // REF CURSOR.
        return 102;
    }

    return typeCode;
  }
};
/
</pre><br />
This is the PL/SQL wrapper for the Java stored procedure that takes a <code>REF CURSOR</code> number as input (<code>ref_cursor_descriptor_java.fnc</code>):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace function ref_cursor_descriptor_java(rcn in pls_integer)
return varchar2 as
language java
name 'com.appatra.blog.RefCursorDescriptor.describe(int) return java.lang.String';
/
</pre><br />
And here is the PL/SQL wrapper for the Java stored procedure that takes a <code>REF CURSOR</code> as input (<code>ref_cursor_descriptor_java2.fnc</code>):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace function ref_cursor_descriptor_java2(rc in sys_refcursor)
return varchar2 as
language java
name 'com.appatra.blog.RefCursorDescriptor.describe(java.sql.ResultSet) return java.lang.String';
/
</pre></p>
<h2>C External Procedures: refcurdesc and refcurdesc2</h2>
<p>According to the Oracle documentation it&#8217;s not possible to pass a <code>REF CURSOR</code> to a C external procedure &#8212; but with Oracle Database 9<em>i</em> Release 2 and newer you can in fact transfer a <code>REF CURSOR</code> directly to your C external procedure and it will be received as <code>OCIStmt **</code>. With Oracle Database 9<em>i</em> Release 1 you get an error if you try this when you call the function (<code>ORA-28577: argument 2 of external procedure refcurdesc2 has unsupported datatype UNKNOWN</code>). With Oracle Database 8 and 8<em>i</em> you get an internal error.</p>
<p>I discovered this when I was working on my followup to this blog post <a href="/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g" target="_blank">Copying/Transforming a REF CURSOR in Oracle 10<i>g</i>+</a>. Here I found that the <code>DBMS_ODCI</code> trick somehow changed the <code>REF CURSOR</code> such that it wasn&#8217;t possible to use it with <code>DBMS_XMLGEN</code> afterwards (<code>ORA-24374: define not done before fetch or execute and fetch</code> errors) and also it didn&#8217;t work consistently with sub <code>REF CURSOR</code>s, rendering them impossible to fetch from after the describe (<code>ORA-01001: invalid cursor</code>, <code>ORA-01008: not all variables bound</code> errors etc).</p>
<p>I&#8217;ve kept the original implementation of the <code>refcurdesc</code> function and added a new function <code>refcurdesc2</code> that uses the direct approach.</p>
<p>Here&#8217;s the implementation of the PL/SQL interfaces to the C external procedures, first <code>ref_cursor_descriptor_c.fnc</code> (indirect):</p>
<p><pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace function ref_cursor_descriptor_c(
  rcn in pls_integer
)
return varchar2 as
language c
name &quot;refcurdesc&quot;
library refcurdesc
with context
parameters (
  context,
  rcn int,
  rcn indicator short,
  return indicator short,
  return length short,
  return string
);
/
</pre><br />
and <code>ref_cursor_descriptor_c2.fnc</code> (direct):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace function ref_cursor_descriptor_c2(
  rc in sys_refcursor
)
return varchar2 as
language c
name &quot;refcurdesc2&quot;
library refcurdesc
with context
parameters (
  context,
  rc,
  return indicator short,
  return length short,
  return string
);
/
</pre></p>
<p>Here&#8217;s the implementation of the C external procedure (<code>refcurdesc.c</code>):<br />
<pre class="brush: cpp; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
/**
 * External C procedure used to describe a REF CURSOR. This is useful for
 * pre-Oracle 11.1 (with Oracle 11.1+ you can convert the REF CURSOR to a
 * DBMS_SQL cursor and then describe). It's also possible to write a Java
 * stored procedure, but obviously this is not supported with 10.2 XE.
 * refcurdesc() requires Oracle 10g Release 1 or newer.
 * refcurdesc2() requires Oracle 9i Release 2 or newer.
 * Feel free to use at your own risk.
 * @version   $Revision: 2 $
 * @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.
 */

#if defined(_WINDOWS)
#define _CRT_SECURE_NO_WARNINGS 1
#endif

#define USE_OCI_STMT_PREPARE 1

#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;oci.h&gt;
#include &lt;ociextp.h&gt;

static text temp[32600];
static text temp2[1000];
static text indent[1000];
static text declare[100];
static sword oci_status;

#define INDENT_PER_LEVEL 2

static OCIExtProcContext *oci_ctx;
static OCIEnv            *envhp;
static OCISvcCtx         *svchp;
static OCIError          *errhp;
static OCIParam          *pard;

static int line;
static int is_cursor_expression;

void describe_statement(int level, OCIStmt *refcur);
void append_element(int level, text *name, text *value);
void append_element2(int level, text *name, sb2 value);
void append2(int level, text *t);
void append(text *t);
int map_type_code(int type_code);
void disable_prefetch(OCIStmt *stmt);
void checkerr(OCIError *errhp, sword status, int line);

/**
 * OCI error handling: Check for returned errors and don't execute future calls
 * if an OCI error is raised. Any errors will be raised in an exception.
 * @param   call    Function call to execute if no previous calls have failed.
 */

#define CHECKERR(call) { line = __LINE__; if (oci_status == OCI_SUCCESS) checkerr(errhp, call, line); }

/**
 * Describe REF CURSOR and return description in XML form in a VARCHAR2.
 * Description is returned in the following XML format (without the XML
 * comments):
 * &lt;ROWSET generator=&quot;C&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 *   &lt;!-- Columns 2 through n-1. --&gt;
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Limitations: 32KB limitation on the return value. The implementation is not
 * thread safe.
 * @param   ctx     OCI context provided by Oracle.
 * @param   refcurno
 *                  REF CURSOR number obtained on the original REF CURSOR
 *                  through a call to DBMS_ODCI.SAVEREFCURSOR. This is required
 *                  as according to the Oracle documentation we can't directly
 *                  transfer the REF CURSOR to this function.
 * @param   refcurno_i
 *                  NULL indicator for refcurno.
 * @param   result_i
 *                  NULL indicator for the VARCHAR2 we're returning.
 * @param   result_l
 *                  Length of the VARCHAR2 we're returning.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

text *refcurdesc(
  OCIExtProcContext *ctx,
  int refcurno,
  short refcurno_i,
  short *result_i,
  short *result_l
)

{
  text *result;

  static OCIStmt *stmhp = (OCIStmt *)0;
  char *plsql_block = &quot;begin dbms_odci.restorerefcursor(:rc, :rcn); end;&quot;;
  char *plsql_block2 = &quot;begin dbms_odci.saverefcursor(:rc, :rcn); end;&quot;;
  OCIStmt *refcur = NULL;
  OCIBind *bndp1 = (OCIBind *)0;
  OCIBind *bndp2 = (OCIBind *)0;

  temp[0] = '&#092;&#048;';

  oci_ctx = ctx;
  oci_status = OCI_SUCCESS;

  if (refcurno_i == OCI_IND_NULL) {
    *result_i = (short)OCI_IND_NULL;
    // PL/SQL has no notion of a NULL ptr, so return a zero-byte string.
    result = (text *)OCIExtProcAllocCallMemory(ctx, 1);
    result[0] = '&#092;&#048;';
  }
  else {
    // Get the OCI handles from our Oracle session.
    CHECKERR(OCIExtProcGetEnv(ctx, &amp;envhp, &amp;svchp, &amp;errhp))

    is_cursor_expression = 0;
#if USE_OCI_STMT_PREPARE
    // Convert the REF CURSOR number back to a REF CURSOR.
    CHECKERR(
      OCIHandleAlloc(
        (dvoid *)envhp, (dvoid **)&amp;stmhp,
        (ub4)OCI_HTYPE_STMT, (size_t)0, (dvoid **)0
      )
    )
    //disable_prefetch(stmhp);
    // Prepare PL/SQL block.
    CHECKERR(
      OCIStmtPrepare(
        stmhp, errhp, (unsigned char *)plsql_block, (ub4)strlen(plsql_block),
        OCI_NTV_SYNTAX, OCI_DEFAULT
      )
    )
#else
    CHECKERR(
      OCIStmtPrepare2(
        svchp, &amp;stmhp, errhp, (unsigned char *)plsql_block, (ub4)strlen(plsql_block),
        NULL, 0,
        OCI_NTV_SYNTAX, OCI_DEFAULT
      )
    )
#endif

    // Bind.
    CHECKERR(
      OCIHandleAlloc(envhp, (void **)&amp;refcur, OCI_HTYPE_STMT, 0, NULL)
    )
    // Position 1: Output REF CURSOR.
    CHECKERR(
      OCIBindByPos(
        stmhp, &amp;bndp1, errhp, (ub4)1,
        (void *)&amp;refcur, (sb4)0, SQLT_RSET, (void *)0, (ub2 *)0,
        (ub2 *)0, (ub4)0, (ub4)0, (ub4)OCI_DEFAULT
      )
    )
    // Position 2: Input REF CURSOR number, previously saved with
    // DBMS_ODCI.SAVEREFCURSOR.
    CHECKERR(
      OCIBindByPos(
        stmhp, &amp;bndp2, errhp, (ub4)2,
        (void *)&amp;refcurno, sizeof(int), SQLT_INT, (void *)&amp;refcurno_i, (ub2 *)0,
        (ub2 *)0, (ub4)0, (ub4)0, (ub4)OCI_DEFAULT
      )
    )

    // Execute.
    CHECKERR(OCIStmtExecute(svchp, stmhp, errhp, 1, 0, NULL, NULL, OCI_DEFAULT))

#if USE_OCI_STMT_PREPARE
    // Free PL/SQL statement.
    CHECKERR(OCIHandleFree(stmhp, OCI_HTYPE_STMT))
    stmhp = NULL;
#endif

    if (oci_status == OCI_SUCCESS)
      describe_statement(1, refcur);

    // Set length.
    *result_l = (short)strlen((char *)temp);
    // Set NULL indicator.
    if (*result_l &gt; 0)
      *result_i = (short)OCI_IND_NOTNULL;
    else *result_i = (short)OCI_IND_NULL;

    // Allocate memory for result string, including NULL terminator.
    result = (text *)OCIExtProcAllocCallMemory(ctx, *result_l + 1);
    strcpy((char *)result, (char *)temp);

    // Free REF CURSOR handle: Seems to close the original REF CURSOR, rendering
    // this useless (ORA-01001: invalid cursor).
    //CHECKERR(OCIHandleFree(refcur, OCI_HTYPE_STMT))
    refcur = NULL;
  }

  // Return pointer, which PL/SQL frees later.
  return result;
}

/**
 * Describe REF CURSOR and return description in XML form in a VARCHAR2.
 * Description is returned in the following XML format (without the XML
 * comments):
 * &lt;ROWSET generator=&quot;C&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 *   &lt;!-- Columns 2 through n-1. --&gt;
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Limitations: 32KB limitation on the return value. The implementation is not
 * thread safe.
 * @param   ctx     OCI context provided by Oracle.
 * @param   refcur
 *                  REF CURSOR (pointer to OCIStmt *). According to the Oracle
 *                  documentation we can't directly transfer a PL/SQL REF
 *                  CURSOR to this function but in fact we can with Oracle 9.2
 *                  and newer.
 * @param   result_i
 *                  NULL indicator for the VARCHAR2 we're returning.
 * @param   result_l
 *                  Length of the VARCHAR2 we're returning.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

text *refcurdesc2(
  OCIExtProcContext *ctx,
  OCIStmt **refcur,
  short *result_i,
  short *result_l
)

{
  text *result;

  temp[0] = '&#092;&#048;';

  oci_ctx = ctx;
  oci_status = OCI_SUCCESS;

  CHECKERR(OCIExtProcGetEnv(ctx, &amp;envhp, &amp;svchp, &amp;errhp))

  if (oci_status == OCI_SUCCESS)
    describe_statement(1, *refcur);

  // Set length.
  *result_l = (short)strlen((char *)temp);
  // Set NULL indicator.
  if (*result_l &gt; 0)
    *result_i = (short)OCI_IND_NOTNULL;
  else *result_i = (short)OCI_IND_NULL;

  // Allocate memory for result string, including NULL terminator.
  result = (text *)OCIExtProcAllocCallMemory(ctx, *result_l + 1);
  strcpy((char *)result, (char *)temp);

  return result;
}

/**
 * Describes given ref cursor statement handle and appends to global description
 * XML.
 */

void describe_statement(int level, OCIStmt *refcur) {
  text column_name[31];

  ub2 type_code, native_type_code;
  ub2 col_width, charset_form, char_semantics, charset_id;
  text *s;
  ub4 slen;
  sb4 param_status;
  sb2 /*ub1*/ precision;
  sb1 scale;

  ub4 column_count;
  int i;

  text owner[31];
  text type_name[129];

  // Number of columns.
  column_count = 0;
  CHECKERR(
    OCIAttrGet(
      (void *)refcur, (ub4)OCI_HTYPE_STMT, (void *)&amp;column_count,
      (ub4 *)0, (ub4)OCI_ATTR_PARAM_COUNT, errhp
    )
  )

  if (column_count &gt; 0)
    append2(level, &quot;&lt;ROWSET generator=\&quot;C\&quot;&gt;\n&quot;);

  for (i = 1; i &lt;= (int)column_count; i++) {
    // For each column.
    append2(level + 1, &quot;&lt;ROW&gt;\n&quot;);

    pard = (OCIParam *)0;

    // Get the column.
    param_status = OCIParamGet(
      (dvoid *)refcur, OCI_HTYPE_STMT, errhp,
      (dvoid **)&amp;pard, (ub4)i
    );

    // Datatype.
    CHECKERR(
      OCIAttrGet(
        (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
        (dvoid *)&amp;native_type_code, (ub4 *)0, (ub4)OCI_ATTR_DATA_TYPE,
        (OCIError *)errhp
      )
    )
    type_code = map_type_code(native_type_code);

    // Column name.
    slen = 0;
    CHECKERR(
      OCIAttrGet(
        (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
        (dvoid **)&amp;s, (ub4 *)&amp;slen, (ub4)OCI_ATTR_NAME,
        (OCIError *)errhp
      )
    )
    strncpy((char *)column_name, (char *)s, slen);
    column_name[slen] = '&#092;&#048;';

    append_element2(level + 2, &quot;ID&quot;, i);
    append_element(level + 2, &quot;NAME&quot;, column_name);
    append_element2(level + 2, &quot;TYPE_CODE&quot;, type_code);
    append_element2(level + 2, &quot;NATIVE_TYPE_CODE&quot;, native_type_code);

    // Character set.
    if (type_code == SQLT_AFC ||
        type_code == SQLT_CLOB ||
        type_code == SQLT_CHR ||
        type_code == SQLT_VCS) {
      CHECKERR(
        OCIAttrGet(
          (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;charset_form, (ub4 *)0, (ub4)OCI_ATTR_CHARSET_FORM,
          (OCIError *)errhp
        )
      )
      append_element2(level + 2, &quot;CHARSET_FORM&quot;, charset_form);
      if (charset_form == SQLCS_EXPLICIT) {
        CHECKERR(
          OCIAttrGet(
            (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
            (dvoid *)&amp;charset_id, (ub4 *)0, (ub4)OCI_ATTR_CHARSET_ID,
            (OCIError *)errhp
          )
        )
        append_element2(level + 2, &quot;CHARSET_ID&quot;, charset_id);
      }
      else charset_id = 0;
    }
    else charset_form = 0;

    // Number/Interval YM/Interval DS: Precision.
    if (type_code == SQLT_NUM ||
        type_code == 182 ||
        type_code == 183) {
      CHECKERR(
        OCIAttrGet(
          (dvoid*)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;precision, (ub4 *)0,
          (ub4)OCI_ATTR_PRECISION, (OCIError *)errhp
        )
      )

      sprintf(temp2, &quot;precision = %d&quot;, (int)precision);
      append_element2(level + 2, &quot;PRECISION&quot;, precision);
    }
    // Number: Scale.
    if (type_code == SQLT_NUM) {
      CHECKERR(
        OCIAttrGet(
          (dvoid*)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;scale, (ub4 *)0,
          (ub4)OCI_ATTR_SCALE, (OCIError *)errhp
        )
      )
      append_element2(level + 2, &quot;SCALE&quot;, scale);
    }
#ifdef OCI_ATTR_FSPRECISION
    // Timestamp/interval DS: Scale.
    if (type_code == 180 ||
        type_code == 181 ||
        type_code == 231 ||
        type_code == 183) {
      CHECKERR(
        OCIAttrGet(
          (dvoid*)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;scale, (ub4 *)0,
          (ub4)OCI_ATTR_FSPRECISION, (OCIError *)errhp
        )
      )
      if (type_code == 183)
        append_element2(level + 2, &quot;SCALE&quot;, scale);
      else append_element2(level + 2, &quot;PRECISION&quot;, scale);
    }
#endif

    // Length, length semantics.
    char_semantics = 0;
    if (type_code == SQLT_AFC ||
        type_code == SQLT_CHR ||
        type_code == SQLT_VCS ||
        type_code == SQLT_BIN ||
        type_code == 208) {
#ifdef OCI_ATTR_CHAR_USED
      CHECKERR(
        OCIAttrGet(
          (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;char_semantics,  (ub4 *)0, (ub4)OCI_ATTR_CHAR_USED,
          (OCIError *)errhp
        )
      )
      col_width = 0;
      if (char_semantics)
        // Column width in characters.
        CHECKERR(
          OCIAttrGet(
            (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
            (dvoid *)&amp;col_width, (ub4 *)0, (ub4)OCI_ATTR_CHAR_SIZE,
            (OCIError *)errhp
          )
        )
      else
#else
      char_semantics = 2;
#endif
        // Column width in bytes.
        CHECKERR(
          OCIAttrGet(
            (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
            (dvoid *)&amp;col_width, (ub4 *)0, (ub4)OCI_ATTR_DATA_SIZE,
            (OCIError *)errhp
          )
        )

      append_element2(level + 2, &quot;LENGTH&quot;, col_width);
      if (charset_form == 1 &amp;&amp; type_code != 208)
        append_element(level + 2, &quot;LENGTH_SEMANTICS&quot;, char_semantics == 2 ? &quot;&quot; : (char_semantics ? &quot;CHAR&quot; : &quot;BYTE&quot;));
    }

    // Object type/collection or REF to object type/collection.
    if (type_code == 109 || type_code == 111) {
      // Schema name.
      slen = 0;
      CHECKERR(
        OCIAttrGet(
          (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;s, (ub4 *)&amp;slen,
          (ub4)OCI_ATTR_SCHEMA_NAME, (OCIError *)errhp
        )
      )
      strncpy((char *)owner, (char *)s, slen);
      owner[slen] = '&#092;&#048;';
      append_element(level + 2, &quot;OWNER&quot;, owner);

      // Type name.
      slen = 0;
      CHECKERR(
        OCIAttrGet(
          (dvoid *)pard, (ub4)OCI_DTYPE_PARAM,
          (dvoid *)&amp;s, (ub4 *)&amp;slen,
          (ub4)OCI_ATTR_TYPE_NAME, (OCIError *)errhp
        )
      )
      strncpy((char *)type_name, (char *)s, slen);
      type_name[slen] = '&#092;&#048;';
      append_element(level + 2, &quot;TYPE_NAME&quot;, type_name);
    }

    append2(level + 1, &quot;&lt;/ROW&gt;\n&quot;);
  }

  if (column_count &gt; 0)
    append2(level, &quot;&lt;/ROWSET&gt;&quot;);
}

/**
 * Append XML element, indented to column 5.
 * @param   level   Level for indentation.
 * @param   name    Element name.
 * @param   value   Element text value.
 */

void append_element(int level, text *name, text *value) {
  text temp[100];

  sprintf(temp, &quot;%*s&lt;%s&gt;%s&lt;/%s&gt;\n&quot;, (level - 1) * INDENT_PER_LEVEL, &quot;&quot;, name, value, name);
  append(temp);
}

/**
 * Append XML element, indented to given level (2 spaces per level, starting
 * from 1).
 * @param   level   Level for indentation.
 * @param   name    Element name.
 * @param   value   Element numeric value.
 */

void append_element2(int level, text *name, sb2 value) {
  text temp[100];

  sprintf(temp, &quot;%*s&lt;%s&gt;%d&lt;/%s&gt;\n&quot;, (level - 1) * INDENT_PER_LEVEL, &quot;&quot;, name, value, name);
  append(temp);
}

/**
 * Appends given text to internal buffer, indented to given level (2 spaces per
 * level, starting from 1).
 * @param   level   Level for indentation.
 * @param   t       Text to append.
 */

void append2(int level, text *t) {
  sprintf(indent, &quot;%*s&quot;, (level - 1) * INDENT_PER_LEVEL, &quot;&quot;);
  append(indent);
  append(t);
}

/**
 * Appends given text to internal buffer.
 * @param   t       Text to append.
 */

void append(text *t) {
  if (strlen(t) + strlen(temp) &gt; sizeof(temp))
    // Too large, raise an exception.
    OCIExtProcRaiseExcpWithMsg(
      oci_ctx,
      20000,
      &quot;refcurdesc: Too many columns, description is larger than 32KB, which isn't supported&quot;,
      0
    );

  strcat(temp, t);
}

/**
 * Maps from OCI type code to &quot;uniform&quot; type code, which differs in a few
 * cases. Refer to blog post
 * http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/
 * @param   type_code
 *                  OCI type code.
 * @return  &quot;Uniform&quot; type code.
 */

int map_type_code(int type_code) {
  switch (type_code) {
#ifdef SQLT_IBFLOAT
    case SQLT_IBFLOAT:
      // BINARY_FLOAT.
      return 21;
#endif
#ifdef SQLT_IBDOUBLE
    case SQLT_IBDOUBLE:
      // BINARY_DOUBLE.
      return 22;
#endif
#ifdef SQLT_TIMESTAMP
    case SQLT_TIMESTAMP:
      // TIMESTAMP.
      return 180;
#endif
#ifdef SQLT_TIMESTAMP_TZ
    case SQLT_TIMESTAMP_TZ:
      // TIMESTAMP WITH TIME ZONE.
      return 181;
#endif
#ifdef SQLT_TIMESTAMP_LTZ
    case SQLT_TIMESTAMP_LTZ:
      // TIMESTAMP WITH LOCAL TIME ZONE.
      return 231;
#endif
#ifdef SQLT_INTERVAL_YM
    case SQLT_INTERVAL_YM:
      // INTERVAL YEAR TO MONTH.
      return 182;
#endif
#ifdef SQLT_INTERVAL_DS
    case SQLT_INTERVAL_DS:
      // INTERVAL DAY TO SECOND.
      return 183;
#endif
#ifdef SQLT_RDD
    case SQLT_RDD:
      // ROWID/UROWID, map to UROWID.
      return 208;
#endif
    case SQLT_NTY:
    case SQLT_NCO:
      // Object type or collection.
      return 109;
    case SQLT_REF:
      // REF to object type or collection.
      return 111;
    case SQLT_RSET:
      // REF CURSOR.
      return 102;
  }

  return type_code;
}

/**
 * OCI error checker. If any error is found, information about this is raised
 * in an exception and the global variable oci_status is updated with the value
 * of the error code, which will prevent any OCI calls wrapped up in CHECKERR
 * from being executed.
 * @param   errhp   Error handle.
 * @parem   status  OCI call return code.
 * @param   line    Line on which error occurred.
 */

void checkerr(OCIError *errhp, sword status, int line) {
  text errbuf[512];
  text message[1000] = &quot;&quot;;
  text message2[1000] = &quot;&quot;;
  sb4 errcode = 0;

  switch (status) {
    case OCI_SUCCESS:
      break;
    case OCI_SUCCESS_WITH_INFO:
      strcpy(message, &quot;Error: OCI_SUCCESS_WITH_INFO&quot;);
      break;
    case OCI_NEED_DATA:
      strcpy(message, &quot;Error: OCI_NEED_DATA&quot;);
      break;
    case OCI_NO_DATA:
      strcpy(message, &quot;Error: OCI_NO_DATA&quot;);
      break;
    case OCI_ERROR:
      OCIErrorGet(
        (dvoid *)errhp, (ub4)1, (text *)NULL, &amp;errcode,
        errbuf, (ub4) sizeof(errbuf), OCI_HTYPE_ERROR
      );
      sprintf(message, &quot;Error: %.*s&quot;, 512, errbuf);
      break;
    case OCI_INVALID_HANDLE:
      strcpy(message, &quot;Error: OCI_INVALID_HANDLE&quot;);
      break;
    case OCI_STILL_EXECUTING:
      strcpy(message, &quot;Error: OCI_STILL_EXECUTING&quot;);
      break;
    case OCI_CONTINUE:
      strcpy(message, &quot;Error: OCI_CONTINUE&quot;);
      break;
    default:
      sprintf(message, &quot;Error: %d&quot;, (int)status);
      break;
  }

  if (status != OCI_SUCCESS) {
    oci_status = status;
    sprintf(message2, &quot;refcurdesc.c line %d: %s&quot;, line, message);
    OCIExtProcRaiseExcpWithMsg(oci_ctx, 20001, message2, 0);
  }
}

void disable_prefetch(OCIStmt *stmt) {
  ub4 prefetch = 0;

  // Set prefetch to 0 in order not to prefetch anything from embedded REF
  // CURSORs.
  CHECKERR(
    OCIAttrSet(
      (dvoid *)stmt, (ub4)OCI_HTYPE_STMT, (dvoid *)&amp;prefetch,
      0, (ub4)OCI_ATTR_PREFETCH_ROWS, errhp
    )
  );
  CHECKERR(
    OCIAttrSet(
      (dvoid *)stmt, (ub4)OCI_HTYPE_STMT, (dvoid *)&amp;prefetch,
      0, (ub4)OCI_ATTR_PREFETCH_MEMORY, errhp
    )
  );
}
</pre><br />
Here&#8217;s <code>refcurdesc.def</code>, required for Windows only:</p>
<p><pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
LIBRARY refcurdesc

EXPORTS
refcurdesc
refcurdesc2
</pre></p>
<h3>Compilation Using Microsoft Visual Studio 2010</h3>
<p>You can use the following commands to create <code>refcurdesc.dll</code> on Windows (32-bit, assuming that you have called <code>vcvars32.bat</code> to set up <code>PATH</code>, <code>INCLUDE</code>, <code>LIB</code>, etc and also that <code>ORACLE_HOME</code> is set, lines wrapped for legibility):</p>
<p><pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
@setlocal
set INCLUDE=%INCLUDE%;%ORACLE_HOME%\oci\include
set LIB=%LIB%;%ORACLE_HOME%\oci\lib\msvc
cl /c /Zi /nologo /W3 /WX- /O2 /Oi /Oy- /GL /D WIN32 /D NDEBUG /D _WINDOWS /D
  _USRDLL /D REFCURDESC_EXPORTS /D _WINDLL /D _UNICODE /D UNICODE /Gm- /EHsc /MT
  /GS /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Fo&quot;Release\\&quot;
  /Fd&quot;Release\vc100.pdb&quot; /Gd /TC /analyze- /errorReport:queue refcurdesc.c
link /ERRORREPORT:QUEUE /OUT:&quot;Release\refcurdesc.dll&quot; /INCREMENTAL:NO /NOLOGO
  oci.lib /DEF:&quot;refcurdesc.def&quot; /MANIFEST
  /ManifestFile:&quot;Release\refcurdesc.dll.intermediate.manifest&quot;
  /MANIFESTUAC:&quot;level='asInvoker' uiAccess='false'&quot; /DEBUG
  /PDB:&quot;Release\refcurdesc.pdb&quot; /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG
  /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:&quot;Release\refcurdesc.lib&quot; /MACHINE:X86
  /DLL Release\refcurdesc.obj
@endlocal
</pre><br />
You need to link against a library file provided by the specific Oracle version you&#8217;re going to use the DLL with.</p>
<h3>Compilation Using GCC</h3>
<p>You can use the following commands to create <code>refcurdesc.so</code> on Linux (32-bit, assuming that <code>ORACLE_HOME</code> is set):</p>
<p><pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
gcc -I $ORACLE_HOME/rdbms/public -fPIC -c refcurdesc.c
gcc -shared -static-libgcc -o refcurdesc.so refcurdesc.o
</pre></p>
<h3>Installation</h3>
<p>The compiled <code>refcurdesc.dll/</code><code>refcurdesc.so</code> must be placed somewhere on the database server&#8217;s file system such that it can be referred from within Oracle. This is typically done in <code>%ORACLE_HOME%\BIN</code> on Windows and <code>$ORACLE_HOME/bin</code> on Linux. Please be aware that the directory name used for the Oracle library is case sensitive, even on Windows.</p>
<h3>Configuration of Extproc Listener</h3>
<p>If you get the following error when trying to execute the external procedure</p>
<pre>ORA-28595: Extproc agent : Invalid DLL Path</pre>
<p>you probably need to amend the configuration of the extproc listener to include a setting like</p>
<pre>(ENVS="EXTPROC_DLLS=ANY")</pre>
<p>or a more specific</p>
<pre>(ENVS="EXTPROC_DLLS=ONLY:C:\ORACLE\O112\BIN\refcurdesc.dll")</pre>
<p>Example:</p>
<p><pre class="brush: plain; collapse: true; highlight: [12]; light: false; pad-line-numbers: false; toolbar: true;">
SID_LIST_O112 =
  (SID_LIST =
    (SID_DESC =
      (GLOBAL_DBNAME = O112)
      (ORACLE_HOME = c:\ORACLE\O112)
      (SID_NAME = O112)
    )
    (SID_DESC =
      (SID_NAME = PLSExtProcO112)
      (ORACLE_HOME = C:\oracle\O112)
      (PROGRAM = extproc)
      (ENVS=&quot;EXTPROC_DLLS=ANY&quot;)
    )
    (SID_DESC =
      (SID_NAME = CLRExtProcO112)
      (ORACLE_HOME = C:\oracle\O112)
      (PROGRAM = extproc)
    )
  )
</pre></p>
<h2>Main Package</h2>
<p>The main package <code>REF_CURSOR_DESCRIBE</code> dispatches the incoming call to the specific implementation based on the following precedence order:</p>
<ol>
<li>If Oracle Database 11<em>g</em> Release 1 or newer: Use pure PL/SQL.</li>
<li>If Oracle Database 10<em>g</em> Release 1 or 2: Use Java if Java option enabled.</li>
<li>If Oracle Database 10<em>g</em> Release 1 or 2: Use C.</li>
</ol>
<p>If you have problems with this precedence order, you can control which implementation is used through setting of a package variable <code>DESCRIBE_IMPL</code>.<br />
<code>REF_CURSOR_DESCRIBE</code> also adds an extra XML element for each column: <code>DECLARATION</code>, which can be used for declaring a placeholder for fetching the given column. The datatype used for the declaration of a placeholder for an embedded <code>REF CURSOR</code> is <code>ANYDATA</code>, which makes it possible to hold the value in an object type attribute.</p>
<p>Here&#8217;s the implementation of the <code>REF_CURSOR_DESCRIBE</code> package specification (<code>ref_cursor_describe.pks</code>):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace package ref_cursor_descriptor
authid current_user as
/**
 * Functionality for describing a REF CURSOR in Oracle Database 10g Release 1 or
 * newer.
 * Feel free to use at your own risk.
 * @version   $Revision: 2 $
 * @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.
 */

  /**
   * Overrides which implementation is used:
   * 'P':  PL/SQL.
   * 'J':  Java using DBMS_ODCI.
   * 'J2': Java direct.
   * 'C':  C using DBMS_ODCI.SAVE-/RESTOREREFCURSOR.
   * 'C2': C direct (not possible according to documentation).
   * '':   PL/SQL for Oracle 11.1 and newer, otherwise Java if Java option
   *       enabled (Java direct on 10.2 and newer, Java indirect on 10.1) and C
   *       if Java is not enabled (C direct).
   */
  describe_impl varchar2(2);

  function describe(rc in out sys_refcursor)
  return xmltype;

  function describe_plsql(rc in out sys_refcursor)
  return xmltype;
  function describe_java(rc in out sys_refcursor)
  return xmltype;
  function describe_c(rc in out sys_refcursor)
  return xmltype;

  function get_declaration(
    name in varchar2,
    type_code in pls_integer,
    charset_form in pls_integer,
    length_semantics in varchar2,
    precision in pls_integer,
    scale in pls_integer,
    length in pls_integer,
    owner in varchar2,
    type_name in varchar2
  )
  return varchar2;

  function quote_identifier(identifier in varchar2)
  return varchar2;
end;
/
</pre><br />
And here&#8217;s the implementation of the <code>REF_CURSOR_DESCRIBE</code> package body (<code>ref_cursor_describe.pkb</code>):<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace package body ref_cursor_descriptor as
  --- $Revision: 2 $

  --- REF CURSOR number.
  rcn pls_integer;
  --- XML to be returned.
  xml varchar2(32767);
  --- Valid identifier characters.
  identifier_chars constant varchar2(40) :=
      'ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_$#';

  function add_declarations(xml in xmltype)
  return xmltype;

/**
 * Describe REF CURSOR and return description in XML form in a VARCHAR2.
 * Description is returned in the following XML format (without the XML
 * comments):
 * &lt;ROWSET generator=&quot;PL/SQL|Java|C&quot;&gt;
 *   &lt;ROW&gt;&lt;!-- Column 1. --&gt;
 *     &lt;ID&gt;column_id_1&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_1&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_1&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_1&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_1&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_1&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_1&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_1&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_1&lt;/TYPE_NAME&gt;
 *     &lt;DECLARATION&gt;declaration_1&lt;/DECLARATION&gt;
 *   &lt;/ROW&gt;
 *   &lt;!-- Columns 2 through n-1. --&gt;
 *   &lt;ROW&gt;&lt;!-- Column n. --&gt;
 *     &lt;ID&gt;column_id_n&lt;/ID&gt;
 *     &lt;NAME&gt;column_name_n&lt;/NAME&gt;
 *     &lt;TYPE_CODE&gt;data_type_code_n&lt;/TYPE_CODE&gt;
 *     &lt;NATIVE_TYPE_CODE&gt;native_data_type_code_n&lt;/NATIVE_TYPE_CODE&gt;
 *     &lt;!-- Character set form: 1: Database. 2: National. --&gt;
 *     &lt;CHARSET_FORM&gt;native_data_type_code_n&lt;/CHARSET_FORM&gt;
 *     &lt;!-- For [N]CHAR, [N]VARCHAR2, RAW, UROWID. --&gt;
 *     &lt;LENGTH&gt;column_length&lt;/LENGTH&gt;
 *     &lt;!-- For CHAR, VARCHAR2. --&gt;
 *     &lt;LENGTH_SEMANTICS&gt;column_name_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- NUMBER, TIMESTAMP%, INTERVAL%. --&gt;
 *     &lt;PRECISION&gt;precision_n&lt;/PRECISION&gt;
 *     &lt;!-- NUMBER, INTERVAL DAY TO SECOND. --&gt;
 *     &lt;SCALE&gt;scale_n&lt;/SCALE&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;OWNER&gt;type_owner_n&lt;/LENGTH_SEMANTICS&gt;
 *     &lt;!-- For object types, including collections. --&gt;
 *     &lt;TYPE_NAME&gt;type_name_n&lt;/TYPE_NAME&gt;
 *     &lt;DECLARATION&gt;declaration_n&lt;/DECLARATION&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Main entry. Dispatches to:
 * 1. PL/SQL if Oracle Database version &gt;= 11.
 * 2. Java if Oracle Database version is 10 and Java option is enabled.
 * 3. C if Oracle Database version is 10.
 * Otherwise, an exception is raised.
 * @param   rc      REF CURSOR.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

  function describe(rc in out sys_refcursor)
  return xmltype as

    result xmltype;

  begin
    case nvl(upper(describe_impl), 'D')
      when 'P' then
        result := describe_plsql(rc);
      when 'J' then
        result := describe_java(rc);
      when 'J2' then
        result := describe_java(rc);
      when 'C' then
        result := describe_c(rc);
      when 'C2' then
        result := describe_c(rc);
      when 'D' then
        -- Default.
        if portable.get_major_version &gt;= 11 then
          result := describe_plsql(rc);
        elsif portable.get_major_version = 10 then
          if portable.is_option_enabled('Java') then
            result := describe_java(rc);
          else
            result := describe_c(rc);
          end if;
        else
          raise_application_error(
            -20000,
            'ref_cursor_descriptor.describe: ' ||
                'Oracle Database 10g Release 1 or newer is required'
          );
        end if;
      else
        raise_application_error(
          -20002,
          'ref_cursor_descriptor.describe: ' ||
              'Invalid value of DESCRIBE_IMPL: &quot;' || describe_impl || '&quot;'
        );
    end case;

    return add_declarations(result);
  end describe;

/**
 * At least Oracle 11.1. Pure PL/SQL solution.
 * @param   rc      REF CURSOR.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

  function describe_plsql(rc in out sys_refcursor)
  return xmltype as

  begin
    xml := ref_cursor_descriptor_plsql(rc);

    if xml is not null then
      return xmltype(xml);
    else
      return null;
    end if;
  end describe_plsql;

/**
 * At least Oracle 10.1 and Java option enabled. Java solution.
 * @param   rc      REF CURSOR.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

  function describe_java(rc in out sys_refcursor)
  return xmltype as

  begin
    if upper(describe_impl) = 'J' then
      /*
       * Necessary to make it work for Oracle 10.1 as Oracle on some platforms
       * will transfer the ResultSet as null.
       */
      dbms_odci.saverefcursor(rc, rcn);
      xml := ref_cursor_descriptor_java(rcn);
      dbms_odci.restorerefcursor(rc, rcn);
    else
      -- 10.2 or newer, no need for using DBMS_ODCI, in fact, for 10.2 and 11.1
      -- this can create problems.
      xml := ref_cursor_descriptor_java2(rc);
    end if;

    if xml is not null then
      return xmltype(xml);
    else
      return null;
    end if;
  end describe_java;

/**
 * At least Oracle 9.2. C solution.
 * @param   rc      REF CURSOR.
 * @return  Description of columns in REF CURSOR on XML form.
 *          An exception is raised if the result is larger than
 *          32KB.
 */

  function describe_c(rc in out sys_refcursor)
  return xmltype as

  begin
    if nvl(describe_impl, 'C2') = 'C2' then
      /*
       * Pass REF CURSOR directly even though this is not possible according to
       * documentation. Works on Oracle 9.2 and newer.
       */
      xml := ref_cursor_descriptor_c2(rc);
    elsif describe_impl = 'C' then
      /*
       * Necessary to make it work as we cannot pass on REF CURSORs to external
       * procedures according to documentation.
       */
      rcn := null;
      dbms_odci.saverefcursor(rc, rcn);
      xml := ref_cursor_descriptor_c(rcn);
      dbms_odci.restorerefcursor(rc, rcn);
    end if;

    if xml is not null then
      return xmltype(xml);
    else
      return null;
    end if;
  exception
    when others then
      if sqlcode in (-31011, -19202, -1001) then
        dbms_output.put_line(xml);
        raise;
      else
        raise;
      end if;
  end describe_c;

/**
 * Builds the declaration for given column. ANYDATA is used for REF CURSOR,
 * which makes it possible to fetch the REF CURSOR into an object type attribute
 * of data type ANYDATA (REF CURSOR is not supported for object type
 * attributes).
 * @param   name    Column name.
 * @param   type_code
 *                  &quot;Uniform&quot; type code.
 * @param   charset_form
 *                  Character set form: 1: Database CS, 2: National CS.
 * @param   length_semantics
 *                  Length semantics: 'BYTE' or 'CHAR'.
 * @param   precision
 *                  Precision.
 * @param   scale   Scale.
 * @param   length  Length.
 * @param   owner   Datatype owner (for object types, incl. collections).
 * @param   type_name
 *                  Datatype name (for object types, incl. collections).
 * @return  Declaration of the form '&lt;name&gt; &lt;datatype&gt;'.
 */

  function get_declaration(
    name in varchar2,
    type_code in pls_integer,
    charset_form in pls_integer,
    length_semantics in varchar2,
    precision in pls_integer,
    scale in pls_integer,
    length in pls_integer,
    owner in varchar2,
    type_name in varchar2
  )
  return varchar2 as

    declaration varchar2(1000);

  begin
    case type_code
      when type_codes.tc_varchar2 then
        declaration :=
            case when charset_form = 2 then 'N' else '' end ||
            'VARCHAR2(' || rtrim(length || ' ' || length_semantics) || ')';
      when type_codes.tc_number then
        -- TODO: Special cases.
        if precision = 0 /*and scale = -127*/ then
          declaration := 'NUMBER';
        else
          declaration := 'NUMBER(' || precision;
          if scale != 0 then
            declaration := declaration || ', ' || scale;
          end if;
          declaration := declaration || ')';
        end if;
      when type_codes.tc_long then
        declaration := 'LONG';
      when type_codes.tc_date then
        declaration := 'DATE';
      when type_codes.tc_binary_float then
        declaration := 'BINARY_FLOAT';
      when type_codes.tc_binary_double then
        declaration := 'BINARY_DOUBLE';
      when type_codes.tc_timestamp then
        declaration := 'TIMESTAMP(' || precision || ')';
      when type_codes.tc_timestamp_tz then
        declaration := 'TIMESTAMP(' || precision || ') WITH TIME ZONE';
      when type_codes.tc_timestamp_ltz then
        declaration := 'TIMESTAMP(' || precision || ') WITH LOCAL TIME ZONE';
      when type_codes.tc_interval_ym then
        declaration := 'INTERVAL YEAR(' || precision || ') TO MONTH';
      when type_codes.tc_interval_ds then
        declaration :=
            'INTERVAL DAY(' || precision || ') ' ||
            'TO SECOND(' || scale || ')';
      when type_codes.tc_raw then
        declaration := 'RAW(' || length || ')';
      when type_codes.tc_long_raw then
        declaration := 'LONG RAW';
      when type_codes.tc_rowid then
        -- Map ROWID to VARCHAR2(18), ROWID object type attributes not allowed.
        declaration := 'VARCHAR2(18)';
      when type_codes.tc_urowid then
        -- Map UROWID to VARCHAR2(18), UROWID object type attributes not allowed.
        declaration := 'VARCHAR2(18)';
      when type_codes.tc_char then
        declaration :=
            case when charset_form = 2 then 'N' else '' end ||
            'CHAR(' || length || ' ' || length_semantics || ')';
      when type_codes.tc_clob then
        declaration :=
            case when charset_form = 2 then 'N' else '' end || 'CLOB';
      when type_codes.tc_blob then
        declaration := 'BLOB';
      when type_codes.tc_bfile then
        declaration := 'BFILE';
      when type_codes.tc_object then
        declaration :=
            quote_identifier(owner) ||
            '.' ||
            quote_identifier(type_name);
      when type_codes.tc_ref then
        declaration :=
            'REF ' ||
            quote_identifier(owner) ||
            '.' ||
            quote_identifier(type_name);
      when type_codes.tc_ref_cursor then
        -- REF CURSOR, to be saved in ANYDATA.
        declaration := 'SYS.ANYDATA';
      else
        raise_application_error(
          -20003,
          'ref_cursor_descriptor.get_declaration: ' || type_code || ': ' ||
              'Unknown datatype'
        );
    end case;

    return quote_identifier(name) || ' ' || declaration;
  end get_declaration;

/**
 * Adds DECLARATION element for each column.
 * @param   xml     Description XML.
 * @return  Description XML with DECLARATION element for each column.
 */

  function add_declarations(xml in xmltype)
  return xmltype as

    declaration varchar2(200);
    result xmltype;

    doc dbms_xmldom.domdocument;
    row dbms_xmldom.domnode;
    rows dbms_xmldom.domnodelist;
    decl_element dbms_xmldom.domelement;
    decl_text_element dbms_xmldom.domtext;
    decl_node dbms_xmldom.domnode;
    decl_text_node dbms_xmldom.domnode;
    root dbms_xmldom.domelement;

  begin
    doc := dbms_xmldom.newdomdocument(xml);
    root := dbms_xmldom.getdocumentelement(doc);
    rows := dbms_xmldom.getchildrenbytagname(root, 'ROW');

    for r in (
          select extractvalue(value(r), '//ID') id,
                 extractvalue(value(r), '//NAME') name,
                 extractvalue(value(r), '//TYPE_CODE') type_code,
                 extractvalue(value(r), '//CHARSET_FORM') charset_form,
                 extractvalue(value(r), '//LENGTH') length,
                 extractvalue(value(r), '//LENGTH_SEMANTICS') length_semantics,
                 extractvalue(value(r), '//PRECISION') precision,
                 extractvalue(value(r), '//SCALE') scale,
                 extractvalue(value(r), '//OWNER') OWNER,
                 extractvalue(value(r), '//TYPE_NAME') type_name
          from   table(
                   xmlsequence(
                     extract(xml, '/ROWSET/ROW')
                   )
                 ) r
        ) loop
      -- Get declaration.
      declaration := get_declaration(
        r.name,
        r.type_code,
        r.charset_form,
        r.length_semantics,
        r.precision,
        r.scale,
        r.length,
        r.owner,
        r.type_name
      );

      row := dbms_xmldom.item(rows, r.id - 1);
      -- New element.
      decl_element := dbms_xmldom.createelement(doc, 'DECLARATION');
      decl_node := dbms_xmldom.makenode(decl_element);
      -- New text node.
      decl_text_element := dbms_xmldom.createtextnode(doc, declaration);
      -- Append text node to element node.
      decl_text_node := dbms_xmldom.appendchild(
        decl_node,
        dbms_xmldom.makenode(decl_text_element)
      );
      -- Insert new element in document.
      row := dbms_xmldom.appendchild(row, decl_node);

      dbms_xmldom.freenode(decl_text_node);
      dbms_xmldom.freenode(decl_node);

/* Requires 10.2 or newer.
      -- Append new DECLARATION element to column with given ID.
      result := result.appendchildxml(
        '/ROWSET/ROW[' || r.id || ']',
        xmltype('&lt;DECLARATION&gt;' || declaration || '&lt;/DECLARATION&gt;')
      );
 */
    end loop;

    result := dbms_xmldom.getxmltype(doc);
    dbms_xmldom.freedocument(doc);

    return result;
  end add_declarations;

/**
 * Double quotes given identifier if it's necessary.
 * @param   identifier
 *                  Identifier.
 * @return  Identifier, potentially double quoted.
 */

  function quote_identifier(identifier in varchar2)
  return varchar2 as

  begin
    if identifier != upper(identifier) or
        ascii(substr(identifier, 1, 1)) not between ascii('A') and ascii('Z') or
        translate(identifier, ' ' || identifier_chars, ' ') is not null then
      return '&quot;' || identifier || '&quot;';
    end if;

    return identifier;
  end quote_identifier;

begin
  if portable.get_major_minor_version = 10.1 then
    /*
     * Necessary to make it work for Oracle 10.1 as Oracle on some platforms
     * will transfer the ResultSet as null or empty.
     */
    describe_impl := 'J';
  end if;
end ref_cursor_descriptor;
/
</pre></p>
<h1>Installation</h1>
<p>You need to provide the following grants to the schema in which you&#8217;re going to install the code:</p>
<p><pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
grant create library to &amp;&amp;1;
grant execute on sys.dbms_system to &amp;&amp;1;
grant create procedure to &amp;&amp;1;
</pre></p>
<p>An alternative to the <code>CREATE LIBRARY</code> privilege is to let your DBA create the library and grant <code>EXECUTE</code> privilege on the library to your schema.</p>
<p>The following installation script is assumed to run within SQL*Plus (<code>install.sql</code>). You should run this again if you migrate this solution to another database version or edition as the foundation for the installation discovered whilst installing could potentially have changed:</p>
<p><pre class="brush: plain; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
-- Installation script assmed to be run with SQL*Plus connected to the schema
-- that will own the database objects. You will need write access to the current
-- directory as dynamic scripts are created during the installation.
-- Feel free to use at your own risk.
-- @version   $Revision: 1 $
-- @author    Finn Ellebaek Nielsen, Ellebaek Consulting ApS.

set serveroutput on format truncated
set trimspool on
set linesize 100

@portable.pks
@portable.pkb
@@type_codes.pks

prompt Creating library...

declare
  oracle_home varchar2(255);
  file_path varchar2(255);
  refcurdesc varchar2(14);
  path_separator char(1);
  bin varchar2(3);
  ddl varchar2(1000);
begin
  sys.dbms_system.get_env('ORACLE_HOME', oracle_home);

  if lower(dbms_utility.port_string) like '%win%' then
    refcurdesc := 'refcurdesc.dll';
    path_separator := '\';
    bin := 'BIN';
  else
    refcurdesc := 'refcurdesc.so';
    path_separator := '/';
    bin := 'bin';
  end if;
  file_path := oracle_home || path_separator || bin;

  begin
    ddl :=
        'create or replace library refcurdesc as ''' ||
            file_path || path_separator || refcurdesc ||
            '''';
    execute immediate ddl;

    dbms_output.put_line(
      'Library REFCURDESC created for &quot;' ||
          file_path || path_separator || refcurdesc ||
          '&quot;.'
    );

    dbms_output.put_line('Please place ' || refcurdesc || ' in &quot;' || file_path || '&quot;.');
  exception
    when others then
      dbms_output.put_line(ddl || ': ' || sqlerrm);
      raise;
  end;
end;
/

prompt done

prompt Creating PL/SQL stored procedure

set feedback off
spool install_plsql.sql

declare
  ddl varchar2(32767);
begin
  if portable.get_major_version &gt;= 11 then
    dbms_output.put_line('@@ref_cursor_descriptor_plsql.fnc');
  else
    dbms_output.put_line('prompt Older than Oracle 11.1');
    dbms_output.put_line('');
    dbms_output.put_line('create or replace function ref_cursor_descriptor_plsql(rc in out sys_refcursor)');
    dbms_output.put_line('return varchar2 as');
    dbms_output.put_line('begin');
    dbms_output.put_line('  raise_application_error(-20002, ''Requires Oracle 11.1 or newer'');');
    dbms_output.put_line('end;');
    dbms_output.put_line('/');
    dbms_output.put_line('');
    dbms_output.put_line('show err');
  end if;
end;
/

spool off
set feedback 1

@@install_plsql.sql

prompt done

prompt Creating Java stored procedure

set feedback off
spool install_java.sql

declare
  ddl varchar2(32767);
begin
  if portable.is_option_enabled('Java') then
    dbms_output.put_line('set scan off');
    dbms_output.put_line('');
    dbms_output.put_line('@@RefCursorDescriptor.sql');
    dbms_output.put_line('');
    dbms_output.put_line('set scan on');
    dbms_output.put_line('');
    dbms_output.put_line('@@ref_cursor_descriptor_java.fnc');
    dbms_output.put_line('@@ref_cursor_descriptor_java2.fnc');
  else
    dbms_output.put_line('prompt Java option not enabled');
    dbms_output.put_line('create or replace function ref_cursor_descriptor_java(rcn in pls_integer)');
    dbms_output.put_line('return varchar2 as');
    dbms_output.put_line('begin');
    dbms_output.put_line('  raise_application_error(-20003, ''Java option not enabled'');');
    dbms_output.put_line('end;');
    dbms_output.put_line('/');
    dbms_output.put_line('');
    dbms_output.put_line('show err');
    dbms_output.put_line('');
    dbms_output.put_line('create or replace function ref_cursor_descriptor_java2(rc in sys_refcursor)');
    dbms_output.put_line('return varchar2 as');
    dbms_output.put_line('begin');
    dbms_output.put_line('  raise_application_error(-20003, ''Java option not enabled'');');
    dbms_output.put_line('end;');
    dbms_output.put_line('/');
    dbms_output.put_line('');
    dbms_output.put_line('show err');
  end if;
end;
/

spool off
set feedback 1

@@install_java.sql

prompt done

prompt Creating C external procedure

@@ref_cursor_descriptor_c.fnc
@@ref_cursor_descriptor_c2.fnc

prompt done

@@ref_cursor_descriptor.pks
@@ref_cursor_descriptor.pkb

@dbms_output_put_line.prc
</pre></p>
<h1>Example</h1>
<p>Here&#8217;s an example:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
set long 50000

variable xml clob

prompt SELECT * FROM EMP

declare
  rc sys_refcursor;
begin
  open rc for
  select *
  from   emp;

  :xml := ref_cursor_descriptor.describe(rc).getclobval;
end;
/

print xml
</pre><br />
which could produce the following output:<br />
<pre class="brush: xml; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
&lt;ROWSET generator=&quot;C&quot;&gt;
  &lt;ROW&gt;
    &lt;ID&gt;1&lt;/ID&gt;
    &lt;NAME&gt;EMPNO&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;2&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;2&lt;/NATIVE_TYPE_CODE&gt;
    &lt;PRECISION&gt;4&lt;/PRECISION&gt;
    &lt;SCALE&gt;0&lt;/SCALE&gt;
    &lt;DECLARATION&gt;EMPNO NUMBER(4)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;2&lt;/ID&gt;
    &lt;NAME&gt;ENAME&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;1&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;1&lt;/NATIVE_TYPE_CODE&gt;
    &lt;CHARSET_FORM&gt;1&lt;/CHARSET_FORM&gt;
    &lt;LENGTH&gt;10&lt;/LENGTH&gt;
    &lt;LENGTH_SEMANTICS&gt;BYTE&lt;/LENGTH_SEMANTICS&gt;
    &lt;DECLARATION&gt;ENAME VARCHAR2(10 BYTE)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;3&lt;/ID&gt;
    &lt;NAME&gt;JOB&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;1&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;1&lt;/NATIVE_TYPE_CODE&gt;
    &lt;CHARSET_FORM&gt;1&lt;/CHARSET_FORM&gt;
    &lt;LENGTH&gt;9&lt;/LENGTH&gt;
    &lt;LENGTH_SEMANTICS&gt;BYTE&lt;/LENGTH_SEMANTICS&gt;
    &lt;DECLARATION&gt;JOB VARCHAR2(9 BYTE)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;4&lt;/ID&gt;
    &lt;NAME&gt;MGR&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;2&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;2&lt;/NATIVE_TYPE_CODE&gt;
    &lt;PRECISION&gt;4&lt;/PRECISION&gt;
    &lt;SCALE&gt;0&lt;/SCALE&gt;
    &lt;DECLARATION&gt;MGR NUMBER(4)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;5&lt;/ID&gt;
    &lt;NAME&gt;HIREDATE&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;12&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;12&lt;/NATIVE_TYPE_CODE&gt;
    &lt;DECLARATION&gt;HIREDATE DATE&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;6&lt;/ID&gt;
    &lt;NAME&gt;SAL&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;2&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;2&lt;/NATIVE_TYPE_CODE&gt;
    &lt;PRECISION&gt;7&lt;/PRECISION&gt;
    &lt;SCALE&gt;2&lt;/SCALE&gt;
    &lt;DECLARATION&gt;SAL NUMBER(7, 2)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;7&lt;/ID&gt;
    &lt;NAME&gt;COMM&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;2&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;2&lt;/NATIVE_TYPE_CODE&gt;
    &lt;PRECISION&gt;7&lt;/PRECISION&gt;
    &lt;SCALE&gt;2&lt;/SCALE&gt;
    &lt;DECLARATION&gt;COMM NUMBER(7, 2)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;ID&gt;8&lt;/ID&gt;
    &lt;NAME&gt;DEPTNO&lt;/NAME&gt;
    &lt;TYPE_CODE&gt;2&lt;/TYPE_CODE&gt;
    &lt;NATIVE_TYPE_CODE&gt;2&lt;/NATIVE_TYPE_CODE&gt;
    &lt;PRECISION&gt;2&lt;/PRECISION&gt;
    &lt;SCALE&gt;0&lt;/SCALE&gt;
    &lt;DECLARATION&gt;DEPTNO NUMBER(2)&lt;/DECLARATION&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre></p>
<h1>Subtleties</h1>
<p>I&#8217;ve found the following subtleties whilst developing the code:</p>
<ul>
<li><code>DBMS_SQL.DESCRIBE_COLUMN%</code> always uses byte length semantics for data types <code>CHAR</code> and <code>VARCHAR2</code>, no matter how they were declared. Eg, if you have declared a table column with <code>VARCHAR2(10 CHAR)</code> in a database with characterset <code>AL32UTF8</code> this will be reported as <code>VARCHAR2(40 BYTE)</code>.</li>
<li>Inversely, <code>java.sql.ResultSetMetaData</code> always uses character length semantics.</li>
<li>It&#8217;s not possible to distinguish <code>ROWID</code> and <code>UROWID</code> in Java and C so both are mapped to <code>UROWID</code>.</li>
</ul>
<h1>Known Issues</h1>
<ul>
<li>If you convert a <code>REF CURSOR</code> to a <code>DBMS_SQL</code> cursor and later invoke <code>DBMS_XMLGEN.NEWCONTEXT</code> on that same cursor (after you&#8217;ve converted it back to a <code>REF CURSOR</code>) you may encounter the following Oracle errors: <code>ORA-01001: invalid cursor</code>, <code>ORA-24338: statement handle not executed</code> and <code>ORA-00600: internal error code, arguments: [psdmsc: psdinvdef#1]</code>. I&#8217;ve only encountered these errors in Oracle 11.1.0.6.0 as they seems to have been fixed in 11.1.0.7.0 and 11.2.0.1.0.</li>
<li>If you describe a <code>REF CURSOR</code> and then call either Oracle code that is implemented in C (not PL/SQL) or another external procedure written in C you&#8217;ll probably get Oracle errors like <code>ORA-24374: define not done before fetch or execute and fetch</code>. The problem seems to be that something on the <code>REF CURSOR</code> OCI statement handle is marked as processed when we obtain it in our C external procedure, which means that eg <code>DBMS_XMLGEN</code> doesn&#8217;t define the columns for its fetch, which results in an error. The workaround is to use the direct C implementation (not the indirect one using <code>DBMS_ODCI</code>) or fetch the <code>REF CURSOR</code> to either memory or a table, open a new <code>REF CURSOR</code> on that copy and then invoke <code>DBMS_XMLGEN</code> on the copy. I&#8217;ll come back to this in a future blog post.</li>
</ul>
<h1>Supporting Oracle Database 9<i>i</i> Release 2</h1>
<p>The techniques in <code>REF_CURSOR_COPY</code> and the related blog can be made to work for Oracle 9.2 as well but you would need to remove calls to <code>DBMS_ODCI.SAVE-/RESTOREREFCURSOR</code> and <code>DBMS_UTILITY.FORMAT_ERROR_BACKTRACE</code> as these procedures are not available in Oracle 9.2.<br />
I haven&#8217;t used Conditional Compilation techniques for this as it would exclude Oracle versions 10.1.0.2.0 and 10.1.0.3.0.</p>
<h1>Improvements</h1>
<p>The following improvements could be implemented:</p>
<ul>
<li>Lift the 32KB limitation by using a <code>CLOB</code> instead of a <code>VARCHAR2</code>.</li>
<li>Make the C implementation thread safe.</li>
</ul>
<h1>Conclusion</h1>
<p>The code shown here has been fairly well tested with the following Oracle versions and editions:</p>
<ul>
<li>Oracle Database 10<em>g</em> Release 1 10.1.0.2.0 Personal Edition on Windows XP SP3.</li>
<li>Oracle Database 10<em>g</em> Release 1 10.1.0.3.0 Enterprise Edition on Intel Solaris 10.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.1.0 Express Edition on Windows XP SP3.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.1.0 Express Edition on Red Hat Enterprise Linux 5.3.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.2.0 Enterprise Edition on Intel Solaris 10.</li>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.6.0 Personal Edition on Windows XP SP3.</li>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.7.0 Personal Edition on Windows XP SP3.</li>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.6.0 Enterprise Edition on SPARC Solaris 10.</li>
<li>Oracle Database 11<em>g</em> Release 2 11.2.0.1.0 Personal Edition on Windows 7.</li>
</ul>
<p>Feel free to use the code at your own risk. I welcome your feedback and suggestions for improvements but the code as such is not supported.</p>
<h1>Source Code</h1>
<p>You can download the source code <a href="http://www.ellebaek-consulting.com/files/blog/refcurdesc.zip">here</a>.</p>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/99/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/99/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/99/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=99&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
		<item>
		<title>Oracle Type Code Mappings</title>
		<link>http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/</link>
		<comments>http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/#comments</comments>
		<pubDate>Fri, 25 Feb 2011 17:34:59 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle SQL]]></category>
		<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[Oracle Call Interface]]></category>
		<category><![CDATA[JDBC]]></category>
		<category><![CDATA[oracle type code]]></category>
		<category><![CDATA[oracle type code mappings]]></category>
		<category><![CDATA[oracle typecode]]></category>
		<category><![CDATA[oracle typecode mappings]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=104</guid>
		<description><![CDATA[Use case: Imagine that you need to represent type codes for the Oracle data types across PL/SQL, Java and C. Challenge: Oracle uses different type codes in different contexts. This is fair enough if they need to adhere to standards (eg JDBC), but there&#8217;s really no excuse for using different type codes in the documentation, [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=104&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<p>Use case: Imagine that you need to represent type codes for the Oracle data types across PL/SQL, Java and C.</p>
<p>Challenge: Oracle uses different type codes in different contexts. This is fair enough if they need to adhere to standards (eg JDBC), but there&#8217;s really no excuse for using different type codes in the documentation, in PL/SQL, in <code>DBMS_TYPES</code> and Oracle Call Interface (OCI). In my view it&#8217;s a bit of a mess.</p>
<p>Solution: I&#8217;ve spent quite some time mapping between these slightly different type codes and also chosen a &#8220;uniform type code&#8221; across these. My rule of thumb has been that I use the type code from the Oracle Database 11<em>g</em> Release 2 documentation (<a href="http://download.oracle.com/docs/cd/E11882_01/server.112/e17118/sql_elements001.htm#i54330" target="_blank">Data Types</a>). If the documentation doesn&#8217;t provide one, I use the one returned by one of the <code>DBMS_SQL.DESCRIBE_COLUMNS%</code> procedures as this is probably what most Oracle developers have used over time.</p>
<p>I&#8217;ve listed the mappings in the table below. If a cell is empty it&#8217;s because it&#8217;s missing or not supported for that particular type system (eg <code>DUMP</code> doesn&#8217;t work with all data types). I&#8217;ve underlined where the type codes differ from the Oracle documentation or between DBMS_SQL and the other type systems.</p>
<table class="apt">
<tbody>
<tr>
<th class="apt">Data Type</th>
<th class="apt">Uniform<br />
Type<br />
Code</th>
<th class="apt">Oracle<br />
Doc.</th>
<th class="apt">DBMS_SQL</th>
<th class="apt">DBMS_TYPES<br />
TYPECODE_%</th>
<th class="apt">JDBC<br />
java.sql.Types</th>
<th class="apt">OCI</th>
<th class="apt">DUMP</th>
<th class="apt">V$SQL_<br />
BIND_<br />
DATA.<br />
DATATYPE</th>
</tr>
<tr>
<td class="apt"><code>VARCHAR2</code></td>
<td class="apt">1</td>
<td class="apt">1</td>
<td class="apt">1</td>
<td class="apt">9<br />
<code>VARCHAR2<br />
</code>1<br />
<code>VARCHAR</code></td>
<td class="apt">12<br />
<code>VARCHAR</code></td>
<td class="apt">1<br />
<code>SQLT_CHR</code></td>
<td class="apt">1</td>
<td class="apt">1</td>
</tr>
<tr>
<td class="apt"><code>NVARCHAR2</code></td>
<td class="apt">1</td>
<td class="apt">1</td>
<td class="apt">1</td>
<td class="apt"><span style="text-decoration:underline;">287</span><br />
<code>NVARCHAR2</code></td>
<td class="apt">12<br />
<code>VARCHAR</code></td>
<td class="apt">1<br />
<code>SQLT_CHR</code></td>
<td class="apt">1</td>
<td class="apt">1</td>
</tr>
<tr>
<td class="apt"><code>NUMBER</code></td>
<td class="apt">2</td>
<td class="apt">2</td>
<td class="apt">2</td>
<td class="apt"><code>NUMBER</code></td>
<td class="apt">2<br />
<code>NUMERIC</code></td>
<td class="apt">2<br />
<code>SQLT_NUM</code></td>
<td class="apt">2</td>
<td class="apt">2</td>
</tr>
<tr>
<td class="apt"><code>FLOAT</code></td>
<td class="apt">2</td>
<td class="apt">2</td>
<td class="apt">2</td>
<td class="apt">2<br />
<code>NUMBER</code></td>
<td class="apt">2<br />
<code>NUMERIC</code></td>
<td class="apt">2<br />
<code>SQLT_NUM</code></td>
<td class="apt">2</td>
<td class="apt">2</td>
</tr>
<tr>
<td class="apt"><code>LONG</code></td>
<td class="apt">8</td>
<td class="apt">8</td>
<td class="apt">8</td>
<td class="apt"></td>
<td class="apt">-1<br />
<code>LONGVARCHAR</code></td>
<td class="apt">8<br />
<code>SQLT_LNG</code></td>
<td class="apt"></td>
<td class="apt">8</td>
</tr>
<tr>
<td class="apt"><code>DATE</code></td>
<td class="apt">12</td>
<td class="apt">12</td>
<td class="apt">12</td>
<td class="apt">12<br />
<code>DATE</code></td>
<td class="apt">93<br />
<code>TIMESTAMP</code><sup>1</sup></td>
<td class="apt">12<br />
<code>SQLT_DAT</code></td>
<td class="apt">12</td>
<td class="apt">12</td>
</tr>
<tr>
<td class="apt"><code>BINARY_FLOAT</code></td>
<td class="apt">21</td>
<td class="apt">21</td>
<td class="apt"><span style="text-decoration:underline;">100</span></td>
<td class="apt"><span style="text-decoration:underline;">100</span><br />
<code>BFLOAT</code></td>
<td class="apt">100</td>
<td class="apt"><span style="text-decoration:underline;">100</span><br />
<code>SQLT_IBFLOAT</code></td>
<td class="apt"><span style="text-decoration:underline;">100</span></td>
<td class="apt"><span style="text-decoration:underline;">100</span></td>
</tr>
<tr>
<td class="apt"><code>BINARY_DOUBLE</code></td>
<td class="apt">22</td>
<td class="apt">22</td>
<td class="apt"><span style="text-decoration:underline;">101</span></td>
<td class="apt"><span style="text-decoration:underline;">101</span><br />
<code>BDOUBLE</code></td>
<td class="apt">101</td>
<td class="apt"><span style="text-decoration:underline;">101</span><br />
<code>SQLT_IBFLOAT</code></td>
<td class="apt"><span style="text-decoration:underline;">101</span></td>
<td class="apt"><span style="text-decoration:underline;">101</span></td>
</tr>
<tr>
<td class="apt"><code>TIMESTAMP</code></td>
<td class="apt">180</td>
<td class="apt">180</td>
<td class="apt">180</td>
<td class="apt"><span style="text-decoration:underline;">187</span><br />
<code>TIMESTAMP</code></td>
<td class="apt">93<br />
<code>TIMESTAMP</code><sup>1</sup></td>
<td class="apt"><span style="text-decoration:underline;">187</span><br />
<code>SQLT_TIMESTAMP</code></td>
<td class="apt">180</td>
<td class="apt">180</td>
</tr>
<tr>
<td class="apt"><code>TIMESTAMP<br />
WITH TIME ZONE</code></td>
<td class="apt">181</td>
<td class="apt">181</td>
<td class="apt">181</td>
<td class="apt"><span style="text-decoration:underline;">188</span><br />
<code>TIMESTAMP_TZ</code></td>
<td class="apt">-101</td>
<td class="apt"><span style="text-decoration:underline;">188</span><br />
<code>SQLT_TIMESTAMP_TZ</code></td>
<td class="apt">181</td>
<td class="apt">181</td>
</tr>
<tr>
<td class="apt"><code>TIMESTAMP<br />
WITH LOCAL<br />
TIME ZONE</code></td>
<td class="apt">231</td>
<td class="apt">231</td>
<td class="apt">231</td>
<td class="apt"><span style="text-decoration:underline;">232</span><br />
<code>TIMESTAMP_LTZ</code></td>
<td class="apt">-102</td>
<td class="apt"><span style="text-decoration:underline;">232</span><br />
<code>SQLT_TIMESTAMP_LTZ</code></td>
<td class="apt">231</td>
<td class="apt">231</td>
</tr>
<tr>
<td class="apt"><code>INTERVAL</code> <code>YEAR<br />
TO</code> <code>MONTH</code></td>
<td class="apt">182</td>
<td class="apt">182</td>
<td class="apt">182</td>
<td class="apt"><span style="text-decoration:underline;">189</span><br />
<code>INTERVAL_YM</code></td>
<td class="apt">-103</td>
<td class="apt"><span style="text-decoration:underline;">189</span><br />
<code>SQLT_INTERVAL_YM</code></td>
<td class="apt">182</td>
<td class="apt">182</td>
</tr>
<tr>
<td class="apt"><code>INTERVAL</code> <code>DAY<br />
TO</code> <code>SECOND</code></td>
<td class="apt">183</td>
<td class="apt">183</td>
<td class="apt">183</td>
<td class="apt"><span style="text-decoration:underline;">190</span><br />
<code>INTERVAL_DS</code></td>
<td class="apt">-104</td>
<td class="apt"><span style="text-decoration:underline;">190</span><br />
<code>SQLT_INTERVAL_DS</code></td>
<td class="apt">183</td>
<td class="apt">183</td>
</tr>
<tr>
<td class="apt"><code>RAW</code></td>
<td class="apt">23</td>
<td class="apt">23</td>
<td class="apt">23</td>
<td class="apt"><span style="text-decoration:underline;">95</span><br />
<code>RAW</code></td>
<td class="apt">-3<br />
<code>VARBINARY</code></td>
<td class="apt">23<br />
<code>SQLT_BIN</code></td>
<td class="apt">23</td>
<td class="apt">23</td>
</tr>
<tr>
<td class="apt"><code>LONG RAW</code></td>
<td class="apt">24</td>
<td class="apt">24</td>
<td class="apt">24</td>
<td class="apt"></td>
<td class="apt">-4<br />
<code>LONGVARBINARY</code></td>
<td class="apt">24<br />
<code>SQLT_LBI</code></td>
<td class="apt"></td>
<td class="apt">24</td>
</tr>
<tr>
<td class="apt"><code>ROWID</code></td>
<td class="apt">69</td>
<td class="apt">69</td>
<td class="apt"><span style="text-decoration:underline;">11</span></td>
<td class="apt"></td>
<td class="apt">-8</td>
<td class="apt"><span style="text-decoration:underline;">104</span><br />
<code>SQLT_RDD</code></td>
<td class="apt">69</td>
<td class="apt">69</td>
</tr>
<tr>
<td class="apt"><code>UROWID</code></td>
<td class="apt">208</td>
<td class="apt">208</td>
<td class="apt">208</td>
<td class="apt"><span style="text-decoration:underline;">104</span><br />
<code>UROWID</code></td>
<td class="apt">-8</td>
<td class="apt"><span style="text-decoration:underline;">104</span><br />
<code>SQLT_RDD</code></td>
<td class="apt">208</td>
<td class="apt">208</td>
</tr>
<tr>
<td class="apt"><code>CHAR</code></td>
<td class="apt">96</td>
<td class="apt">96</td>
<td class="apt">96</td>
<td class="apt">96<br />
<code>CHAR</code></td>
<td class="apt">1<br />
<code>CHAR</code></td>
<td class="apt">96<br />
<code>SQLT_AFC</code></td>
<td class="apt">96</td>
<td class="apt">96</td>
</tr>
<tr>
<td class="apt"><code>NCHAR</code></td>
<td class="apt">96</td>
<td class="apt">96</td>
<td class="apt">96</td>
<td class="apt"><span style="text-decoration:underline;">286</span><br />
<code>NCHAR</code></td>
<td class="apt">1<br />
<code>CHAR</code></td>
<td class="apt">96<br />
<code>SQLT_AFC</code></td>
<td class="apt">96</td>
<td class="apt">96</td>
</tr>
<tr>
<td class="apt"><code>CLOB</code></td>
<td class="apt">112</td>
<td class="apt">112</td>
<td class="apt">112</td>
<td class="apt">112<br />
<code>CLOB</code></td>
<td class="apt">2005<br />
<code>CLOB</code></td>
<td class="apt">112<br />
<code>SQLT_CLOB</code></td>
<td class="apt"></td>
<td class="apt">112</td>
</tr>
<tr>
<td class="apt"><code>NCLOB</code></td>
<td class="apt">112</td>
<td class="apt">112</td>
<td class="apt">112</td>
<td class="apt"><span style="text-decoration:underline;">288</span><br />
<code>NCLOB</code></td>
<td class="apt">2005<br />
<code>CLOB</code></td>
<td class="apt">112<br />
<code>SQLT_CLOB</code></td>
<td class="apt"></td>
<td class="apt">112</td>
</tr>
<tr>
<td class="apt"><code>BLOB</code></td>
<td class="apt">113</td>
<td class="apt">113</td>
<td class="apt">113</td>
<td class="apt">113<br />
<code>BLOB</code></td>
<td class="apt">2004<br />
<code>BLOB</code></td>
<td class="apt">113<br />
<code>SQLT_BLOB</code></td>
<td class="apt"></td>
<td class="apt">113</td>
</tr>
<tr>
<td class="apt"><code>BFILE</code></td>
<td class="apt">114</td>
<td class="apt">114</td>
<td class="apt">114</td>
<td class="apt">114<br />
<code>BFILE</code></td>
<td class="apt">-13</td>
<td class="apt">114<br />
<code>SQLT_BFILEE</code></td>
<td class="apt"></td>
<td class="apt">114</td>
</tr>
<tr>
<td class="apt"><code>XMLTYPE</code></td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">58</span><br />
<code>OPAQUE</code><sup>2</sup></td>
<td class="apt">2007</td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"><span style="text-decoration:underline;">58</span></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt"><code>ANYDATA</code></td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">58</span><br />
<code>OPAQUE</code><sup>2</sup></td>
<td class="apt">2007</td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"><span style="text-decoration:underline;">58</span></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt"><code>ANYDATASET</code></td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">58</span><br />
<code>OPAQUE</code><sup>2</sup></td>
<td class="apt">2007</td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"><span style="text-decoration:underline;">58</span></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt"><code>ANYTYPE</code></td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">58</span><br />
<code>OPAQUE</code><sup>2</sup></td>
<td class="apt">2007</td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"><span style="text-decoration:underline;">58</span></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt">Object type</td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>OBJECT</code></td>
<td class="apt">2002<br />
<code>STRUCT</code></td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"><span style="text-decoration:underline;">121</span></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt"><code>VARRAY</code></td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">247</span><br />
<code>VARRAY</code></td>
<td class="apt">2003<br />
<code>ARRAY</code></td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt">Nested table</td>
<td class="apt">109</td>
<td class="apt"></td>
<td class="apt">109</td>
<td class="apt"><span style="text-decoration:underline;">248</span><br />
<code>TABLE</code></td>
<td class="apt">2003<br />
<code>ARRAY</code></td>
<td class="apt"><span style="text-decoration:underline;">108</span><br />
<code>SQLT_NTY</code></td>
<td class="apt"></td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt"><code>REF</code></td>
<td class="apt">111</td>
<td class="apt"></td>
<td class="apt">111</td>
<td class="apt"><span style="text-decoration:underline;">110</span><br />
<code>REF</code></td>
<td class="apt">2006<br />
<code>REF</code></td>
<td class="apt"><span style="text-decoration:underline;">110</span><br />
<code>SQLT_REF</code></td>
<td class="apt">111</td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt">Strong <code>REF<br />
CURSOR</code></td>
<td class="apt">102</td>
<td class="apt"></td>
<td class="apt">102</td>
<td class="apt"></td>
<td class="apt">-10</td>
<td class="apt"><span style="text-decoration:underline;">116</span><br />
<code>SQLT_RSET</code></td>
<td class="apt">102</td>
<td class="apt"></td>
</tr>
<tr>
<td class="apt">Weak <code>REF<br />
CURSOR</code></td>
<td class="apt">102</td>
<td class="apt"></td>
<td class="apt">102</td>
<td class="apt"></td>
<td class="apt">-10</td>
<td class="apt"><span style="text-decoration:underline;">116</span><br />
<code>SQLT_RSET</code></td>
<td class="apt">102</td>
<td class="apt"></td>
</tr>
</tbody>
</table>
<p><sup>1</sup> Probably because in Java a true <code>Date</code> cannot hold a time value.</p>
<p><sup>2</sup> <code>XMLTYPE</code>, <code>ANYDATA</code>, <code>ANYDATASET</code> and <code>ANYTYPE</code> are not &#8220;normal object types&#8221;. They are all declared like &#8220;<code>CREATE TYPE &lt;OWNER&gt;.&lt;OBJECT_TYPE&gt; AS OPAQUE VARYING (*) USING LIBRARY...</code>&#8220;<code>.</code></p>
<p>The type codes from PL/SQL, Java and C have been verified through creation of a table or <code>SELECT</code> statement using the various data types and description of these through:</p>
<ul>
<li>PL/SQL: <code>DBMS_SQL.DESCRIBE_COLUMNS3</code> on a <code>DBMS_SQL</code> cursor representing the above mentioned table/<code>SELECT</code>.</li>
<li>Java: <code>java.sql.ResultSetMetaData</code> obtained through the <code>java.sql.ResultSet</code> representing the above mentioned table/<code>SELECT</code>.</li>
<li>C: <code>OCIParamGet</code>(&#8230;, <code>OCI_HTYPE_STMT</code>, &#8230;) + <code>OCIAttrGet</code>(&#8230;, <code>OCI_ATTR_DATA_TYPE</code>, &#8230;) on a statement handle representing the above mentioned table/<code>SELECT</code>.</li>
</ul>
<p>The type codes have been verified on Oracle Database 11<em>g</em> Release 2 11.2.0.1.0 Personal Edition on Windows 7. When I have more time I&#8217;ll create an automated test suite in order to verify my findings on Oracle Database 10<em>g</em> Release 1, Release 2 and Oracle Database 11<em>g</em> Release 1 and also verify on other platforms like Linux and Solaris.</p>
<p>I welcome your feedback and suggestions for improvements and corrections.</p>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/104/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/104/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/104/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=104&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2011/02/25/oracle-type-code-mappings/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
		<item>
		<title>Comparing XML in Oracle</title>
		<link>http://ellebaek.wordpress.com/2011/02/01/comparing-xml-in-oracle/</link>
		<comments>http://ellebaek.wordpress.com/2011/02/01/comparing-xml-in-oracle/#comments</comments>
		<pubDate>Tue, 01 Feb 2011 12:44:07 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[Oracle SQL]]></category>
		<category><![CDATA[diff]]></category>
		<category><![CDATA[oracle]]></category>
		<category><![CDATA[xml]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=74</guid>
		<description><![CDATA[Introduction This blog post is about comparing a specific type of XML documents with the purpose of obtaining details about their differences, within an Oracle database. This specific type is documents representing table data/resultsets conforming to Oracle&#8217;s canonical form, ie XML with the following structure (the comments &#60;!-- ... --&#62; are only included for illustration): [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=74&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<h1>Introduction</h1>
<p>This blog post is about comparing a specific type of XML documents with the purpose of obtaining details about their differences, within an Oracle database. This specific type is documents representing table data/resultsets conforming to Oracle&#8217;s canonical form, ie  XML with the following structure (the comments <code>&lt;!-- ... --&gt;</code> are only included for illustration):</p>
<pre>&lt;ROWSET&gt;
  &lt;ROW&gt;&lt;!-- Row 1 --&gt;
    &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
    &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
    &lt;!-- ... --&gt;
    &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;&lt;!-- Row 2 --&gt;
    &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
    &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
    &lt;!-- ... --&gt;
    &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
  &lt;/ROW&gt;
  &lt;!-- ... --&gt;
  &lt;ROW&gt;&lt;!-- Row n --&gt;
    &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
    &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
    &lt;!-- ... --&gt;
    &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
   &lt;/ROW&gt;
&lt;/ROWSET&gt;</pre>
<p>So basically there&#8217;s an XML element for each column and the name of the element corresponds to the column name. There&#8217;s a <code>&lt;ROW&gt;</code> fragment for each row in the resultset and the root element is called <code>&lt;ROWSET&gt;</code> (all XML documents must have at most one root element).</p>
<p>The diffing method I&#8217;m going to describe and implement in this blog post is focusing on a limited version of Oracle canonical form, namely it is assumed that:</p>
<ul>
<li>None of the columns in the table are using object types, including collections and <code>XMLTYPE</code>.</li>
<li>Object tables are not used.</li>
<li>XML element attributes are not used.</li>
<li><code>NULL</code> values are represented by an empty tag, eg <code>&lt;ENAME/&gt;</code> or <code>&lt;ENAME&gt;&lt;/ENAME&gt;</code>.</li>
<li>We regard the document order to be significant &#8212; like comparing sets in SQL.</li>
</ul>
<p>This means that we can assume that all the column values are atomic, single and simple values, which means that the comparison implementation can be simplified.</p>
<p>The primary use case for this XML is comparison (or XML diffing) for automated unit/regression tests that need to verify that resultsets from the Program Under Test (PUT) are identical to a given expected outcome. It&#8217;s a convenient way to specify the expected outcome in XML and the PUT resultsets can then be converted to XML and compared. My blog post <a href="http://ellebaek.wordpress.com/2011/01/27/converting-between-oracle-data-and-xml/" target="_blank">Converting Between Oracle Data and XML</a> describes how to convert the Oracle data to XML.</p>
<p>The differ should return <code>NULL</code> in case there are no differences and it must support node values larger than 32KB.</p>
<p>The code in this blog post requires Oracle Database 11<em>g</em> Release 1 or newer and has been tested on the following versions (both on Windows XP Service Pack 3 32-bit):</p>
<ul>
<li>Oracle Database 11<em>g</em> Release 1 11.1.0.6.0.</li>
<li>Oracle Database 11<em>g</em> Release 2 11.2.0.1.0.</li>
</ul>
<p>In a future follow up to this blog post I&#8217;ll implement a more generic XML differ that will work with Oracle Database 10<em>g</em> Release 1 or newer.</p>
<h1>Example</h1>
<p>Here&#8217;s an example of what I would like to be able to do. Given the following two XML documents:</p>
<pre>&lt;ROWSET&gt;
<span style="text-decoration:underline;">  &lt;ROW&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
  &lt;/ROW&gt;</span>
  &lt;ROW&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;LOC&gt;DALLAS&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;LOC&gt;CHICAGO&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
    &lt;LOC&gt;BOSTON&lt;/LOC&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;</pre>
<p>and</p>
<pre>&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;LOC&gt;DALLAS&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;LOC&gt;CHICAGO&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;<span style="text-decoration:underline;">XOPERATIONSETC</span>&lt;/DNAME&gt;
    &lt;LOC&gt;<span style="text-decoration:underline;">NEW YORK</span>&lt;/LOC&gt;
  &lt;/ROW&gt;
<span style="text-decoration:underline;">  &lt;ROW&gt;
    &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;LOC&gt;ALBANY&lt;/LOC&gt;
  &lt;/ROW&gt;</span>
&lt;/ROWSET&gt;</pre>
<p>I would like an XML differ to pick up the following differences and only those:</p>
<ul>
<li>Department 10 has been removed.</li>
<li>Department 50 has been added.</li>
<li>Department 40&#8242;s name and location have changed.</li>
</ul>
<p>The XML differ should return this in a structured form, preferably in XML, perhaps something like this:</p>
<pre>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!-- Differencing export generated by Altova® DiffDog® version 2009 sp1
  Enterprise Edition - for more information please visit www.altova.com --&gt;
&lt;diff_result&gt;
  &lt;diff_info comparison_mode="text_or_xml"&gt;
    &lt;source_left name="c:\temp\xml1.xml" uri="file:///c:/temp/xml1.xml"/&gt;
    &lt;source_right name="c:\temp\xml2.xml" uri="file:///c:/temp/xml2.xml"/&gt;
  &lt;/diff_info&gt;
  &lt;xml_diff&gt;
    &lt;left_location&gt;
      &lt;parent xpath="/ROWSET"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/left_location&gt;
    &lt;right_location&gt;
      &lt;parent xpath="/ROWSET"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/right_location&gt;
    &lt;left_content&gt;
      &lt;element&gt;
        &lt;ROW&gt;
          &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
          &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
          &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
        &lt;/ROW&gt;
      &lt;/element&gt;
    &lt;/left_content&gt;
  &lt;/xml_diff&gt;
  &lt;xml_diff&gt;
    &lt;left_location&gt;
      &lt;parent xpath="/ROWSET/ROW[4]/DNAME"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/left_location&gt;
    &lt;right_location&gt;
      &lt;parent xpath="/ROWSET/ROW[3]/DNAME"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/right_location&gt;
    &lt;left_content&gt;
      &lt;element&gt;OPERATIONS&lt;/element&gt;
    &lt;/left_content&gt;
    &lt;right_content&gt;
      &lt;element&gt;XOPERATIONSETC&lt;/element&gt;
    &lt;/right_content&gt;
  &lt;/xml_diff&gt;
  &lt;xml_diff&gt;
    &lt;left_location&gt;
      &lt;parent xpath="/ROWSET/ROW[4]/LOC"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/left_location&gt;
    &lt;right_location&gt;
      &lt;parent xpath="/ROWSET/ROW[3]/LOC"/&gt;
      &lt;position&gt;1&lt;/position&gt;
    &lt;/right_location&gt;
    &lt;left_content&gt;
      &lt;element&gt;BOSTON&lt;/element&gt;
    &lt;/left_content&gt;
    &lt;right_content&gt;
      &lt;element&gt;NEW YORK&lt;/element&gt;
    &lt;/right_content&gt;
  &lt;/xml_diff&gt;
  &lt;xml_diff&gt;
    &lt;left_location&gt;
      &lt;parent xpath="/ROWSET"/&gt;
      &lt;position&gt;5&lt;/position&gt;
    &lt;/left_location&gt;
    &lt;right_location&gt;
      &lt;parent xpath="/ROWSET"/&gt;
      &lt;position&gt;4&lt;/position&gt;
    &lt;/right_location&gt;
    &lt;right_content&gt;
      &lt;element&gt;
        &lt;ROW&gt;
          &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;
          &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
          &lt;LOC&gt;ALBANY&lt;/LOC&gt;
        &lt;/ROW&gt;
      &lt;/element&gt;
    &lt;/right_content&gt;
  &lt;/xml_diff&gt;
&lt;/diff_result&gt;</pre>
<p>which is what Altova DiffDog 2009 returns, or even better:</p>
<pre>&lt;DIFFERENCES&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[DEPTNO="10"]]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;DELETE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[&lt;ROW&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
&lt;/ROW&gt;]]&gt;&lt;/VALUE1&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[DEPTNO="50"]]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;INSERT&lt;/OPERATION&gt;
    &lt;VALUE2&gt;&lt;![CDATA[&lt;ROW&gt;
  &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;LOC&gt;ALBANY&lt;/LOC&gt;
&lt;/ROW&gt;]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[DEPTNO="40"]/DNAME]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[OPERATIONS]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[XOPERATIONSETC]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[DEPTNO="40"]/LOC]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[BOSTON]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[NEW YORK]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
&lt;/DIFFERENCES&gt;</pre>
<p>which provides the same information but in a more compact and useful format.</p>
<h1>Alternatives</h1>
<p>For XML diffing within the Oracle database we have various alternatives:</p>
<ul>
<li>Open source PL/SQL XML Compare Project: <code>DBI_XML_COMPAREDBI_XML_COMPARE.GET_XML_DIFFERENCES</code>.</li>
<li>Commercial software Quest Code Tester for Oracle&#8217;s built-in XML differ: <code>QU_XML.GET_XML_DIFFERENCES</code>.</li>
<li>Open source Java code that could be loaded into the database as Java stored procedures, eg XMLUnit.</li>
<li>Oracle&#8217;s <code>XMLDIFF</code> function that was introduced in Oracle Database 11<em>g</em> Release 1.</li>
<li>Write your own from scratch.</li>
</ul>
<p>Let&#8217;s look at each of these alternatives in turn.</p>
<h2>PL/SQL XML Compare Project</h2>
<p>The function returns a collection of records describing the nodes of the two documents that differ. You can then traverse the result to understand the differences and you could also convert this to XML. Let&#8217;s look at what <code>DBI_XML_COMPAREDBI_XML_COMPARE.GET_XML_DIFFERENCES</code> returns when converted to XML using the following code snippet:</p>
<pre>create or replace function xmlcdata2(
  c in clob
)
return clob as

/**
 * Wraps CLOB value up in a CDATA section. Oracle's built-in XMLCDATA function
 * was not introduced until 10.2, doesn't supporting "chaining" CDATA sections
 * and doesn't support CLOBs.
 * @param   c       CLOB to be wrapped up in a CDATA section. CDATA section
 *                  chaining is used if C contains the sequence ']]&gt;'.
 * @return  CDATA section.
 */

  result clob;

begin
  dbms_lob.createtemporary(result, false, dbms_lob.call);
  dbms_lob.append(result, '&lt;![CDATA[');
  if c is not null then
    -- Append the value. Replace embedded ]]&gt; -- CDATA chaining.
    dbms_lob.append(result, replace(c, ']]&gt;', ']]]]&gt;&lt;![CDATA[&gt;'));
  end if;
  dbms_lob.append(result, ']]&gt;');

  return result;
end xmlcdata2;
/

create or replace function xmldiff_dbi_xml_compare(
  xml1 in xmltype,
  xml2 in xmltype
)
return xmltype as

/**
 * Compares two XML documents and returns the differences in XML. Based on
 * DBI_XML_COMPARE.
 * @param   xml1    First XML document.
 * @param   xml2    Second XML document.
 * @return  Document describing differences based on an XML version of what is
 *          return from DBI_XML_COMPARE.GET_XML_DIFFERENCES. NULL if no
 *          differences.
 */

  result clob;
  differences dbi_xml_compare.t_nodes;

begin
  differences := dbi_xml_compare.get_xml_differences(
    xml1,
    xml2,
    p_ignore_whitespace =&gt; dbi_xml_compare.gc_whitespace_normalize
  );

  if differences is not null and differences.count &gt; 0 then
    dbms_lob.createtemporary(result, true, dbms_lob.call);
    dbms_lob.append(result, '&lt;dbi_xml_compare&gt;');
  end if;

  for i in nvl(differences.first, 0) .. nvl(differences.last, -1) loop
    dbms_lob.append(result, '&lt;difference&gt;');
    dbms_lob.append(result, '&lt;document_id&gt;');
    dbms_lob.append(result, xmlcdata2(to_char(differences(i).document_id)));
    dbms_lob.append(result, '&lt;/document_id&gt;');
    dbms_lob.append(result, '&lt;node_id&gt;');
    dbms_lob.append(result, xmlcdata2(differences(i).node_id));
    dbms_lob.append(result, '&lt;/node_id&gt;');
    dbms_lob.append(result, '&lt;node_values&gt;');

    for j in
        nvl(differences(i).node_values.first, 0) ..
        nvl(differences(i).node_values.last, -1) loop
      dbms_lob.append(result, '&lt;node_value id="' || j || '"&gt;');
      dbms_lob.append(result, xmlcdata2(differences(i).node_values(j)));
      dbms_lob.append(result, '&lt;/node_value&gt;');
    end loop;

    dbms_lob.append(result, '&lt;/node_values&gt;');
    dbms_lob.append(
      result,
      '&lt;difference_type&gt;' ||
        case differences(i).difference_type
          when dbi_xml_compare.gc_identical_value then
            'dbi_xml_compare.gc_identical_value'
          when dbi_xml_compare.gc_different_value then
            'dbi_xml_compare.gc_different_value'
          when dbi_xml_compare.gc_no_equivalent_node then
            'dbi_xml_compare.gc_no_equivalent_node'
          when dbi_xml_compare.gc_ambiguous_element then
            'dbi_xml_compare.gc_ambiguous_element'
        end ||
      '&lt;/difference_type&gt;&lt;/difference&gt;'

    );
  end loop;

  if differences is not null and differences.count &gt; 0 then
    dbms_lob.append(result, '&lt;/dbi_xml_compare&gt;');
  end if;

  if result is not null then
    return xmltype(result);
  end if;

  return null;
end xmldiff_dbi_xml_compare;
/</pre>
<p>we would get the following differences for our example (abbreviated for legibility, a total of 48 differences were found):</p>
<pre>&lt;dbi_xml_compare&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[1]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{1}/LOC{NEW YORK}]]&gt;&lt;/node_id&gt;
    &lt;node_values/&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[1]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/LOC{BOSTON}]]&gt;&lt;/node_id&gt;
    &lt;node_values/&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
  &lt;!-- snip --&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[1]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{1}/DEPTNO{10}]]&gt;&lt;/node_id&gt;
    &lt;node_values/&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[2]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/DNAME{ACCOUNTING}/#text]]&gt;&lt;/node_id&gt;
    &lt;node_values&gt;
      &lt;node_value id="1"&gt;&lt;![CDATA[ACCOUNTING]]&gt;&lt;/node_value&gt;
    &lt;/node_values&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[2]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/LOC{ALBANY}]]&gt;&lt;/node_id&gt;
    &lt;node_values/&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
  &lt;!-- snip --&gt;
  &lt;difference&gt;
    &lt;document_id&gt;&lt;![CDATA[2]]&gt;&lt;/document_id&gt;
    &lt;node_id&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/DNAME{ACCOUNTING}]]&gt;&lt;/node_id&gt;
    &lt;node_values/&gt;
    &lt;difference_type&gt;dbi_xml_compare.gc_no_equivalent_node&lt;/difference_type&gt;
  &lt;/difference&gt;
&lt;/dbi_xml_compare&gt;</pre>
<p>which clearly is not what we need &#8212; basically the differ got so confused that it couldn&#8217;t match anything.</p>
<p>This was tested with <code>DBI_XML_COMPARE</code> version 1.3. <code>DBI_XML_COMPARE</code> does not support element values larger than 32KB.</p>
<h2>Quest Code Tester for Oracle</h2>
<p>Like <code>DBI_XML_COMPAREDBI_XML_COMPARE.GET_XML_DIFFERENCES</code>, <code>QU_XML.GET_XML_DIFFERENCES</code> returns a collection of records. Using the following snippet to convert this to XML:</p>
<pre>create or replace function xmldiff_qu_xml(
  xml1 in xmltype,
  xml2 in xmltype
)
return xmltype as

/**
 * Compares two XML documents and returns the differences in XML. Based on
 * QU_XML.
 * @param   xml1    First XML document.
 * @param   xml2    Second XML document.
 * @return  Document describing differences based on an XML version of what is
 *          return from QU_XML.GET_XML_DIFFERENCES. NULL if no differences.
 */

  nodes qu_xml.t_nodes;
  value varchar2(32767);

  result clob;

begin
  nodes := qu_xml.get_xml_differences(xml1, xml2);

  if nodes.count &gt; 0 then
    dbms_lob.createtemporary(result, true, dbms_lob.call);
    dbms_lob.append(result, '&lt;qu_xml&gt;');
    for i in nvl(nodes.first, 0) .. nvl(nodes.last, -1) loop
      if nodes(i).node_values.count &gt; 0 then
        value := nodes(i).node_values(nodes(i).node_values.first);
      else
        value := '';
      end if;
      dbms_lob.append(result, '&lt;diff_node&gt;');
      dbms_lob.append(result, '&lt;name&gt;');
      dbms_lob.append(result, xmlcdata2(nodes(i).node_id));
      dbms_lob.append(result, '&lt;/name&gt;');
      dbms_lob.append(result, '&lt;value&gt;');
      dbms_lob.append(result, xmlcdata2(value));
      dbms_lob.append(result, '&lt;/value&gt;');
      dbms_lob.append(result, '&lt;difference&gt;' ||
          case nodes(i).difference_type
            when 1 then
              'Value difference'
            when 2 then
              'Node removed/added'
            when 3 then
              'Ambiguous'
          end ||
          '&lt;/difference&gt;&lt;/diff_node&gt;'
      );
    end loop;
    dbms_lob.append(result, '&lt;/qu_xml&gt;');
  end if;

  if result is not null then
    return xmltype(result);
  end if;

  return null;
end xmldiff_qu_xml;
/</pre>
<p>we would get the following differences (abbreviated for legibility, a total of 48 differences were found):</p>
<pre>&lt;qu_xml&gt;
  &lt;diff_node&gt;
    &lt;name&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{1}/LOC{NEW YORK}]]&gt;&lt;/name&gt;
    &lt;value&gt;&lt;![CDATA[]]&gt;&lt;/value&gt;
    &lt;difference&gt;Node removed/added&lt;/difference&gt;
  &lt;/diff_node&gt;
  &lt;diff_node&gt;
    &lt;name&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/LOC{BOSTON}]]&gt;&lt;/name&gt;
    &lt;value&gt;&lt;![CDATA[]]&gt;&lt;/value&gt;
    &lt;difference&gt;Node removed/added&lt;/difference&gt;
  &lt;/diff_node&gt;
  &lt;!-- snip --&gt;
  &lt;diff_node&gt;
    &lt;name&gt;&lt;![CDATA[/#document/ROWSET{1}/ROW{4}/DNAME{ACCOUNTING}]]&gt;&lt;/name&gt;
    &lt;value&gt;&lt;![CDATA[]]&gt;&lt;/value&gt;
    &lt;difference&gt;Node removed/added&lt;/difference&gt;
  &lt;/diff_node&gt;
&lt;/qu_xml&gt;</pre>
<p>This was tested with Quest Code Tester for Oracle 1.9.1.505. <code>QU_XML</code> does not support element values larger than 32KB.</p>
<h2>Open Source Java Code</h2>
<p>It would be convenient if we could find an open source Java project that  meets our requirements. If one was found, we could load it into the database using <code>loadjava</code>, assuming that the JDK used by the Java code is compatible with the JDK version of our Oracle database. One such open source project to investigate is XMLUnit, which is an extension to JUnit &#8212; the Java unit testing framework.</p>
<p>Here&#8217;s some Java code that uses XMLUnit for comparison of our example (setup of the XML documents has been left out):</p>
<pre>package com.appatra.blog;

import com.topologi.diffx.DiffXException;

import java.io.IOException;

import java.util.List;

import javax.xml.transform.TransformerConfigurationException;

import org.custommonkey.xmlunit.DetailedDiff;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.XMLTestCase;

import org.jdom.JDOMException;

import org.xml.sax.SAXException;

/**
 * Test of XMLUnit's capabilities of XML comparison.
 */

public class XmlDiffXmlUnit extends XMLTestCase {
  public XmlDiffXmlUnit() {
    super("XmlDiffXmlUnit");
  }

  private void test()
  throws SAXException, IOException {
    String xml1 = /* Left out... */;
    String xml2 = /* Left out... */;

    DetailedDiff myDiff = new DetailedDiff(compareXML(xml2, xml1));
    List&lt;Difference&gt; allDifferences = (List&lt;Difference&gt;)myDiff.getAllDifferences();

    for (Difference diff : allDifferences) {
      System.err.println(diff);
    }
  }

  public static void main(String[] args)
  throws SAXException, IOException {
    XmlDiffXmlUnit xmlDiffXmlUnit = new XmlDiffXmlUnit();

    xmlDiffXmlUnit.test();
  }
}</pre>
<p>This produces the following output (12 differences, wrapped for improved legibility):</p>
<pre>Expected text value '20' but was '10' -
  comparing &lt;DEPTNO ...&gt;20&lt;/DEPTNO&gt; at
  /ROWSET[1]/ROW[1]/DEPTNO[1]/text()[1] to
  &lt;DEPTNO ...&gt;10&lt;/DEPTNO&gt; at /ROWSET[1]/ROW[1]/DEPTNO[1]/text()[1]
Expected text value 'RESEARCH' but was 'ACCOUNTING' -
  comparing &lt;DNAME ...&gt;RESEARCH&lt;/DNAME&gt; at
  /ROWSET[1]/ROW[1]/DNAME[1]/text()[1] to
  &lt;DNAME ...&gt;ACCOUNTING&lt;/DNAME&gt; at /ROWSET[1]/ROW[1]/DNAME[1]/text()[1]
Expected text value 'DALLAS' but was 'NEW YORK' -
  comparing &lt;LOC ...&gt;DALLAS&lt;/LOC&gt; at
  /ROWSET[1]/ROW[1]/LOC[1]/text()[1] to
  &lt;LOC ...&gt;NEW YORK&lt;/LOC&gt; at /ROWSET[1]/ROW[1]/LOC[1]/text()[1]
Expected text value '30' but was '20' -
  comparing &lt;DEPTNO ...&gt;30&lt;/DEPTNO&gt; at
  /ROWSET[1]/ROW[2]/DEPTNO[1]/text()[1] to
  &lt;DEPTNO ...&gt;20&lt;/DEPTNO&gt; at /ROWSET[1]/ROW[2]/DEPTNO[1]/text()[1]
Expected text value 'SALES' but was 'RESEARCH' -
  comparing &lt;DNAME ...&gt;SALES&lt;/DNAME&gt; at
  /ROWSET[1]/ROW[2]/DNAME[1]/text()[1] to
  &lt;DNAME ...&gt;RESEARCH&lt;/DNAME&gt; at /ROWSET[1]/ROW[2]/DNAME[1]/text()[1]
Expected text value 'CHICAGO' but was 'DALLAS' -
  comparing &lt;LOC ...&gt;CHICAGO&lt;/LOC&gt; at
  /ROWSET[1]/ROW[2]/LOC[1]/text()[1] to
  &lt;LOC ...&gt;DALLAS&lt;/LOC&gt; at /ROWSET[1]/ROW[2]/LOC[1]/text()[1]
Expected text value '40' but was '30' -
  comparing &lt;DEPTNO ...&gt;40&lt;/DEPTNO&gt; at
  /ROWSET[1]/ROW[3]/DEPTNO[1]/text()[1] to
  &lt;DEPTNO ...&gt;30&lt;/DEPTNO&gt; at /ROWSET[1]/ROW[3]/DEPTNO[1]/text()[1]
Expected text value 'XOPERATIONSETC' but was 'SALES' -
  comparing &lt;DNAME ...&gt;XOPERATIONSETC&lt;/DNAME&gt; at
  /ROWSET[1]/ROW[3]/DNAME[1]/text()[1] to
  &lt;DNAME ...&gt;SALES&lt;/DNAME&gt; at /ROWSET[1]/ROW[3]/DNAME[1]/text()[1]
Expected text value 'NEW YORK' but was 'CHICAGO' -
  comparing &lt;LOC ...&gt;NEW YORK&lt;/LOC&gt; at
  /ROWSET[1]/ROW[3]/LOC[1]/text()[1] to
  &lt;LOC ...&gt;CHICAGO&lt;/LOC&gt; at /ROWSET[1]/ROW[3]/LOC[1]/text()[1]
Expected text value '50' but was '40' -
  comparing &lt;DEPTNO ...&gt;50&lt;/DEPTNO&gt; at
  /ROWSET[1]/ROW[4]/DEPTNO[1]/text()[1] to
  &lt;DEPTNO ...&gt;40&lt;/DEPTNO&gt; at /ROWSET[1]/ROW[4]/DEPTNO[1]/text()[1]
Expected text value 'ACCOUNTING' but was 'OPERATIONS' -
  comparing &lt;DNAME ...&gt;ACCOUNTING&lt;/DNAME&gt; at
  /ROWSET[1]/ROW[4]/DNAME[1]/text()[1] to
  &lt;DNAME ...&gt;OPERATIONS&lt;/DNAME&gt; at /ROWSET[1]/ROW[4]/DNAME[1]/text()[1]
Expected text value 'ALBANY' but was 'BOSTON' -
  comparing &lt;LOC ...&gt;ALBANY&lt;/LOC&gt; at
  /ROWSET[1]/ROW[4]/LOC[1]/text()[1] to
  &lt;LOC ...&gt;BOSTON&lt;/LOC&gt; at /ROWSET[1]/ROW[4]/LOC[1]/text()[</pre>
<p>This was tested with <code>XMLUnit</code> version 1.3.</p>
<h2>XMLDIFF</h2>
<p>Oracle introduced a new function <code>XMLDIFF</code> in Oracle Database 11<em>g</em> Release 1. This function generates an XML document with the differences conforming to an Xdiff schema. Let's look at how this function works on our example:</p>
<pre>create or replace function xmldiff_xmldiff(
  xml1 in xmltype,
  xml2 in xmltype
)
return xmltype as

/**
 * Compares two XML documents and returns the differences in XML. Based on
 * XMLDIFF, hence only supported with Oracle Database 11g Release 1 and newer.
 * @param   xml1    First XML document.
 * @param   xml2    Second XML document.
 * @return  Document describing differences based on an XML version of what is
 *          return from QU_XML.GET_XML_DIFFERENCES. NULL if no differences.
 */

  diff xmltype;

begin
  select xmldiff(xml1, xml2)
  into   diff
  from   dual;
  if diff is not null then
    if diff.extract('/*/*') is not null then
      return diff;
    end if;
  end if;

  return null;
end xmldiff_xmldiff;
/</pre>
<p>which returns the following based on our example (once again, 12 differences):</p>
<pre>&lt;xd:xdiff
    xsi:schemaLocation="http://xmlns.oracle.com/xdb/xdiff.xsd
    http://xmlns.oracle.com/xdb/xdiff.xsd"
    xmlns:xd="http://xmlns.oracle.com/xdb/xdiff.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&gt;
  &lt;?oracle-xmldiff operations-in-docorder="true"
    output-model="snapshot"
    diff-algorithm="global"?&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[1]/DEPTNO[1]/text()[1]"&gt;
    &lt;xd:content&gt;20&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[1]/DNAME[1]/text()[1]"&gt;
    &lt;xd:content&gt;RESEARCH&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[1]/LOC[1]/text()[1]"&gt;
    &lt;xd:content&gt;DALLAS&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[2]/DEPTNO[1]/text()[1]"&gt;
    &lt;xd:content&gt;30&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[2]/DNAME[1]/text()[1]"&gt;
    &lt;xd:content&gt;SALES&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[2]/LOC[1]/text()[1]"&gt;
    &lt;xd:content&gt;CHICAGO&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[3]/DEPTNO[1]/text()[1]"&gt;
    &lt;xd:content&gt;40&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[3]/DNAME[1]/text()[1]"&gt;
    &lt;xd:content&gt;XOPERATIONSETC&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[3]/LOC[1]/text()[1]"&gt;
    &lt;xd:content&gt;NEW YORK&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[4]/DEPTNO[1]/text()[1]"&gt;
    &lt;xd:content&gt;50&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[4]/DNAME[1]/text()[1]"&gt;
    &lt;xd:content&gt;ACCOUNTING&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
  &lt;xd:update-node xd:node-type="text"
      xd:xpath="/ROWSET[1]/ROW[4]/LOC[1]/text()[1]"&gt;
    &lt;xd:content&gt;ALBANY&lt;/xd:content&gt;
  &lt;/xd:update-node&gt;
&lt;/xd:xdiff&gt;</pre>
<h2>Common Issue</h2>
<p>A common problem with these alternatives is that none of these differs has a concept of a primary key for an XML fragment. In our example, the XML is Oracle&#8217;s canoncial form representing the data in the <code>SCOTT.DEPT</code> table. The primary key of this table is the <code>DEPTNO</code> column. If the differs had known this or had spent time analyzing the XML and detected it, they would have produced a much more useful output, instead of getting very confused and thinking that everything has changed.</p>
<p>The last one of the alternatives was to write your own XML differ from  scratch and I&#8217;ll come back to this in a later blog post. For now, let&#8217;s look at how we can improve on <code>XMLDIFF</code> by writing a wrapper around it that detects primary key usage and internally uses <code>XMLDIFF</code> to diff the fragments for each primary key value found.</p>
<h1>XMLDIFF2</h1>
<p>The algorithm in our extended <code>XMLDIFF</code> function that we&#8217;re going to call <code>XMLDIFF2</code> is:</p>
<ol>
<li>Prepare <code>CLOB</code> variable for constructing the difference XML.</li>
<li>Find minimum number of leading elements that form primary key values (unique lookup).</li>
<li>Look for primary key values in <code>XML1</code> not present in <code>XML2</code>: These primary key values have been deleted.</li>
<li>Look for primary key values in <code>XML2</code> not present in <code>XML1</code>: These primary key values have been inserted.</li>
<li>Diff all common primary key values, ie in both <code>XML1</code> and <code>XML2</code>.</li>
<li>Return <code>NULL</code> if no differences found.</li>
</ol>
<p>The implementation uses the <code>XMLCDATA2</code> function that I&#8217;ve listed earlier. Here is the listing of <code>XMLDIFF2</code>:</p>
<pre>create or replace function xmldiff2(
  xml1 in xmltype,
  xml2 in xmltype
)
return xmltype as

/**
 * Extension to Oracle's XMLDIFF function, available with Oracle 11.1 and newer.
 * This version is to be used primarily with simple XML documents in Oracles
 * "canonical form", ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Assumptions: It is assumed that none of the column elements in the XML
 * contain XML, such that the maximum level of XML elements is 3. It is also
 * assumed that datetime XML elements are passed in using XML Schema compliant
 * formats ('yyyy-mm-dd"T"hh24:mi:ss' for DATE, 'yyyy-mm-dd"T"hh24:mi:ss.ff9'
 * for TIMESTAMP and 'yyyy-mm-dd"T"hh24:mi:ss.ff9tzh:tzm' for TIMESTAMP WITH
 * [LOCAL] TIME ZONE).
 * Extensions: This version understands a concept of leading elements at level 3
 * forming a primary key and ignores document element order.
 * Differences are returned in the following form:
 * &lt;DIFFERENCES&gt;
 *   &lt;DIFFERENCE&gt;
 *     &lt;XPATH&gt;&lt;![CDATA[&lt;XPATH1&gt;]]&gt;&lt;/XPATH&gt;
 *     &lt;OPERATION&gt;(INSERT|DELETE|UPDATE)&lt;/OPERATION&gt;
 *     &lt;VALUE1&gt;&lt;![CDATA[&lt;VALUE_FROM_XML1&gt;]]&gt;&lt;/VALUE1&gt;
 *     &lt;VALUE2&gt;&lt;![CDATA[&lt;VALUE_FROM_XML2&gt;]]&gt;&lt;/VALUE2&gt;
 *   &lt;/DIFFERENCE&gt;
 *   &lt;DIFFERENCE&gt;
 *     &lt;XPATH&gt;&lt;![CDATA[&lt;XPATH1&gt;]]&gt;&lt;/XPATH&gt;
 *     &lt;OPERATION&gt;(INSERT|DELETE|UPDATE)&lt;/OPERATION&gt;
 *     &lt;VALUE1&gt;&lt;![CDATA[&lt;VALUE_FROM_XML1&gt;]]&gt;&lt;/VALUE1&gt;
 *     &lt;VALUE2&gt;&lt;![CDATA[&lt;VALUE_FROM_XML2&gt;]]&gt;&lt;/VALUE2&gt;
 *   &lt;/DIFFERENCE&gt;
 *   ...
 *   &lt;DIFFERENCE&gt;
 *     &lt;XPATH&gt;&lt;![CDATA[&lt;XPATH1&gt;]]&gt;&lt;/XPATH&gt;
 *     &lt;OPERATION&gt;(INSERT|DELETE|UPDATE)&lt;/OPERATION&gt;
 *     &lt;VALUE1&gt;&lt;![CDATA[&lt;VALUE_FROM_XML1&gt;]]&gt;&lt;/VALUE1&gt;
 *     &lt;VALUE2&gt;&lt;![CDATA[&lt;VALUE_FROM_XML2&gt;]]&gt;&lt;/VALUE2&gt;
 *   &lt;/DIFFERENCE&gt;
 * &lt;/DIFFERENCES&gt;
 * &lt;VALUE1&gt; is only present for DELETE and UPDATE operations. &lt;VALUE2&gt; is only
 * present for INSERT and UPDATE operations.
 * Limitations. Namespace and attributes are not supported. Works only with
 * Oracle 11.1 and newer.
 * @param   xml1    First XML document.
 * @param   xml2    Second XML document.
 * @return  Differences, NULL if none found.
 */

  ---  Xdiff namespace.
  xmldiff_ns constant varchar2(48) :=
      'xmlns:xd="http://xmlns.oracle.com/xdb/xdiff.xsd"';

  --- Element overview, ie name and count.
  type element_overview_t is record (
    name   varchar2(4000),
    occurs integer
  );
  type level2_elements_c is
  table of element_overview_t
  index by binary_integer;

  --- Level 2 element overview for XML1.
  level2_elements1 level2_elements_c;
  --- Level 2 element overview for XML2.
  level2_elements2 level2_elements_c;

  -- Primary key XPath expressions.
  type varchar2_4000_lookup is
  table of boolean
  index by varchar2(4000);
  pkvs varchar2_4000_lookup;

  --- Number of elements required to form primary key.
  pk_element_count integer := null;

  --- Assuming Oracle canonica form: Root element name.
  element1_name constant varchar2(6) := 'ROWSET';
  --- Assuming Oracle canonica form: Element name for each row (level 2).
  element2_name constant varchar2(3) := 'ROW';

  ---
  type pk_element_t is record (
    name  varchar2(4000),
    value varchar2(4000)
  );
  type pk_element_c is
  table of pk_element_t
  index by binary_integer;
  pk_elements pk_element_c;

  -- Extract of XML1.
  xml1e xmltype;
  -- Extract of XML2.
  xml2e xmltype;
  --- Result from SYS.XMLDIFF.
  diff2 xmltype;
  --- Return result.
  result xmltype;

  --- Column value for XML1.
  cv1 clob;
  --- Column value for XML2.
  cv2 clob;
  --- Primary key XPath.
  pk_xpath varchar2(32767);
  --- Primary key XPath for XML1.
  pk_xpath1 varchar2(4000);
  --- Primary key XPath for XML2.
  pk_xpath2 varchar2(4000);
  --- Primary key value.
  pkv varchar2(4000);
  --- Column name.
  cname varchar2(32767);

  -- XPath.
  xpath varchar2(4000);

  --- Difference document.
  diff clob;
  --- XML update fragment.
  xmlu clob;

  procedure diff_nodes;

/**
 * Parses the level 2 elements in given XML document.
 * @param   xml   XML document.
 * @return  Collection of level 2 elements.
 */

  function get_level2_elements(
    xml in xmltype
  )
  return level2_elements_c as

    level2_elements level2_elements_c;

  begin
    select name,
           count(*)
    bulk collect into level2_elements
    from   (
             select x.getrootelement() name
             from   table(xmlsequence(extract(xml, '/*/*'))) x
           )
    group  by name;

    return level2_elements;
  end get_level2_elements;

/**
 * Finds the number of leading elements it takes to form a unique lookup, ie the
 * minimum number of elements that are required for a primary key.
 * @param   xml     XML document.
 * @param   level2_elements
 *                  Collection of level 2 elements.
 * @return  Number of elements in primary key for this document.
 */

  function get_pk_element_count(
    xml in xmltype,
    level2_elements in level2_elements_c
  )
  return pls_integer as

    j pls_integer;
    n pls_integer := 1;
    m pls_integer;
    xpath varchar2(32767);
    pk_count pls_integer;
    done boolean := false;

  begin
    for i in 1 .. level2_elements(1).occurs loop
      -- For each element in the collection.
      j := 1;
      xpath := '/' || element1_name || '/' || element2_name || '[';
      for pk in (
            select value(x) element
            from   table(
                     xmlsequence(
                       extract(xml, '/' || element1_name || '/' ||
                           element2_name || '[' || i || ']/*'
                       )
                     )
                   ) x
          ) loop
        -- For each element for the ith element of the collection.
        pk_elements(j).name := pk.element.getrootelement;
        pk_elements(j).value := pk.element.extract('//text()').getstringval;

        if j &gt; 1 then
          xpath := rtrim(xpath, ']') || ' and ';
        end if;
        xpath := xpath || pk_elements(j).name || '="' ||
            pk_elements(j).value || '"';
        xpath := xpath || ']';

        if j = n then
          select count(*)
          into   pk_count
          from   table(xmlsequence(extract(xml, xpath)));

          done := pk_count = 1 or j = 32;
          exit when done;

          n := n + 1;
        end if;

        j := j + 1;
      end loop;
    end loop;

    return n;
  end get_pk_element_count;

/**
 * Appends a fragment to the differences XML document.
 */

  procedure append_fragment(
    fragment in clob
  ) as

  begin
    dbms_lob.append(diff, fragment);
  end append_fragment;

/**
 * Check that each fragment from first document identified through each primary
 * key can be found in the second document.
 * @param   operation
 *                  Operation to use for difference fragment, ie 'DELETE' if
 *                  it's missing or 'INSERT' if it has been inserted.
 * @param   xml1    First XML document.
 * @param   xml2    Second XML document.
 */

  procedure check_existence(
    operation in varchar2,
    xml1 in xmltype,
    xml2 in xmltype
  ) as

    relative_xpath varchar2(32767);
    pk_exists boolean;
    value_id pls_integer;

  begin
    for collection_elements in (
          select value(x) node
          from   table(
                   xmlsequence(
                     extract(
                       xml1, '/' || element1_name || '/' || element2_name
                     )
                   )
                 ) x
        ) loop
      relative_xpath := '/' || element1_name || '/' || element2_name || '[';
      for i in 1 .. pk_element_count loop
        if i &gt; 1 then
          relative_xpath := relative_xpath || ' and ';
        end if;
        relative_xpath := relative_xpath || pk_elements(i).name || '="' ||
            collection_elements.node.extract(
              '/*/*[' || i || ']/text()'
            ).getstringval || '"';
      end loop;
      relative_xpath := relative_xpath || ']';

      pk_exists := pkvs.exists(relative_xpath);
      if pk_exists or xml2.existsnode(relative_xpath) = 1 then
        pkvs(relative_xpath) := true;
      else
        dbms_lob.createtemporary(xmlu, true, dbms_lob.call);
        dbms_lob.append(
          xmlu,
          '&lt;DIFFERENCE&gt;' ||
            '&lt;XPATH&gt;' || xmlcdata2(relative_xpath) || '&lt;/XPATH&gt;' ||
            '&lt;OPERATION&gt;' || operation || '&lt;/OPERATION&gt;'
        );
        if operation = 'INSERT' then
          value_id := 2;
        elsif operation = 'DELETE' then
          value_id := 1;
        end if;
        dbms_lob.append(xmlu, '&lt;VALUE' || value_id || '&gt;');
        dbms_lob.append(
          xmlu,
          xmlcdata2(rtrim(xml1.extract(relative_xpath).getclobval, chr(10)))
        );
        dbms_lob.append(xmlu, '&lt;/VALUE' || value_id || '&gt;');
        dbms_lob.append(xmlu, '&lt;/DIFFERENCE&gt;');
        append_fragment(xmlu);
      end if;
    end loop;
  end check_existence;

/**
 * Diff the fragments found for the primary key values common between XML1 and
 * XML2. This is done by calling Oracle's XMLDIFF function.
 */

  procedure diff_common_pks as

  begin
    pkv := pkvs.first;
    while pkv is not null loop
      xpath := pkv;
      -- Note the extra GETCLOBVAL and XMLTYPE. Seem that Oracle XMLTYPE has a
      -- bug here as XML1E = XML1 if not done!
      xml1e := xmltype(xml1.extract(xpath).getclobval);
      xml2e := xmltype(xml2.extract(xpath).getclobval);

      select xmldiff(xml1e, xml2e)
      into   diff2
      from   dual;

      diff_nodes;

      pkv := pkvs.next(pkv);
    end loop;
  end diff_common_pks;

/**
 * Inspect the diff result from Oracle's XMLDIFF and append to our difference
 * document.
 */

  procedure diff_nodes as

    node_type varchar2(100);
    operation varchar2(6);
    diff_type varchar2(18);
    xpath xmltype;
    content xmltype;

  begin
    for cv in (
          select value(x) v
          from   table(xmlsequence(extract(diff2, '/xd:xdiff/*', xmldiff_ns))) x
        ) loop
      -- For each difference found.
      xpath := cv.v.extract('//@xd:xpath', xmldiff_ns);
      if xpath is null then
        xpath := cv.v.extract('//@xd:parent-xpath', xmldiff_ns);
      end if;
      cname := xpath.getstringval;
      cname := substr(cname, instr(cname, '/', 2) + 1);
      cname := substr(cname, 1, instr(cname, '[') - 1);
      node_type := cv.v.extract('//@xd:node-type', xmldiff_ns).getstringval;

      diff_type := cv.v.getrootelement;
      if diff_type like '%delete-node' then
        -- Node was deleted. Was it an element value or an element?
        cv1 := xml1e.extract('//' || cname || '/text()').getclobval;
        cv2 := '';
        if node_type = 'text' then
          -- Was non-NULL but is now NULL.
          operation := 'UPDATE';
        else
          -- Node has been removed.
          operation := 'DELETE';
        end if;
      elsif diff_type like '%append-node' then
        -- Node has been added. Was it an element value or an element?
        cv1 := '';
        cv2 := xml2e.extract('//' || cname || '/text()').getclobval;
        if node_type = 'text' then
          operation := 'UPDATE';
        else
          -- Node has been inserted.
          operation := 'INSERT';
        end if;
      elsif diff_type like '%insert-node-before' then
        -- Node has been added, find which.
        content := cv.v.extract('/*/xd:content/*', xmldiff_ns);
        cname := content.getrootelement;
        cv1 := '';
        cv2 := content.extract('//text()').getclobval;
        operation := 'INSERT';
      elsif diff_type like '%update-node' then
        -- Node has been updated.
        cv1 := xml1e.extract('//' || cname || '/text()').getclobval;
        cv2 := cv.v.extract('//xd:content/text()', xmldiff_ns).getclobval;
        operation := 'UPDATE';
      end if;

      dbms_lob.createtemporary(xmlu, false, dbms_lob.call);
      dbms_lob.append(
        xmlu,
        '&lt;DIFFERENCE&gt;' ||
          '&lt;XPATH&gt;' || xmlcdata2(pkv || '/' || cname) || '&lt;/XPATH&gt;' ||
          '&lt;OPERATION&gt;' || operation || '&lt;/OPERATION&gt;'
      );

      if operation in ('DELETE', 'UPDATE') then
        dbms_lob.append(xmlu, '&lt;VALUE1&gt;');
        dbms_lob.append(xmlu, xmlcdata2(cv1));
        dbms_lob.append(xmlu, '&lt;/VALUE1&gt;');
      end if;
      if operation in ('INSERT', 'UPDATE') then
        dbms_lob.append(xmlu, '&lt;VALUE2&gt;');
        dbms_lob.append(xmlu, xmlcdata2(cv2));
        dbms_lob.append(xmlu, '&lt;/VALUE2&gt;');
      end if;

      dbms_lob.append(xmlu, '&lt;/DIFFERENCE&gt;');
      append_fragment(xmlu);
    end loop;
  end diff_nodes;

begin
  pk_xpath1 := substr(pk_xpath, 1, instr(pk_xpath, '/', -1) - 1);
  pk_xpath2 := substr(pk_xpath, length(pk_xpath1) + 2);

  dbms_lob.createtemporary(diff, true, dbms_lob.call);
  dbms_lob.append(diff, '&lt;DIFFERENCES&gt;');
  -- TODO: NULL handling.

  level2_elements1 := get_level2_elements(xml1);
  level2_elements2 := get_level2_elements(xml2);

  -- Find minimum number of leading elements that form PK values.
  pk_element_count := greatest(
    get_pk_element_count(xml1, level2_elements1),
    get_pk_element_count(xml2, level2_elements2)
  );

  -- Look for PKs in XML1 not present in XML2: DELETE fragment.
  check_existence('DELETE', xml1, xml2);

  -- Look for PKs in XML2 not present in XML1: INSERT fragment.
  check_existence('INSERT', xml2, xml1);

  -- Diff all common PKs: UPDATE fragment.
  diff_common_pks;

  dbms_lob.append(diff, '&lt;/DIFFERENCES&gt;');

  if length(diff) = 13 + 14 and diff = '&lt;DIFFERENCES&gt;&lt;/DIFFERENCES&gt;' then
    -- No differences.
    result := null;
  else
    result := xmltype(diff);
  end if;

  return result;
end xmldiff2;
/</pre>
<h1>Examples</h1>
<p>Here are a few examples on how to use <code>XMLDIFF2</code>.</p>
<p>The first example is the example used for comparison of the various alternatives described earlier:</p>
<pre>declare
  xml1 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;LOC&gt;DALLAS&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;LOC&gt;CHICAGO&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
    &lt;LOC&gt;BOSTON&lt;/LOC&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  xml2 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;LOC&gt;DALLAS&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;LOC&gt;CHICAGO&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;XOPERATIONSETC&lt;/DNAME&gt;
    &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW&gt;
    &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;LOC&gt;ALBANY&lt;/LOC&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  diff xmltype;
begin
  diff := xmldiff2(xml1, xml2);
  dbms_output.put_line(diff.getclobval(0, 2));
end;
/</pre>
<p>which results in:</p>
<pre>&lt;DIFFERENCES&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[<span style="text-decoration:underline;">DEPTNO="10"</span>]]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;<span style="text-decoration:underline;">DELETE</span>&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">&lt;ROW&gt;</span>
<span style="text-decoration:underline;">  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;</span>
<span style="text-decoration:underline;">  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;</span>
<span style="text-decoration:underline;">  &lt;LOC&gt;NEW YORK&lt;/LOC&gt;</span>
<span style="text-decoration:underline;">&lt;/ROW&gt;</span>]]&gt;&lt;/VALUE1&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[<span style="text-decoration:underline;">DEPTNO="50"</span>]]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;<span style="text-decoration:underline;">INSERT</span>&lt;/OPERATION&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">&lt;ROW&gt;</span>
<span style="text-decoration:underline;">  &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;</span>
<span style="text-decoration:underline;">  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;</span>
<span style="text-decoration:underline;">  &lt;LOC&gt;ALBANY&lt;/LOC&gt;</span>
<span style="text-decoration:underline;">&lt;/ROW&gt;</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[<span style="text-decoration:underline;">DEPTNO="40"]/DNAME</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">OPERATIONS</span>]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">XOPERATIONSETC</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[<span style="text-decoration:underline;">DEPTNO="40"]/LOC</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">BOSTON</span>]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">NEW YORK</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
&lt;/DIFFERENCES&gt;</pre>
<p>Another example, this time with just one row in the resultset:</p>
<pre>declare
  xml1 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
    &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
    &lt;JOB&gt;<span style="text-decoration:underline;">CLERK&lt;</span>/JOB&gt;
    &lt;MGR&gt;7782&lt;/MGR&gt;
    &lt;HIREDATE&gt;1982-01-23T00:00:00&lt;/HIREDATE&gt;
    &lt;SAL&gt;<span style="text-decoration:underline;">1300</span>&lt;/SAL&gt;
    <span style="text-decoration:underline;">&lt;COMM/&gt;</span>
    &lt;DEPTNO&gt;<span style="text-decoration:underline;">10</span>&lt;/DEPTNO&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  xml2 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
    &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
    &lt;JOB&gt;<span style="text-decoration:underline;">ANALYST</span>&lt;/JOB&gt;
    &lt;MGR&gt;7782&lt;/MGR&gt;
    &lt;HIREDATE&gt;1982-01-23T00:00:00&lt;/HIREDATE&gt;
    &lt;SAL&gt;<span style="text-decoration:underline;">1500</span>&lt;/SAL&gt;
    &lt;COMM&gt;<span style="text-decoration:underline;">100</span>&lt;/COMM&gt;
    &lt;DEPTNO&gt;<span style="text-decoration:underline;">20</span>&lt;/DEPTNO&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  diff xmltype;
begin
  diff := xmldiff2(xml1, xml2);
  dbms_output.put_line(diff.getclobval(0, 2));
end;
/</pre>
<p>which results in the following:</p>
<pre>&lt;DIFFERENCES&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">JOB</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">CLERK</span>]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">ANALYST</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">SAL</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">1300</span>]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">1500</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">COMM</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">100</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">DEPTNO</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;UPDATE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[<span style="text-decoration:underline;">10</span>]]&gt;&lt;/VALUE1&gt;
    &lt;VALUE2&gt;&lt;![CDATA[<span style="text-decoration:underline;">20</span>]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
&lt;/DIFFERENCES&gt;</pre>
<p>And a final example, this time an element that is removed and an element that is added:</p>
<pre>declare
  xml1 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
    &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
    &lt;JOB&gt;CLERK&lt;/JOB&gt;
    &lt;MGR&gt;7782&lt;/MGR&gt;
    &lt;HIREDATE&gt;1982-01-23T00:00:00&lt;/HIREDATE&gt;
    <span style="text-decoration:underline;">&lt;REMOVED&gt;abc&lt;/REMOVED&gt;</span>
    &lt;SAL&gt;1300&lt;/SAL&gt;
    &lt;COMM/&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  xml2 xmltype := xmltype('
&lt;ROWSET&gt;
  &lt;ROW&gt;
    &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
    &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
    &lt;JOB&gt;CLERK&lt;/JOB&gt;
    &lt;MGR&gt;7782&lt;/MGR&gt;
    <span style="text-decoration:underline;">&lt;ADDED&gt;xyz&lt;/ADDED&gt;</span>
    &lt;HIREDATE&gt;1982-01-23T00:00:00&lt;/HIREDATE&gt;
    &lt;SAL&gt;1300&lt;/SAL&gt;
    &lt;COMM/&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;');
  diff xmltype;
begin
  diff := xmldiff2(xml1, xml2);
  dbms_output.put_line(diff.getclobval(0, 2));
end;
/</pre>
<p>which gives the following result:</p>
<pre>&lt;DIFFERENCES&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">ADDED</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;INSERT&lt;/OPERATION&gt;
    &lt;VALUE2&gt;&lt;![CDATA[xyz]]&gt;&lt;/VALUE2&gt;
  &lt;/DIFFERENCE&gt;
  &lt;DIFFERENCE&gt;
    &lt;XPATH&gt;&lt;![CDATA[/ROWSET/ROW[EMPNO="7934"]/<span style="text-decoration:underline;">REMOVED</span>]]&gt;&lt;/XPATH&gt;
    &lt;OPERATION&gt;DELETE&lt;/OPERATION&gt;
    &lt;VALUE1&gt;&lt;![CDATA[abc]]&gt;&lt;/VALUE1&gt;
  &lt;/DIFFERENCE&gt;
&lt;/DIFFERENCES&gt;</pre>
<h1>Conclusion</h1>
<p>I&#8217;ve described different options for comparing XML documents in the Oracle database and implemented an enhanced version of Oracle&#8217;s <code>XMLDIFF</code> that can be used to compare SQL datasets for relational tables not using object types, when represented on Oracle canonical form.</p>
<h1>References</h1>
<ul>
<li>Altova DiffDog: <a href="http://www.altova.com/diffdog" target="_blank">http://www.altova.com/diffdog</a>.</li>
<li>PL/SQL XML Compare Utility: <a href="http://sourceforge.net/projects/plsql-xml-diff/" target="_blank">http://sourceforge.net/projects/plsql-xml-diff/</a>.</li>
<li>Quest Code Tester for Oracle: <a href="http://www.quest.com/code-tester-for-oracle/" target="_blank">http://www.quest.com/code-tester-for-oracle/</a>.</li>
<li>XMLUnit: <a href="http://xmlunit.sourceforge.net/" target="_blank">http://xmlunit.sourceforge.net/</a>.</li>
</ul>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/74/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/74/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/74/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=74&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2011/02/01/comparing-xml-in-oracle/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
		<item>
		<title>Converting Between Oracle Data and XML</title>
		<link>http://ellebaek.wordpress.com/2011/01/27/converting-between-oracle-data-and-xml/</link>
		<comments>http://ellebaek.wordpress.com/2011/01/27/converting-between-oracle-data-and-xml/#comments</comments>
		<pubDate>Thu, 27 Jan 2011 00:17:42 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[Oracle SQL]]></category>
		<category><![CDATA[oracle]]></category>
		<category><![CDATA[xml]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=53</guid>
		<description><![CDATA[Introduction This blog post describes various mechanisms for converting between Oracle data and XML, and vice versa, focusing on the former. Many blog posts have been written on this subject. Most are very short and only demonstrate the usage but not the associated issues and I&#8217;ll try to dig a little deeper than most others [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=53&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<h1>Introduction</h1>
<p>This blog post describes various mechanisms for converting between Oracle data and XML, and vice versa, focusing on the former. Many blog posts have been written on this subject. Most are very short  and only demonstrate the usage but not the associated issues and I&#8217;ll try to dig a little deeper than most others and also mention the issues I&#8217;ve encountered or are aware of using these techniques.</p>
<p>I find combining Oracle data and XML useful for the following use cases:</p>
<ul>
<li>Extraction, Transformation and Load (ETL) for a datawarehouse or other types of transfers: Data is on a structured form with widespread tool (XML editors), programming language and standard support (eg XSLT, JAX, DOM etc) and is easier to transform in the process.</li>
<li>Easier to manipulate in a decent XML editor with expand/collapse (hiding irrelevant data sets) and XPath functionality (used for extracting fragments of the XML document).</li>
<li>Comparison: Comparing data sets for equality, eg in automated unit or regression testing. It&#8217;s easier to write a good, generic XML differ than a good, generic SQL result set differ. My next blog post will cover this.</li>
</ul>
<p>The examples in this blog post assume that you are logged on as <code>SCOTT</code> and that you work in a SQL*Plus environment with the following settings and data in <code>DEPT</code> and <code>EMP</code> tables intact from installation:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
set serveroutput on format truncated
set long 100000
set pagesize 50000
set linesize 1000

alter session set nls_date_format = 'dd.mm.yyyy hh24:mi:ss';
alter session set nls_timestamp_format = 'dd.mm.yyyy hh24:mi:ss.ff';
alter session set nls_timestamp_tz_format = 'dd.mm.yyyy hh24:mi:ss.ff tzh:tzm';
</pre></p>
<h1>Oracle Functionality</h1>
<p>In the following sections I&#8217;ll give an overview of the functionality Oracle provides in terms of converting between Oracle data and XML. Most of this functionality works on what Oracle calls canonical form, which is XML with the following structure (the comments <code>&lt;!-- ... --&gt;</code> are only included for illustration):<br />
<pre class="brush: plain; collapse: false; pad-line-numbers: false;">
&lt;ROWSET&gt;
 &lt;ROW&gt;&lt;!-- Row 1 --&gt;
  &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
  &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
  &lt;!-- ... --&gt;
  &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;&lt;!-- Row 2 --&gt;
 &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
  &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
  &lt;!-- ... --&gt;
  &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 &lt;/ROW&gt;
 &lt;!-- ... --&gt;
 &lt;ROW&gt;&lt;!-- Row n --&gt;
  &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
  &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
  &lt;!-- ... --&gt;
  &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
So basically there&#8217;s an XML element for each column and the name of the element corresponds to the column name. There&#8217;s a <code>&lt;ROW&gt;</code> fragment for each row in the query and the root element is called <code>&lt;ROWSET&gt;</code> (all XML documents must have at most one root element).</p>
<p>In this blog post I&#8217;ll focus on <code>DBMS_XMLGEN</code> and <code>XMLTYPE</code>.</p>
<h2>DBMS_XMLQUERY</h2>
<p><code>DBMS_XMLQUERY</code> provides functionality for obtaining XML data based on a dynamic SQL <code>SELECT</code> statement, optionally using bind parameters (all of datatype <code>VARCHAR2</code>). It&#8217;s possible to control names of tags generated and you can also control the date/time format (same format for all) but unfortunately ownly down to milliseconds as internally, Java&#8217;s <code>java.text.SimpleDateFormat</code> is used, which doesn&#8217;t support higher precision than milliseconds. Also, it&#8217;s inconvenient that it&#8217;s not possible to have empty elements with no attributes for <code>NULL</code> values, only either no element or an empty element with an attribute <code>NULL="YES"</code>. XML is returned as <code>CLOB</code>.</p>
<p>There are two ways of using <code>DBMS_XMLQUERY</code>:</p>
<ul>
<li>Quick and easy in one call, but less flexible: <code>GETXML</code> that works on a <code>SQLQUERY</code> parameter of type <code>VARCHAR2</code> or <code>CLOB</code> and returning a <code>CLOB</code>.</li>
<li>Fully flexible by a series of calls, all working on a context handle:
<ul>
<li><code>NEWCONTEXT</code> called with <code>SQLQUERY</code> parameter of type <code>VARCHAR2</code> or <code>CLOB</code> and returning a context handle of type <code>CTXTYPE</code>. This handle must be used in subsequent calls.</li>
<li>One or more calls that set various options controlling the XML generation (the <code>SET%</code> procedures), such as <code>NULL</code> value handling, number of rows to process, bind variable values, tag names, etc.</li>
<li><code>CLOSECONTEXT</code> called with the context handle, which closes the context and frees resources.</li>
</ul>
</li>
</ul>
<p><code>DBMS_XMLQUERY</code> was introduced with Oracle 8<em>i</em> Release 3 and it has been superseded by <code>DBMS_XMLGEN</code>. <code>DBMS_XMLQUERY</code> is implemented in Java and hence is not supported with Oracle Database Express Edition.</p>
<h2>DBMS_XMLGEN</h2>
<p><code>DBMS_XMLGEN</code> was introduced with Oracle Database 9<em>i</em> Release 1. It supersedes <code>DBMS_XMLQUERY</code>, is implemented in C (hence faster) and is a little more flexible than <code>DBMS_XMLQUERY</code> in some ways so in general you should use <code>DBMS_XMLGEN</code> over <code>DBMS_XMLQUERY</code>. Bind variable support (all of datatype <code>VARCHAR2</code>), XSLT support and pretty printing was introduced in Oracle Database 10<em>g</em> Release 1.</p>
<p>In general, <code>DBMS_XMLGEN</code> mimics the functionality in <code>DBMS_XMLQUERY</code> with subtle differences. One of these is that it&#8217;s not possible to pass in a query larger than 32KB, but contrary to <code>DBMS_XMLQUERY</code> you can also pass in a <code>SYS_REFCURSOR</code>, so you can use the <code>OPEN-FOR</code> statement that from Oracle Database 11<em>g</em> Release 1 supports <code>CLOB</code>. It&#8217;s also possible to obtain the XML as an <code>XMLTYPE</code>, not only a <code>CLOB</code>.</p>
<p>Here are a few examples:</p>
<p>Selecting all rows and columns from <code>SCOTT.DEPT</code> will produce the  following result:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
select dbms_xmlgen.getxml('select * from dept order by deptno') xml
from   dual;

XML
---------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
  &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
  &lt;LOC&gt;DALLAS&lt;/LOC&gt;
 &lt;/ROW&gt;
 &lt;!-- Abbreviated... --&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
  &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
  &lt;LOC&gt;BOSTON&lt;/LOC&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
Selecting all rows and columns from <code>SCOTT.EMP</code> will produce the following result:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
select dbms_xmlgen.getxml('select * from emp order by empno') xml
from   dual;

XML
--------------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;EMPNO&gt;7369&lt;/EMPNO&gt;
  &lt;ENAME&gt;SMITH&lt;/ENAME&gt;
  &lt;JOB&gt;CLERK&lt;/JOB&gt;
  &lt;MGR&gt;7902&lt;/MGR&gt;
  &lt;HIREDATE&gt;17.12.1980 00:00:00&lt;/HIREDATE&gt;
  &lt;SAL&gt;800&lt;/SAL&gt;
  &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;EMPNO&gt;7499&lt;/EMPNO&gt;
  &lt;ENAME&gt;ALLEN&lt;/ENAME&gt;
  &lt;JOB&gt;SALESMAN&lt;/JOB&gt;
  &lt;MGR&gt;7698&lt;/MGR&gt;
  &lt;HIREDATE&gt;20.02.1981 00:00:00&lt;/HIREDATE&gt;
  &lt;SAL&gt;1600&lt;/SAL&gt;
  &lt;COMM&gt;300&lt;/COMM&gt;
  &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
 &lt;/ROW&gt;
 &lt;!-- Abbreviated... --&gt;
 &lt;ROW&gt;
  &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
  &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
  &lt;JOB&gt;CLERK&lt;/JOB&gt;
  &lt;MGR&gt;7782&lt;/MGR&gt;
  &lt;HIREDATE&gt;23.01.1982 00:00:00&lt;/HIREDATE&gt;
  &lt;SAL&gt;1300&lt;/SAL&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
It&#8217;s noted how the date values match the session <code>NLS_DATE_FORMAT 'dd.mm.yyyy hh24:mi:ss'</code>. It is also noted that no elements are returned for column with <code>NULL</code> values, eg for <code>COMM</code> for <code>EMPNO = 7934</code>. If you prefer to always have an element, either with or without an attribute indicating <code>NULL</code>, you can control this with the following type of code, also demonstrating how to work with the context handle:<br />
<pre class="brush: sql; collapse: false; highlight: [7,26]; pad-line-numbers: false;">
variable xml clob

declare
  context dbms_xmlgen.ctxtype;
begin
  context := dbms_xmlgen.newcontext('select * from emp where empno = 7934');
  dbms_xmlgen.setnullhandling(context, dbms_xmlgen.empty_tag);
  :xml := dbms_xmlgen.getxml(context);
  dbms_xmlgen.closecontext(context);
end;
/

print :xml

XML
------------------------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
  &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
  &lt;JOB&gt;CLERK&lt;/JOB&gt;
  &lt;MGR&gt;7782&lt;/MGR&gt;
  &lt;HIREDATE&gt;23.01.1982 00:00:00&lt;/HIREDATE&gt;
  &lt;SAL&gt;1300&lt;/SAL&gt;
  &lt;COMM/&gt;
  &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
Note how the empty element <code>&lt;COMM/&gt;</code> is now included in the XML. Another alternative is:<br />
<pre class="brush: sql; collapse: false; highlight: [7,19,27]; pad-line-numbers: false;">
variable xml clob

declare
  context dbms_xmlgen.ctxtype;
begin
  context := dbms_xmlgen.newcontext('select * from emp where empno = 7934');
  dbms_xmlgen.setnullhandling(context, dbms_xmlgen.null_attr);
  dbms_xmlgen.setindentationwidth(context, 2);
  :xml := dbms_xmlgen.getxml(context);
  dbms_xmlgen.closecontext(context);
end;
/

print :xml

XML
----------------------------------------------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET xmlns:xsi = &quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;
  &lt;ROW&gt;
    &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
    &lt;ENAME&gt;MILLER&lt;/ENAME&gt;
    &lt;JOB&gt;CLERK&lt;/JOB&gt;
    &lt;MGR&gt;7782&lt;/MGR&gt;
    &lt;HIREDATE&gt;23.01.1982 00:00:00&lt;/HIREDATE&gt;
    &lt;SAL&gt;1300&lt;/SAL&gt;
    &lt;COMM xsi:nil = &quot;true&quot;/&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
  &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
Note how the attribute xsi:nil is now included with a value of <code>"true"</code>. Seen that this uses a namespace <code>xsi</code> a namespace specification has been added to the root element <code>&lt;ROWSET&gt;</code>. Also note that we added a call to control the indentation width for each level, in this case a value of 2 instead of the default 1.</p>
<p>If you work with <code>LONG</code> columns it&#8217;s worth knowing that <code>DBMS_XMLGEN</code> truncates values of <code>LONG</code> columns to 32KB. For more information, please refer to Adrian Billington&#8217;s blog post mentioned at the end of this blog post. I&#8217;m also describing a slightly different technique for converting <code>LONG</code> to <code>CLOB</code> in my blog post <a href="/2010/12/06/converting-a-long-column-to-a-clob-on-the-fly" target="_blank">Converting a LONG Column to a CLOB on the fly</a>.</p>
<h2>XMLELEMENT, XMLFOREST, XMLAGG etc</h2>
<p>Oracle provides a collections of functions that you can use to construct the XML based on your relational (optionally object-relational) data. Here&#8217;s an example on a hierarchical XML construction with employees under departments:<br />
<pre class="brush: sql; collapse: true; highlight: [1,3,4,6,10,11,13,15]; light: false; pad-line-numbers: false; toolbar: true;">
select xmlelement(
         &quot;DEPARTMENTS&quot;,
         xmlagg(
           xmlelement(
             &quot;DEPARTMENT&quot;,
             xmlforest(
               d.deptno,
               d.dname,
               (
                 select xmlagg(
                          xmlelement(
                            &quot;EMPLOYEE&quot;,
                             xmlforest(
                               e.empno,
                               xmlcdata(e.ename) ename,
                               e.hiredate
                             )
                           )
                        )
                 from   emp e
                 where  e.deptno = d.deptno and
                        e.empno in (7369, 7499, 7934)
               ) employees
             )
           )
         )
       ) x
from   dept d;

(output wrapped and indented for improved readability)

&lt;DEPARTMENTS&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[MILLER]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1982-01-23&lt;/HIREDATE&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7369&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[SMITH]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1980-12-17&lt;/HIREDATE&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7499&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[ALLEN]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1981-02-20&lt;/HIREDATE&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
  &lt;/DEPARTMENT&gt;
&lt;/DEPARTMENTS&gt;
</pre><br />
If you prefer empty tags for <code>NULL</code> values, you can take this approach instead, writing a little more code (it would have been nice if <code>XMLFOREST</code>/<code>XMLELEMENT</code> etc would take options for handling <code>NULL</code> values):<br />
<pre class="brush: sql; collapse: true; highlight: [13,14,15,16,40,52]; light: false; pad-line-numbers: false; toolbar: true;">
select xmlelement(
         &quot;DEPARTMENTS&quot;,
         xmlagg(
           xmlelement(
             &quot;DEPARTMENT&quot;,
             xmlforest(
               d.deptno,
               d.dname,
               (
                 select xmlagg(
                          xmlelement(
                            &quot;EMPLOYEE&quot;,
                             xmlelement(&quot;EMPNO&quot;, e.empno),
                             xmlelement(&quot;ENAME&quot;, xmlcdata(e.ename)),
                             xmlelement(&quot;HIREDATE&quot;, e.hiredate),
                             xmlelement(&quot;COMM&quot;, e.comm)
                           )
                        )
                 from   emp e
                 where  e.deptno = d.deptno and
                        e.empno in (7369, 7499, 7934)
               ) employees
             )
           )
         )
       ).getclobval() x
from   dept d;

(output wrapped and indented for improved readability)

&lt;DEPARTMENTS&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
    &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7934&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[MILLER]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1982-01-23&lt;/HIREDATE&gt;
        &lt;COMM&gt;&lt;/COMM&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
    &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7369&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[SMITH]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1980-12-17&lt;/HIREDATE&gt;
        &lt;COMM&gt;&lt;/COMM&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
    &lt;DNAME&gt;SALES&lt;/DNAME&gt;
    &lt;EMPLOYEES&gt;
      &lt;EMPLOYEE&gt;
        &lt;EMPNO&gt;7499&lt;/EMPNO&gt;
        &lt;ENAME&gt;&lt;![CDATA[ALLEN]]&gt;&lt;/ENAME&gt;
        &lt;HIREDATE&gt;1981-02-20&lt;/HIREDATE&gt;
        &lt;COMM&gt;300&lt;/COMM&gt;
      &lt;/EMPLOYEE&gt;
    &lt;/EMPLOYEES&gt;
  &lt;/DEPARTMENT&gt;
  &lt;DEPARTMENT&gt;
    &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
    &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
  &lt;/DEPARTMENT&gt;
&lt;/DEPARTMENTS&gt;
</pre><br />
It&#8217;s worth noting that these functions generate XML with date/time values conforming to XML Schema standards, which is great. This was changed in Oracle Database 10<em>g</em> Release 2 and in earlier releases the NLS settings were used (not so great). In this example we enclosed the values of <code>ENAME</code> in a <code>CDATA</code> section (starting with <code>&lt;![CDATA[</code> and ending with <code>]]&gt;</code>), which means that Oracle doesn&#8217;t have to escape the special characters that could be found in the <code>ENAME</code> column. The Oracle function <code>XMLCDATA</code> is used for this, but it has several limitations: It doesn&#8217;t support values larger than 4000 bytes and it doesn&#8217;t handle embedded <code>]]&gt;</code> characters within the text (that would terminate the <code>CDATA</code> section), which is normally treated by chaining several <code>CDATA</code> sections for the element value, ie by replacing any occurrence of <code>']]&gt;'</code> with <code>']]]]&gt;&lt;![CDATA[&gt;'</code>.</p>
<p>Please be aware that <code>DATE</code> values with time parts are truncated to the date value only, which is very unfortunate:<br />
<pre class="brush: sql; collapse: false; highlight: [4,5,15,16]; pad-line-numbers: false;">
select xmlelement(
         &quot;DATETIME&quot;,
         xmlforest(
           trunc(sysdate) date_no_time,
           sysdate date_with_time,
           systimestamp timestamp_with_tz,
           localtimestamp timestamp_local_tz
         )
       ) xml
from   dual;

(output wrapped and indented for improved readability)

&lt;DATETIME&gt;
  &lt;DATE_NO_TIME&gt;2011-01-26&lt;/DATE_NO_TIME&gt;
  &lt;DATE_WITH_TIME&gt;2011-01-26&lt;/DATE_WITH_TIME&gt;
  &lt;TIMESTAMP_WITH_TZ&gt;2011-01-26T10:47:38.026000+01:00&lt;/TIMESTAMP_WITH_TZ&gt;
  &lt;TIMESTAMP_LOCAL_TZ&gt;2011-01-26T10:47:38.026000&lt;/TIMESTAMP_LOCAL_TZ&gt;
&lt;/DATETIME&gt;
</pre><br />
You can convert the dates to timestamps, but it's a little inconvenient and this means you get fractional seconds as well, even though they'll always be zero:<br />
<pre class="brush: sql; collapse: false; highlight: [5,16]; pad-line-numbers: false;">
select xmlelement(
         &quot;DATETIME&quot;,
         xmlforest(
           trunc(sysdate) date_no_time,
           cast(sysdate as timestamp) date_with_time,
           systimestamp timestamp_with_tz,
           localtimestamp timestamp_local_tz
         )
       ) xml
from   dual;

(output wrapped and indented for improved readability)

&lt;DATETIME&gt;
  &lt;DATE_NO_TIME&gt;2011-01-26&lt;/DATE_NO_TIME&gt;
  &lt;DATE_WITH_TIME&gt;2011-01-26T10:50:32.000000&lt;/DATE_WITH_TIME&gt;
  &lt;TIMESTAMP_WITH_TZ&gt;2011-01-26T10:47:38.026000+01:00&lt;/TIMESTAMP_WITH_TZ&gt;
  &lt;TIMESTAMP_LOCAL_TZ&gt;2011-01-26T10:47:38.026000&lt;/TIMESTAMP_LOCAL_TZ&gt;
&lt;/DATETIME&gt;
</pre><br />
Personally, I prefer <code>DBMS_XMLGEN</code> to these functions as you need to write more fiddly SQL to get it working (it would be nice if you could work on all columns with a <code>*</code> for instance, but instead you need to list all the columns), but I guess that's a matter of taste.</p>
<h2>XMLTYPE</h2>
<p>The <code>XMLTYPE</code> object type has various means of conversions, eg from any object type instance to corresponding XML (<code>CREATEXML</code> static function and constructor taking a parameter of type <code>"&lt;ADT_1&gt;"</code>) and vice versa, ie from the <code>XMLTYPE</code> instance to an object type instance (member procedure <code>TOOBJECT</code>).</p>
<p>Here's an example:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
select xmltype(sys.aq$_agent('name', 'address', 1)).getclobval() xml
from   dual;

XML
--------------------------------------------------------------------------------
&lt;AQ_x0024__AGENT&gt;&lt;NAME&gt;name&lt;/NAME&gt;&lt;ADDRESS&gt;address&lt;/ADDRESS&gt;&lt;PROTOCOL&gt;1&lt;/PROTOCO
L&gt;&lt;/AQ_x0024__AGENT&gt;
</pre><br />
This output is not that user-friendly as <code>GETCLOBVAL</code> by default has it's own idea on how and when to indent the elements. If you're on Oracle Database 11<em>g</em> Release 1 or newer, there's an enhanced version of <code>GETCLOBVAL</code> you can use:<br />
<pre class="brush: sql; collapse: false; pad-line-numbers: false;">
select xmltype(sys.aq$_agent('name', 'address', 1)).getclobval(0, 2) xml
from   dual;

XML
----------------------------
&lt;AQ_x0024__AGENT&gt;
  &lt;NAME&gt;name&lt;/NAME&gt;
  &lt;ADDRESS&gt;address&lt;/ADDRESS&gt;
  &lt;PROTOCOL&gt;1&lt;/PROTOCOL&gt;
&lt;/AQ_x0024__AGENT&gt;
</pre><br />
It may come as a surprise that the root tag isn't called &lt;<code>AQ$_AGENT&gt;</code> but <code>$</code> is replaced by <code>_x0024_</code> because <code>$</code> is an invalid character for XML element names.</p>
<h2>DBMS_XMLSAVE</h2>
<p><code>DBMS_XMLSAVE</code> can be used to <code>INSERT</code>, <code>UPDATE</code> and <code>DELETE</code> data in a table based on XML in Oracle canonical form. It works on a context handle that you obtain, set options, call either <code>INSERTXML</code>, <code>UPDATEXML</code> or <code>DELETEXML</code> and then close the context again.</p>
<p>Example:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
select *
from   dept
order  by deptno;

declare
  context dbms_xmlsave.ctxtype;
  row_count integer;
  xml_insert xmltype := xmltype('
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;50&lt;/DEPTNO&gt;
  &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
  &lt;LOC&gt;ALBANY&lt;/LOC&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;60&lt;/DEPTNO&gt;
  &lt;DNAME&gt;TO_BE_DELETED&lt;/DNAME&gt;
  &lt;LOC&gt;N/A&lt;/LOC&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
  ');
  xml_update xmltype := xmltype('
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
  &lt;DNAME&gt;XOPERATIONSETC&lt;/DNAME&gt;
  &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
  ');
  xml_delete xmltype := xmltype('
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;60&lt;/DEPTNO&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
  ');
begin
  -- Create new context for INSERT.
  context := dbms_xmlsave.newcontext('SCOTT.DEPT');
  -- Insert rows.
  row_count := dbms_xmlsave.insertxml(context, xml_insert.getclobval);
  -- Close the context.
  dbms_xmlsave.closecontext(context);
  dbms_output.put_line(row_count || ' row(s) inserted.');

  -- Create new context for UPDATE.
  context := dbms_xmlsave.newcontext('SCOTT.DEPT');
  -- Set primary key column for UPDATE to work.
  dbms_xmlsave.clearupdatecolumnlist(context);
  dbms_xmlsave.setkeycolumn(context, 'DEPTNO');
  -- UPDATE the document.
  row_count := dbms_xmlsave.updatexml(context, xml_update.getclobval);
  -- Close the context.
  dbms_xmlsave.closecontext(context);
  dbms_output.put_line(row_count || ' row(s) updated.');

  -- Create new context for DELETE.
  context := dbms_xmlsave.newcontext('SCOTT.DEPT');
  -- Set primary key column for DELETE to work.
  dbms_xmlsave.clearupdatecolumnlist(context);
  dbms_xmlsave.setkeycolumn(context, 'DEPTNO');
  -- DELETE the document.
  row_count := dbms_xmlsave.deletexml(context, xml_delete.getclobval);
  -- Close the context.
  dbms_xmlsave.closecontext(context);
  dbms_output.put_line(row_count || ' row(s) deleted.');
end;
/

select *
from   dept
order  by deptno;

rollback;
</pre><br />
Running the above would produce the following output:<br />
<pre class="brush: plain; collapse: true; highlight: [8,9,10,19,20]; light: false; pad-line-numbers: false; toolbar: true;">
    DEPTNO DNAME          LOC
---------- -------------- -------------
        10 ACCOUNTING     NEW YORK
        20 RESEARCH       DALLAS
        30 SALES          CHICAGO
        40 OPERATIONS     BOSTON

2 row(s) inserted.
1 row(s) updated.
1 row(s) deleted.

PL/SQL procedure successfully completed.

    DEPTNO DNAME          LOC
---------- -------------- -------------
        10 ACCOUNTING     NEW YORK
        20 RESEARCH       DALLAS
        30 SALES          CHICAGO
        40 XOPERATIONSETC NEW YORK
        50 ACCOUNTING     ALBANY

Rollback complete.
</pre><br />
Like <code>DBMS_XMLQUERY</code>, <code>DBMS_XMLSAVE</code> is implemented in Java and hence it's not supported with Oracle Database Express Edition.</p>
<h2>DBMS_XMLSTORE</h2>
<p><code>DBMS_XMLSTORE</code> was introduced with Oracle Database 10<em>g</em> Release 1 and should be used instead of <code>DBMS_XMLSAVE</code> in order to work around limitations in the latter, such as unsupported <code>TIMESTAMP</code> fractional seconds with precisions larger than 3 decimal places and also that the time part of <code>DATE</code> values is mandatory. <code>DBMS_XMLSTORE</code> is written in C and provides better performance than <code>DBMS_XMLSAVE</code>. <code>DBMS_XMLSTORE</code> uses NLS settings for date/time values so you must be careful to set these for XML Schema compliance as described elsewhere in this blog post.</p>
<p>Example: As the example for <code>DBMS_XMLSAVE</code>, simply replace <code>DBMS_XMLSAVE</code> with <code>DBMS_XMLSTORE</code>.</p>
<h2>DBMS_METADATA</h2>
<p><code>DBMS_METADATA</code> can be used to obtain DDL for your database objects. This can either be returned as DDL or XML. Example:<br />
<pre class="brush: xml; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
select dbms_metadata.get_xml('TABLE', 'DEPT') xml
from   dual;

(Output abbreviated for legibility)

DBMS_METADATA.GET_XML('TABLE','DEPT')
------------------------------------------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;TABLE_T&gt;
   &lt;VERS_MAJOR&gt;1&lt;/VERS_MAJOR&gt;
   &lt;VERS_MINOR&gt;3 &lt;/VERS_MINOR&gt;
   &lt;OBJ_NUM&gt;73194&lt;/OBJ_NUM&gt;
   &lt;SCHEMA_OBJ&gt;
    &lt;OBJ_NUM&gt;73194&lt;/OBJ_NUM&gt;
    &lt;DATAOBJ_NUM&gt;73194&lt;/DATAOBJ_NUM&gt;
    &lt;OWNER_NUM&gt;84&lt;/OWNER_NUM&gt;
    &lt;OWNER_NAME&gt;SCOTT&lt;/OWNER_NAME&gt;
    &lt;NAME&gt;DEPT&lt;/NAME&gt;
    &lt;NAMESPACE&gt;1&lt;/NAMESPACE&gt;
    &lt;!-- Snip --&gt;
   &lt;/SCHEMA_OBJ&gt;
   &lt;STORAGE&gt;
    &lt;FILE_NUM&gt;4&lt;/FILE_NUM&gt;
    &lt;BLOCK_NUM&gt;130&lt;/BLOCK_NUM&gt;
    &lt;TYPE_NUM&gt;5&lt;/TYPE_NUM&gt;
    &lt;TS_NUM&gt;4&lt;/TS_NUM&gt;
    &lt;BLOCKS&gt;8&lt;/BLOCKS&gt;
    &lt;EXTENTS&gt;1&lt;/EXTENTS&gt;
    &lt;INIEXTS&gt;8&lt;/INIEXTS&gt;
    &lt;MINEXTS&gt;1&lt;/MINEXTS&gt;
    &lt;MAXEXTS&gt;2147483645&lt;/MAXEXTS&gt;
    &lt;EXTSIZE&gt;128&lt;/EXTSIZE&gt;
    &lt;!-- Snip --&gt;
   &lt;/STORAGE&gt;
   &lt;COL_LIST&gt;
    &lt;COL_LIST_ITEM&gt;
     &lt;OBJ_NUM&gt;73194&lt;/OBJ_NUM&gt;
     &lt;COL_NUM&gt;1&lt;/COL_NUM&gt;
     &lt;INTCOL_NUM&gt;1&lt;/INTCOL_NUM&gt;
     &lt;SEGCOL_NUM&gt;1&lt;/SEGCOL_NUM&gt;
     &lt;PROPERTY&gt;0&lt;/PROPERTY&gt;
     &lt;NAME&gt;DEPTNO&lt;/NAME&gt;
     &lt;TYPE_NUM&gt;2&lt;/TYPE_NUM&gt;
     &lt;LENGTH&gt;22&lt;/LENGTH&gt;
     &lt;PRECISION_NUM&gt;2&lt;/PRECISION_NUM&gt;
     &lt;SCALE&gt;0&lt;/SCALE&gt;
     &lt;NOT_NULL&gt;1&lt;/NOT_NULL&gt;
     &lt;!-- Snip --&gt;
    &lt;/COL_LIST_ITEM&gt;
   &lt;/COL_LIST&gt;
  &lt;/TABLE_T&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre></p>
<h2>XSQL</h2>
<p>Oracle XSQL pages publishing framework is an extensible platform for publishing XML in multiple formats. It can also be used to call PL/SQL APIs, <code>INSERT</code>/<code>UPDATE</code>/<code>DELETE</code> data based on one (unfortunately only one) posted document. It has been available since Oracle Database 8<em>i</em> Release 3 and is accessible through either a Java Servlet hosted on your application server, the command line (xsql.bat batch file/xsql shell script) or your own Java program. XSQL is not available from inside the database, but you can load the <code>oraclexsql.jar</code> into the database with <code>loadjava</code> and write your own PL/SQL wrapper for it. Example of how to insert two rows into <code>DEPT</code> and query all the rows afterwards:<br />
Contents of dept.xsql:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
&lt;page connection=&quot;scott&quot; xmlns:xsql=&quot;urn:oracle-xsql&quot;&gt;
  &lt;xsql:insert-request table=&quot;dept&quot;/&gt;
  &lt;xsql:query&gt;
    select *
    from   dept
  &lt;/xsql:query&gt;
&lt;/page&gt;
</pre><br />
Contents of dept.xml:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;-1&lt;/DEPTNO&gt;
  &lt;DNAME&gt;Dept A&lt;/DNAME&gt;
  &lt;LOC&gt;Loc A&lt;/LOC&gt;
 &lt;/ROW&gt;
 &lt;ROW&gt;
  &lt;DEPTNO&gt;-2&lt;/DEPTNO&gt;
  &lt;DNAME&gt;Dept B&lt;/DNAME&gt;
  &lt;LOC&gt;Loc B&lt;/LOC&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
Contents of dept_xsql.bat (adapt this, potentially %ORACLE_HOME%\bin\xsql.bat and XSQLConfig.xml to your environment):<br />
<pre class="brush: plain; collapse: false; pad-line-numbers: false;">
@setlocal
rem %ORACLE_HOME%\xdk\admin\XSQLConfig.xml must be configured with a connection called &quot;scott&quot;.
rem Set all CP in C:\Oracle\O112\BIN\xsql.bat
set ORACLE_HOME=c:\oracle\o112
set JAVA_HOME=%ORACLE_HOME%\jdk
set CP=%ORACLE_HOME%\RDBMS\jlib\xdb.jar
xsql.bat dept.xsql posted-xml=dept.xml
@endlocal
</pre><br />
After invoking <code>dept_xsql.bat</code> the result is (output wrapped and indented for improved legibility):<br />
<pre class="brush: xml; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
&lt;page&gt;
 &lt;xsql-status action=&quot;xsql:insert-request&quot; rows=&quot;2&quot;/&gt;
 &lt;ROWSET&gt;
  &lt;ROW num=&quot;1&quot;&gt;
   &lt;DEPTNO&gt;-1&lt;/DEPTNO&gt;
   &lt;DNAME&gt;Dept A&lt;/DNAME&gt;
   &lt;LOC&gt;Loc A&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW num=&quot;2&quot;&gt;
   &lt;DEPTNO&gt;-2&lt;/DEPTNO&gt;
   &lt;DNAME&gt;Dept B&lt;/DNAME&gt;
   &lt;LOC&gt;Loc B&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW num=&quot;3&quot;&gt;
   &lt;DEPTNO&gt;10&lt;/DEPTNO&gt;
   &lt;DNAME&gt;ACCOUNTING&lt;/DNAME&gt;
   &lt;LOC&gt;NEW YORK&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW num=&quot;4&quot;&gt;
   &lt;DEPTNO&gt;20&lt;/DEPTNO&gt;
   &lt;DNAME&gt;RESEARCH&lt;/DNAME&gt;
   &lt;LOC&gt;DALLAS&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW num=&quot;5&quot;&gt;
   &lt;DEPTNO&gt;30&lt;/DEPTNO&gt;
   &lt;DNAME&gt;SALES&lt;/DNAME&gt;
   &lt;LOC&gt;CHICAGO&lt;/LOC&gt;
  &lt;/ROW&gt;
  &lt;ROW num=&quot;6&quot;&gt;
   &lt;DEPTNO&gt;40&lt;/DEPTNO&gt;
   &lt;DNAME&gt;OPERATIONS&lt;/DNAME&gt;
   &lt;LOC&gt;BOSTON&lt;/LOC&gt;
  &lt;/ROW&gt;
 &lt;/ROWSET&gt;
&lt;/page&gt;
</pre></p>
<h1>XML Schema Compliance</h1>
<p>In order to be compliant with XML Schema, all <code>DATE</code> and <code>TIMESTAMP</code> values must adhere to the following Oracle formats:</p>
<ul>
<li><code>DATE</code>: <code>'yyyy-mm-dd"T"hh24:mi:ss'</code> (for time part as well, note the <code>T</code> between date and time part) or <code>'yyyy-mm-dd'</code> without time part.</li>
<li><code>TIMESTAMP</code>: <code>'yyyy-mm-dd"T"hh24:mi:ss.ff9'</code> or <code>'yyyy-mm-dd"T"hh24:mi:ss.ff'</code>.</li>
<li><code>TIMESTAMP WITH [LOCAL] TIME ZONE</code>: <code>'yyyy-mm-dd"T"hh24:mi:ss.ff9tzh:tzm'</code> or <code>'yyyy-mm-dd"T"hh24:mi:ss.fftzh:tzm'</code>.</li>
</ul>
<p>Unfortunately, Oracle's <code>DBMS_XMLGEN</code> and <code>XMLTYPE</code> (plus <code>XMLELEMENT</code>, <code>XMLFOREST</code> etc prior to Oracle Database 10<em>g</em> Release 2) do not adhere to the XML Schema standard and instead by default they generate the values according to the current NLS settings. Your session's <code>NLS_DATE_FORMAT</code>, <code>NLS_TIMESTAMP_FORMAT</code> and <code>NLS_TIMESTAMP_TZ_FORMAT</code> must be set to one of the values mentioned above in order to work around this.</p>
<h1>XML Character Entities</h1>
<p>The default behavior is that characters that have a special meaning in XML are converted to their corresponding XML character entities as it would otherwise be invalid XML. Such characters are <code>&lt;</code>, <code>&gt;</code>, <code>"</code> and <code>'</code>, which are converted to <code>&amp;lt;</code>, <code>&amp;gt;</code>, <code>&amp;quot;</code> and <code>&amp;apos;</code> respectively:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
select dbms_xmlgen.getxml(q'[select '&lt;&gt;&quot;''' test from dual]') xml
from   dual;

XML
-----------------------------------
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;ROWSET&gt;
 &lt;ROW&gt;
  &lt;TEST&gt;&amp;lt;&amp;gt;&amp;quot;&amp;apos;&lt;/TEST&gt;
 &lt;/ROW&gt;
&lt;/ROWSET&gt;
</pre><br />
By enclosing the text in CDATA sections you avoid this required escaping as previously mentioned.</p>
<h1>Encoding</h1>
<p>Encoding of an XML document can be specified in the document prolog, such as:<br />
<pre class="brush: xml; collapse: false; pad-line-numbers: false;">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;
&lt;blogs&gt;
  &lt;blog&gt;
    &lt;url&gt;ellebaek.wordpress.com&lt;/url&gt;
    &lt;author&gt;Finn Ellebæk Nielsen&lt;/author&gt;
  &lt;/blog&gt;
&lt;/blogs&gt;
</pre><br />
If you omit the encoding, <code>UTF-8</code> is assumed by default by most XML parsers.</p>
<h1>Code</h1>
<p>I've written a PL/SQL package in order to demonstrate how to generate XML based on Oracle data, using <code>DBMS_XMLGEN</code>, <code>XMLTYPE</code> and a few tricks. My primary goal was to achieve the the following functionality:</p>
<ul>
<li>Get XML from dynamic SQL query with optional support for bind variables.</li>
<li>Get XML from open <code>REF CURSOR</code>.</li>
<li>Get XML from object type instance.</li>
<li>Get XML from collection.</li>
<li>Get XML from <code>ANYDATA</code> instance.</li>
<li>Get XML from <code>ANYDATASET</code> instance.</li>
<li>Get XML from <code>ANYTYPE</code> instance.</li>
</ul>
<p>The code has been tested with the following releases and editions (all 32-bit versions under Windows XP SP3):</p>
<ul>
<li>Oracle Database 10<em>g</em> Release 1 10.1.0.5.0 Personal Edition.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.4.0 Personal Edition.</li>
<li>Oracle Database 10<em>g</em> Release 2 10.2.0.1.0 Express Edition.</li>
<li>Oracle Database 1<em>1g</em> Release 1 11.1.0.6.0 Personal Edition.</li>
<li>Oracle Database 11<em>g</em> Release 2 11.2.0.1.0 Personal Edition.</li>
</ul>
<p>The first version of this package had the following issues:</p>
<ol>
<li>It used packaged types for the bind variable collection. This is fine if you use PL/SQL to call the function <code>FROM_SELECT</code> with a bind variable collection. However, it wasn't possible to call this function directly from a PL/SQL expression or from SQL, so I changed the implementation to use two object types instead.</li>
<li><code>FROM_REF_CURSOR</code> didn't generate date values in the XML Schema compliant format. Instead, date values were generated according to the <code>NLS_DATE_FORMAT</code> active when the <code>REF CURSOR</code> was opened, at least on some Oracle versions on some platforms and sometimes only for the first row. This was attempted fixed by calling <code>DBMS_XMLGEN.RESTARTQUERY</code> after having changed the NLS settings but this also proved unstable.</li>
<li>Changing the NLS settings on the fly has the following side effect: Any call to <code>TO_CHAR</code>/<code>TO_DATE</code>/<code>TO_TIMESTAMP</code> etc in the incoming query/<code>REF CURSOR</code> (including PL/SQL functions called) without a specific date/time format will change semantics as the result will adhere to the XML Schema compliant formats (for dates, truncated to the least of the lengths of the original <code>NLS_DATE_FORMAT</code> and the XML compliant <code>NLS_DATE_FORMAT</code>), instead of the original NLS setting in place when calling <code>XML_GEN</code>. This may seem critical, however it's considered poor practice to call these functions without a specific format.</li>
</ol>
<p>Anyway, in order to work around the issues with XML Schema-compliant date/time values I decided to fix this for the third updated version of the code. My idea was to implement some code that would do the following:</p>
<ul>
<li>Fetch the dynamic SQL/<code>REF CURSOR</code> into memory or a table.</li>
<li>Change the NLS settings for XML Schema-compliance.</li>
<li>Open a new <code>REF CURSOR</code> on the copy in memory/table.</li>
<li>Call <code>DBMS_XMLGEN</code> on the new <code>REF CURSOR</code>.</li>
<li>Change the NLS settings back to the previous session NLS.</li>
</ul>
<p>This way we would have no side effects as the NLS would be changed after the original data had been fetched. However, this proved to be a major task to implement and in fact I've written two separate blog posts about it:</p>
<ul>
<li><a href="/2011/03/11/describing-a-ref-cursor-in-oracle-10g-using-plsql-java-and-c/" target="_blank">Describing a REF CURSOR in Oracle 10<i>g</i>+ Using PL/SQL, Java and C</a></li>
<li><a href="/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g/" target="_blank">Copying/Transforming a REF CURSOR in Oracle 10<i>g</i>+</a></li>
</ul>
<p>In fact, had I known it would have been that difficult, I would probably have chosen to write an improved version of <code>DBMS_XMLGEN</code> instead. Perhaps a subject for a future blog post or product?</p>
<p>You control the NLS handling through the optional parameter <code>DATE_TIME_OPTION</code>. If using this with a value of either <code>DATE_TIME_XML_SET_NLS_COPY_MEM</code> or <code>DATE_TIME_XML_SET_NLS_COPY_TAB</code> you need the version of <code>REF_CURSOR_COPY</code> from this blog post: <a href="/2011/03/29/copying-transforming-a-ref-cursor-in-oracle-10g/" target="_blank">Copying/Transforming a REF CURSOR in Oracle 10<i>g</i>+</a>.</p>
<p>Here is the code:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
create or replace type bind_variable_t as object (

/**
 * Bind variables: Name and value.
 * @version     $Revision: 1 $
 */

  --- Name.
  name varchar2(30),
  --- Value.
  value varchar2(32767)
);
/

create or replace type bind_variable_c as

/**
 * Unordered collection of bind variables.
 * @version     $Revision: 1 $
 */

table of bind_variable_t;
/

create or replace package xml_gen
authid current_user as
/**
 * XML utilities for converting Oracle data and objects to XML.
 * @version   $Revision: 3 $
 * @author    Finn Ellebaek Nielsen.
 */

  /**
   * Generate date/time values according to current session NLS settings.
   */
  date_time_sess_nls             constant pls_integer := 1;

  /**
   * Generate XML Schema-compliant date/time values. This is done by internally
   * by changing the NLS settings and generating the XML. This has the side
   * effect that any implit/explicit calls to TO_CHAR without a specific format
   * mask will work on the internal NLS setting. Also, for REF CURSORs, the
   * internal NLS setting will not be in effect in all cases, because Oracle
   * pre-allocated bindings to the session NLS in effect when the REF CURSOR was
   * opened.
   */
  date_time_xml_set_nls          constant pls_integer := 2;

  /**
   * Generate XML Schema-compliant date/time values. This is done by internally
   * by changing the NLS settings, restarting the query and generating the XML.
   * For dynamic SQL, this has the side effect that any implit/explicit calls to
   * TO_CHAR without a specific format mask will work on the internal NLS
   * setting. For REF CURSORs, the internal NLS setting sometimes will be in
   * effect because the REF CURSOR was restarted. However, when is very unstable
   * across Oracle versions. On some versions DATE columns will have a truncated
   * value if the session NLS_DATE_FORMAT was shorter than the XML Schema-
   * compliant one, on some it will have a format according to the session
   * NLS_DATE_FORMAT. TIMESTAMP% columns seem to work correctly. On some
   * versions Oracle will throw an error &quot;ORA-01801: date format is too long for
   * internal buffer&quot; if the session NLS format mask in effect when opening the
   * REF CURSOR was shorter that what is required for XML Schema-compliance.
   * This also depends on how many rows are returned by the REF CURSOR.
   * Sometimes it doesn't work correctly for the first row, but it works
   * correctly for subsequent rows.
   * In general, you should avoid using this setting as it's very unstable.
   * Setting your session's NLS_DATE_FORMAT to at least 23 characters makes it
   * slightly more stable.
   */
  date_time_xml_set_nls_restart  constant pls_integer := 3;

  /**
   * Generate XML Schema-compliant date/time values. This is done by internally
   * by fetching the incoming dynamic SQL or REF CURSOR to memory, changing the
   * NLS settings, generating the XML on the memory copy. This has no side
   * effects on implit/explicit calls to TO_CHAR without a specific format mask
   * but the performance is affected.
   * Used with the FROM_SELECT function this is only supported on Oracle 11.1
   * and newer.
   */
  date_time_xml_set_nls_copy_mem constant pls_integer := 4;

  /**
   * Generate XML Schema-compliant date/time values. This is done by internally
   * by fetching the incoming dynamic SQL or REF CURSOR to a global temporary
   * table, changing the NLS settings, generating the XML on the table copy.
   * This has no side effects on implit/explicit calls to TO_CHAR without a
   * specific format mask but the performance is affected.
   * Used with the FROM_SELECT function on Oracle versions older than 11 (or if
   * you're using quoted bind variable names -- necessary because DBMS_SQL does
   * not support quoted bind variable names) this attempts to locate the bind
   * variables in SELECT_STATEMENT and this is not done in a fool proof manner
   * if you're using quoted bind variable names using non-identifier characters.
   */
  date_time_xml_set_nls_copy_tab constant pls_integer := 5;

  function from_select(
    select_statement in varchar2,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  function from_select(
    select_statement in varchar2,
    bind_variables in bind_variable_c,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  function from_ref_cursor(
    ref_cursor in out sys_refcursor,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  function from_anydata(
    ad in anydata,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  function from_anydataset(
    ads in anydataset,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  function from_anytype(
    &quot;at&quot; in anytype,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

  procedure set_xml_nls_formats;
  procedure set_session_nls_formats;
end xml_gen;
/

create or replace package body xml_gen as
  --- @version  $Revision: 3 $

  /**
   * XML Schema compliant datetime format, no fraction, no time zone
   * (Oracle DATE).
   */
  xml_nls_date_format constant varchar2(23) :=
      'yyyy-mm-dd&quot;T&quot;hh24:mi:ss';
  /**
   * XML Schema compliant datetime format, with fraction, no time zone (Oracle
   * TIMESTAMP).
   */
  xml_nls_timestamp_format constant varchar2(27) :=
      'yyyy-mm-dd&quot;T&quot;hh24:mi:ss.ff9';
  /**
   * XML Schema compliant datetime format, with fraction, with time zone (Oracle
   * TIMESTAMP WITH [LOCAL] TIME ZONE).
   */
  xml_nls_timestamp_tz_format constant varchar2(34) :=
      'yyyy-mm-dd&quot;T&quot;hh24:mi:ss.ff9tzh:tzm';

  /**
   * Collection of V$NLS_PARAMETERS.VALUE, indexed by
   * V$NLS_PARAMETERS.PARAMETER.
   */
  type nls_parameters_c is table of
  v$nls_parameters.value%type
  index by v$nls_parameters.parameter%type;

  --- Session NLS formats.
  session_nls_parameters nls_parameters_c;
  --- NLS Formats required for XML compliance.
  xml_nls_parameters nls_parameters_c;

  -- Empty bind variable collection.
  bind_variables_empty bind_variable_c;

  function get_xml(
    context in dbms_xmlgen.ctxtype,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype;

/**
 * Gets the result set of a given SELECT statement as XML in Oracle's
 * &quot;canonical form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   select_statement
 *                  SELECT statement.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  Result set as XML.
 */

  function from_select(
    select_statement in varchar2,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

  begin
    return from_select(
      select_statement, bind_variables_empty, date_time_option
    );
  end from_select;

/**
 * Gets the result set of a given SELECT statement as XML in Oracle's
 * &quot;canonical form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   select_statement
 *                  SELECT statement with bind variables.
 * @param   bind_variables
 *                  Collection of bind variables to be bound.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  Result set as XML.
 */

  function from_select(
    select_statement in varchar2,
    bind_variables in bind_variable_c,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

    context dbms_xmlgen.ctxtype;
    dbms_sql_cursor binary_integer;
    n pls_integer;
    ref_cursor sys_refcursor;
    quoted_bind_variable_names boolean := false;

  begin
    if substr(
         upper(ltrim(select_statement, chr(13) || chr(10) || ' ')),
         1,
         6
       ) != 'SELECT' then
      -- Invalid statement given, not a SELECT. Attempting to prevent injection.
      raise_application_error(
        -20000,
        'xml_gen.from_select(select_statement =&gt; ''' ||
          select_statement || '''): ' ||
          'Only SELECT statements are allowed'
      );
    end if;
    if date_time_option in (
          date_time_xml_set_nls_copy_mem,
          date_time_xml_set_nls_copy_tab
        ) then
      if bind_variables is null or bind_variables.count = 0 then
        open ref_cursor for
        select_statement;
      else
        for i in
              nvl(bind_variables.first, 0) ..
              nvl(bind_variables.last, -1)
            loop
          if substr(bind_variables(i).name, 1, 1) = '&quot;' then
            quoted_bind_variable_names := true;
          end if;
        end loop;

        if portable.get_major_version &lt; 11 or quoted_bind_variable_names then
          -- Older than Oracle 11.1. Dynamically build REF CURSOR OPEN
          -- statement. This is not foolproof if quoted bind variable names are
          -- used and we might not find them correctly.
          declare
            select_statement_upper varchar2(32767) := upper(select_statement);
            first_bind_positions dbms_utility.number_array;
            sorted_bind_variables bind_variable_c := bind_variable_c();
            j pls_integer;
            open_statement varchar2(32767);
            next_char varchar2(1);
            quoted_bind_variable_name boolean;
          begin
            for i in
                  nvl(bind_variables.first, 0) ..
                  nvl(bind_variables.last, -1)
                loop
              -- Locate the bind variables in the SELECT statement.
              n := 1;
              quoted_bind_variable_name :=
                substr(bind_variables(i).name, 1, 1) = '&quot;';
              loop
                if quoted_bind_variable_name then
                  -- Case sensitive search, bind variable name quoted..
                  j := instr(
                    select_statement,
                    ':' || bind_variables(i).name,
                    1,
                    n
                  );
                else
                  -- Case insensitive search, bind variable name not quoted.
                  j := instr(
                    select_statement_upper,
                    ':' || upper(bind_variables(i).name),
                    1,
                    n
                  );
                end if;
                -- Ensure that this is the whole word and not part of another
                -- bind variable. Check that next character cannot be part of an
                -- identifier.
                next_char := substr(
                  select_statement_upper,
                  j + length(bind_variables(i).name) + 1,
                  1
                );
                exit when next_char is null or
                    ascii(next_char) &lt;= 32 or
                    instr('+-/*(),|''', next_char) &gt; 0;
                n := n + 1;
              end loop;
              if j = 0 then
                -- Not found, raise error.
                raise_application_error(
                  -20001,
                  'xml_gen.from_select: Bind variable ' ||
                      bind_variables(i).name || ' not found in select_statement'
                );
              end if;
              first_bind_positions(j) := i;
            end loop;

            j := first_bind_positions.first;
            while j is not null loop
              -- Sorted list of bind positions, sort the bind variables
              -- accordingly.
              sorted_bind_variables.extend(1);
              sorted_bind_variables(sorted_bind_variables.count) :=
                  bind_variables(first_bind_positions(j));
              j := first_bind_positions.next(j);
            end loop;

            open_statement :=
                'begin' || chr(10) ||
                '  open :rc for ' || chr(10) ||
                '''' || replace(select_statement, '''', '''''') || '''' || chr(10) ||
                '  using ';

            for i in
                  nvl(sorted_bind_variables.first, 0) ..
                  nvl(sorted_bind_variables.last, -1)
                loop
              open_statement := open_statement ||
                  '''' || replace(sorted_bind_variables(i).value, '''', '''''') ||
                      ''', ';
            end loop;
            open_statement :=
                rtrim(open_statement, ', ') || ';' || chr(10) ||
                'end;';

            execute immediate open_statement
            using in out ref_cursor;
          end;
        else
          -- Oracle 11.1 or newer -- open with DBMS_SQL, then convert to REF
          -- CURSOR. This does not support quoted bind variable names as
          -- DBMS_SQL will raise a &quot;ORA-01008: not all variables bound&quot; even
          -- through it doesn't raise a &quot;ORA-01006: bind variable does not
          -- exist&quot;.
          dbms_sql_cursor := dbms_sql.open_cursor;
          dbms_sql.parse(dbms_sql_cursor, select_statement, dbms_sql.native);

          for i in
                nvl(bind_variables.first, 0) ..
                nvl(bind_variables.last, -1)
              loop
            -- Bind each bind variable.
            dbms_sql.bind_variable(
              dbms_sql_cursor,
              bind_variables(i).name,
              bind_variables(i).value
            );
          end loop;

          begin
            n := dbms_sql.execute(dbms_sql_cursor);
          exception
            when others then
              dbms_output.put_line('xml_gen.from_select');
              dbms_output_put_line(
                'select_statement = ''' || select_statement || ''''
              );
              if sqlcode = -1008 then
                -- ORA-01008: not all variables bound.
                for i in
                      nvl(bind_variables.first, 0) ..
                      nvl(bind_variables.last, -1)
                    loop
                  -- Bind each bind variable.
                  dbms_output_put_line(
                    bind_variables(i).name || ' =&gt; ' ||
                        '''' || bind_variables(i).value || ''''
                  );
                  if substr(bind_variables(i).name, 1, 1) = '&quot;' then
                    dbms_output.put_line(
                      'ERROR: DBMS_SQL doesn''t support quoted bind ' ||
                          'variable names (EXECUTE IMMEDIATE does)'
                    );
                  end if;
                end loop;
              end if;
              dbms_output.put_line(sqlerrm);
              dbms_output.put_line(dbms_utility.format_error_backtrace);
              raise;
          end;

          execute immediate
          'begin :ref_cursor := dbms_sql.to_refcursor(:dbms_sql_cursor); end;'
          using out ref_cursor, in out dbms_sql_cursor;
        end if;
      end if;

      return from_ref_cursor(ref_cursor, date_time_option);
    else
      context := dbms_xmlgen.newcontext(select_statement);

      if bind_variables is not null then
        for i in
              nvl(bind_variables.first, 0) ..
              nvl(bind_variables.last, -1)
            loop
          -- Bind each bind variable.
          dbms_xmlgen.setbindvalue(
            context,
            bind_variables(i).name,
            bind_variables(i).value
          );
        end loop;
      end if;
    end if;

    return get_xml(context, date_time_option);
  end from_select;

/**
 * Gets the result set of a given REF CURSOR as XML in Oracle's &quot;canonical
 * form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   select_statement
 *                  SELECT statement.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  Result set as XML.
 */

  function from_ref_cursor(
    ref_cursor in out sys_refcursor,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype is

    context dbms_xmlgen.ctxtype;
    result xmltype;
    rc2 sys_refcursor;
    destination char(1);
    type_name user_types.type_name%type;

  begin
    if date_time_option in (
          date_time_xml_set_nls_copy_mem,
          date_time_xml_set_nls_copy_tab
        ) then
      -- Copy to memory/table first, then change NLS, then generate off the
      -- copy.
      if date_time_option = date_time_xml_set_nls_copy_mem then
        destination := 'M';
      else
        destination := 'T';
      end if;

      rc2 := ref_cursor_copy.to_ref_cursor(
        ref_cursor,
        type_name,
        destination,
        plsql_block_before_copy_open =&gt; 'xml_gen.set_xml_nls_formats;'
      );

      ref_cursor := rc2;
    end if;

    context := dbms_xmlgen.newcontext(ref_cursor);
    result := get_xml(context, date_time_option);
    begin
      if ref_cursor%isopen then
        close ref_cursor;
      end if;
    exception
      when others then
        null;
    end;
    return result;
  end from_ref_cursor;

/**
 * Gets the result set of a given ANYDATA instance as XML in Oracle's &quot;canonical
 * form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * This is the function to use when you need to obtain XML for object type/
 * collection instances and ANYDATA in general. You use ANYDATA.CONVERTOBJECT
 * and ANYDATA.CONVERTCOLLECTION respectively to convert from object types and
 * collections for this. XMLTYPE can also convert these directly but has several
 * flaws in terms of incurrent DATE/TIMESTAMP formats, missing elements for NULL
 * values, etc.
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   ad      ANYDATA instance.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  ANYDATA instance as XML.
 */

  function from_anydata(
    ad in anydata,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

    ref_cursor sys_refcursor;

  begin
    open ref_cursor for
    select ad
    from   dual;

    -- Get rid of surrounding XML due to the SELECT statement.
    return from_ref_cursor(ref_cursor, date_time_option).extract('/*/*/*/*');
  end from_anydata;

/**
 * Gets the result set of a given ANYDATASET instance as XML in Oracle's
 * &quot;canonical form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   ads     ANYDATASET instance.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  ANYDATASET instance as XML.
 */

  function from_anydataset(
    ads in anydataset,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

    ref_cursor sys_refcursor;

  begin
    open ref_cursor for
    select ads
    from   dual;

    -- Get rid of surrounding XML due to the SELECT statement.
    return from_ref_cursor(ref_cursor, date_time_option).extract('/*/*/*/*');
  end from_anydataset;

/**
 * Gets the result set of a given ANYTYPE instance as XML in Oracle's &quot;canonical
 * form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   &quot;at&quot;    ANYTYPE instance.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  ANYTYPE instance as XML.
 */

  function from_anytype(
    &quot;at&quot; in anytype,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

    ref_cursor sys_refcursor;

  begin
    open ref_cursor for
    select &quot;at&quot;
    from   dual;

    -- Get rid of surrounding XML due to the SELECT statement.
    return from_ref_cursor(ref_cursor, date_time_option).extract('/*/*/*/*');
  end from_anytype;

/**
 * Gets the result set of a given DBMS_XMLGEN context as XML in Oracle's
 * &quot;canonical form&quot;, ie:
 * &lt;ROWSET&gt;
 *   &lt;ROW&gt;
 *     &lt;COLUMN_NAME_1&gt;column_value_1&lt;/COLUMN_NAME_1&gt;
 *     &lt;COLUMN_NAME_2&gt;column_value_2&lt;/COLUMN_NAME_2&gt;
 *     ...
 *     &lt;COLUMN_NAME_N&gt;column_value_n&lt;/COLUMN_NAME_N&gt;
 *   &lt;/ROW&gt;
 * &lt;/ROWSET&gt;
 * Text columns will have their values converted to XML character entities, also
 * if it's XML, HTML etc. This is not done with XMLTYPE column values as we
 * assume it's valid XML.
 * @param   context DBMS_XMLGEN context created based on either SELECT statement
 *                  or REF CURSOR.
 * @param   date_time_option
 *                  Date/time options. One of the DATE_TIME_% constants from the
 *                  package specification.
 * @return  Result set as XML.
 */

  function get_xml(
    context in dbms_xmlgen.ctxtype,
    date_time_option in pls_integer := date_time_xml_set_nls
  )
  return xmltype as

    result xmltype;

  begin
    if date_time_option in (
          date_time_xml_set_nls,
          date_time_xml_set_nls_restart
        ) then
      set_xml_nls_formats;
    end if;
    if date_time_option = date_time_xml_set_nls_restart then
      dbms_xmlgen.restartquery(context);
    end if;
    dbms_xmlgen.setnullhandling(context, dbms_xmlgen.empty_tag);
    dbms_xmlgen.setconvertspecialchars(context, true);
    result := dbms_xmlgen.getxmltype(context);
    dbms_xmlgen.closecontext(context);
    if date_time_option in (
          date_time_xml_set_nls,
          date_time_xml_set_nls_restart,
          date_time_xml_set_nls_copy_mem,
          date_time_xml_set_nls_copy_tab
        ) then
      set_session_nls_formats;
    end if;

    return result;
  exception
    when others then
      if date_time_option in (
            date_time_xml_set_nls,
            date_time_xml_set_nls_restart,
            date_time_xml_set_nls_copy_mem,
            date_time_xml_set_nls_copy_tab
          ) then
        set_session_nls_formats;
      end if;
      raise;
  end get_xml;

/**
 * Sets specific NLS date/timestamp formats required for XML compliance.
 */

  procedure set_xml_nls_formats is
  pragma autonomous_transaction;

    needs_commit boolean := false;

  begin
    for nls in (
          select /*+ cursor_sharing_exact */
                 parameter,
                 value
          from   v$nls_parameters
          where  parameter like 'NLS_%FORMAT' and
                 parameter not like 'NLS\_TIME\_%' escape '\'
        ) loop
      session_nls_parameters(nls.parameter) := nls.value;
      if nls.value != xml_nls_parameters(nls.parameter) then
        -- Change NLS format for XML-compliance.
        dbms_session.set_nls(
          nls.parameter,
          '''' || xml_nls_parameters(nls.parameter) || ''''
        );

        needs_commit := true;
      end if;
    end loop;

    if needs_commit then
      commit;
    end if;
  end set_xml_nls_formats;

/**
 * Restores NLS date/timestamp formats as it was before setting up for XML
 * compliance.
 */

  procedure set_session_nls_formats is
  pragma autonomous_transaction;

    needs_commit boolean := false;
    nls_parameter v$nls_parameters.parameter%type;

  begin
    nls_parameter := xml_nls_parameters.first;
    while nls_parameter is not null loop
      if xml_nls_parameters(nls_parameter) !=
          session_nls_parameters(nls_parameter) then
        -- Restore original NLS format.
        dbms_session.set_nls(
          nls_parameter,
          '''' || session_nls_parameters(nls_parameter) || ''''
        );

        needs_commit := true;
      end if;

      nls_parameter := xml_nls_parameters.next(nls_parameter);
    end loop;

    if needs_commit then
      commit;
    end if;
  exception
    when no_data_found then
      -- Ignore that we don't have a backup. Presumably because
      -- SET_XML_NLS_FORMATS hasn't been called.
      null;
  end set_session_nls_formats;

begin
  xml_nls_parameters('NLS_DATE_FORMAT') := xml_nls_date_format;
  xml_nls_parameters('NLS_TIMESTAMP_FORMAT') := xml_nls_timestamp_format;
  xml_nls_parameters('NLS_TIMESTAMP_TZ_FORMAT') := xml_nls_timestamp_tz_format;
end xml_gen;
/
</pre></p>
<h1>Example Usage</h1>
<p>Here are a few examples of usage:<br />
<pre class="brush: sql; collapse: true; light: false; pad-line-numbers: false; toolbar: true;">
set long 100000
set pagesize 50000
set linesize 1000

alter session set nls_date_format = 'dd.mm.yyyy hh24:mi:ss';
alter session set nls_timestamp_format = 'dd.mm.yyyy hh24:mi:ss.ff';
alter session set nls_timestamp_tz_format = 'dd.mm.yyyy hh24:mi:ss.ff tzh:tzm';

variable xml clob

prompt INSERT

begin
  :xml := '';
  :xml := xml_gen.from_select(
    'insert into dept values (50, ''abc'', ''def'')'
  ).getclobval();
end;
/

print :xml

prompt FROM_SELECT

begin
  :xml := '';
  :xml := xml_gen.from_select('select * from dept').getclobval();
end;
/

print :xml

prompt FROM_SELECT with bind variables

begin
  :xml := xml_gen.from_select(
    'select * from emp where empno in (:empno1, :empno2)',
    bind_variable_c(
      bind_variable_t('empno1', '7499'),
      bind_variable_t('empno2', '7934')
    )
  ).getclobval();
end;
/

print :xml

prompt FROM_REF_CURSOR with bind variables

declare
  rc sys_refcursor;
begin
  open rc for
  'select * from emp where empno in (:empno1, :empno2)'
  using 7369, 7499;

  :xml := xml_gen.from_ref_cursor(rc).getclobval();
end;
/

print :xml

-- grant execute on sys.aq$_subscribers to scott;
-- grant execute on sys.aq$_agent to scott;

prompt FROM_ANYDATA object type instance

begin
  :xml := '';
  :xml := xml_gen.from_anydata(
    anydata.convertobject(sys.aq$_agent('name', 'address', 1))
  ).getclobval();
end;
/

print :xml

prompt FROM_ANYDATA collection

begin
  :xml := '';
  :xml := xml_gen.from_anydata(
    anydata.convertcollection(
      sys.aq$_subscribers(
        sys.aq$_agent('name1', 'address1', 1),
        sys.aq$_agent('name2', 'address2', 2)
      )
    )
  ).getclobval();
end;
/

print :xml

prompt FROM_ANYTYPE throws ORA-19201

declare
  a anytype;
begin
  anytype.begincreate(
    typecode =&gt; dbms_types.typecode_object,
    atype =&gt; a
  );
  a.addattr(
    typecode =&gt; dbms_types.typecode_number,
    aname =&gt; 'n',
    prec =&gt; 38,
    scale =&gt; 0,
    len =&gt; null,
    csid =&gt; null,
    csfrm =&gt; null
  );
  a.addattr(
    typecode =&gt; dbms_types.typecode_date,
    aname =&gt; 'd',
    prec =&gt; 5,
    scale =&gt; 5,
    len =&gt; null,
    csid =&gt; null,
    csfrm =&gt; null
  );
  a.endcreate;

  begin
    :xml := '';
    :xml := xml_gen.from_anytype(a).getclobval();
  exception
    when others then
      dbms_output.put_line(sqlerrm);
  end;

  begin
    select sys_xmlgen(a).getclobval()
    into   :xml
    from   dual;
  exception
    when others then
      dbms_output.put_line(sqlerrm);
  end;
end;
/

print :xml

prompt FROM_ANYDATASET throws ORA-19201

declare
  a anytype;
  ads anydataset;
begin
  anytype.begincreate(
    typecode =&gt; dbms_types.typecode_object,
    atype =&gt; a
  );
  a.addattr(
    typecode =&gt; dbms_types.typecode_number,
    aname =&gt; 'n',
    prec =&gt; 38,
    scale =&gt; 0,
    len =&gt; null,
    csid =&gt; null,
    csfrm =&gt; null
  );
  a.addattr(
    typecode =&gt; dbms_types.typecode_varchar2,
    aname =&gt; 'vc2',
    prec =&gt; null,
    scale =&gt; null,
    len =&gt; 10,
    csid =&gt; null,
    csfrm =&gt; null
  );
  a.endcreate;

  anydataset.begincreate( dbms_types.typecode_object, a, ads);
  for i in 1 .. 2 loop
    ads.addinstance;
    ads.piecewise;
    ads.setnumber(i);
    ads.setvarchar2('vc2 ' || i);
  end loop;
  ads.endcreate;

  begin
    :xml := '';
    :xml := xml_gen.from_anydataset(ads).getclobval();
  exception
    when others then
      dbms_output.put_line(sqlerrm);
  end;

  begin
    select sys_xmlgen(ads).getclobval()
    into   :xml
    from   dual;
  exception
    when others then
      dbms_output.put_line(sqlerrm);
  end;
end;
/

print :xml
</pre></p>
<h1>Source Code</h1>
<p>You can download the source code <a href="http://www.ellebaek-consulting.com/files/blog/xmlgen.zip">here</a>.<br />
Feel free to use the code at your own risk. I welcome your feedback and suggestions for improvements but the code as such is not supported.</p>
<h1>Conclusion</h1>
<p>Did I achieve my goals? Not entirely, but close. I'll describe this in detail in the following sections.</p>
<h2>Object Type Instance</h2>
<p>Object type instance: Unfortunately, Oracle doesn't allow us to write custom PL/SQL code that can receive or return every possible object type instance. Oracle reserves the datatypes <code>"&lt;ADT_1&gt;"</code> (ADT is an acronym for Abstract Data Type, another term used for object types) and <code>"&lt;COLLECTION_1&gt;"</code> for <code>SYS</code> objects so we're not allowed to use them. This is very unfortunate, as in this case we could just pass them on to other Oracle functionality that would know what to do with them.</p>
<p>We have a few workarounds:</p>
<ul>
<li>Convert the object type instance and collection to an <code>ANYDATA</code> instance by calling the <code>ANYDATA.CONVERTOBJECT</code> and <code>ANYDATA.CONVERTCOLLECTION</code> "constructors" respectively and then calling <code>XML_GEN.FROM_ANYDATA</code>.</li>
<li>Add an overloaded <code>FROM_OBJECT</code> function in <code>XML_GEN</code> for each object type you need to support. This should simply convert the object type instance to an <code>ANYDATA</code> instance (by calling the <code>ANYDATA.CONVERTOBJECT</code> "constructor") and invoke <code>XML_GEN.FROM_ANYDATA</code>. If your object types inherit from a few base object types this would be easily done by just adding overloads for the base object types.</li>
<li>Add a member function to each of the object types called <code>TO_XML</code>, which basically does what the overloaded <code>FROM_OBJECT</code> function mentioned above would do.</li>
</ul>
<h2>ANYTYPE and ANYDATASET</h2>
<p><code>ANYTYPE</code> and <code>ANYDATASET</code> are not supported by <code>DBMS_XMLGEN</code> and any of the other XML generating functionality. Oracle throws the following error if you invoke with any of those datatypes:</p>
<pre>ORA-19201: Datatype not supported</pre>
<p>I considered implementing a workaround that would generate the XML by recursively inspecting the structure (and values for <code>ANYDATASET</code>) but I'll leave this for a future version of my <code>XML_GEN</code> package.</p>
<h1>Related Blog Posts</h1>
<p>A few other related and interesting blog posts are (please notify me if you know of other interesting, relevant blog posts on this subject and I'll add them to this list):</p>
<ul>
<li>Kevin Meade's blog: Easy XML - Let the Database do the Work: <a href="http://www.orafaq.com/node/1025" target="_blank">http://www.orafaq.com/node/1025</a>.</li>
<li>Adrian Billington's blog: working with long columns: <a href="http://www.oracle-developer.net/display.php?id=430" target="_blank">http://www.oracle-developer.net/display.php?id=430</a>.</li>
</ul>
<h1>References</h1>
<ul>
<li>Oracle® XML DB Developer's Guide 11<em>g</em> Release 2 (11.2) Chapter 18 Generating XML Data from the Database: <a href="http://download.oracle.com/docs/cd/E11882_01/appdev.112/e16659/xdb13gen.htm#g1047191" target="_blank">http://download.oracle.com/docs/cd/E11882_01/appdev.112/e16659/xdb13gen.htm#g1047191</a>.</li>
<li>Oracle XSQL: Combining SQL, Oracle Text, XSLT...: Michael D. Thomas. Wiley 2003. ISBN 0471271209: <a href="http://wiley.com/compbooks/thomas/" target="_blank">http://wiley.com/compbooks/thomas/</a>.</li>
<li>Oracle® XML Developer's Kit Programmer's Guide 11<em>g</em> Release 2 (11.2) Chapter 15 Using the XSQL Pages Publishing Framework: Advanced Topics: <a href="http://download.oracle.com/docs/cd/E11882_01/appdev.112/e10708/adx_j_xsqladv.htm#BABCIGBB" target="_blank">http://download.oracle.com/docs/cd/E11882_01/appdev.112/e10708/adx_j_xsqladv.htm#BABCIGBB</a>.</li>
<li>Oracle® XML Developer's Kit Programmer's Guide 11<em>g</em> Release 2 (11.2) Chapter 30 XSQL Pages Reference: <a href="http://download.oracle.com/docs/cd/E11882_01/appdev.112/e10708/adx_ref_xsql.htm#BJEEABJJ" target="_blank">http://download.oracle.com/docs/cd/E11882_01/appdev.112/e10708/adx_ref_xsql.htm#BJEEABJJ</a>.</li>
</ul>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/53/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/53/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/53/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=53&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2011/01/27/converting-between-oracle-data-and-xml/feed/</wfw:commentRss>
		<slash:comments>14</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
		<item>
		<title>Converting a LONG Column to a CLOB on the fly</title>
		<link>http://ellebaek.wordpress.com/2010/12/06/converting-a-long-column-to-a-clob-on-the-fly/</link>
		<comments>http://ellebaek.wordpress.com/2010/12/06/converting-a-long-column-to-a-clob-on-the-fly/#comments</comments>
		<pubDate>Mon, 06 Dec 2010 13:01:08 +0000</pubDate>
		<dc:creator>ellebaek</dc:creator>
				<category><![CDATA[Oracle PL/SQL]]></category>
		<category><![CDATA[Oracle SQL]]></category>
		<category><![CDATA[oracle]]></category>
		<category><![CDATA[convert]]></category>
		<category><![CDATA[long]]></category>
		<category><![CDATA[clob]]></category>

		<guid isPermaLink="false">http://ellebaek.wordpress.com/?p=6</guid>
		<description><![CDATA[Background The arcane LONG datatype has as you&#8217;re aware of many intrinsic limitations but unfortunately Oracle still uses quite a few LONG columns in the data dictionary. It&#8217;s beyond me why these haven&#8217;t been converted to CLOB a very long time ago (when Oracle deprecated usage of LONG). At the same time, Oracle only provides [...]<img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=6&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></description>
			<content:encoded><![CDATA[<h2>Background</h2>
<p>The arcane <code>LONG</code> datatype has as you&#8217;re aware of many intrinsic limitations but unfortunately Oracle still uses quite a few <code>LONG</code> columns in the data dictionary. It&#8217;s beyond  me why these haven&#8217;t been converted to CLOB a very long time ago (when Oracle deprecated usage of LONG). At the same time, Oracle only provides the following means of converting these <code>LONG</code> columns to eg <code>CLOB</code>:</p>
<ol>
<li><code>TO_LOB</code>: A function that converts a <code>LONG</code> column to a <code>CLOB</code>. Can only be used in a <code>SELECT</code> list of a subquery in an <code>INSERT</code> statement. This means that the function is only useful if you&#8217;re converting the underlying table definition.</li>
<li><code>ALTER TABLE &lt;table_name&gt; MODIFY &lt;long_column_name&gt; CLOB</code>: This converts the column datatype and the data as well.</li>
<li>Use <code>DBMS_REDEFINITION</code> to redefine the column datatype.</li>
<li>Use Oracle Data Pump to convert the column datatype.</li>
<li>Use <code>CAST</code> function to cast from <code>LONG</code> to <code>CLOB</code>: Unfortunately, <code>CAST</code> doesn&#8217;t support <code>LONG</code>.</li>
<li>Write a PL/SQL function that performs a <code>SELECT ... INTO l FROM user_views</code>, where <code>l</code> is a PL/SQL variable of type <code>LONG</code>. However, in PL/SQL, a <code>LONG</code> variable can only hold up to 32,760 characters (yes, not 32,767, ie different from <code>VARCHAR2</code>) so this will only solve the problem for small to medium sized views.</li>
<li>Use dynamic SQL with <code>DBMS_SQL.COLUMN_VALUE_LONG</code> to access the <code>LONG</code> piecewise.</li>
</ol>
<h2>Problem</h2>
<p>Imagine the following scenario:</p>
<ul>
<li>You need to access the source of a given view.</li>
<li><code>USER_VIEWS.TEXT</code> contains the view source but it is a column with datatype <code>LONG</code> so very limited in use.</li>
<li>Using <code>DBMS_METADATA</code> to obtain the source is not an option for various reasons, eg performance, practicality, etc.</li>
</ul>
<h2>Solution</h2>
<p>Seen that we need to access the data from the view, we cannot use <code>TO_LOB</code> or any of the other solutions that alter the underlying table column definition. However, the <code>DBMS_SQL.COLUMN_VALUE_LONG</code> function comes to the rescue as this allows us to fetch the <code>LONG</code> data piecewise and construct a <code>CLOB</code> with the same data. This obviously means that we have to use dynamic SQL for the query of the table/view we&#8217;re trying to convert the <code>LONG</code> column for.</p>
<p>We have two different methods of applying dynamic SQL to this problem:</p>
<ol>
<li>Dynamic SQL for just the <code>LONG</code> column. We need to write a function that receives the primary key value(s) of the underlying table/view (could be <code>ROWID</code> if a table) as input parameter(s), builds a <code>SELECT</code> statement for the underlying table/view for the <code>LONG</code> column using bind variable(s) for the primary key value(s), does the <code>DBMS_SQL</code> magic and uses the <code>DBMS_SQL.COLUMN_VALUE_LONG</code> function. For scalability, the solution should keep a collection of parsed statements and re-use those without re-parsing.</li>
<li>Dynamic SQL for the whole underlying table/view generated and called in a pipelined table function that utilizes object types.</li>
</ol>
<p>For this blog post I&#8217;ll use solution 2, for which I&#8217;ll demonstrate the following:</p>
<ol>
<li>Create a standalone function with &#8220;LONG-to-CLOB&#8221; functionality.</li>
<li>Create an object type with attributes corresponding to <code>USER_VIEWS</code>. This object type will have member methods used with <code>DBMS_SQL</code>.</li>
<li>Create an object type collection based on the object type from 2.</li>
<li>Create a standalone function that takes an optional argument for a <code>WHERE</code> clause used against <code>USER_VIEWS</code>, using a combination of <code>DBMS_SQL</code> and 1., 2., and 3. above. This method is a pipelined table function.</li>
<li>Optionally create a view on top of the standalone function from 4.</li>
</ol>
<p>You can apply the structure of this solution in use cases where you need to access a <code>LONG</code> value in a table/view as a <code>CLOB</code>, without converting the underlying persistent column: The standalone function from 1. above is generic &#8212; items 2. through 5. are specific to the underlying table/view.</p>
<p>We&#8217;ll go through the different parts in the following sections.</p>
<h2>&#8220;LONG-to-CLOB&#8221; Function</h2>
<p>This is the function that converts a <code>LONG</code> column to a <code>CLOB</code> value through a <code>DBMS_SQL</code> cursor that has been parsed, prepared (given column &#8220;defined&#8221; with <code>DBMS_SQL.DEFINE_COLUMN_LONG</code>) and executed:</p>
<pre>create or replace function long_to_clob(
  dbms_sql_cursor in integer,
  col_id in integer
)
return clob as

/**
 * Fetches LONG column value and converts it to a CLOB.
 * @param   dbms_sql_cursor
 *                  DBMS_SQL cursor parsed, prepared (given column "defined"
 *                  with DBMS_SQL.DEFINE_COLUMN_LONG) and executed.
 * @param   col_id  Column ID.
 * @return  LONG column value as a CLOB.
 */

  long_val long;
  long_len integer;
  buf_len  integer := 32760;
  cur_pos  number := 0;

  result   clob;

begin
  -- Create CLOB.
  dbms_lob.createtemporary(result, false, dbms_lob.call);

  -- Piecewise fetching of the LONG column, appending to the CLOB.
  loop
    dbms_sql.column_value_long(
      dbms_sql_cursor,
      col_id,
      buf_len,
      cur_pos,
      long_val,
      long_len
    );
    exit when long_len = 0;
    dbms_lob.append(result, long_val);
    cur_pos := cur_pos + long_len;
  end loop;

  return result;
end long_to_clob;
/</pre>
<h2>Object Type</h2>
<p>In Oracle 11.2.0.1.0, <code>USER_VIEWS</code> has the following columns:</p>
<pre>SQL&gt; desc user_views
 Name             Null?    Type
 ---------------- -------- --------------
 VIEW_NAME        NOT NULL VARCHAR2(30)
 TEXT_LENGTH               NUMBER
 TEXT                      LONG
 TYPE_TEXT_LENGTH          NUMBER
 TYPE_TEXT                 VARCHAR2(4000)
 OID_TEXT_LENGTH           NUMBER
 OID_TEXT                  VARCHAR2(4000)
 VIEW_TYPE_OWNER           VARCHAR2(30)
 VIEW_TYPE                 VARCHAR2(30)
 SUPERVIEW_NAME            VARCHAR2(30)
 EDITIONING_VIEW           VARCHAR2(1)
 READ_ONLY                 VARCHAR2(1)</pre>
<p>which means that we could create our object type like the following (notice how <code>TEXT</code> is represented by a <code>CLOB</code> instead of the original <code>LONG</code>):</p>
<pre>create or replace type user_views_t as object (
/**
 * Object type representing columns in data dictionary view USER_VIEWS, with the
 * TEXT column represented by a CLOB instead of a LONG.
 */

  view_name        varchar2(30),
  text_length      number,
  -- CLOB instead of LONG.
  text             clob,
  type_text_length number,
  type_text        varchar2(4000),
  oid_text_length  number,
  oid_text         varchar2(4000),
  view_type_owner  varchar2(30),
  view_type        varchar2(30),
  superview_name   varchar2(30),
  editioning_view  varchar2(1),
  read_only        varchar2(1),

  constructor function user_views_t
  return self as result,
  constructor function user_views_t(dbms_sql_cursor in integer)
  return self as result,

  member procedure define_columns(dbms_sql_cursor in integer)
);
/</pre>
<p>The object type has two constructors and one member function. The member function is used to define the columns for <code>DBMS_SQL</code> and in order to be able to use the attributes, this has to be a member function (working on an object type instance as opposed to a static function) and the first constructor is used to create such a dummy instance, with all the attributes set to <code>NULL</code>. The second constructor sets all attributes to corresponding column values in a given fetched <code>DBMS_SQL</code> cursor.</p>
<p>This is the implementation of the object type body:</p>
<pre>create or replace type body user_views_t as
/**
 * Constructor. Sets all attributes to NULL.
 * @return  New object type instance.
 */

  constructor function user_views_t
  return self as result as

  begin
    return;
  end user_views_t;

/**
 * Constructor. Sets all attributes to corresponding column values in fetched
 * DBMS_SQL cursor.
 * @param   dbms_sql_cursor
 *                  Executed and fetched DBMS_SQL cursor on a query from
 *                  USER_VIEWS.
 * @return  New object type instance.
 */

  constructor function user_views_t(dbms_sql_cursor in integer)
  return self as result as

  begin
    dbms_sql.column_value(dbms_sql_cursor, 01, view_name);
    dbms_sql.column_value(dbms_sql_cursor, 02, text_length);
    -- Convert LONG to CLOB.
    text := long_to_clob(dbms_sql_cursor,  03);
    dbms_sql.column_value(dbms_sql_cursor, 04, type_text_length);
    dbms_sql.column_value(dbms_sql_cursor, 05, type_text);

    dbms_sql.column_value(dbms_sql_cursor, 06, oid_text_length);
    dbms_sql.column_value(dbms_sql_cursor, 07, oid_text);
    dbms_sql.column_value(dbms_sql_cursor, 08, view_type_owner);
    dbms_sql.column_value(dbms_sql_cursor, 09, view_type);
    dbms_sql.column_value(dbms_sql_cursor, 10, superview_name);

    dbms_sql.column_value(dbms_sql_cursor, 11, editioning_view);
    dbms_sql.column_value(dbms_sql_cursor, 12, read_only);

    return;
  end user_views_t;

/**
 * Defines all columns in DBMS_SQL cursor.
 * @param   Parsed DBMS_SQL cursor on a query from USER_VIEWS.
 */

  member procedure define_columns(dbms_sql_cursor in integer) as

  begin
    dbms_sql.define_column(dbms_sql_cursor, 01, view_name, 30);
    dbms_sql.define_column(dbms_sql_cursor, 02, text_length);
    -- LONG column.
    dbms_sql.define_column_long(dbms_sql_cursor, 03);
    dbms_sql.define_column(dbms_sql_cursor, 04, type_text_length);
    dbms_sql.define_column(dbms_sql_cursor, 05, type_text, 4000);

    dbms_sql.define_column(dbms_sql_cursor, 06, oid_text_length);
    dbms_sql.define_column(dbms_sql_cursor, 07, oid_text, 4000);
    dbms_sql.define_column(dbms_sql_cursor, 08, view_type_owner, 30);
    dbms_sql.define_column(dbms_sql_cursor, 09, view_type, 30);
    dbms_sql.define_column(dbms_sql_cursor, 10, superview_name, 30);

    dbms_sql.define_column(dbms_sql_cursor, 11, editioning_view, 1);
    dbms_sql.define_column(dbms_sql_cursor, 12, read_only, 1);
  end define_columns;
end;
/</pre>
<p>Notice how the second constructor uses our <code>LONG_TO_CLOB</code> function.</p>
<p>The process of matching the original table/view columns into attributes and handling of these in one of the constructors and the <code>DEFINE_COLUMNS</code> method is tedious and error prone. If you need to do this often you should consider writing a generator that generates the object type specification and body based on the definition of a given table/view.</p>
<h2>Object Type Collection</h2>
<p>This is the implementation of the object type collection, using a nested table:</p>
<pre>create or replace type user_views_c as
table of user_views_t;
/</pre>
<p>We need this object type collection for the pipelined table function as this pipes back a collection of object type instances back to the SQL engine &#8212; namely, one object type instance for each row in <code>USER_VIEWS</code> this function finds.</p>
<h2>Pipelined Table Function</h2>
<p>This is the implementation of the standalone pipelined table function that takes an optional parameter to be matched against the <code>VIEW_NAME</code> column in a <code>LIKE</code> expression and uses <code>DBMS_SQL</code> to parse the query from <code>USER_VIEWS</code>, uses the <code>USER_VIEWS_T</code> and <code>USER_VIEWS_C</code> object types (which in turn calls <code>LONG_TO_CLOB</code>) and pipes the rows back to the SQL engine (type <code>PTF</code> suffix refers to &#8220;Pipelined Table Function&#8221;):</p>
<pre>create or replace function user_views_ptf(
  view_name_like in varchar2 := '%'
)
return user_views_c pipelined as

/**
 * Gets collection of user views representing rows in USER_VIEWS. Resolved
 * through a dynamic SQL query against USER_VIEWS and the TEXT column is
 * converted from a LONG to a CLOB.
 * @param   view_name_like
 *                  LIKE expression used in a WHERE clause predicate against
 *                  USER_VIEWS.VIEW_NAME. Default '%', ie all.
 * @return  Collection that can be used in a FROM clause with a TABLE() cast.
 */

  query           varchar2(200);
  dbms_sql_cursor binary_integer;
  n               pls_integer;
  each            user_views_t;

begin
  query :=
     'select * ' ||
     'from   user_views uv ' ||
     'where  uv.view_name like :view_name_like';

  -- Create cursor, parse and bind.
  dbms_sql_cursor := dbms_sql.open_cursor;
  dbms_sql.parse(dbms_sql_cursor, query, dbms_sql.native);
  dbms_sql.bind_variable(dbms_sql_cursor, 'view_name_like', view_name_like);

  -- Define columns through dummy object type instance.
  each := user_views_t();
  each.define_columns(dbms_sql_cursor);

  -- Execute.
  n := dbms_sql.execute(dbms_sql_cursor);

  -- Fetch all rows, pipe each back.
  while dbms_sql.fetch_rows(dbms_sql_cursor) &gt; 0 loop
    each := user_views_t(dbms_sql_cursor);

    pipe row(each);
  end loop;

  dbms_sql.close_cursor(dbms_sql_cursor);
exception
  when others then
    dbms_output.put_line('long_to_clob: ' || sqlerrm);
    dbms_output.put_line(dbms_utility.format_error_backtrace);
    if dbms_sql.is_open(dbms_sql_cursor) then
      dbms_sql.close_cursor(dbms_sql_cursor);
    end if;

    raise;
end user_views_ptf;
/</pre>
<p>It&#8217;s inconvenient that we can&#8217;t create this function as a member function on <code>USER_VIEWS_T</code> where it really belongs. However this is not possible because that would introduce a cyclical dependency between <code>USER_VIEWS_T</code> and <code>USER_VIEWS_C</code>, which is not allowed (even using forward object type declarations).</p>
<h2>SELECT</h2>
<p>You can select from the pipelined table function with a <code>TABLE</code> expression, such as:</p>
<pre>select *
from   table(user_views_ptf('V%'));</pre>
<h2>Installation</h2>
<p>You need to install the solution objects in the following order:</p>
<ol>
<li><code>LONG_TO_CLOB</code> function.</li>
<li><code>USER_VIEWS_T</code> object type specification.</li>
<li><code>USER_VIEWS_T</code> object type body.</li>
<li><code>USER_VIEWS_C</code> object type collection.</li>
<li><code>USER_VIEWS_PTF</code> function.</li>
</ol>
<h2>Test</h2>
<p>A small test case:</p>
<pre>create or replace view v1 as
select 1 n,
       'abc' vc2,
       sysdate d,
       systimestamp(6) t
from   dual;

SQL&gt; select view_name,
  2         text
  3  from   table(user_views_ptf('V%'));

VIEW_NAME                      TEXT
------------------------------ ------------------------
V1                             select 1 n,
                                      'abc' vc2,
                                      sysdate d,
                                      systimestamp(6) t
                               from   dual</pre>
<h2>View</h2>
<p>You can optionally create a view on top of the pipelined table function. In this case, you cannot push the <code>LIKE</code> expression into the argument to the table function so the optimizer has no alternative to perform a full table scan on <code>USER_VIEWS</code> and then a match on the returned rows on whatever predicate the view is used with.</p>
<h2>Maintenance</h2>
<p>Every time you need to convert a <code>LONG</code> column in a table/view to a <code>CLOB</code> you need to do the following:</p>
<ol>
<li>Create the object type with attributes corresponding to the columns of the table/view you need to obtain data for. The object type needs two constructors and the member function <code>DEFINE_COLUMNS</code> as for the <code>USER_VIEWS_T</code> object type.</li>
<li>Create the object type collection for 1.</li>
<li>Create the standalone pipelined table function.</li>
<li>Optionally create a view on top of 3.</li>
</ol>
<p>If you prefer to use packages over standalone functions, you could bundle them up in a package called <code>LONG_TO_CLOB</code> and rename the function <code>LONG_TO_CLOB</code> to <code>TO_CLOB</code>.</p>
<br />  <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godelicious/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gofacebook/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gotwitter/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gostumble/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/godigg/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/ellebaek.wordpress.com/6/" /></a> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/goreddit/ellebaek.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/ellebaek.wordpress.com/6/" /></a> <img alt="" border="0" src="http://stats.wordpress.com/b.gif?host=ellebaek.wordpress.com&amp;blog=10540081&amp;post=6&amp;subd=ellebaek&amp;ref=&amp;feed=1" width="1" height="1" />]]></content:encoded>
			<wfw:commentRss>http://ellebaek.wordpress.com/2010/12/06/converting-a-long-column-to-a-clob-on-the-fly/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
	
		<media:content url="http://0.gravatar.com/avatar/4b3dbf694bf312db3661cc6178e1f56b?s=96&#38;d=identicon&#38;r=G" medium="image">
			<media:title type="html">ellebaek</media:title>
		</media:content>
	</item>
	</channel>
</rss>
