How to parse data inside CDATA in oracle - sql

There is a column in the table with type CLOB and it stores the payloads.
I need to get the value with respect to tenantId (7 in this case). How do write a query to get the same. Can't use substr or instr since the order of tenantId changes from record to record.
<PayLoad><![CDATA[{"order":{"entityErrors":[],"action":"CONFIRM","tenantId":"7","Id":"2","deliveryReservationDetails":[{"reservationId":"c05e0c77-1c8f-4dce-a388-fe97fd36f96e","fulfillmentLocationType":"Store"}]}}]]></PayLoad>

In Oracle 12 you should be able to do:
SELECT tenantId
FROM your_table t
LEFT OUTER JOIN
XMLTABLE(
'/PayLoad/.'
PASSING XMLTYPE( t.your_xml_column )
COLUMNS cdata CLOB PATH '.'
) x
ON ( 1 = 1 )
LEFT OUTER JOIN
JSON_TABLE(
x.cdata,
'$'
COLUMNS ( tenantId VARCHAR2(10) PATH '$.order.tenantId' )
) j
ON ( 1 = 1 );
(Untested as I'm on 11g for the next few hours)
On Oracle 11:
SELECT REGEXP_SUBSTR( x.cdata, '"tenantId":"((\\"|[^"])*)"', 1, 1, NULL, 1 ) AS tenantId
FROM your_table t
LEFT OUTER JOIN
XMLTABLE(
'/PayLoad/.'
PASSING XMLTYPE( t.your_xml_column )
COLUMNS cdata CLOB PATH '.'
) x
ON ( 1 = 1 )
Or (if the JSON string will not occur in another branch of the XML) you could just use:
SELECT REGEXP_SUBSTR(
your_xml_column,
'"tenantId":"((\\"|[^"])*)"',
1,
1,
NULL,
1
) AS tenantId
FROM your_table

Your CDATA payload appears to be JSON, so if you're on 12c and only have one order per CDATA you can do:
select json_value(x.cdata, '$.order.tenantId' returning number) as tenantid
from your_table t
cross join xmltable('/PayLoad'
passing xmltype(your_clob)
columns cdata clob path './text()'
) x;
As #MTO pointed out, treating the JSON value as a number may not be valid, as it's shown as a string in the raw JSON. Making it returning varchar2(10) or some other suitable size may be more appropriate (and safer, at least until you use it).
Quick demo with your XML as a CTE:
with your_table (your_clob) as (
select to_clob('<PayLoad><![CDATA[{"order":{"entityErrors":[],"action":"CONFIRM","tenantId":"7","Id":"2","deliveryReservationDetails":[{"reservationId":"c05e0c77-1c8f-4dce-a388-fe97fd36f96e","fulfillmentLocationType":"Store"}]}}]]></PayLoad>') from dual
)
select json_value(x.cdata, '$.order.tenantId' returning number) as tenantid
from your_table t
cross join xmltable('/PayLoad'
passing xmltype(your_clob)
columns cdata clob path './text()'
) x;
TENANTID
----------------------
7

Related

PL/SQL For one line how to get the number of cols with distinct value

I'm stuck on something simple. I have to work with this type of table :
CREATE TABLE FORM (
ID_FORM INT,
ACT_1 VARCHAR2 (10),
ACT_2 VARCHAR2 (10),
ACT_3 VARCHAR2 (10),
ACT_4 VARCHAR2 (10),
DESC_1 VARCHAR2 (10),
DESC_2 VARCHAR2 (10),
DESC_3 VARCHAR2 (10),
DESC_4 VARCHAR2 (10),
ECH_1 INT,
ECH_2 INT,
ECH_3 INT,
ECH_4 INT
);
INSERT INTO FORM VALUES ('1','A1','A2','A3',null,'D1','D2','D3',null,'2','12','6',null);
INSERT INTO FORM VALUES ('2','A1','A1','A3',null,'D1','D2','D1',null,'2','2','2',null);
INSERT INTO FORM VALUES ('3','A3','A3','A1',null,'D4','D4','D1',null,'2','2','12',null);
I want to create a function that return for one ID_FORM (PRIMARY KEY) the number of distinct values group by / concatanate cols like :
ACT_1|| DESC_1|| ECH_1 -> presta1
ACT_2|| DESC_2|| ECH_2 -> presta2
ACT_3|| DESC_3|| ECH_3 -> presta3
ACT_4|| DESC_4|| ECH_4 -> presta4
And i have to "COUNT DISTINCT" item
For example for :
select
ACT_1|| DESC_1|| ECH_1 as presta1,
ACT_2|| DESC_2|| ECH_2 as presta2,
ACT_3|| DESC_3|| ECH_3 as presta3,
ACT_4|| DESC_4|| ECH_4 as presta4
from FORM;
PRESTA1 PRESTA2 PRESTA3 PRESTA4
A1D12 A2D212 A3D36 - (Function have to return 3)
A1D12 A1D22 A3D12 - (Function have to return 3)
A3D42 A3D42 A1D112 - (function have to return 2)
Note that in reality the table is much larger and extends up to 8 "presta" (till ACT_8, DESC_8, ECH_8)
Someone can help me ?
You can unpivot your column groups to rows and do COUNT(DISTINCT ) on them.
Fiddle
select
id_form,
count(distinct act || desc_ || ech) as cnt
from form
unpivot (
(act, desc_, ech) for presta_no in (
(act_1, desc_1, ech_1) as '1',
(act_2, desc_2, ech_2) as '2',
(act_3, desc_3, ech_3) as '3',
(act_4, desc_4, ech_4) as '4'
)
) p
group by id_form
You'd better normalize the table to make queries simple. If it is not an option you may unpivot it first in the query. Or proceed with lateral join
select ID_FORM, n
from FORM f
cross apply (
select count(*) n
from (
select f.act_1 || f.DESC_1 || f.ECH_1 presta from dual
union
select f.act_2 || f.DESC_2 || f.ECH_2 from dual
union
select f.act_3 || f.DESC_3 || f.ECH_3 from dual
union
select f.act_4 || f.DESC_4 || f.ECH_4 from dual
)
where presta is not null
)
I think the following query will actually answer your question:
WITH destruct AS
(
SELECT ID_FORM,
SUBSTR(COL_TYPE, 1, INSTR(COL_TYPE, '_')-1) AS COL_TYPE,
SUBSTR(COL_TYPE, INSTR(COL_TYPE, '_')+1) AS COL_NUM,
VAL
FROM (SELECT ID_FORM, ACT_1, ACT_2, ACT_3, ACT_4, DESC_1, DESC_2, DESC_3, DESC_4,
TO_CHAR(ECH_1) AS ECH_1, TO_CHAR(ECH_2) AS ECH_2,
TO_CHAR(ECH_3) AS ECH_3, TO_CHAR(ECH_4) AS ECH_4
FROM FORM
/*WHERE id_form = :1*/)
UNPIVOT INCLUDE NULLS (VAL FOR COL_TYPE IN (ACT_1, ACT_2, ACT_3, ACT_4,
DESC_1, DESC_2, DESC_3, DESC_4,
ECH_1, ECH_2, ECH_3, ECH_4))
)
SELECT da.id_form, da.val, dd.val, de.val
FROM destruct da
INNER JOIN destruct dd ON da.id_form = dd.id_form AND da.col_num = dd.col_num AND dd.col_type = 'DESC'
INNER JOIN destruct de ON da.id_form = de.id_form AND da.col_num = de.col_num AND de.col_type = 'ECH'
WHERE da.col_type = 'ACT';
This is actually a bit more complicated than it needs to be due to the ECH columns being of a different type. If you want it for just one ID_FORM you can uncomment the WHERE clause and use the bind variable from your function.
Edit: Adding a DBFiddle (Link)
Precompute PRESTA columns (8 columns is not too much IMHO) and count distinct values in correlated subquery on some collection of that columns. (I used built-in sys.ku$_vcnt here, some consider it as antipattern, you can easily create your own nested table.)
with pr(PRESTA1, PRESTA2, PRESTA3, ...) as (
select ACT_1|| DESC_1|| ECH_1, ... (your concatenating expression)
from form
)
select pr.*
, (select count(distinct column_value)
from table(sys.ku$_vcnt(PRESTA1, PRESTA2, PRESTA3, ...))
)
from pr

REGEX get all matched patterns by SQL DB2

all.
I need to extract from the string by REGEX all that matching the pattern "TTT\d{3}"
For the string in example i would like to get:
TTT108,TTT109,TTT111,TTT110
The DB2 function i would like to use is REGEXP_REPLACE(str,'REGEX pattern', ',').
The number of matching can be 0,1,2,3... in each string.
Thank you.
The example:
TTT108(optional);TTT109(optional);TTT111(optional);TTT110optional);ENTITYLIST_2=(optional);ENTITYLIST_3=(optional);Containment_Status=(optional)
If you want to extract the valid instead of replacing the invalid characters, please check if this helps:
with data (s) as (values
('TTT108(optional);TTT109(optional);TTT111(optional);TTT110optional);ENTITYLIST_2=(optional);ENTITYLIST_3=(optional);Containment_Status=(optional)')
)
select listagg(sst,', ') within group (order by n)
from (
select n,
regexp_substr(s,'(TTT[0-9][0-9][0-9])', 1, n)
from data
cross join (values (1),(2),(3),(4),(5)) x (n) -- any numbers table
where n <= regexp_count(s,'(TTT[0-9][0-9][0-9])')
) x (n,sst)
For any number of tokens & Db2 versions before 11.1:
select id, listagg(tok, ',') str
from
(
values
(1, 'TTT108(optional);TTT109(optional);TTT111(optional);TTT110optional);ENTITYLIST_2=(optional);ENTITYLIST_3=(optional);Containment_Status=(optional)')
) mytable (id, str)
, xmltable
(
'for $id in tokenize($s, ";") let $new := replace($id, "(TTT\d{3}).*", "$1") where matches($id, "(TTT\d{3}).*") return <i>{string($new)}</i>'
passing mytable.str as "s"
columns tok varchar(6) path '.'
) t
group by id;

