XML Parsing in Oracle database - oracle

I am using the below query to parse a HTML table as an XML in Oracle dynamically
SELECT ID,caption,amdtype AS Amendment_Reason
FROM
(
with tbl as
(
SELECT ID,xmltype('<html><body>'|| REPLACE(REPLACE(REPLACE(CAST(Note AS VARCHAR(4000)),'<br>',''),'<br/>',''),'&','&') || '</body></html>') AS xml_data
FROM TBL_EVENT
WHERE EVENT_TYPE='Amended note'
AND to_Char(CREATED_DATE,'mm-yyyy') = to_char(sysdate-1,'mm-yyyy')
AND NUMBER NOT LIKE '%c%'
)
SELECT ROW_NUMBER() OVER (PARTITION BY tbl.ID ORDER BY tbl.ID) AS Rankord,tbl.ID,
x.caption,
x.amdtype
FROM tbl
CROSS JOIN
XMLTABLE(
'/html/body/table'
PASSING tbl.xml_data
COLUMNS
caption VARCHAR2(50) PATH 'caption',
amdtype VARCHAR2(50) PATH 'tr[1]/td[1]'
) x
WHERE x.caption='Amendment Reason'
)
It is working for some texts but I am getting the below error
LPX-00243: element attribute value must be enclosed in quotes
Since I am parsing it as XML dynamically I am not sure how to make the changes anyone please guide me how to do it

Related

conditionally add an xmlattribute to an xmlelement with Oracle SQL

I'm dealing with a system that accepts data loads in XML format. For example, there's a field called "col1", and that field has the value "world" in it. The system interprets <col1 />, <col1></col1>, and a missing <col1> element as "no change" to the field called col1. (This is good because, if we were creating new data, "no change" would mean to accept whatever the default value is.) If I need to delete whatever is in the field, the <col1> element needs to have an xsi:nil attribute with a value of true.
So, when I'm extracting data from one instance of the system to load into another instance (inserting with SQL is not an option), I need to conditionally add xsi:nil="true" attribute to the XML returned from a query in Oracle 12c to explicitly indicate that the value of the element is null. (Always adding xsi:nil with a value of true or false, as appropriate, could work but is not desirable as it breaks convention and bloats file size.)
A test case can be set up as follows.
create table table1 (id number(10), col1 varchar2(5));
insert into table1 values (1,'hello');
insert into table1 values (2,null);
commit;
I want to to get this back from a query:
<outer><ID>1</ID><COL1>hello</COL1></outer>
<outer><ID>2</ID><COL1 xsi:nil="true"></COL1></outer>
This query throws an error.
select
xmlelement("outer",
xmlforest(id),
(case col1
when null then xmlelement(COL1, xmlattributes('xsi:nil="true"'), null)
else xmlforest(col1)
end)
)
from table1
;
Is there some other way to conditionally include the xmlattributes call, or some other way to get the output I want?
You can use NVL2 to make it slightly less verbose:
Query 1:
SELECT XMLELEMENT(
"outer",
XMLFOREST( id ),
XMLELEMENT( col1, xmlattributes( NVL2(col1,NULL,'true') as "xsi:nil"), col1 )
).getClobVal() AS element
FROM table1;
Result:
OUTPUT
-----------------------------------------------------
<outer><ID>1</ID><COL1>hello</COL1></outer>
<outer><ID>2</ID><COL1 xsi:nil="true"></COL1></outer>
Query 2: You could also use XMLFOREST to generate the elements and then APPENDCHILDXML to append the missing element (including namespaces are left as an exercise to the OP):
SELECT APPENDCHILDXML(
XMLELEMENT( "outer", XMLFOREST( id, col1 ) ),
'/outer',
NVL2( col1, NULL, XMLTYPE('<COL1 nil="true"></COL1>') )
).getClobVal() AS element
FROM table1;
Result:
OUTPUT
-------------------------------------------
<outer><ID>1</ID><COL1>hello</COL1></outer>
<outer><ID>2</ID><COL1 nil="true"/></outer>
I found that this query works, but it's more verbose than I would like.
select
xmlelement("outer",
xmlforest(id),
xmlelement(col1,xmlattributes(case when col1 is null then 'true' else null end as "xsi:nil"), col1)
).getClobVal()
from table1
;

