I don't think I will have this ready as a built-in feature of the website for at least 6 months. In the meantime, however, my friend and co-founder of the PL/SQL Challenge, Finn Ellebaek Nielsen, has encountered an obstacle in one of his development efforts and I agreed to post it here and solicit solutions.
Please address a description of all possible solutions in comments to this blog post, which Finn fill monitor.
Finn will choose the optimal solution and I will then announce a winner through the blog. I will also post the solution (perhaps multiple solutions) so that everyone can benefit from the effort. Finn is offering an Amazon gift card of USD 100 for the person who first submitted the optimal solution.
Regards, Steven
Problem: DBMX_XMLGEN Doesn't Respect current NLS_DATE_FORMAT for DATE Values of REF CURSORs
Here's the issue:DBMS_XMLGEN does not generate XML with correct format for DATE column values when called with a REF CURSOR, even though the NLS_DATE_FORMAT has been changed prior to calling DBMS_XMLGEN, but after the REF CURSOR has been opened. If NLS_DATE_FORMAT is changed prior to opening the REF CURSOR it works OK. I realize that Oracle allocates some resources when opening the cursor but oddly, it works correct for TIMESTAMP and TIMESTAMP WITH [LOCAL] TIME ZONE. It also works correctly when using DBMS_XMLGEN with dynamic SQL. I need this working for Oracle Database 10g Release 1 and newer, supporting Express Edition as well (so no Java stored procedures). Preferably, the solution is in PL/SQL, not using a callout in C/C++ as that would require access to C/C++ compilers on all sorts of platforms. Also, the solution cannot alter the REF CURSOR query, so suggesting converting the DATE columns using TO_CHAR is not an acceptable solution.This challenge closes at Monday 14 February 2011 23:59:59 UTC.
The Desired Solution
Make iterations 1 and 3 in the SQL*Plus script below produce the XML for theD column in the correct format (the correct value '2011-02-01T14:25:30' instead of the incorrect value '01.02.2011 14:25:30'). It must be a generic solution, so using DBMS_DESCRIBE to describe the REF CURSOR columns and dynamically reformatting any DATE values is not good enough, also because we cannot be certain that the NLS_DATE_FORMAT in place when opening the REF CURSOR incorporates time values.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';
prompt Checks NLS formats
select sysdate,
localtimestamp,
systimestamp
from dual;
declare
--- REF CURSOR.
rc sys_refcursor;
--- DBMX_XMLGEN context.
context dbms_xmlgen.ctxtype;
/**
* XML Schema compliant datetime format, no fraction, no time zone
* (Oracle DATE).
*/
xml_nls_date_format constant varchar2(23) :=
'yyyy-mm-dd"T"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"T"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"T"hh24:mi:ss.ff9tzh:tzm';
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;
--- Query.
query varchar2(1000);
--- Generated XML.
xml xmltype;
xmlc clob;
/**
* Wrapper for DBMS_OUTPUT.PUT_LINE, in order to be able to print text larger
* than 255 characters for Oracle 10.1 and earlier.
* @param c CLOB to be put on DBMS_OUTPUT.
*/
procedure dbms_output_put_line(c in clob) as
i integer := 1;
j integer;
l integer := nvl(length(c), 0);
o integer;
begin
while i <= l loop
-- Try to find next LF.
j := instr(c, chr(10), i);
if j = 0 or j - i >= 255 then
-- Too long or no LF found, need to cut off.
j := i + 255;
o := 0;
else
o := 1;
end if;
dbms_output.put_line(substr(c, i, j - i));
i := j + o;
end loop;
if l = 0 then
dbms_output.put_line('');
end if;
end dbms_output_put_line;
/**
* 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 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;
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;
query :=
q'[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]';
for i in 1 .. 6 loop
-- Iteration 1: Sets NLS after REF CURSOR has been opened. XMLTYPE returned.
-- Iteration 2: Sets NLS before REF CURSOR has been opened. XMLTYPE returned.
-- Iteration 3: Sets NLS after REF CURSOR has been opened. CLOB returned.
-- Iteration 4: Sets NLS before REF CURSOR has been opened. CLOB returned.
-- Iteration 5: Dynamic SQL. XMLTYPE returned.
-- Iteration 6: Dynamic SQL. CLOB returned.
if i in (2, 4) then
set_xml_nls_formats;
end if;
if i in (1, 2, 3, 4) then
open rc for query;
end if;
if i in (1, 3, 5, 6) then
set_xml_nls_formats;
end if;
if i in (1, 2, 3, 4) then
context := dbms_xmlgen.newcontext(rc);
else
context := dbms_xmlgen.newcontext(query);
end if;
dbms_xmlgen.setnullhandling(context, dbms_xmlgen.empty_tag);
dbms_output.put_line('Iteration ' || i);
if i in (1, 2, 5) then
xml := dbms_xmlgen.getxmltype(context);
dbms_output_put_line(xml.getclobval);
else
xmlc := dbms_xmlgen.getxml(context);
dbms_output_put_line(xmlc);
end if;
dbms_xmlgen.closecontext(context);
set_session_nls_formats;
end loop;
end;
/
prompt Checks NLS formats
select sysdate,
localtimestamp,
systimestamp
from dual;The script will produce the following output for the anonymous PL/SQL block:Iteration 1 <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>01.02.2011 14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>01.02.2011 14:25:30</D_TC> <T1_TC>01.02.2011 14:25:30.123456789</T1_TC> <T2_TC>01.02.2011 14:25:30.123456789 +02:00</T2_TC> </ROW> </ROWSET> Iteration 2 <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>2011-02-01T14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>2011-02-01T14:25:30</D_TC> <T1_TC>2011-02-01T14:25:30.123456789</T1_TC> <T2_TC>2011-02-01T14:25:30.123456789+02:00</T2_TC> </ROW> </ROWSET> Iteration 3 <?xml version="1.0"?> <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>01.02.2011 14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>01.02.2011 14:25:30</D_TC> <T1_TC>01.02.2011 14:25:30.123456789</T1_TC> <T2_TC>01.02.2011 14:25:30.123456789 +02:00</T2_TC> </ROW> </ROWSET> Iteration 4 <?xml version="1.0"?> <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>2011-02-01T14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>2011-02-01T14:25:30</D_TC> <T1_TC>2011-02-01T14:25:30.123456789</T1_TC> <T2_TC>2011-02-01T14:25:30.123456789+02:00</T2_TC> </ROW> </ROWSET> Iteration 5 <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>2011-02-01T14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>2011-02-01T14:25:30</D_TC> <T1_TC>2011-02-01T14:25:30.123456789</T1_TC> <T2_TC>2011-02-01T14:25:30.123456789+02:00</T2_TC> </ROW> </ROWSET> Iteration 6 <?xml version="1.0"?> <ROWSET> <ROW> <ID>1</ID> <N>123.456</N> <VC>abcDEF</VC> <D>2011-02-01T14:25:30</D> <T1>2011-02-01T14:25:30.123456789</T1> <T2>2011-02-01T14:25:30.123456789+02:00</T2> <D_TC>2011-02-01T14:25:30</D_TC> <T1_TC>2011-02-01T14:25:30.123456789</T1_TC> <T2_TC>2011-02-01T14:25:30.123456789+02:00</T2_TC> </ROW> </ROWSET>So why do I bother? Because I've written a package
XML_GEN that can be used to generated XML off dynamic queries, REF CURSORS, ANYDATA instances etc and I would like to avoid that callers need to call my package like thisdeclare
rc sys_refcursor;
xml xmltype;
begin
xml_gen.set_xml_nls_formats;
open rc for
select sysdate
from dual;
xml := xml_gen.from_ref_cursor(rc);
xml_gen.set_session_nls_formats;
exception
when others then
xml_gen.set_session_nls_formats;
raise;
end;
/but instead this should suffice:declare rc sys_refcursor; xml xmltype; begin open rc for select sysdate from dual; xml := xml_gen.from_ref_cursor(rc); end; /Much cleaner and less error prone. I've written about this on my own blog: Converting Between Oracle Data and XML.
I hope the challenge is clear. If not, please ask.
Thanks in advance for your assistance and best of luck in winning the USD 100 gift card.
Best regards
Finn