How to apply trim inside a query for a column in a table

I have data in a row for column TEST1 (Table Name: TableTest) like
<n0:RouterName Value="ST_APOP"/>
<n0:ExtZone/>
<n0:SalesOrders>
<n0:SalesOrder ValidationResult="SUCCESS">
<n0:SalesOrderID Value="4F47N006800000_0261"/>
I need to select 4F47N006800000_0261 value in my select query. How to trim that.
Your XML is not valid - so, lets add closing tags:
CREATE TABLE your_table ( xml ) AS
SELECT '<n0:RouterName Value="ST_APOP"/>
<n0:ExtZone/>
<n0:SalesOrders>
<n0:SalesOrder ValidationResult="SUCCESS">
<n0:SalesOrderID Value="4F47N006800000_0261"/>
</n0:SalesOrder>
</n0:SalesOrders>' FROM DUAL
Query:
SELECT SalesOrderId
FROM your_table t,
XMLTABLE(
XMLNAMESPACES( 'http://your.server/namespaces/n0' AS "n0" ),
'//root/n0:SalesOrders/n0:SalesOrder'
PASSING XMLTYPE(
'<root xmlns:n0="http://your.server/namespaces/n0">'
|| t.xml
|| '</root>'
)
COLUMNS SalesOrderId VARCHAR2(100) PATH '//n0:SalesOrderID/#Value'
);
Output:
SALESORDERID
-------------------
4F47N006800000_0261
db<>fiddle here
try this:
select
substr(column_name,instr(column_name,'Value',1,2)+7,length(substr(column_name,instr(column_name,'Value',1,2)+7))-3)
from
test1