How to only select existing values from oracle?

I have a table with a massive number of columns. So many, that when I do SELECT * I can't even see any values because all the columns fill up the screen. I'd like to do something like this:
SELECT * FROM my_table WHERE NAME LIKE '%unique name%' AND <THIS COLUMN> IS NOT NULL
Is this possible? Note: VALUE is not a column.
There are so many questions on SO that ask this same question, but they have some bizarre twist, and the actual question is not answered.
I've tried:
SELECT * FROM my_table WHERE NAME LIKE '%unique name%' AND VALUE NOT NULL
*
Invalid relational operator
SELECT * FROM my_table WHERE NAME LIKE '%unique name%' AND VALUE <> ''
*
'VALUE': invalid identifier
SELECT * FROM my_table WHERE NAME LIKE '%unique name%' AND COLUMN NOT NULL
*
Missing Expression
Bonus Questions:
Is there any way to force Oracle to only show one output screen at a time?
Is there a variable to use in the WHERE clause that relates to the current column? Such as: WHERE this.column = '1', where it would check each column to match that expression?
Is there any way to get back your last command in Oracle? (I have to remote into a Linux box running Oracle - it's all command line - can't even copy/paste, so I have to type every command by hand, with a wonky connection, so it's taking an extremely long time to debug this stuff)
If you are trying to find all the non null column values for a particular record you could try an unpivot provided all the columns you are unpivoting have the same data type:
SELECT *
FROM (select * from my_table where name like '%unique value%')
UNPIVOT [include nulls] (col_value FOR col_name IN (col1, col2, ..., coln))
with the above code null values will be excluded unless you include the optional include nulls statement, also you will need to explicitly list each column you want unpivoted.
If they don't all have the same data type, you can use a variation that doesn't necessarily prune away all the null values:
select *
from (select * from my_table where name like '%unique value%')
unpivot ((str_val, num_val, date_val)
for col_name in ((cola, col1, date1)
,(colb, col2, date2)
,(colc, col3, date1)));
You can have a fairly large set of column groups, though here I'm showing just three, one for each major data type, with the IN list you need to have a column listed for each column in your column group, though you can reuse columns as shown by the date_val column where I've used date1 twice. As an alternative to reusing an existing column, you could use a dummy column with a null value:
select *
from (select t1.*, null dummy from my_table t1 where name like '%unique value%')
unpivot ((str_val, num_val, date_val)
for col_name in ((dummy, col1, date1)
,(colb, dummy, date2)
,(colc, col3, dummy)));
Have tried this?
SELECT * FROM my_table WHERE NAME LIKE '%unique name%' AND value IS NOT NULL;
Oracle / PLSQL: IS NOT NULL Condition
For row number:
SELECT field1, field2, ROW_NUMBER() OVER (PARTITION BY unique_field) R WHERE R=1;
Usually in Linux consoles you can use arrow up&down to repeat the last sentence.

Oracle PL/SQL Use Merge command on data from XML Table

I have a PL/SQL procedure that currently gets data from an XML service and only does inserts.
xml_data := xmltype(GET_XML_F('http://test.example.com/mywebservice');
--GET_XML_F gets the XML text from the site
INSERT INTO TEST_READINGS (TEST, READING_DATE, CREATE_DATE, LOCATION_ID)
SELECT round(avg(readings.reading_val), 2),
to_date(substr(readings.reading_dt, 1, 10),'YYYY-MM-DD'), SYSDATE,
p_location_id)
FROM XMLTable(
XMLNamespaces('http://www.example.com' as "ns1"),
'/ns1:test1/ns1:series1/ns1:values1/ns1:value'
PASSING xml_data
COLUMNS reading_val VARCHAR2(50) PATH '.',
reading_dt VARCHAR2(50) PATH '#dateTime') readings
GROUP BY substr(readings.reading_dt,1,10), p_location_id;
I would like to be able to insert or update the data using a merge statement in the event that it needs to be re-run on the same day to find added records. I'm doing this in other procedures using the code below.
MERGE INTO TEST_READINGS USING DUAL
ON (LOCATION_ID = p_location_id AND READING_DATE = p_date)
WHEN NOT MATCHED THEN INSERT
(TEST_reading_id, site_id, test, reading_date, create_date)
VALUES (TEST_readings_seq.nextval, p_location_id,
p_value, p_date, SYSDATE)
WHEN MATCHED THEN UPDATE
SET TEST = p_value;
The fact that I'm pulling it from an XMLTable is throwing me off. Is there way to get the data from the XMLTable while still using the (much cleaner) merge syntax? I would just delete the data beforehand and re-import or use lots of conditional statements, but I would like to avoid doing so if possible.
Can't you simply put your SELECT into MERGE statement?
I believe, this should look more less like this:
MERGE INTO TEST_READINGS USING (
SELECT
ROUND(AVG(readings.reading_val), 2) AS test
,TO_DATE(SUBSTR(readings.reading_dt, 1, 10),'YYYY-MM-DD') AS reading_date
,SYSDATE AS create_date
,p_location_id AS location_id
FROM
XMLTable(
XMLNamespaces('http://www.example.com' as "ns1")
,'/ns1:test1/ns1:series1/ns1:values1/ns1:value'
PASSING xml_data
COLUMNS
reading_val VARCHAR2(50) PATH '.',
reading_dt VARCHAR2(50) PATH '#dateTime'
) readings
GROUP BY
SUBSTR(readings.reading_dt,1,10)
,p_location_id
) readings ON (
LOCATION_ID = readings.location_id
AND READING_DATE = readings.reading_date
)
WHEN NOT MATCHED THEN
...
WHEN MATCHED THEN
...
;

Error when selecting timestamp from XMLType column in Oracle 11g

I have 2 Oracle 11g databases with a table containing a XMLType column and some test data differing only in the separator (.,) for the milliseconds of the timestamp:
create table TEST_TIMESTAMP (
ID number(19,0) constraint "NN_TEST_TIMESTAMP_ID" not null,
DOC xmltype constraint "NN_TEST_TIMESTAMP_DOC" not null
);
insert into TEST_TIMESTAMP values ( 1, xmltype('<?xml version="1.0" encoding="utf-8"?><test><ts>2015-04-08T04:55:33.11</ts></test>'));
insert into TEST_TIMESTAMP values ( 2, xmltype('<?xml version="1.0" encoding="utf-8"?><test><ts>2015-04-08T04:55:33,11</ts></test>'));
When I try to extract the timestamp with the following statements, it fails either with the first document on one database or with the second document on the other database.
select x.*
from TEST_TIMESTAMP t,
xmltable(
'/test'
passing t.DOC
columns
ORIGINAL varchar2(50) path 'ts',
RESULT timestamp with time zone path 'ts'
) x
where t.ID = 1;
select x.*
from TEST_TIMESTAMP t,
xmltable(
'/test'
passing t.DOC
columns
ORIGINAL varchar2(50) path 'ts',
RESULT timestamp with time zone path 'ts'
) x
where t.ID = 2;
The error I get:
ORA-01858: a non-numeric character was found where a numeric was expected
01858. 00000 - "a non-numeric character was found where a numeric was expected"
*Cause: The input data to be converted using a date format model was
incorrect. The input data did not contain a number where a number was
required by the format model.
*Action: Fix the input data or the date format model to make sure the
elements match in number and type. Then retry the operation.
The only differences between those databases I've found are:
DB1: version=11.2.0.1.0, NLS_CHARACTERSET=AL32UTF8 -> fails on document 2
DB2: version=11.2.0.2.0, NLS_CHARACTERSET=WE8MSWIN1252 -> fails on document 1
DB1 has the behaviour that I would expect. Does anybody know why those databases behave differently and how to fix the issue in DB2?
Thanks in advance,
Oliver
My guess is that the nls_timestamp_format is different between the two databases.
However, rather than forcing the implicit conversion down at the XMLTABLE level, I would do an explicit conversion in the select list:
with test_timestamp as (select 1 id, xmltype('<?xml version="1.0" encoding="utf-8"?><test><ts>2015-04-08T04:55:33.11</ts></test>') doc from dual union all
select 2 id, xmltype('<?xml version="1.0" encoding="utf-8"?><test><ts>2015-04-08T04:55:33,11</ts></test>') doc from dual)
select x.original,
to_timestamp(x.original, 'yyyy-mm-dd"T"hh24:mi:ss,ff2') result
from test_timestamp t,
xmltable('/test' passing t.doc
columns original varchar2(50) path 'ts') x;
ORIGINAL RESULT
-------------------------------------------------- --------------------------------------------------
2015-04-08T04:55:33.11 08/04/2015 04:55:33.110000000
2015-04-08T04:55:33,11 08/04/2015 04:55:33.110000000
N.B. I found that using "ss.ff2" errored, but "ss,ff2" handled both cases just fine. I'm not sure if that's reliant on some other nls setting or not, though.

Cursor for loop using a selection instead of a table ( Oracle )

I'm writing a procedure to fill up a child table from a parent table. The child table however has more fields than the parent table ( as it should be ). I've conjured a cursor which point to a selection, which is essentially a join of multiple tables.
Here's the code I got so far :
CREATE OR REPLACE PROCEDURE Pop_occ_lezione
AS
x Lezione%rowtype;
CURSOR cc IS
WITH y as(
SELECT Codice_corso,
nome_modulo,
Data_inizio_ed_modulo diem,
Giorno_lezione,
ora_inizio_lezione o_i,
ora_fine_lezione o_f,
anno,
id_cdl,
nome_sede,
locazione_modulo loc
FROM lezione
join ( select id_cdl, anno, codice_corso from corso ) using (codice_corso)
join ( select codice_corso, locazione_modulo from modulo ) using (codice_corso)
join ( select nome_sede, id_cdl from cdl ) using (id_cdl)
WHERE
case
when extract (month from Data_inizio_ed_modulo) < 9 then extract (year from Data_inizio_ed_modulo) - 1
else extract (year from Data_inizio_ed_modulo)
end = extract (year from sysdate+365)
)
SELECT *
FROM y
WHERE sem_check(y.diem,sysdate+365) = 1;
--
BEGIN
FETCH cc into x;
EXIT when cc%NOTFOUND;
INSERT INTO Occr_lezione
VALUES (
x.Codice_corso,
x.Nome_modulo,
x.diem,x.giorno_lezione,
x.Ora_inizio_lezione,
to_date(to_char(next_day(sysdate,x.Giorno_lezione),'DD-MM-YYYY') || to_char(x.Ora_inizio_lezione,' hh24:mi'),'dd-mm-yyyy hh24:mi'),
to_date(to_char(next_day(sysdate,x.Giorno_lezione),'DD-MM-YYYY') || to_char(x.Ora_fine_lezione,' hh24:mi'),'dd-mm-yyyy hh24:mi'),
x.nome_sede,
0,
x.loc
);
END LOOP;
END;
/
But of course it won't work, because the variable x has the type of my initial table row, which has less columns then my selection. Unfortunately As far as I know a rowtype variable is needed to cycle trough a cursor, in order to fetch data from it. Can you see the contradiction? How can I change the code? Is there a certain type of variable which can be crafted to reflect a row from my query result? Or maybe a way to cycle trough the data in the cursor without using a support variable? Or maybe something entirely different? Please let me know.
Ok, so as suggested I tried something like this:
INSERT INTO Occr_lezione(
Codice_corso,
Nome_modulo,
Data_inizio_ed_modulo,
Giorno_lezione,
Ora_inizio_lezione,
Ora_fine_lezione,
Anno,
Id_cdl,
Nome_sede,
Locazione_modulo
)
WITH y as(
SELECT Codice_corso,
Nome_modulo,
Data_inizio_ed_modulo,
Giorno_lezione,
Ora_inizio_lezione,
Ora_fine_lezione,
Anno,
Id_cdl,
Nome_sede,
Locazione_modulo
FROM Lezione
join ( select Id_cdl, Anno, Codice_corso from Corso ) using (codice_corso)
join ( select Codice_corso, Locazione_modulo from Modulo ) using (Codice_corso)
join ( select Nome_sede, Id_cdl from Cdl ) using (id_cdl)
WHERE
case
when extract (month from Data_inizio_ed_modulo) < 9 then extract (year from Data_inizio_ed_modulo) - 1
else extract (year from Data_inizio_ed_modulo)
end = extract (year from sysdate+365)
)
SELECT *
FROM y
WHERE sem_check(y.Data_inizio_ed_modulo,sysdate+365) = 1;
END;
/
But it says PL/SQL: ORA-00904: "LOCAZIONE_MODULO": invalid identifier
which isn't true, because the query return a table in which such column is present... am I missing something?
The code is compiled with no errors, it occurs when I try to fire the procedure.
In the table Occr_lezione as you can see:
CREATE TABLE Occr_lezione (
Codice_corso varchar2(20) NOT NULL,
Nome_modulo varchar2(50) NOT NULL,
Data_inizio_ed_modulo date NOT NULL,
Giorno_lezione number(1) NOT NULL,
Ora_inizio_lezione date NOT NULL,
Data_inizio_occr_lezione date,
Data_fine_occr_lezione date NOT NULL,
Nome_sede varchar2(30) NOT NULL,
Num_aula varchar2(3) NOT NULL,
Tipo_aula varchar2(20) NOT NULL,
--
CONSTRAINT fk_Occr_lezione_lezione FOREIGN KEY (Codice_corso,Nome_modulo,Data_inizio_ed_modulo,Giorno_lezione,Ora_inizio_lezione) REFERENCES Lezione(Codice_corso,Nome_modulo,Data_inizio_ed_modulo,Giorno_lezione,Ora_inizio_lezione) ON DELETE CASCADE,
CONSTRAINT fk_Occr_lezione_aula FOREIGN KEY (Nome_sede,Num_aula,Tipo_aula) REFERENCES Aula(Nome_sede,Num_aula,Tipo_aula) ON DELETE SET NULL,
CONSTRAINT pk_Occr_lezione PRIMARY KEY (Codice_corso,Nome_modulo,Data_inizio_ed_modulo,Giorno_lezione,Ora_inizio_lezione,Data_inizio_occr_lezione),
CHECK ( trunc(Data_inizio_occr_lezione) = trunc(Data_fine_occr_lezione) ), -- data inizio = data fine // prenotazione giornaliera
CHECK ( Data_inizio_occr_lezione < Data_fine_occr_lezione ) -- ora inizio < ora fine // coerenza temporale
there is not a column named Locazione_modulo, however the last column Tipo_aula as the same type and size of Locazione modulo :
CREATE TABLE Modulo (
Codice_corso varchar2(20) NOT NULL,
Nome_modulo varchar2(50),
Locazione_modulo varchar2(20) NOT NULL,
--
CONSTRAINT fk_Modulo_Corso FOREIGN KEY(Codice_corso) REFERENCES Corso(Codice_corso) ON DELETE CASCADE,
CONSTRAINT pk_Modulo PRIMARY KEY(Codice_corso,Nome_modulo),
CHECK (Locazione_modulo IN ('Aula','Laboratorio','Conferenze'))
);
So it should be irrelevant, right?
If you really want to use explicit cursors, you can declare x to be of type cc%rowtype
CREATE OR REPLACE PROCEDURE Pop_occ_lezione
AS
CURSOR cc IS ...
x cc%rowtype;
...
Unless you are using explicit cursors because you want to be able to explicitly fetch the data into local collections that you can leverage later on in your procedure, code using implicit cursors tends to be preferrable. That eliminates the need to FETCH and CLOSE the cursor or to write an EXIT condition and it implicitly does a bulk fetch to minimize context shifts.
BEGIN
FOR x IN cc
LOOP
INSERT INTO Occr_lezione ...
END LOOP;
END;
Of course, in either case, I would hope that you'd choose more meaningful names for your local variables-- x and cc don't tell you anything about what the variables are doing.
If all you are doing is taking data from one set of tables and inserting it into another table, it would be more efficient to write a single INSERT statement rather than coding a PL/SQL loop.
INSERT INTO Occr_lezione( <<column list>> )
SELECT <<column list>>
FROM <<tables you are joining together in the cursor definition>>
WHERE <<conditions from your cursor definition>>

Resources