Oracle - Iterate over XMLTYPE And Return Nodes As Individual Rows - sql

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

Related

Oracle SQL - table type in cursor causing ORA-21700: object does not exist or is marked for delete

I have problem with my function, I'm getting ORA-21700: object does not exist or is marked for delete error. It's caused by table type parameter in cursor, but I've no idea how to fix it.
I've read that table type part should be assigned to a variable, but it can't be done in cursor, right? I've marked the part which causing the issue
Can anyone help? Is there any other way I can do this?
My package looks something like this:
FUNCTION createCSV(DateFrom date
,DateTo date)
RETURN clob IS
CURSOR c_id (c_DateFrom date
,c_DateTo date) IS
SELECT id
FROM limits
WHERE utcDateFrom <= NVL(c_DateTo, utcDateFrom)
AND NVL(utcDateTo, c_DateFrom + 1) >= c_DateFrom + 1;
CURSOR c (c_DateFrom date
,c_DateTo date
,pc_tDatePeriods test_pkg.t_date_periods) IS -- this is table type (TYPE xx AS TABLE OF records)
SELECT l.id limit_id
,TO_CHAR(time_cond.utcDateFrom, og_domain.cm_yyyymmddhh24mi) time_stamp_from
,TO_CHAR(time_cond.utcDateTo, og_domain.cm_yyyymmddhh24mi) time_stamp_to
FROM limits l
JOIN (SELECT limit_id, utcDateFrom, utcDateTo FROM TABLE(pc_tDatePeriods) --This part is causing the issue
) time_cond
ON l.id = time_cond.limit_id
WHERE l.utcDateFrom <= NVL(c_DateTo, l.utcDateFrom)
AND NVL(l.utcDateTo, c_DateFrom + 1) >= c_DateFrom + 1;
CSV clob;
tDatePeriods test_pkg.t_date_periods := test_pkg.t_date_periods();
BEGIN
FOR r_id IN c_id(DateFrom, DateTo)
LOOP
tDatePeriods := test_pkg.includeTimeGaps(p_Id => r_id.id); --this loop is ok
FOR r IN c(DateFrom, DateTo, tDatePeriods) --here I'm getting error
LOOP
CSV := CSV || chr(13) || r.limit_id || ',' || r.time_stamp_from || ',' || r.time_stamp_to;
END LOOP;
END LOOP;
RETURN CSV;
END createCSV;
Your problem should be solved by declaring type test_pkg.t_date_periods on schema level instead of in package.
Similar answer can be found here but with more details.

Cursor in Oracle - after passing parameters, select does not filter result rows

I am facing for me strange issue with following parametrical cursor.
I have defined cursor in this way:
CURSOR cur_action ( product_code VARCHAR2(100) , action_master_list VARCHAR2(100))
IS
SELECT
act.ACTION_DETAIL_KEY,
act.ACTION_MASTER_KEY,
act.PRODUCT_CODE,
act.REF_ACTION_DETAIL_KEY
FROM XMLTABLE(action_master_list) x
JOIN ETDW.MFE_AR_ACTION_DETAILS act ON TO_NUMBER(x.COLUMN_VALUE) = act.ACTION_MASTER_KEY
WHERE 1=1
AND act.LAST_FLAG = 'Y'
AND act.PRODUCT_CODE = product_code;
Then I am using it in following way:
OPEN cur_action ( iFromProductCode , iActionMasterKeyList);
LOOP
FETCH cur_action BULK COLLECT INTO vActionDetailKey, vActionMasterKey, vProductCode, vRefActionDetailKey LIMIT 100;
FOR j IN 1..cur_action%ROWCOUNT
LOOP
dbms_output.put_line('vActionDetailKey: ' || vActionDetailKey (j) ||' vActionMasterKey: '|| vActionMasterKey (j) || ' vProductCode: ' || vProductCode (j));
END LOOP;
END LOOP;
Result seems to be unfilterd. It doesnt return 3 rows as expected result (this result is returned in with cusor query, when i run in outside procedure/pl block), but it returns all rows for actions in list. So it seems, that WHERE condition "act.PRODUCT_CODE = product_code" was not applied. Why?
Thank you
Why? Because you named parameter the same as column, so Oracle reads it as if it was where 1 = 1, i.e. no filtering at all.
Rename parameters to e.g.
CURSOR cur_action ( par_product_code V
----
this
and, later,
AND act.PRODUCT_CODE = par_product_code;
----
this

Passing a comma delimited list into XMLQuery

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'

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;

error using two cursors in plsql code

I have two tables ERROR_DESCRIPTION and ERROR_COLUMN.
ERROR_DESCRIPTION has below data :
"error processing column a_type"
"error processing column a_type"
"error processing column a_type"
ERROR_COLUMN has below data:
"abc",123334,"jdjjd"
"jdjd",2344,"djjd"
"djjd",234,"kkfkf"
at last my data should look like this :
error processing column a_type -"abc",123334,"jdjjd"
error processing column a_type - "jdjd",2344,"djjd"
so on ...
"a_type" is column name from ERROR_COLUMN table
i am trying to achieve this using cursors .
declare
cursor c_log is select * from ERROR_DESCRIPTION where error_data_log like'error%' ORDER BY error_data_log;
r_log ERROR_DESCRIPTION %ROWTYPE;
v_error varchar2(1000);
cursor c_dsc is select * from ERROR_COLUMN;
r_dsc ERROR_COLUMN%ROWTYPE;
begin
open c_log;
loop
fetch c_log into v_error;
open c_dsc ;
fetch c_dsc into r_dsc
dbms_output.put_line( 'error is'||v_error||'-'||r_dsc.xyz);
close c_dsc ;
end loop;
close c_log;
end ;
i am not able to get desired result .
r_dsc.xyz is column defined for that record type
can any one tell how i can i get above result.
I prefer you not use cursor when you can achieve the result with simple queries, you can get the result you mentioned with below join using substr function:
select d.val || substr(d.val,24) || c.val2 || c.val3
from ERROR_DESCRIPTION d
join ERROR_COLUMN c on substr(d.val,24)=c.val1
assuming your structure is: ERROR_DESCRIPTION(val), ERROR_COLUMN(val1,val2,val2) according to sample data you provided.
EDIT:(after comments and edit of question) if you don't have a specific formula or pattern for join and you want to join them only on base of number of recor then use rownum within subquery:
select d.val || '-' || c.val1,c.val2,c.val3
from (select rownum rn,val from ERROR_DESCRIPTION) d
join (select rownum rn,val1,val2,val3 from ERROR_COLUMN) c
on d.rn=c.rn