Select where record does not exists

I am trying out my hands on oracle 11g. I have a requirement such that I want to fetch those id from list which does not exists in table.
For example:
SELECT * FROM STOCK
where item_id in ('1','2'); // Return those records where result is null
I mean if item_id '1' is not present in db then the query should return me 1.
How can I achieve this?
You need to store the values in some sort of "table". Then you can use left join or not exists or something similar:
with ids as (
select 1 as id from dual union all
select 2 from dual
)
select ids.id
from ids
where not exists (select 1 from stock s where s.item_id = ids.id);
You can use a LEFT JOIN to an in-line table that contains the values to be searched:
SELECT t1.val
FROM (
SELECT '1' val UNION ALL SELECT '2'
) t1
LEFT JOIN STOCK t2 ON t1.val = t2.item_id
WHERE t2.item_id IS NULL
First create the list of possible IDs (e.g. 0 to 99 in below query). You can use a recursive cte for this. Then select these IDs and remove the IDs already present in the table from the result:
with possible_ids(id) as
(
select 0 as id from dual
union all
select id + 1 as id from possible_ids where id < 99
)
select id from possible_ids
minus
select item_id from stock;
A primary concern of the OP seems to be a terse notation of the query, notably the set of values to test for. The straightforwwrd recommendation would be to retrieve these values by another query or to generate them as a union of queries from the dual table (see the other answers for this).
The following alternative solution allows for a verbatim specification of the test values under the following conditions:
There is a character that does not occur in any of the test values provided ( in the example that will be - )
The number of values to test stays well below 2000 (to be precise, the list of values plus separators must be written as a varchar2 literal, which imposes the length limit ). However, this should not be an actual concern - If the test involves lists of hundreds of ids, these lists should definitely be retrieved froma table/view.
Caveat
Whether this method is worth the hassle ( not to mention potential performance impacts ) is questionable, imho.
Solution
The test values will be provided as a single varchar2 literal with - separating the values which is as terse as the specification as a list argument to the IN operator. The string starts and ends with -.
'-1-2-3-156-489-4654648-'
The number of items is computed as follows:
select cond, regexp_count ( cond, '[-]' ) - 1 cnt_items from (select '-1-2-3-156-489-4654648-' cond from dual)
A list of integers up to the number of items starting with 1 can be generated using the LEVEL pseudocolumn from hierarchical queries:
select level from dual connect by level < 42;
The n-th integer from that list will serve to extract the n-th value from the string (exemplified for the 4th value) :
select substr ( cond, instr(cond,'-', 1, 4 )+1, instr(cond,'-', 1, 4+1 ) - instr(cond,'-', 1, 4 ) - 1 ) si from (select cond, regexp_count ( cond, '[-]' ) - 1 cnt_items from (select '-1-2-3-156-489-4654648-' cond from dual) );
The non-existent stock ids are generated by subtracting the set of stock ids from the set of values. Putting it all together:
select substr ( cond, instr(cond,'-',1,level )+1, instr(cond,'-',1,level+1 ) - instr(cond,'-',1,level ) - 1 ) si
from (
select cond
, regexp_count ( cond, '[-]' ) - 1 cnt_items
from (
select '-1-2-3-156-489-4654648-' cond from dual
)
)
connect by level <= cnt_items + 1
minus
select item_id from stock
;

