How to simple change node's attribute value of XMLTYPE in Oracle 11g r2? - oracle

I just wanna to change in this XML (contained in XMLTYPE variable) all nodes named "ChildNode" with "Name"="B" attribute values to "C":
<RootNode>
<ChildNodes>
<ChildNode Name="A"/>
<ChildNode Name="B"/>
</ChildNodes>
</RootNode>
DECLARE
FXML XMLTYPE;
BEGIN
FXML := ...; -- see text before
-- what next?
END;
Thanks!

You can use updatexml function:
declare
fOrigXml XmlType := XmlType(
'<RootNode>
<ChildNodes>
<ChildNode Name="A"/>
<ChildNode Name="B"/>
</ChildNodes>
</RootNode>');
fResXml XmlType;
begin
select updatexml((fOrigXml), '/RootNode/ChildNodes/ChildNode[#Name="B"]/#Name', 'C') into fResXml from dual;
end;

One more:
SET SERVEROUTPUT ON;
DECLARE
DOC DBMS_XMLDOM.DOMDocument;
var XMLTYPE := XMLType('<RootNode>
<ChildNodes>
<ChildNode Name="A"/>
<ChildNode Name="B"/>
</ChildNodes>
</RootNode>');
xmlvalue CLOB;
PROCEDURE changeNameAttributes(
DOC dbms_xmldom.domdocument)
IS
nl dbms_xmldom.domnodelist;
v_clob CLOB;
LEN NUMBER;
n dbms_xmldom.domnode;
nodename VARCHAR2(4000);
nodevalue VARCHAR2(4000);
PROCEDURE changeAttributeB(
n dbms_xmldom.domnode)
IS
e dbms_xmldom.domelement;
dn dbms_xmldom.domnode;
nnm dbms_xmldom.domnamednodemap;
attrname VARCHAR2(100);
attrval VARCHAR2(100);
LEN NUMBER;
BEGIN
e := dbms_xmldom.makeelement(n); -- get all attributes of element
nnm := xmldom.getattributes(n);
IF(xmldom.isnull(nnm) = FALSE) THEN
LEN := dbms_xmldom.getlength(nnm); -- loop through attributes
FOR i IN 0 .. LEN -1
LOOP
dn := dbms_xmldom.item(nnm, i);
attrname := dbms_xmldom.getnodename(dn);
IF(attrname = 'Name' ) THEN
attrval := dbms_xmldom.getnodevalue(dn);
IF(attrval = 'B') THEN
dbms_xmldom.setnodevalue(dn,'C');
END IF;
END IF;
END LOOP;
END IF;
END changeAttributeB;
BEGIN
nl := dbms_xmldom.getelementsbytagname(DOC, '*');
LEN := dbms_xmldom.getlength(nl);
FOR i IN 0 .. LEN -1
LOOP
n := dbms_xmldom.item(nl, i);
nodename := dbms_xmldom.getnodename(n);
IF ( nodename = 'ChildNode') THEN
changeAttributeB(n);
END IF;
END LOOP;
END changeNameAttributes;
BEGIN
DOC := DBMS_XMLDOM.newDOMDocument(var);
--Before
DBMS_OUTPUT.PUT_LINE('BEFORE');
DBMS_LOB.createtemporary (xmlvalue, TRUE);
DBMS_XMLDOM.writeToClob(DOC, xmlvalue);
DBMS_OUTPUT.PUT_LINE(xmlvalue);
-- Modify
changeNameAttributes(DOC);
-- After
DBMS_OUTPUT.PUT_LINE('AFTER');
DBMS_XMLDOM.writeToClob(DOC, xmlvalue);
DBMS_OUTPUT.PUT_LINE(xmlvalue);
dbms_xmldom.freedocument(DOC);
END;
/

Here is one solution:
Declare
xml_nl DBMS_XMLDOM.DOMNodeList;
xml_node DBMS_XMLDOM.DOMNode;
xml_doc DBMS_XMLDOM.DOMDocument;
v_xml_clob CLOB;
v_name VARCHAR2(32767);
v_xml VARCHAR2(32767) :=
'<RootNode>
<ChildNodes>
<ChildNode Name="A"/>
<ChildNode Name="B"/>
</ChildNodes>
</RootNode>';
Begin
xml_doc := DBMS_XMLDOM.NewDOMDocument(XMLType.createXML(v_xml));
xml_nl := DBMS_XMLDOM.GetElementsByTagName(xml_doc, 'ChildNode');
FOR i IN 0 .. (DBMS_XMLDOM.getLength(xml_nl) - 1) LOOP
xml_node := DBMS_XMLDOM.Item(xml_nl, i);
DBMS_XSLPROCESSOR.valueOf(xml_node, '#Name', v_name);
IF v_name IS NOT NULL AND v_name = 'B' THEN
DBMS_XMLDOM.setAttribute(DBMS_XMLDOM.makeElement(xml_node), 'Name', 'C');
END IF;
END LOOP;
DBMS_LOB.createTemporary(v_xml_clob, cache => FALSE);
DBMS_LOB.Open(v_xml_clob, DBMS_LOB.lob_readwrite);
DBMS_XMLDOM.writeToCLob(xml_doc, v_xml_clob);
DBMS_OUTPUT.put_line(v_xml_clob);
End;

/* Formatted on 6/19/2016 3:02:05 PM (QP5 v5.126) */
DECLARE
var XMLTYPE;
doc DBMS_XMLDOM.domdocument;
ndoc DBMS_XMLDOM.domnode;
docelem DBMS_XMLDOM.domelement;
node DBMS_XMLDOM.domnode;
childnode DBMS_XMLDOM.domnode;
nodelist DBMS_XMLDOM.domnodelist;
nodelist2 DBMS_XMLDOM.domnodelist;
buf VARCHAR2 (2000);
newnode DBMS_XMLDOM.domnode;
clonenode DBMS_XMLDOM.domnode;
elem DBMS_XMLDOM.domelement;
PROCEDURE duyethoiquy (clonenode IN OUT DBMS_XMLDOM.domnode)
IS
childnode DBMS_XMLDOM.domnode;
simpletypechildnodemap DBMS_XMLDOM.domnamednodemap;
simpletypeattributenode DBMS_XMLDOM.domnode;
BEGIN
-- xu ly clonenode nay
-- sau do lay may con de duyet tiep
-- thay doi node con lev 1..
IF NOT DBMS_XMLDOM.isnull (clonenode)
THEN
-- xu ly node nay
-- thay doi mot vai thuoc tinh cua cay nay
simpletypechildnodemap := DBMS_XMLDOM.getattributes (clonenode);
simpletypeattributenode :=
DBMS_XMLDOM.getnameditem (simpletypechildnodemap, 'r');
IF NOT DBMS_XMLDOM.isnull (simpletypeattributenode)
THEN
DBMS_XMLDOM.setnodevalue (simpletypeattributenode, '');
DBMS_XMLDOM.writetobuffer (simpletypeattributenode, buf);
DBMS_OUTPUT.put_line ('attr:' || buf);
END IF;
IF DBMS_XMLDOM.haschildnodes (clonenode)
THEN
childnode := DBMS_XMLDOM.getfirstchild (clonenode);
--- ghi nhan gia tri
WHILE NOT DBMS_XMLDOM.isnull (childnode)
LOOP
-- xu ly con cua no:
duyethoiquy (childnode);
childnode := DBMS_XMLDOM.getnextsibling (childnode);
-------------------------------------
END LOOP;
ELSE -- is leaf
-- reset gia tri cua node duoc clone nay roi
DBMS_XMLDOM.setnodevalue (clonenode, '');
END IF;
END IF;
END;
BEGIN
var :=
xmltype('<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac"
xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">
<sheetData>
<row r="1"><c r="A1" s="2" t="s"><v>0</v></c><c r="B1" s="2" t="s"><v>0</v></c></row>
<row r="3"><c r="A3" s="2" t="s"><v>1</v></c><c r="B3" s="2" t="s"><v>1</v></c></row>
</sheetData>
</worksheet>');
-- Create DOMDocument handle:
ndoc :=
DBMS_XMLDOM.makenode(DBMS_XMLDOM.getdocumentelement (
DBMS_XMLDOM.newdomdocument (var)));
newnode :=
DBMS_XMLDOM.makenode(DBMS_XMLDOM.getdocumentelement(DBMS_XMLDOM.newdomdocument(xmltype('<row r="2"><c r="A2" s="1" t="s"><v>0</v></c></row>'))));
-- ghi vao node
nodelist := DBMS_XSLPROCESSOR.selectnodes (ndoc, '/worksheet/sheetData');
IF NOT DBMS_XMLDOM.isnull (nodelist)
THEN
node := DBMS_XMLDOM.item (nodelist, 0);
childnode := DBMS_XMLDOM.getlastchild (node);
clonenode := DBMS_XMLDOM.clonenode (childnode, TRUE);
-- thay doi node cha
duyethoiquy (clonenode);
-- DBMS_XMLDOM.writetobuffer (newnode, buf);
-- DBMS_OUTPUT.put_line ('LastChild:' || buf);
elem :=
DBMS_XMLDOM.makeelement(DBMS_XMLDOM.appendchild (
node,
DBMS_XMLDOM.makenode(DBMS_XMLDOM.makeelement(DBMS_XMLDOM.importnode (
DBMS_XMLDOM.getownerdocument(node),
clonenode, --newnode,
TRUE)))));
END IF;
DBMS_XMLDOM.writetobuffer (ndoc, buf);
DBMS_OUTPUT.put_line ('After:' || buf);
END;
-- vi du voi xm;

Related

Export query result to csv in oracle stored procedure

So i have a query that i would like to execute through a stored procedure and export the output of the query to a CSV file. So i am using the following stored procedure to do it:
CREATE OR REPLACE PROCEDURE parseCSV(
p_file_dir VARCHAR2, -- Oracle directory name
p_file_name VARCHAR2, -- filename
p_sql_query VARCHAR2, -- select * from table or some such query
p_delimiter CHAR -- column delimiter
)
AS
l_cursor_handle INTEGER;
l_dummy NUMBER;
l_col_cnt INTEGER;
l_rec_tab DBMS_SQL.DESC_TAB;
l_current_col NUMBER(16);
l_current_line VARCHAR2(2047);
l_column_value VARCHAR2(300);
l_file_handle UTL_FILE.FILE_TYPE;
l_print_text VARCHAR2(100);
l_record_count NUMBER(16) := 0;
BEGIN
l_file_handle := UTL_FILE.FOPEN(p_file_dir, p_file_name, 'a', 2047);
l_cursor_handle := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(l_cursor_handle, p_sql_query, DBMS_SQL.native);
l_dummy := DBMS_SQL.EXECUTE(l_cursor_handle);
DBMS_SQL.DESCRIBE_COLUMNS(l_cursor_handle, l_col_cnt, l_rec_tab);
l_current_col := l_rec_tab.FIRST;
IF (l_current_col IS NOT NULL) THEN
LOOP
DBMS_SQL.DEFINE_COLUMN(l_cursor_handle, l_current_col, l_column_value, 300);
l_print_text := l_rec_tab(l_current_col).col_name || p_delimiter;
UTL_FILE.PUT (l_file_handle, l_print_text);
l_current_col := l_rec_tab.NEXT(l_current_col);
EXIT WHEN (l_current_col IS NULL);
END LOOP;
END IF;
UTL_FILE.PUT_LINE (l_file_handle,' ');
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(l_cursor_handle) = 0;
l_current_line := '';
FOR l_current_col IN 1..l_col_cnt LOOP
DBMS_SQL.COLUMN_VALUE (l_cursor_handle, l_current_col, l_column_value);
l_print_text := l_column_value || p_delimiter;
l_current_line := l_current_line || l_column_value || p_delimiter;
END LOOP;
l_record_count := l_record_count + 1;
UTL_FILE.PUT_LINE (l_file_handle, l_current_line);
END LOOP;
UTL_FILE.FCLOSE (l_file_handle);
DBMS_SQL.CLOSE_CURSOR(l_cursor_handle);
END;
/
The procedure when executed processes the query and then stores the result into a delimited file. For example, the output of the procedure for a regular SELECT statement will be of this form:
ID,ROLL_NO,RANK,
1,123456,1620,
2,987654,1344,
Now herein lies my issue. As you can see each row within the output file is ending with an extra trailing ,. Now, due to my lack of knowledge in plsql, i can't think of a modification that i can do to the procedure so that the expected output file would be of this form:
ID,ROLL_NO,RANK
1,123456,1620
2,987654,1344
Could someone be kind enough to help out an Oracle newbie here and give me some pointers as to how i can do it? I would appreciate it a lot.
Please try the below where comments have been added
CREATE OR REPLACE PROCEDURE parseCSV(
p_file_dir VARCHAR2, -- Oracle directory name
p_file_name VARCHAR2, -- filename
p_sql_query VARCHAR2, -- select * from table or some such query
p_delimiter CHAR -- column delimiter
)
AS
l_cursor_handle INTEGER;
l_dummy NUMBER;
l_col_cnt INTEGER;
l_rec_tab DBMS_SQL.DESC_TAB;
l_current_col NUMBER(16);
l_current_line VARCHAR2(2047);
l_column_value VARCHAR2(300);
l_file_handle UTL_FILE.FILE_TYPE;
l_print_text VARCHAR2(100);
l_record_count NUMBER(16) := 0;
BEGIN
l_file_handle := UTL_FILE.FOPEN(p_file_dir, p_file_name, 'a', 2047);
l_cursor_handle := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(l_cursor_handle, p_sql_query, DBMS_SQL.native);
l_dummy := DBMS_SQL.EXECUTE(l_cursor_handle);
DBMS_SQL.DESCRIBE_COLUMNS(l_cursor_handle, l_col_cnt, l_rec_tab);
l_current_col := l_rec_tab.FIRST;
IF (l_current_col IS NOT NULL) THEN
LOOP
DBMS_SQL.DEFINE_COLUMN(l_cursor_handle, l_current_col, l_column_value, 300);
l_print_text := l_rec_tab(l_current_col).col_name ||
p_delimiter;
l_current_col := l_rec_tab.NEXT(l_current_col);
IF l_current_col IS NULL/*handling for last delimiter for
column */
THEN
l_print_text:=substr(l_print_text,-1);
END IF;
UTL_FILE.PUT (l_file_handle, l_print_text);
EXIT WHEN (l_current_col IS NULL);
END LOOP;
END IF;
UTL_FILE.PUT_LINE (l_file_handle,' ');
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(l_cursor_handle) = 0;
l_current_line := '';
FOR l_current_col IN 1..l_col_cnt LOOP
DBMS_SQL.COLUMN_VALUE (l_cursor_handle, l_current_col, l_column_value);
l_print_text := l_column_value || p_delimiter;
IF l_current_col =l_col_cnt
then
l_current_line := l_current_line || l_column_value;
ELSE
l_current_line := l_current_line || l_column_value ||
p_delimiter;
END IF;
END LOOP;
l_record_count := l_record_count + 1;
UTL_FILE.PUT_LINE (l_file_handle, l_current_line);
END LOOP;
UTL_FILE.FCLOSE (l_file_handle);
DBMS_SQL.CLOSE_CURSOR(l_cursor_handle);
END;
First of all, I'd say procedure which exports CSV file should be named createCSV, makeCSV or something similar, but never parseCSV.
Next, I'm not sure that this application design in best. Usually database should worry of data while external client thinking about medias, formats and so on.
At last, for elimination of trailing delimiter you should use something like this:
...
p_delimiter CHAR -- column delimiter
)
AS
l_delimiter varchar2(1 char);
...
BEGIN
....
l_current_col := l_rec_tab.FIRST;
l_delimiter := '';
....
l_print_text := l_delimiter || l_rec_tab(l_current_col).col_name;
l_delimiter := p_delimiter;
....
/
Try this procedure:
CREATE OR REPLACE PROCEDURE parseCSV(
p_file_dir VARCHAR2, -- Oracle directory name
p_file_name VARCHAR2, -- filename
p_sql_query VARCHAR2, -- select * from table or some such query
p_delimiter CHAR -- column delimiter
)
AS
l_cursor_handle INTEGER;
l_dummy NUMBER;
l_col_cnt INTEGER;
l_rec_tab DBMS_SQL.DESC_TAB;
l_current_col NUMBER(16);
l_current_line VARCHAR2(2047);
l_column_value VARCHAR2(300);
l_file_handle UTL_FILE.FILE_TYPE;
l_print_text VARCHAR2(100);
l_record_count NUMBER(16) := 0;
BEGIN
l_file_handle := UTL_FILE.FOPEN(p_file_dir, p_file_name, 'a', 2047);
l_cursor_handle := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(l_cursor_handle, p_sql_query, DBMS_SQL.native);
l_dummy := DBMS_SQL.EXECUTE(l_cursor_handle);
DBMS_SQL.DESCRIBE_COLUMNS(l_cursor_handle, l_col_cnt, l_rec_tab);
l_current_col := l_rec_tab.FIRST;
IF (l_current_col IS NOT NULL) THEN
LOOP
DBMS_SQL.DEFINE_COLUMN(l_cursor_handle, l_current_col, l_column_value, 300);
IF l_print_text IS NOT NULL THEN
l_print_text := l_print_text || p_delimiter;
END IF;
l_print_text := l_rec_tab(l_current_col).col_name;
UTL_FILE.PUT (l_file_handle, l_print_text);
l_current_col := l_rec_tab.NEXT(l_current_col);
EXIT WHEN (l_current_col IS NULL);
END LOOP;
END IF;
UTL_FILE.PUT_LINE (l_file_handle,' ');
l_print_text := NULL;
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(l_cursor_handle) = 0;
l_current_line := '';
FOR l_current_col IN 1..l_col_cnt LOOP
DBMS_SQL.COLUMN_VALUE (l_cursor_handle, l_current_col, l_column_value);
IF l_print_text IS NOT NULL THEN
l_print_text := l_print_text || p_delimiter;
END IF;
l_print_text := l_column_value;
END LOOP;
l_record_count := l_record_count + 1;
UTL_FILE.PUT_LINE (l_file_handle, l_print_text );
END LOOP;
UTL_FILE.FCLOSE (l_file_handle);
DBMS_SQL.CLOSE_CURSOR(l_cursor_handle);
END;
You can do two things. The first is to update your call to UTL_FILE.put so you conditionally add the delimiter (example below for the header record but the same can be applied to the data) :
IF l_current_col < l_rec_tab.LAST THEN
l_print_text := l_rec_tab(l_current_col).col_name || p_delimiter;
ELSE
l_print_text := l_rec_tab(l_current_col).col_name ;
END IF ;
UTL_FILE.PUT (l_file_handle, l_print_text);
The second approach would be to build up a string with a complete row of data and then manipulate that string before you call UTL FILE (in this case I am assuming l_print_text is long enough for a row of data) :
IF (l_current_col IS NOT NULL) THEN
LOOP
DBMS_SQL.DEFINE_COLUMN(l_cursor_handle, l_current_col, l_column_value, 300);
--append to variable
l_print_text := l_print_text||l_rec_tab(l_current_col).col_name || p_delimiter;
l_current_col := l_rec_tab.NEXT(l_current_col);
EXIT WHEN (l_current_col IS NULL);
END LOOP;
END IF;
--trim trailing delimiter
l_print_text := TRIM(TRAILING p_delimiter FROM l_print_text) ;
--send whole line to file
UTL_FILE.PUT_LINE (l_file_handle,l_print_text);
I have changed your code a little. User this procedure:
CREATE OR REPLACE PROCEDURE PARSECSV
(
P_FILE_DIR VARCHAR2, -- Oracle directory name
P_FILE_NAME VARCHAR2, -- filename
P_SQL_QUERY VARCHAR2, -- select * from table or some such query
P_DELIMITER CHAR -- column delimiter
) IS
L_CURSOR_HANDLE INTEGER;
L_DUMMY NUMBER;
L_COL_CNT INTEGER;
L_REC_TAB DBMS_SQL.DESC_TAB;
L_COLUMN_VALUE VARCHAR2(300);
L_FILE_HANDLE UTL_FILE.FILE_TYPE;
L_PRINT_TEXT CLOB;
BEGIN
L_FILE_HANDLE := UTL_FILE.FOPEN(P_FILE_DIR,
P_FILE_NAME,
'a',
2047);
L_CURSOR_HANDLE := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(L_CURSOR_HANDLE,
P_SQL_QUERY,
DBMS_SQL.NATIVE);
DBMS_SQL.DESCRIBE_COLUMNS(L_CURSOR_HANDLE,
L_COL_CNT,
L_REC_TAB);
FOR L_CURRENT_COL IN 1 .. L_COL_CNT
LOOP
DBMS_SQL.DEFINE_COLUMN(L_CURSOR_HANDLE,
L_CURRENT_COL,
L_COLUMN_VALUE,
300);
IF L_PRINT_TEXT IS NOT NULL THEN
L_PRINT_TEXT := L_PRINT_TEXT || P_DELIMITER;
END IF;
L_PRINT_TEXT := L_PRINT_TEXT || L_REC_TAB(L_CURRENT_COL).COL_NAME;
END LOOP;
L_PRINT_TEXT := L_PRINT_TEXT || CHR(10) || CHR(13);
UTL_FILE.PUT(L_FILE_HANDLE,
L_PRINT_TEXT);
L_PRINT_TEXT := NULL;
L_DUMMY := DBMS_SQL.EXECUTE(L_CURSOR_HANDLE);
DBMS_OUTPUT.PUT_LINE(L_DUMMY);
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(L_CURSOR_HANDLE) = 0;
FOR L_CURRENT_COL IN 1 .. L_COL_CNT
LOOP
DBMS_SQL.COLUMN_VALUE(L_CURSOR_HANDLE,
L_CURRENT_COL,
L_COLUMN_VALUE);
IF L_PRINT_TEXT IS NOT NULL THEN
L_PRINT_TEXT := L_PRINT_TEXT || P_DELIMITER;
END IF;
L_PRINT_TEXT := L_PRINT_TEXT || L_COLUMN_VALUE;
END LOOP;
L_PRINT_TEXT := L_PRINT_TEXT || CHR(10) || CHR(13);
UTL_FILE.PUT(L_FILE_HANDLE,
L_PRINT_TEXT);
END LOOP;
UTL_FILE.FCLOSE(L_FILE_HANDLE);
DBMS_SQL.CLOSE_CURSOR(L_CURSOR_HANDLE);
END;
First, create a directory in the database and provide the read, write access to that directory.
To create a directory:
CREATE OR REPLACE DIRECTORY alias AS 'pathname';
To Grant Read,Write:
GRANT read,write ON DIRECTORY alias TO {user | role | PUBLIC};
After that use below stored procedure to get the output of SQL query in any file format.
CREATE OR REPLACE PROCEDURE CSV_EXPORT AS
CURSOR c_data IS
SELECT * from table_name;
v_file UTL_FILE.FILE_TYPE;
BEGIN
v_file := UTL_FILE.FOPEN(location => 'FILES1',
filename => 'csv_exp.txt',
open_mode => 'w',
max_linesize => 32767);
FOR cur_rec IN c_data LOOP
UTL_FILE.PUT_LINE(v_file,
cur_rec.column1 || ',' ||
cur_rec.column2 );
END LOOP;
UTL_FILE.FCLOSE(v_file);
EXCEPTION
WHEN OTHERS THEN
UTL_FILE.FCLOSE(v_file);
RAISE;
END;
To run the stored procedure:
EXEC CSV_EXPORT;
here in code FILES1 is directory name.

Dump table containing CLOB to csv using PL/SQL proc

I have assembled a procedure to dump a query containing CLOB columns to a csv file.
It seems to be working fine until I encounter a query containing dates.
ORA-00932: inconsistent datatypes: expected CLOB got DATE
Is there a way to dynamically convert those dates to some default string format to be able to use the procedure as it is now. Or how can I refactor it if necessary?
create or replace
PROCEDURE export_query_csv(
p_query IN VARCHAR2,
p_filename IN VARCHAR2)
IS
l_separator VARCHAR2 (10 CHAR) := ';';
l_dir VARCHAR2 (128 CHAR) := 'MY_DIR';
l_output utl_file.file_type;
l_theCursor INTEGER DEFAULT dbms_sql.open_cursor;
l_columnValue CLOB;
l_status INTEGER;
l_colCnt NUMBER DEFAULT 0;
l_cnt NUMBER DEFAULT 0;
l_descTbl dbms_sql.desc_tab;
l_substrVal VARCHAR2(4000) ;
l_offset NUMBER :=1;
l_amount NUMBER := 3000;
l_clobLen NUMBER :=0;
BEGIN
EXECUTE IMMEDIATE 'alter session set nls_date_format = ''dd-mon-yyyy hh24:mi:ss''';
l_output := utl_file.fopen(l_dir, p_filename, 'wb');
dbms_sql.parse(l_theCursor, p_query, dbms_sql.native);
FOR i IN 1 .. 1000
LOOP
BEGIN
dbms_sql.define_column(l_theCursor, i, l_columnValue);
l_colCnt := i;
EXCEPTION
WHEN OTHERS THEN
IF ( SQLCODE = -1007 ) THEN
EXIT;
ELSE
RAISE;
END IF;
END;
END LOOP;
dbms_sql.describe_columns( l_theCursor, l_colCnt, l_descTbl );
FOR i IN 1 .. l_colCnt
LOOP
utl_file.put_raw(l_output,utl_raw.cast_to_raw('"'));
utl_file.put_raw(l_output,utl_raw.cast_to_raw(l_descTbl(i).col_name));
utl_file.put_raw(l_output,utl_raw.cast_to_raw('"'));
IF i < l_colCnt THEN
utl_file.put_raw(l_output,utl_raw.cast_to_raw(l_separator));
END IF;
END LOOP;
utl_file.put_raw(l_output,utl_raw.cast_to_raw(chr(13) || chr(10)));
l_status := dbms_sql.execute(l_theCursor);
LOOP
EXIT WHEN (dbms_sql.fetch_rows(l_theCursor) <= 0);
FOR i IN 1 .. l_colCnt
LOOP
dbms_sql.column_value(l_theCursor, i, l_columnValue);
l_clobLen := dbms_lob.getlength(l_columnValue);
WHILE l_offset <= l_clobLen
LOOP
l_substrVal := dbms_lob.substr(l_columnValue,l_amount,l_offset);
utl_file.put_raw(l_output,utl_raw.cast_to_raw('"'));
utl_file.put_raw(l_output,utl_raw.cast_to_raw(l_substrVal));
utl_file.put_raw(l_output,utl_raw.cast_to_raw('"'));
l_offset:=l_offset+l_amount;
END LOOP;
l_offset := 1;
IF i < l_colCnt THEN
utl_file.put_raw(l_output,utl_raw.cast_to_raw(l_separator));
END IF;
END LOOP;
utl_file.put_raw(l_output,utl_raw.cast_to_raw(chr(13) || chr(10)));
l_cnt := l_cnt + 1;
END LOOP;
dbms_sql.close_cursor(l_theCursor);
utl_file.fclose(l_output);
END;
Found it myself, following this pattern:
-- Define columns
FOR i IN 1 .. colcnt LOOP
IF desctab(i).col_type = 2 THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, numvar);
ELSIF desctab(i).col_type = 12 THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, datevar);
......
ELSE
DBMS_SQL.DEFINE_COLUMN(curid, i, namevar);
END IF;
END LOOP;

