Oracle extract values from xmltype - oracle

This is the code I am currently using:
SET serveroutput ON
CREATE OR REPLACE
PROCEDURE test_proc(i_xml varchar2)
IS
l_name VARCHAR2(20);
l_age NUMBER;
l_xml xmltype;
BEGIN
l_xml := xmltype(i_xml);
FOR x IN
(SELECT VALUE(p) col_val
FROM TABLE(XMLSEQUENCE(EXTRACT(l_xml, '/ROWSET/ROW'))) p
)
LOOP
IF x.col_val.existSNode('/ROW/name/text()') > 0 THEN
l_name:= x.col_val.EXTRACT('/ROW/name/text()').getstringVal();
END IF;
IF x.col_val.existSNode('/ROW/age/text()') > 0 THEN
l_age := x.col_val.EXTRACT('/ROW/age/text()').getstringVal();
END IF;
end loop;
end;
/
BEGIN
test_proc('<ROWSET>
<ROW>
<name>aa</name>
<age>20</age>
</ROW>
<ROW>
<name>bbb</name>
<age>25</age>
</ROW>
</ROWSET>');
END;
/
The above code uses xml to extract and save the existing node values to particular local variables. It is been used in the case for multiple sets of data and is working fine. I just wanted to know whether can I able to use the same without "for x loop", because I will only have one data in the i_xml from now onwards and I will only have either
name or age tags .
The following code should be used to save into l_name or l_age without the "loop" method like I used above:
<ROWSET>
<ROW>
<name>aa</name>
</ROW>
</ROWSET>
or
<ROWSET>
<ROW>
<age>18</age>
</ROW>
</ROWSET>
/
And I've tried using the following:
SELECT
CASE
WHEN VALUE(p).existsNode('/ROW/name/text()') = 1
THEN p.EXTRACT('/ROW/name/text()').getstringVal()
WHEN VALUE(P).existsNode('/ROW/age/text()') = 1
THEN p.EXTRACT('/ROW/age/text()').getstringVal()
END
INTO l_new
FROM TABLE(xmlsequence(EXTRACT(l_xml, '/ROWSET/ROW'))) p;
/
Any better way is appreciated.. Thanks

If you're really sure you'll only have one ROW then you can do:
begin
l_xml := xmltype(i_xml);
if l_xml.existsnode('/ROWSET/ROW/name') > 0 then
l_name := l_xml.extract('/ROWSET/ROW/name/text()').getstringval();
end if;
if l_xml.existsnode('/ROWSET/ROW/age') > 0 then
l_age := l_xml.extract('/ROWSET/ROW/age/text()').getnumberval();
end if;
end;
That will work if you have name or age, or both, or neither (where 'work' means doesn't error, at least). If you did have more than one row it would concatenate the results, so with your original data, l_name would be aabbb, and l_age would be 2025. Which might not be what you expect.

Related

Adding an attribute to all nodes matching an XPATH expression using Oracle XML DB

I cannot come to a solution to this task: my goal is to pass a cursor to a PL/SQL procedure and get the results as an XMLType. The function dbms_xmlgen.getxmltype() makes this task straightforward
<ROWSET>
<ROW>
<FIRST_NAME>John</FIRST_NAME>
<LAST_NAME>Goodman</LAST_NAME>
<HIRE_DATE>22-JUN-2011</HIRE_DATE>
</ROW>
</ROWSET>
Now I want to add the cursor column data type as an attribute to each corresponding XML element.
<ROWSET>
<ROW>
<FIRST_NAME type="VARCHAR2">John</FIRST_NAME>
<LAST_NAME type="VARCHAR2">Goodman</LAST_NAME>
<HIRE_DATE type="DATE">22-JUN-2011</HIRE_DATE>
</ROW>
</ROWSET>
This could be done using dynamic SQL, so I can write a PL/SQL function to get an associative array mapping each column to the corresponding data type.
Supposing I have both the aforementioned associativa array and the XMLType, how can I apply a set of transformations using a XPATH expression such as
-- pseudocode ;)
func(myXMLType, '//FIRST_NAME', ?add attribute to the matching node?)
Any other approach to get the job done will be fine
You could convert your metadata information to its own XML representation, and then have an XPath that finds the matching entry:
select *
from xmltable('for $i in $x/ROWSET return (element {"ROWSET"} {
for $j in $i/ROW
return (element {"ROW"} {
for $k in $j/*
return (element {$k/name()} {
attribute type { $m/metadata/column[#name=$k/name()]/#type },
$k/text()
} )
} )
} )'
passing generated_xml as "x", metadata_xml as "m"
columns result xmltype path '.');
Each ROWSET (there's only one, of course) generates a new ROWSET element; then each ROW under that generates a new ROW element; then each mode under that generates a new node with the same name and value, but the name is also used to find the matching entry in the metadata and extract it's type attribute and use it as an attribute for this node instead.
A worked example:
create or replace function cursor_to_xml(p_cursor sys_refcursor) return xmltype is
l_cursor sys_refcursor;
l_ctx dbms_xmlgen.ctxhandle;
l_xmltype xmltype;
l_cursor_num pls_integer;
l_col_cnt pls_integer;
l_desc_tab dbms_sql.desc_tab2;
l_metadata varchar2(32767);
l_result xmltype;
begin
-- get generated XMl as shown in the question
l_cursor := p_cursor;
l_ctx := dbms_xmlgen.newcontext(querystring => l_cursor);
l_xmltype := dbms_xmlgen.getxmltype(ctx => l_ctx);
dbms_xmlgen.closecontext(ctx => l_ctx);
-- use DBMS_SQL to get the data types
l_cursor_num := dbms_sql.to_cursor_number(rc => l_cursor);
dbms_sql.describe_columns2(c => l_cursor_num, col_cnt => l_col_cnt,
desc_t => l_desc_tab);
dbms_sql.close_cursor(l_cursor_num);
-- manually create an XML version of the column name/data type mappings
-- which could be extended easily to include length/scale/precision/etc.
l_metadata := '<metadata>';
for i in 1..l_desc_tab.count loop
l_metadata := l_metadata || '<column name="' || l_desc_tab(i).col_name ||
'" type="' || case l_desc_tab(i).col_type
when 1 then 'VARCHAR2'
when 2 then 'NUMBER'
when 12 then 'DATE'
-- ...
end
|| '"/>';
end loop;
l_metadata := l_metadata || '</metadata>';
-- use XMLTable with an XPath that deconstructs and reconstructs the
-- generated XML to add an attribute with the type; this is passed two
-- XML objects, referred to internally as $x and $m
-- xmlserialize() formats the result with indentation; xmltype then just
-- gets it back to that type - you may not need either really
select xmltype(xmlserialize(document result as varchar2(4000) indent))
into l_result
from xmltable('for $i in $x/ROWSET return (element {"ROWSET"} {
for $j in $i/ROW
return (element {"ROW"} {
for $k in $j/*
return (element {$k/name()} {
attribute type { $m/metadata/column[#name=$k/name()]/#type },
$k/text()
} )
} )
} )'
passing l_xmltype as "x", xmltype(l_metadata) as "m"
columns result xmltype path '.');
return l_result;
end cursor_to_xml;
/
Then an block that generates a cursor - similar to your example but with two rows just to check that works - and then calls the function to get the modified XML:
set serveroutput on;
declare
l_cursor sys_refcursor;
begin
open l_cursor for
select cast('John' as varchar2(10)) as first_name,
cast('Goodman' as varchar2(10)) as last_name,
date '2011-06-22' as hire_date
from dual
union all
select cast('Rhea' as varchar2(10)) as first_name,
cast('Perlman' as varchar2(10)) as last_name,
date '2012-07-23' as hire_date
from dual;
dbms_output.put_line(cursor_to_xml(l_cursor).getstringval);
end;
/
PL/SQL procedure successfully completed.
<ROWSET>
<ROW>
<FIRST_NAME type="VARCHAR2">John</FIRST_NAME>
<LAST_NAME type="VARCHAR2">Goodman</LAST_NAME>
<HIRE_DATE type="DATE">22-JUN-11</HIRE_DATE>
</ROW>
<ROW>
<FIRST_NAME type="VARCHAR2">Rhea</FIRST_NAME>
<LAST_NAME type="VARCHAR2">Perlman</LAST_NAME>
<HIRE_DATE type="DATE">23-JUL-12</HIRE_DATE>
</ROW>
</ROWSET>
You may want more data types defined in the CASE, of course.

How to populate nested object table in pl/sql block?

I struggle a problem, which, i think, is rather simple.
I have a type T_OPERATION_TAG in a database which is created as:
CREATE OR REPLACE TYPE t_operation_tag AS OBJECT(
tag_name VARCHAR2(30),
tag_value VARCHAR2(30),
CONSTRUCTOR FUNCTION t_operation_tag RETURN SELF AS RESULT
)
I also have another type T_OPERATION_TAGS, which is defined as follows
CREATE OR REPLACE TYPE t_operation_tags AS TABLE OF t_operation_tag;
Then in my pl/sql block i have the following code
DECLARE
p_op_tags t_operation_tags;
BEGIN
p_op_tags := t_operation_tags();
FOR i IN (SELECT tag_name, tag_value
FROM op_tags_table
WHERE some_condition)
LOOP
--How to append new lines to p_op_tags ?
END LOOP;
END;
So, if the SELECT-query in the FOR LOOP returns,e.g., five lines then how can I populate my P_OP_TAGS object table with these five lines?
Like this:
DECLARE
p_op_tags t_operation_tags;
p_cursor sys_refcursor;
p_limit number := 5;
BEGIN
open p_cursor for
SELECT t_operation_tag(tag_name, tag_value)
FROM op_tags_table
;
fetch p_cursor bulk collect into p_op_tags limit p_limit;
DBMS_OUTPUT.put_line(p_op_tags(4).tag_name);
close p_cursor;
END;
Or if you prefer the loop clause:
DECLARE
p_op_tag t_operation_tag;
p_op_tags t_operation_tags;
p_limit number := 5;
BEGIN
p_op_tags := t_operation_tags();
for i in (SELECT tag_name, tag_value
FROM op_tags_table
WHERE some_condition
and rownum < p_limit + 1)
loop
p_op_tag := t_operation_tag(i.tag_name, i.tag_value);
p_op_tags.extend();
p_op_tags(p_op_tags.COUNT) := p_op_tag;
end loop;
DBMS_OUTPUT.put_line(p_op_tags(4).tag_name);
END;
/
You don't really need a cursor or loop at all, if you're populating the collection entirely from your query; you can bulk collect straight into it:
DECLARE
p_op_tags t_operation_tags;
BEGIN
SELECT t_operation_tag(tag_name, tag_value)
BULK COLLECT INTO p_op_tags
FROM op_tags_table
WHERE some_condition;
...
END;
/

How to loop through a delimited list in Oracle PLSQL

I am working on an Oracle procedure that calls another procedure within it. One of my parameters (parm1) can contain one or more values in a comma separated list. How can I loop through these values to pass them one at a time to another procedure?
Here is an example of what I would like it to do:
When Parm1 = 123,312
callProcedure2(123)
callProcedure2(321)
-or-
When Parm1 123
callProcedure2(123)
I think this can be accomplished using a loop but I can't figure out how to get it to use each value as a separated call within the loop.
Any help would be appreciated!
Thanks!
CURSOR V_CUR IS
select regexp_substr(Parm1 ,'[^,]+', 1, level) As str from dual
connect by regexp_substr(Parm1, '[^,]+', 1, level) is not null;
This curor will give you result like this
123
321
Now iterate the cursor and call the procedure in loop.
For i IN V_CUR
LOOP
callProdcedure2(i.str);
END LOOP;
Just loop through substrings:
declare
parm1 varchar2(1000) := '123,234,345,456,567,789,890';
vStartIdx binary_integer;
vEndIdx binary_integer;
vCurValue varchar2(1000);
begin
vStartIdx := 0;
vEndIdx := instr(parm1, ',');
while(vEndIdx > 0) loop
vCurValue := substr(parm1, vStartIdx+1, vEndIdx - vStartIdx - 1);
-- call proc here
dbms_output.put_line('->'||vCurValue||'<-');
vStartIdx := vEndIdx;
vEndIdx := instr(parm1, ',', vStartIdx + 1);
end loop;
-- Call proc here for last part (or in case of single element)
vCurValue := substr(parm1, vStartIdx+1);
dbms_output.put_line('->'||vCurValue||'<-');
end;
There is a utility procedure COMMA_TO_TABLE and array type DBMS_UTILITY.UNCL_ARRAY dedicated for this task. Since Oracle 10g.
It is well document here.
Here is a sample solution:
SET SERVEROUTPUT ON
DECLARE
csvListElm VARCHAR2(4000) := 'elm1, elm2,elm3 ,elm4 , elm5';
csvListTable DBMS_UTILITY.UNCL_ARRAY;
csvListLen BINARY_INTEGER;
currTableName VARCHAR2(222);
BEGIN
DBMS_UTILITY.COMMA_TO_TABLE(csvListElm, csvListLen, csvListTable);
FOR csvElm IN 1..(csvListTable.COUNT - 1) LOOP
dbms_output.put_line('-- CSV element : <'||csvListTable(csvElm)||'>');
dbms_output.put_line('-- Trimmed CSV element: <'||trim(csvListTable(csvElm))||'>');
END LOOP;
END;
/
Sample output:
-- CSV element : <elm1>;
-- Trimmed CSV element: <elm1>;
-- CSV element : < elm2>;
-- Trimmed CSV element: <elm2>;
-- CSV element : <elm3 >;
-- Trimmed CSV element: <elm3>;
-- CSV element : <elm4 >;
-- Trimmed CSV element: <elm4>;
-- CSV element : < elm5>;
-- Trimmed CSV element: <elm5>;
It is possible to use a function that you can use in a for loop (without regexp for ThinkJet):
Create a type and function
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
CREATE OR REPLACE
FUNCTION cto_table(p_sep in Varchar2, p_list IN VARCHAR2)
RETURN t_my_list
AS
l_string VARCHAR2(32767) := p_list || p_sep;
l_sep_index PLS_INTEGER;
l_index PLS_INTEGER := 1;
l_tab t_my_list := t_my_list();
BEGIN
LOOP
l_sep_index := INSTR(l_string, p_sep, l_index);
EXIT
WHEN l_sep_index = 0;
l_tab.EXTEND;
l_tab(l_tab.COUNT) := TRIM(SUBSTR(l_string,l_index,l_sep_index - l_index));
l_index := l_sep_index + 1;
END LOOP;
RETURN l_tab;
END cto_table;
/
Then how to call it in the for loop:
DECLARE
parm1 varchar2(4000) := '123,234,345,456,567,789,890';
BEGIN
FOR x IN (select * from (table(cto_table(',', parm1)) ) )
LOOP
dbms_output.put_line('callProdcedure2 called with ' || x.COLUMN_VALUE);
callProdcedure2(x.COLUMN_VALUE);
END LOOP;
END;
/
Notice the default name COLUMN_VALUE given by Oracle, which is necessary for the use I want to make of the result.
Result as expected:
callProdcedure2 called with 123
callProdcedure2 called with 234
...

how to set the empty tag in generating xml from oracle table

SET PAGES 0;
SET LINE 1000;
SET LONG 9999999;
SPOOL C:\pensionnew.xml;
col foo format a60000
SELECT DBMS_XMLGEN.GETXML('SELECT * FROM DATAAG')foo FROM DUAL;
SPOOL OFF;
i use this code for generating xml.. the answer is:suppose the the column not have value means the tag wont came . i need empty tag for that for that
use the API for it, dont use the quick fire getxml(string) version.
eg:
SQL> variable xml clob;
SQL> declare
2 ctx number;
3 begin
4 dbms_lob.createtemporary(:xml, true, dbms_lob.call);
5 ctx := dbms_xmlgen.newcontext('select * from foo');
6 dbms_xmlgen.setnullhandling(ctx, dbms_xmlgen.EMPTY_TAG);
7 dbms_xmlgen.getxml(ctx, :xml);
8 dbms_xmlgen.closecontext(ctx);
9 end;
10 /
PL/SQL procedure successfully completed.
SQL> print xml
XML
--------------------------------------------------------------------------------
<?xml version="1.0"?>
<ROWSET>
<ROW>
<ID>1</ID>
<A>a</A>
</ROW>
<ROW>
<ID>2</ID>
<A/>
</ROW>
</ROWSET>

How can a table be returned from an Oracle function without a custom type or cursor?

I am looking to return a table of results from an Oracle function. Using a cursor would be easiest, but the application I have to work this into will not accept a cursor as a return value. The alternative is to create a type (likely wrapped in a package) to go along with this function. However, it seems somewhat superfluous to create several types (I have 4+ functions to write) just so I can return table results. Is there an alternative that I am missing?
UPDATE: See the first comment for TABLE solution withou limit of size.
Return an VARRAY or use a PIPELINED function to query from them.
For VARRAY look in this article for a detail. Code example from there:
CREATE OR REPLACE TYPE EMPARRAY is VARRAY(20) OF VARCHAR2(30)
/
CREATE OR REPLACE FUNCTION getEmpArray RETURN EMPARRAY
AS
l_data EmpArray := EmpArray();
CURSOR c_emp IS SELECT ename FROM EMP;
BEGIN
FOR emp_rec IN c_emp LOOP
l_data.extend;
l_data(l_data.count) := emp_rec.ename;
END LOOP;
RETURN l_data;
END;
For PiPELINED functions checkout here. Code example:
create or replace function Lookups_Fn return lookups_tab
pipelined
is
v_row lookup_row;
begin
for j in 1..10
loop
v_row :=
case j
when 1 then lookup_row ( 1, 'one' )
--...
when 7 then lookup_row ( 7, 'seven' )
else lookup_row ( j, 'other' )
end;
pipe row ( v_row );
end loop;
return;
end Lookups_Fn;
/
select * from table ( Lookups_Fn );
You could always return XML from your function if that suits the application developers.
XML can be generated a number of ways in Oracle, depending on what you have installed and what version you're using.
XMLTYPE is very useful in certain contexts, and can be generated from SQL using the XMLElement, XMLAttributes, XMLAgg, etc. built-in functions. If the client doesn't support XMLTYPE it can easily be cast to CLOB values.
Perhaps the simplest, though not the best (IMO) option is to use the dbms_xmlgen package:
SQL> set serveroutput on size 1000;
SQL> exec dbms_output.put_line( dbms_xmlgen.getXML( 'select * from dual' ) );
Output:
<?xml version="1.0"?>
<ROWSET>
<ROW>
<DUMMY>X</DUMMY>
</ROW>
</ROWSET>
That gives you your "table" results in a single CLOB value.

Resources