Recursive CTE in H2: No data returned

I am trying to convert a proprietary Oracle CONNECT BY query into a standard SQL query that will run on H2, and generate the same data in the same order.
This is the Oracle query, which works:
SELECT id, name, parent
FROM myschema.mytable
START WITH id = 1
CONNECT BY PRIOR id = parent
This is what I've come up - however, it returns no rows in the ResultSet.
WITH RECURSIVE T(id, name, parent, path) AS (
SELECT id, name, '' AS parent, id AS path
FROM myschema.mytable WHERE id = 1
UNION ALL
SELECT ou.id, ou.name, ou.parent,
(T.path + '.' + CAST (ou.id AS VARCHAR)) AS path
FROM T INNER JOIN myschema.mytable AS ou ON T.id = ou.parent
) SELECT id, name, parent FROM T ORDER BY path
The initial row, and the related rows, both exist in the table.
I am not using H2's Oracle compatibility mode (which doesn't support CONNECT BY, by the way).
The following works for me, for both H2 as well as PostgreSQL (this you can test online using the SQL Fiddle). I had to make a few changes and assumptions (see below):
create table mytable(id int, name varchar(255), parent int);
insert into mytable values(1, 'root', null), (2, 'first', 1),
(3, 'second', 1), (4, '2b', 3);
WITH RECURSIVE T(id, name, parent, path) AS (
SELECT id, name, 0 AS parent,
cast(id as varchar) AS path
FROM mytable WHERE id = 1
UNION ALL
SELECT ou.id, ou.name, ou.parent,
(T.path || '.' || CAST (ou.id AS VARCHAR)) AS path
FROM T INNER JOIN mytable AS ou ON T.id = ou.parent
) SELECT id, name, parent, path FROM T ORDER BY path
Changes:
I assumed id and parent are integers. Because of that, I had to use cast(id as varchar) in the first select.
I replace + with || when concatenating strings.
I used 0 AS parent.
This seems to have been a problem with either the Anorm database access library or the JDBC driver not substituting a query parameter correctly (the query substitution was not shown in the question, because I assumed it wasn't relevant).