utl file oracle with buffer

I've read this article about Tuning UTL_FILE
Technically the approach is to concatenate records while the size is less than maximum length of the buffer and write the entire buffer when the length is greater
Excerpt from site (code):
IF LENGTH(v_buffer) + c_eollen + LENGTH(r.csv) <= c_maxline THEN
v_buffer := v_buffer || c_eol || r.csv;
ELSE
IF v_buffer IS NOT NULL THEN
UTL_FILE.PUT_LINE(v_file, v_buffer);
END IF;
v_buffer := r.csv;
END IF;
So, I've decided to to move this one in a function
-- constants
C_CHR CONSTANT VARCHAR2(2) := CHR(10);
C_CHRLEN CONSTANT PLS_INTEGER := LENGTH(C_CHR);
C_MAXLEN CONSTANT PLS_INTEGER := 32767;
function FN_GET_BUFFER(p_rec IN VARCHAR2, p_buffer IN VARCHAR2) RETURN VARCHAR2
is
begin
IF LENGTH(p_buffer) + C_CHRLEN + LENGTH(p_rec) <= C_MAXLEN THEN
RETURN p_buffer || C_CHR || p_rec;
ELSE
IF p_buffer IS NOT NULL THEN
RETURN p_buffer;
END IF;
RETURN p_rec;
END IF;
end FN_GET_BUFFER;
And here's how I call my function which doesn't work as expected..
procedure export as
l_tmp_file_name VARCHAR2(30);
l_csv_file_name VARCHAR2(30);
l_file UTL_FILE.FILE_TYPE;
l_buffer VARCHAR2(32767);
CURSOR cur_table
IS
SELECT * FROM table
begin
l_tmp_file_name := 'file.tmp';
BEGIN
l_file := UTL_FILE.FOPEN(C_DIRECTORY_PATH, l_tmp_file_name,'A',C_MAXLEN);
FOR rec IN cur_table
LOOP
l_rec := CONVERT(rec.id || ',' || rec.user ,'AL32UTF8');
l_buffer := l_buffer || FN_GET_BUFFER(l_rec, l_buffer);
if l_buffer is NOT NULL then
UTL_FILE.PUT_LINE(l_file, l_buffer);
l_buffer := NULL;
end if;
END LOOP rec;
UTL_FILE.FCLOSE(l_file);
l_csv_file_name := 'file.csv';
UTL_FILE.FRENAME(src_location => C_DIRECTORY_PATH, src_filename => l_tmp_file_name, dest_location => C_DIRECTORY_PATH, dest_filename => l_csv_file_name, overwrite => FALSE);
EXCEPTION
WHEN OTHERS THEN
UTL_FILE.FCLOSE(l_file);
UTL_FILE.FREMOVE(location => C_DIRECTORY_PATH, filename => l_tmp_file_name);
END;
end export;
The problem is that I get
1,user1
2,user2
3,user3
4,user4
5,
user5
6,user6
7,user7
8,user8
9,user9
10,user10
11,user11
12,user12
13,user13
14,
user14
15,user15
16,user16
17,user17
18,user19
As you can see, after 4 records the buffer is 'full' so it writes instead
the buffer which is user14 instead of writing all on the same line
Thank you
The problem is not your function as such, it's the test you make after the call:
if l_buffer is NOT NULL then
UTL_FILE.PUT_LINE(l_file, l_buffer);
l_buffer := NULL;
end if;
l_buffer is always populated, it's never null. So the test is always true and you write to the file for each row in the table. You need to test for the length of l_buffer and only write when the length is greater than your limit.
But don't just change the test. You need to unpick the logic of FN_GET_BUFFER() to include the buffer population and flushing in a single subroutine, otherwise you will lose data. Something like this:
FOR rec IN cur_table
LOOP
l_rec := CONVERT(rec.id || ',' || rec.user ,'AL32UTF8');
IF LENGTH(l_buffer) + LENGTH(C_CHRLEN) + LENGTH(l_rec) > C_MAXLEN THEN
-- buffer full, write to file
UTL_FILE.PUT_LINE(l_file, l_buffer);
l_buffer := l_rec;
ELSIF LENGTH(l_buffer) = 0 THEN
-- first record
l_buffer := l_rec;
ELSE
-- buffer not full
l_buffer := _l_buffer || C_CHRLEN || l_rec;
END IF;
END LOOP rec;
if LENGTH(l_buffer) > 0 THEN
-- end of table, write last record
UTL_FILE.PUT_LINE(l_file, l_buffer);
end if;
warning coded wildstyle, not tested

