Passing a comma delimited list into XMLQuery - sql

I am trying to pass a list of node names into my XMLQuery so I can delete all of the nodes except the ones in the list. When I hard code the list it works fine but when I pass it as an additional parameter it does not work...
This is the XML I am reading in:
<Activity>
<Math>
<ExpressionInteger2>7</ExpressionInteger2>
<ExpressionInteger3>1</ExpressionInteger3>
<ExpressionInteger4>5</ExpressionInteger4>
<ExpressionInteger0>1</ExpressionInteger0>
<ExpressionInteger1>1</ExpressionInteger1>
<ExpressionInteger6>-2</ExpressionInteger6>
<ExpressionInteger7>670000000</ExpressionInteger7>
</Math>
</Activity>
Then this is the query I am using to parse it where Activity.XMLData equal the XML and KeepFields holds the only node names I want to keep.
SELECT XMLQuery('copy $i := $p1
modify (delete node $i/Activity/Math/*[not(name()=($p2))])
return $i' PASSING BY VALUE XMLType(ActivityTable.XMLData) AS "p1", CAST(KeepList AS VARCHAR2(1000)) AS "p2" RETURNING CONTENT),
FROM ActivityTable
JOIN (SELECT VariableKey, '"' || LISTAGG(VariableName, '","') WITHIN GROUP (ORDER BY VariableName) || '"' AS KeepList
FROM KeepMathVariable
GROUP BY VariableKey) KeepFields ON KeepFields.VariableKey = ActivityTable.VariableKey
WHERE ActivityTable.VariableKey = 'DF6D6BB0-0BFF-4C9B-8251-374831DAD19E'
If I replace $p2 with the hard coded list "ExpressionInteger1","ExpressionInteger3" it will properly delete everything but those two nodes.
The result of the query should be :
<Activity>
<Math>
<ExpressionInteger3>1</ExpressionInteger3>
<ExpressionInteger1>1</ExpressionInteger1>
</Math>
</Activity>
This is the hardcoded working query:
SELECT XMLQuery('copy $i := $p1
modify (delete node $i/Activity/Math/*[not(name()=("ExpressionInteger1","ExpressionInteger3"))])
return $i' PASSING BY VALUE XMLType(ActivityTable.XMLData) AS "p1" RETURNING CONTENT),
FROM ActivityTable
WHERE ActivityTable.VariableKey = 'DF6D6BB0-0BFF-4C9B-8251-374831DAD19E'

When you put a variable into XQuery string, it is treated not as just replacement for some part of text, but as some atomic data. So providing a concatenated text that text is treated as a whole value and results in not what you wanted.
One possible way is to use tokenize() function to turn the string into array of strings right in XQuery.
with ActivityTable as (
/*base table with data*/
select '<Activity>
<Math>
<ExpressionInteger2>7</ExpressionInteger2>
<ExpressionInteger3>1</ExpressionInteger3>
<ExpressionInteger4>5</ExpressionInteger4>
<ExpressionInteger0>1</ExpressionInteger0>
<ExpressionInteger1>1</ExpressionInteger1>
<ExpressionInteger6>-2</ExpressionInteger6>
<ExpressionInteger7>670000000</ExpressionInteger7>
</Math>
</Activity>' as XMLData
, 1 as VariableKey
from dual
)
, KeepMathVariable as (
/*base table with keep variables*/
select 'ExpressionInteger1' as VariableName
, 1 as VariableKey
from dual
union all
select 'ExpressionInteger3', 1
from dual
)
, sep as (
/*separator for consistency in tokenize and listagg*/
select '#' as sep from dual
)
, KeepFields as (
/*build field list*/
select
v.VariableKey
, listagg(v.VariableName, s.sep) within group(order by 1) as KeepList
from KeepMathVariable v
cross join sep s
group by v.VariableKey, s.sep
)
select
/*Pretty-printing*/
XMLSerialize( content
/*Split separated list from KeepFields by separator into string array*/
XMLQuery('copy $i := $p1
modify (delete node $i/Activity/Math/*[not(name()=tokenize($p2, $p3))])
return $i'
PASSING BY value
XMLType(ActivityTable.XMLData) AS "p1",
KeepFields.KeepList as "p2",
s.sep as "p3"
RETURNING CONTENT
)
indent size = 2
) as res
from ActivityTable
join KeepFields
on ActivityTable.VariableKey = KeepFields.VariableKey
cross join sep s
| RES |
| :----------------------------------------------|
| <Activity> |
| <Math> |
| <ExpressionInteger3>1</ExpressionInteger3> |
| <ExpressionInteger1>1</ExpressionInteger1> |
| </Math> |
| </Activity> |
db<>fiddle here
Another option could be playing with passing collection type, but I didn't manage it to work.

I got it working with Tokenize
SELECT XMLQuery('copy $i := $p1
modify (delete node $i/Activity/Math/*[not(name()=(tokenize($p2)))])
return $i' PASSING BY VALUE XMLType(ActivityTable.XMLData) AS "p1", KeepList AS "p2" RETURNING CONTENT),
FROM ActivityTable
JOIN (SELECT LISTAGG(VariableName, ',') WITHIN GROUP (ORDER BY VariableName) AS KeepList
FROM KeepMathVariable
GROUP BY VariableKey) KeepFields ON KeepFields.VariableKey = ActivityTable.VariableKey
WHERE ActivityTable.VariableKey = 'DF6D6BB0-0BFF-4C9B-8251-374831DAD19E'

Related

How to use Oracle JSON_VALUE

I'm working on a trigger.
declare
v_time number(11, 0);
begin
for i in 0..1
loop
select JSON_VALUE(body, '$.sections[0].capsules[0].timer.seconds') into v_time from bodycontent where contentid=1081438;
dbms_output.put_line(v_time);
end loop;
end;
However, index references do not become dynamic.
like JSON_VALUE(body, '$.sections[i].capsules[i].timer.seconds')
Is there any way I can do this?
You can use JSON_TABLE:
declare
v_time number(11, 0);
begin
for i in 0..1 loop
SELECT time
INTO v_time
FROM bodycontent b
CROSS APPLY
JSON_TABLE(
b.body,
'$.sections[*]'
COLUMNS (
section_index FOR ORDINALITY,
NESTED PATH '$.capsules[*]'
COLUMNS (
capsule_index FOR ORDINALITY,
time NUMBER(11,0) PATH '$.timer.seconds'
)
)
) j
WHERE j.section_index = i + 1
AND j.capsule_index = i + 1
AND b.contentid=1081438;
dbms_output.put_line(v_time);
end loop;
end;
/
Which, for the test data:
CREATE TABLE bodycontent ( body CLOB CHECK ( body IS JSON ), contentid NUMBER );
INSERT INTO bodycontent ( body, contentid ) VALUES (
'{"sections":[
{"capsules":[{"timer":{"seconds":0}},{"timer":{"seconds":1}},{"timer":{"seconds":2}}]},
{"capsules":[{"timer":{"seconds":3}},{"timer":{"seconds":4}},{"timer":{"seconds":5}}]},
{"capsules":[{"timer":{"seconds":6}},{"timer":{"seconds":7}},{"timer":{"seconds":8}}]}]}',
1081438
);
Outputs:
0
4
Or, you can just use a query:
SELECT section_index, capsule_index, time
FROM bodycontent b
CROSS APPLY
JSON_TABLE(
b.body,
'$.sections[*]'
COLUMNS (
section_index FOR ORDINALITY,
NESTED PATH '$.capsules[*]'
COLUMNS (
capsule_index FOR ORDINALITY,
time NUMBER(11,0) PATH '$.timer.seconds'
)
)
) j
WHERE ( j.section_index, j.capsule_index) IN ( (1,1), (2,2) )
AND b.contentid=1081438;
Which outputs:
SECTION_INDEX | CAPSULE_INDEX | TIME
------------: | ------------: | ---:
1 | 1 | 0
2 | 2 | 4
(Note: the indexes from FOR ORDINALITY are 1 higher than the array indexes in the JSON path.)
db<>fiddle here
You would need to concatenate the variable in the json path:
JSON_VALUE(body, '$.sections[' || to_char(i) || '].capsules[0].timer.seconds')
I don't really see how your question relates to a trigger.
So, the problem is that you want to loop over an index (to go diagonally through the array of arrays, picking up just two elements) - but the JSON_* functions don't work with variables as array indices - they require hard-coded indexes.
PL/SQL has an answer for that - native dynamic SQL, as demonstrated below.
However, note that this approach makes repeated calls to JSON_VALUE() over the same document. Depending on the actual requirement (I assume the one in your question is just for illustration) and the size of the document, it may be more efficient to make a single call to JSON_TABLE(), as illustrated in MT0's answer. If in fact you are indeed only extracting two scalar values from a very large document, two calls to JSON_VALUE() may be justified, especially if the document is much larger than shown here; but if you are extracting many scalar values from a document that is not too complicated, then a single call to JSON_TABLE() may be better, even if in the end you don't use all the data it produces.
Anyway - as an illustration of native dynamic SQL, here's the alternative solution (using MT0's table):
declare
v_time number(11, 0);
begin
for i in 0..1 loop
execute immediate q'#
select json_value(body, '$.sections[#' || i ||
q'#].capsules[#' || i || q'#].timer.seconds')
from bodycontent
where contentid = 1081438#'
into v_time;
dbms_output.put_line(v_time);
end loop;
end;
/
0
4
PL/SQL procedure successfully completed.
You can see the example below:
Just hit this query in your console and try to understand the implementation.
SELECT JSON_VALUE('{
"increment_id": "2500000043",
"item_id": "845768",
"options": [
{
"firstname": "Kevin"
},
{
"firstname": "Okay"
},
{
"lastname": "Test"
}
]
}', '$.options[0].firstname') AS value
FROM DUAL;

How to Insert data using cursor into new table having single column containing XML type data in Oracle?

I'm able to insert values into table 2 from table 1 and execute the PL/SQL procedure successfully but somehow the output is clunky. I don't know why?
Below is the code :
create table airports_2_xml
(
airport xmltype
);
declare
cursor insert_xml_cr is select * from airports_1_orcl;
begin
for i in insert_xml_cr
loop
insert into airports_2_xml values
(
xmlelement("OneAirport",
xmlelement("Rank", i.Rank) ||
xmlelement("airport",i.airport) ||
xmlelement("Location",i.Location) ||
xmlelement("Country", i.Country) ||
xmlelement("Code_iata",i.code_iata) ||
xmlelement("Code_icao", i.code_icao) ||
xmlelement("Total_Passenger",i.Total_Passenger) ||
xmlelement("Rank_change", i.Rank_change) ||
xmlelement("Percent_Change", i.Percent_change)
));
end loop;
end;
/
select * from airports_2_xml;
Output:
Why it is showing &lt ,&gt in the output ? And why am I unable to see the output fully?
Expected output:
<OneAirport>
<Rank>3</Rank>
<Airport>Dubai International</Airport>
<Location>Garhoud</Location>
<Country>United Arab Emirates</Country>
<Code_IATA>DXB</Code_IATA>
<Code_ICAO>OMDB</Code_ICAO>
<Total_passenger>88242099</Total_passenger>
<Rank_change>0</Rank_change>
<Percent_Change>5.5</Percent_Change>
</OneAirport>
The main issue is how you are constructnig the XML. You have an outer XMLElement for OneAirport, and the content of that element is a single string.
You are generating individual XMLElements from the cursor fields, but then you are concenating those together, which gives you a single string which still has the angle brackets you're expecting. So you're trying to do something like, simplified a bit:
select
xmlelement("OneAirport", '<Rank>1</Rank><airport>Hartsfield-Jackson</airport>')
from dual;
XMLELEMENT("ONEAIRPORT",'<RANK>1</RANK><AIRPORT>HARTSFIELD-JACKSON</AIRPORT>')
--------------------------------------------------------------------------------
<OneAirport><Rank>1</Rank><airport>Hartsfield-Jackson</airp
and by default XMLElement() escapes entities in the passed-in values, so the angle-brackets are being converted to 'safe' equivalents like <. If it didn't do that, or you told it not to with noentityescaping:
select xmlelement(noentityescaping "OneAirport", '<Rank>1</Rank><airport>Hartsfield-Jackson</airport>')
from dual;
XMLELEMENT(NOENTITYESCAPING"ONEAIRPORT",'<RANK>1</RANK><AIRPORT>HARTSFIELD-JACKS
--------------------------------------------------------------------------------
<OneAirport><Rank>1</Rank><airport>Hartsfield-Jackson</airport></OneAirport>
then that would appear to be better, but you still actually have a single element with a single string (with characters that are likely to cause problems down the line), rather than the XML structure you almost certainly intended.
A simple way to get an zctual structure is with XMLForest():
xmlelement("OneAirport",
xmlforest(i.Rank, i.airport, i.Location, i.Country, i.code_iata,
i.code_icao, i.Total_Passenger, i.Rank_change, i.Percent_change)
)
You don't need the cursor loop, or any PL/SQL; you can just do:
insert into airports_2_xml (airport)
select xmlelement("OneAirport",
xmlforest(i.Rank, i.airport, i.Location, i.Country, i.code_iata,
i.code_icao, i.Total_Passenger, i.Rank_change, i.Percent_change)
)
from airports_1_orcl i;
The secondary issue is the display. You'll see more data if you issue some formatting commands, such as:
set lines 120
set long 32767
set longchunk 32767
Those will tell your client to retrieve and show more of the long (XMLType here) data, rather the default 80 characters it's giving you now.
Once you are generating a nested XML structure you can use XMLSerialize() to display that more readable when you query your second table.
Try this below block :
declare
cursor insert_xml_cr is select * from airports_1_orcl;
v_airport_xml SYS.XMLTYPE;
begin
for i in insert_xml_cr
loop
SELECT XMLELEMENT ( "OneAirport",
XMLFOREST(i.Rank as "Rank"
,i.airport as "Airport"
,i.Location as "Location"
,i.Country as "Country"
,i.code_iata as "Code_iata"
,i.code_icao as "code_icao"
,i.Total_Passenger as "Total_Passenger"
, i.Rank_change as "Rank_change"
,i.Percent_change as "Percent_Change"
))
into v_airport_xml
FROM DUAL;
insert into airports_2_xml values (v_airport_xml);
end loop;
end;

Selecting one out of many strings with concatenation in Redshift

I have to match the following URLs by writing a query in Amazon Redshift:
<some url>/www.abc.com/a/<more url>
<some url>/www.abc.com/b/<more url>
<some url>/www.abc.com/c/<more url>
<some url>/www.abc.com/d/<more url>
Here, obviously the "/www.abc.com/" is constant, but the text after '/' can change. It can take one of the many values that I have a list of (a,b,c,d in this case). How do I match this part that comes immediately after "/www.abc.com/"?
I can think of the following:
select text,
case
when text ilike '%/www.abc.com/' || <what should go here?> || '/%' then 'URLType1'
when <some other condition> then 'URLType2'
end as URLType
from table
I have to maintain the CASE structure.
Any help would be much appreciated.
THe options are:
1) put the list of values into a subquery and then join to this list like this:
with
value_list as (
select 'a' as val union select 'b' union select 'c' union select 'd'
)
select text
from table
join value_list
on text ilike '%/www.abc.com/' || val || '/%'
2) use OR:
select text,
case
when text ilike '%/www.abc.com/a/%'
or text ilike '%/www.abc.com/b/%'
or text ilike '%/www.abc.com/c/%'
or text ilike '%/www.abc.com/d/%'
then 'URLType1'
when <some other condition>
then 'URLType2'
end as URLType
from table
3) Write a Python UDF that takes the url and the list and returns true or false like this:
CREATE OR REPLACE FUNCTION multi_ilike(str varchar(max),arr varchar(max))
RETURNS boolean
STABLE AS $$
if str==None or arr==None:
return None
arr = arr.split(',')
str = str.lower()
for i in arr:
if i.lower() in str:
return True
return False
$$ LANGUAGE plpythonu;
select multi_ilike('<some url>/www.abc.com/a/<more url>','/www.abc.com/a/,/www.abc.com/b/,/www.abc.com/c/,/www.abc.com/d/'); -- returns true
select multi_ilike('<some url>/www.abc.com/F/<more url>','/www.abc.com/a/,/www.abc.com/b/,/www.abc.com/c/,/www.abc.com/d/'); -- returns false

Oracle : String Concatenation is too long

I have below SQL as a part of a view. In one of the schema I am getting "String Concatenation is too long" error and not able to execute the view.
Hence I tried the TO_CLOB() and now VIEW is not throwing ERROR, but it not returning the result as well it keep on running..
Please suggest....
Sql:
SELECT Iav.Item_Id Attr_Item_Id,
LISTAGG(La.Attribute_Name
||'|~|'
|| Lav.Attribute_Value
||' '
|| Lau.Attribute_Uom, '}~}') WITHIN GROUP (
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME) AS ATTR
FROM Item_Attribute_Values Iav,
Loc_Attribute_Values Lav,
Loc_Attribute_Uoms Lau,
Loc_Attributes La,
(SELECT *
FROM Item_Classification Ic,
CATEGORY_ATTRIBUTES CA
WHERE IC.DEFAULT_CATEGORY='Y'
AND IC.TAXONOMY_TREE_ID =CA.TAXONOMY_TREE_ID
) ICA
WHERE IAV.ITEM_ID =ICA.ITEM_ID(+)
AND IAV.ATTRIBUTE_ID =ICA.ATTRIBUTE_ID(+)
AND Iav.Loc_Attribute_Id =La.Loc_Attribute_Id
AND La.Locale_Id =1
AND Iav.Loc_Attribute_Uom_Id =Lau.Loc_Attribute_Uom_Id(+)
AND Iav.Loc_Attribute_Value_Id=Lav.Loc_Attribute_Value_Id
GROUP BY Iav.Item_Id;
Error:
ORA-01489: result of string concatenation is too long
01489. 00000 - "result of string concatenation is too long"
*Cause: String concatenation result is more than the maximum size.
*Action: Make sure that the result is less than the maximum size.
You can use the COLLECT() function to aggregate the strings into a collection and then use a User-Defined function to concatenate the strings:
Oracle Setup:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
CREATE FUNCTION concat_List(
strings IN stringlist,
delim IN VARCHAR2 DEFAULT ','
) RETURN CLOB DETERMINISTIC
IS
value CLOB;
i PLS_INTEGER;
BEGIN
IF strings IS NULL THEN
RETURN NULL;
END IF;
value := EMPTY_CLOB();
IF strings IS NOT EMPTY THEN
i := strings.FIRST;
LOOP
IF i > strings.FIRST AND delim IS NOT NULL THEN
value := value || delim;
END IF;
value := value || strings(i);
EXIT WHEN i = strings.LAST;
i := strings.NEXT(i);
END LOOP;
END IF;
RETURN value;
END;
/
Query:
SELECT Iav.Item_Id AS Attr_Item_Id,
CONCAT_LIST(
CAST(
COLLECT(
La.Attribute_Name || '|~|' || Lav.Attribute_Value ||' '|| Lau.Attribute_Uom
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME
)
AS stringlist
),
'}~}'
) AS ATTR
FROM your_table
GROUP BY iav.item_id;
LISTAGG is limited to 4000 characters unfortunately. So you may want to use another approach to concatenate the values.
Anyway ...
It is strange to see LISTAGG which is a rather new feature combined with error-prone SQL1992 joins. I'd suggest you re-write this. Are the tables even properly joined? It looks strange that there seems to be no relation between Loc_Attributes and, say, Loc_Attribute_Values. Doesn't have Loc_Attribute_Values a Loc_Attribute_Id so an attribute value relates to an attribute? It would be hard to believe that there is no such relation.
Moreover: Is it guaranteed that your classification subquery doesn't return more than one record per attribute?
Here is your query re-written:
select
iav.item_id as attr_item_id,
listagg(la.attribute_name || '|~|' || lav.attribute_value || ' ' || lau.attribute_uom,
'}~}') within group (order by ica.disp_seq, la.attribute_name) as attr
from item_attribute_values iav
join loc_attribute_values lav
on lav.loc_attribute_value_id = iav.loc_attribute_value_id
and lav.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
join loc_attributes la
on la.loc_attribute_id = lav.loc_attribute_id
and la.loc_attribute_id = lav.loc_attribute_id -- <== maybe?
and la.locale_id = 1
left join loc_attribute_uoms lau
on lau.loc_attribute_uom_id = iav.loc_attribute_uom_id
and lau.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
left join
(
-- aggregation needed to get no more than one sortkey per item attribute?
select ic.item_id, ca.attribute_id, min (ca.disp_seq) as disp_seq
from item_classification ic
join category_attributes ca on ca.taxonomy_tree_id = ic.taxonomy_tree_id
where ic.default_category = 'y'
group by ic.item_id, ca.attribute_id
) ica on ica.item_id = iav.item_id and ica.attribute_id = iav.attribute_id
group by iav.item_id;
Well, you get the idea; check your keys and alter your join criteria where necessary. Maybe this gets rid of duplicates, so LISTAGG has to concatenate less attributes, and maybe the result even stays within 4000 characters.
Xquery approach.
Creating extra types or function isn't necessary.
with test_tab
as (select object_name
from all_objects
where rownum < 1000)
, aggregate_to_xml as (select xmlagg(xmlelement(val, object_name)) xmls from test_tab)
select xmlcast(xmlquery('for $row at $idx in ./*/text() return if($idx=1) then $row else concat(",",$row)'
passing aggregate_to_xml.xmls returning content) as Clob) as list_in_lob
from aggregate_to_xml;
I guess you need to write a small function to concatenate the strings into a CLOB, because even when you cast TO_CLOB() the LISTAGG at the end, this might not work.
HereĀ“s a sample-function that takes a SELECT-Statement (which MUST return only one string-column!) and a separator and returns the collected values as a CLOB:
CREATE OR REPLACE FUNCTION listAggCLob(p_stringSelect VARCHAR2
, p_separator VARCHAR2)
RETURN CLOB
AS
cur SYS_REFCURSOR;
s VARCHAR2(4000);
c CLOB;
i INTEGER;
BEGIN
dbms_lob.createtemporary(c, FALSE);
IF (p_stringSelect IS NOT NULL) THEN
OPEN cur FOR p_stringSelect;
LOOP
FETCH cur INTO s;
EXIT WHEN cur%NOTFOUND;
dbms_lob.append(c, s || p_separator);
END LOOP;
END IF;
i := length(c);
IF (i > 0) THEN
RETURN dbms_lob.substr(c,i-length(p_separator));
ELSE
RETURN NULL;
END IF;
END;
This function can be used f.e. like this:
WITH cat AS (
SELECT DISTINCT t1.category
FROM lookup t1
)
SELECT cat.category
, listAggCLob('select t2.name from lookup t2 where t2.category = ''' || cat.category || '''', '|') allcategorynames
FROM cat;

Oracle - Iterate over XMLTYPE And Return Nodes As Individual Rows

We have a table called audit1. It has a column 'changes' that contains XML as blob. The xml looks like below. The audit table basically records changes that happen to other tables via our app's UI.
<c>
<f n="VersNo" b="1" a="2"/>
<f n="LstDate" b="20160215" a="20160217"/>
<f n="FileSweepId" b="Test" a="Test1"/>
</c>
c stands for changes
f stands for field
n attribute stands for name (as in name of the field)
b stands for before value
a stands for after value
I need to create a report that lists all changes that have occurred since a given date to certain tables. Once I have the audit1 records I am interested in, I need to list all the f nodes in the report.
That is each f node in each of the relevant audit record needs to become a row in the report.
The report is generated by our app. What the app can take for generating the report is one of the following:
A SQL Query
Or name of a stored procedure that must return its result in a sys ref cursor which will be passed to the procedure when the app calls it. The ref cursor will be called out_cursor.
I can't think of a way to achieve this via a single sql query. So, I am going down the road of writing a stored proc.
The first problem I am facing is how to iterate over the f nodes in the procedure.
Secondly, I need to figure out how to return these f nodes along with other info from audit records in out_cursor.
BEGIN
FOR item IN (SELECT auditno, xmltype(changes, 1) as changes, extract(xmltype(changes, 1), '/c/f') as fields from audit1 where runlistno is null and rownum < 2 )
LOOP
dbms_output.put_line(item.auditno || ' ' || item.changes.getStringVal() || ' ' || item.fields.getStringVal());
-- stumped about how to iterate over the f nodes
--FOR field in ('select extractvalue(object_value, '/') x FROM TABLE(XMLSequence(' + item.fields.getStringVal() + ') ')
FOR field in (select f from XMLTable('for $i in / return $i' passing item.fields columns f varchar2(200) path 'f'))
LOOP
dbms_output.put_line(field.f);
END LOOP;
END LOOP;
END;
The above PL/SQL at present errors with:
ORA-19114: XPST0003 - error during parsing the XQuery expression:
LPX-00801: XQuery syntax error at 'i' 1 for $i in / return $i
- ^ ORA-06512: at line 6
Why don't you use simple SQL, with chained XMLTable functions that extract reqired fields ?
Look at simple example:
CREATE TABLE audit1(
changes CLOB
);
INSERT INTO audit1 VALUES( '<c>
<f n="VersNo" b="1" a="2"/>
<f n="LstDate" b="20160215" a="20160217"/>
<f n="FileSweepId" b="Test" a="Test1"/>
</c>'
);
INSERT INTO audit1 VALUES(
'<c>
<f n="VersNo" b="22" a="32"/>
<f n="LstDate" b="20160218" a="2016020"/>
<f n="FileSweepId" b="Test 555" a="Test1234"/>
</c>'
);
commit;
and now:
SELECT rec_no, rn, n, b, a
FROM ( select rownum rec_no, a.* FROM audit1 a ),
XMLTable( '/c'
passing xmltype( changes )
columns f_fields xmltype path './f'
) x1,
XMLTable( '/f'
passing x1.f_fields
columns rn for ordinality,
n varchar2(20) path './#n',
b varchar2(20) path './#b',
a varchar2(20) path './#a'
)
REC_NO RN N B A
---------- ---------- -------------------- -------------------- --------------------
1 1 VersNo 1 2
1 2 LstDate 20160215 20160217
1 3 FileSweepId Test Test1
2 1 VersNo 22 32
2 2 LstDate 20160218 2016020
2 3 FileSweepId Test 555 Test1234
6 rows selected
I was able to get rid of the error by modifying the plsql to:
BEGIN
FOR item IN (SELECT auditno, xmltype(changes, 1) as changes, extract(xmltype(changes, 1), '/c/f') as fields from audit1 where runlistno is null and rownum < 2 )
LOOP
dbms_output.put_line('parsing fields from: ' || item.fields.getStringVal());
dbms_output.put_line('name||beforevalue||aftervalue');
FOR field in (select n, b, a from XMLTable('//f' passing item.fields columns n varchar2(30) path '#n', b varchar(30) path '#b', a varchar(30) path '#a' ))
LOOP
dbms_output.put_line(field.n || '|| ' || field.b || '|| ' || field.a);
END LOOP;
END LOOP;
END;
So I am now able to iterate over the fields. But not sure yet how I can return info from the fields in the sys ref cursor. Example output from above plsql:
parsing fields from: <f n="VersNo" b="1" a="2"/><f n="LstDate" b="20160215" a="20160217"/><f n="FileSweepId" b="Test" a="Test1"/>
name||beforevalue||aftervalue
VersNo|| 1|| 2
LstDate|| 20160215|| 20160217
FileSweepId|| Test|| Test1