Can I iterate over columns of a composite type?

Let's say I have the following table named bar:
key | columnA | columnB | columnC
A | B | C | D
E | F | G | H
I want to write a function taking a key and a string and doing the following (best described by examples):
Input: ('A', '${columnB} - ${columnA}') / Output : 'C - B'
Input: ('B', 'Hello ${columnC}') / Output: 'Hello H'
For the moment, I have this implementation:
CREATE OR REPLACE FUNCTION foo
( param_key IN VARCHAR2
, format_string IN VARCHAR2
)
RETURN VARCHAR2
IS
my_row bar%ROWTYPE;
retval VARCHAR2(4000);
BEGIN
BEGIN SELECT * INTO my_row FROM bar WHERE "key" = param_key;
EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL;
END;
retval := format_string;
retval := REPLACE(retval, '${columnA}', my_row.columnA);
retval := REPLACE(retval, '${columnB}', my_row.columnB);
retval := REPLACE(retval, '${columnC}', my_row.columnC);
RETURN retval;
END;
/
I would like to avoid enumerating all columns one by one in the last part, because the structure of my table can change (new columns for instance). Is there a way to iterate on all columns of my_row, and to replace ${the column name} with the value stored in that column, in a generic way?
Thank you
Another way to achieve this.
Create xmltype from table row.
Create xsl-transform from format_string.
Transform xml using xsl
declare
v_string_format varchar2(200) := '{columnA} + {columnB} + {columnA}{columnB}';
v_key varchar2(10) := 'A';
v_cursor sys_refcursor;
l_xml xmltype;
v_xslt VARCHAR2(500):='<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template match="/ROWSET/ROW">{patern}</xsl:template></xsl:stylesheet>';
begin
-- create xsl transform
v_string_format := upper(v_string_format);
v_string_format := REPLACE(v_string_format,'{','<xsl:value-of select="');
v_string_format := REPLACE(v_string_format,'}','"/>');
v_xslt := replace(v_xslt,'{patern}',v_string_format);
dbms_output.put_line(v_string_format);
-- open cursor for table
open v_cursor for select * from bar where key = v_key;
-- get v_cursor as xmltype.
l_xml := xmltype(v_cursor);
-- print xml
dbms_output.put_line(l_xml.getClobVal());
-- tranform xml and print result
dbms_output.put_line(l_xml.transform(xmltype(v_xslt)).getClobVal());
close v_cursor;
end;
A more efficient solution is this one. For sure you have to write more code and it uses the full scope of dynamic SQL.
CREATE OR REPLACE FUNCTION foo (param_key IN VARCHAR2, format_string IN VARCHAR2)
RETURN VARCHAR2 IS
retval VARCHAR2(4000) := format_string;
cur SYS_REFCURSOR;
curId INTEGER;
descTab DBMS_SQL.DESC_TAB;
colCnt NUMBER;
numvar NUMBER;
datevar DATE;
namevar VARCHAR2(4000);
tsvar TIMESTAMP;
BEGIN
OPEN cur FOR SELECT * FROM bar WHERE "key" = param_key;
curId := DBMS_SQL.TO_CURSOR_NUMBER(cur);
DBMS_SQL.DESCRIBE_COLUMNS(curId, colCnt, descTab);
-- Define columns
FOR i IN 1..colcnt LOOP
IF desctab(i).col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, numvar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_DATE THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, datevar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_TIMESTAMP THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, tsvar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
DBMS_SQL.DEFINE_COLUMN(curid, i, namevar, 4000);
--ELSIF desctab(i).col_type = ... THEN
--DBMS_SQL.DEFINE_COLUMN(curid, i, ...);
END IF;
END LOOP;
-- Fetch Rows
IF DBMS_SQL.FETCH_ROWS(curid) > 0 THEN
-- Fetch only the first row and do not consider if further rows exist,
-- otherwise use WHILE DBMS_SQL.FETCH_ROWS(curid) > 0 LOOP
FOR i IN 1..colcnt LOOP
IF desctab(i).col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
DBMS_SQL.COLUMN_VALUE(curid, i, namevar);
retval := REPLACE(retval, '${'||desctab(i).col_name||'}', namevar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
DBMS_SQL.COLUMN_VALUE(curid, i, numvar);
retval := REPLACE(retval, '${'||desctab(i).col_name||'}', numvar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_DATE THEN
DBMS_SQL.COLUMN_VALUE(curid, i, datevar);
retval := REPLACE(retval, '${'||desctab(i).col_name||'}', datevar);
ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_TIMESTAMP THEN
DBMS_SQL.COLUMN_VALUE(curid, i, tsvar);
retval := REPLACE(retval, '${'||desctab(i).col_name||'}', tsvar);
--ELSIF desctab(i).col_type = ... THEN
--DBMS_SQL.COLUMN_VALUE(curid, i, ...);
--retval := REPLACE(retval, '${'||desctab(i).col_name||'}', ...);
END IF;
END LOOP;
ELSE
retval := NULL;
END IF;
DBMS_SQL.CLOSE_CURSOR(curId);
RETURN retval;
END;
You can get the result you are after using dynamic queries...
CREATE OR REPLACE FUNCTION foo
( param_key IN VARCHAR2
, format_string IN VARCHAR2
)
RETURN VARCHAR2
IS
retval VARCHAR2(4000) := format_string;
cols SYS.ODCIVARCHAR2LIST;
BEGIN
SELECT COLUMN_NAME
BULK COLLECT INTO cols
FROM USER_TAB_COLUMNS
WHERE TABLE_NAME = 'bar'
ORDER BY COLUMN_ID;
FOR i IN 1 .. cols.COUNT LOOP
EXECUTE IMMEDIATE 'SELECT REPLACE( :1, ''${' || cols(i) || '}'', ' || cols(i) || ' ) FROM bar WHERE key = :2'
INTO retval
USING retval, param_key;
END LOOP;
RETURN retval;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL;
END;
/
... but:
This uses dynamic SQL to query the table directly and does not use a %ROWTYPE record.
You may not have access to USER_TAB_COLUMNS (or may need ALL_TAB_COLUMNS) and the DBA might not want you to have access to the data dictionary tables.
It is probably (almost certainly) very inefficient.
I've seen this done before and never let it pass a code review (writing out the explicit column names has always seemed preferable).
So, while it is possible, I would say don't do this.

Dynamic typing or generics in Oracle PL/SQL

For some reason, I have certain fields in a table, that are collections of chars. Chars of different length. Example:
create or replace type t_charray_1 as varray(5) of char(1);
create or replace type t_charray_2 as varray(5) of char(2);
create or replace type t_charray_3 as varray(5) of char(3);
create or replace type t_charray_4 as varray(5) of char(4);
create table mytable (
field1 number,
field2 t_charray_1,
field3 t_charray_3,
Also, I have a function that returns a (fixed length) string representation of a mytable record. This function calls other functions that are returning the string representation of a given collection-typed field. Examples:
function to_chr(
p_array in t_charray_1,
pad_length in number,
p_list_length in number
) return char as
v_res varchar2(255) := '';
begin
for i in 1 .. p_list_length loop
if p_array is not null and p_array.exists(i) and p_array(i) is not null then
v_res := v_res || rpad(p_array(i), pad_length, ' ');
else
v_res := v_res || rpad(' ', pad_length, ' ');
end if;
end loop;
return v_res;
end to_chr;
------------------------------------------------------------------------------
function to_chr(
p_array in t_charray_2,
pad_length in number,
p_list_length in number
) return char as
v_res varchar2(255) := '';
begin
for i in 1 .. p_list_length loop
if p_array is not null and p_array.exists(i) and p_array(i) is not null then
v_res := v_res || rpad(p_array(i), pad_length, ' ');
else
v_res := v_res || rpad(' ', pad_length, ' ');
end if;
end loop;
return v_res;
end to_chr;
Note that these functions are overloaded versions of each other. The only difference in their signature is the type of the p_array argument.
Please also note that the bodies of these functions are identical.
Motivation
I want to eliminate duplicate code. What are my choices?
EDIT I have heard of sys.anydata but never used it. Can it be a solution?
You could write a procedure that takes the largest type, and explicitly CAST the smaller types to the larger type before passing them. Note that CAST can only be used in SQL
DECLARE
x t_charray_1 := t_charray_1();
y t_charray_2 := t_charray_2();
PROCEDURE foo( p_foo t_charray_2 )
AS
BEGIN
FOR i IN p_foo.FIRST..p_foo.LAST loop
dbms_output.put_line( p_foo(i) );
END LOOP;
END;
BEGIN
x.EXTEND;
x.EXTEND;
x(1) := 'A';
x(2) := 'B';
y.EXTEND;
y.EXTEND;
y(1) := 'AA';
y(2) := 'BB';
foo(y);
SELECT CAST(x AS t_charray_2) INTO y FROM dual;
foo(y);
END;
/
Try:
create or replace function to_chr(p_array in anydata,
pad_length in number,
p_list_length in number) return char as
v_res varchar2(255) := '';
x number;
v_array t_charray_4;
v_array1 t_charray_1;
v_array2 t_charray_2;
v_array3 t_charray_3;
begin
dbms_output.put_line(p_array.GetTypeName);
case p_array.GetTypeName
when '<schema>.T_CHARRAY_1' then
x := p_array.GetCollection(v_array1);
select cast(v_array1 as t_charray_4) into v_array from dual;
when '<schema>.T_CHARRAY_2' then
x := p_array.GetCollection(v_array2);
select cast(v_array2 as t_charray_4) into v_array from dual;
when '<schema>.T_CHARRAY_3' then
x := p_array.GetCollection(v_array3);
select cast(v_array3 as t_charray_4) into v_array from dual;
when '<schema>.T_CHARRAY_4' then
x := p_array.GetCollection(v_array);
end case;
for i in 1 .. p_list_length loop
if v_array is not null and v_array.exists(i) and v_array(i) is not null then
v_res := v_res || rpad(v_array(i), pad_length, ' ');
else
v_res := v_res || rpad(' ', pad_length, ' ');
end if;
end loop;
return v_res;
end to_chr;
you can run it like this:
declare
p_array anydata;
v_array t_charray_3 := new t_charray_3('aaa', 'bbb');
v_res varchar2(255);
begin
p_array := anydata.convertcollection(v_array);
v_res := to_chr(p_array => p_array, pad_length => 2, p_list_length => 3);
end;

Resources