Oracle appending to a clob - oracle

I have a working scenario that adds a newline to a CLOB every time during an update.
I have a similar scenario, below, that doesn't seem to be working. I can't understand why and was hoping someone can explain what the problem is as I would prefer to have the newline code embedded in the procedure instead of having the user having to add it for every update.
-- Works
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE table t(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
c CLOB,
create_date DATE DEFAULT SYSDATE
);
/
insert into t (c) values (
rpad('X',20,'X')
);
/
create or replace procedure lob_append( p_id in number, p_text in varchar2 )
as
l_clob clob;
begin
select c into l_clob from t where seq_num = p_id for update;
dbms_lob.writeappend( l_clob, length(p_text), p_text );
end;
/
select * from t;
/
exec lob_append(1, chr(10)|| rpad('Z',20,'Z'));
/
select * from t;
/
-- Doesn't work
DROP table t;
/
CREATE table t(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
c CLOB,
create_date DATE DEFAULT SYSDATE
);
/
insert into t (c) values (
rpad('X',20,'X')
);
/
create or replace procedure lob_append( p_id in number, p_text in varchar2 )
as
l_clob clob;
begin
select c into l_clob from t where seq_num = p_id for update;
-- newline not added
l_clob := l_clob || chr(10);
dbms_lob.writeappend( l_clob, length(p_text), p_text );
end;
/
select * from t;
/
-- Data not added
exec lob_append(1, rpad('Z',20,'Z'));
select * from t;
/

select c into l_clob from t where seq_num = p_id for update;
At this point in your code, l_clob contains a lob locator (let's call it LOCATOR_A) which points to a specific string in the database. If you call dbms_lob.writeappend(l_clob, ...) it will update the string at LOCATOR_A with the new value.
l_clob := l_clob || chr(10);
When you call this line, the first part l_clob || chr(10) creates a NEW lob locator (let's call it LOCATOR_B) pointing to a temporary CLOB location, and gives it the string value + newline. The second part l_clob := value assigns the temporary lob locator LOCATOR_B to the l_clob variable. At this point, you've lost the reference to the permanent lob pointer LOCATOR_A that's stored in the database.
dbms_lob.writeappend( l_clob, length(p_text), p_text )
Now you update the temporary lob LOCATOR_B. It is immediately discarded when the procedure returns. The database clob which LOCATOR_A points to is not changed.
I would recommend adding the newline to your varchar2 instead, so you don't create a new temporary clob.
create or replace procedure lob_append( p_id in number, p_text in varchar2 )
as
l_clob clob;
begin
select c into l_clob from t where seq_num = p_id for update;
p_text := chr(10) || p_text;
dbms_lob.writeappend( l_clob, length(p_text), p_text );
end;
/

Related

Oracle prefix data to a clob

I have a procedure (see test case below), which works fine that appends data to a CLOB. In addition to appending the data in the CLOB I'm encapsulating the VALUE of SYSDATE in tags so I can keep track of when the data was updated in the CLOB.
Though I'm only showing data with 10-20 characters in my example the CLOB can be extremely big and in many cases concatenated within a block before being placed into the CLOB.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE table t(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
c CLOB DEFAULT ' ',
create_date DATE DEFAULT SYSDATE
);
/
insert into t (c) values (' ')
/
CREATE OR REPLACE PROCEDURE lob_append(
p_clob IN OUT CLOB,
p_text IN VARCHAR2
)
AS
l_text varchar2(32760);
l_date_string VARCHAR2(50);
BEGIN
select '[' || TO_CHAR (SYSDATE, 'MMDDYYYY-HH24:MI:SS') || ']'
into l_date_string from dual;
-- newline each time code is appended for clarity.
l_text :=chr(10) || l_date_string || chr(10)
|| p_text || chr(10)
|| l_date_string||chr(10);
dbms_lob.writeappend(p_clob, length(l_text), l_text );
END;
/
DECLARE
l_clob CLOB := empty_clob();
BEGIN
SELECT c INTO l_clob FROM t WHERE seq_num = 1 FOR UPDATE;
lob_append(l_clob, rpad('Z',20,'Z'));
l_clob := empty_clob();
SELECT c INTO l_clob FROM t WHERE seq_num = 1 FOR UPDATE;
lob_append(l_clob, rpad('Y',10,'Y'));
END;
/
I'm looking to do something like this but by calling a procedure similar to the one posted above.
UPDATE T
SET C = RPAD('A',20,'A') || CHR(10) || C
WHERE SEQ_NUM = 1
/
I welcome any and all helpful suggestions and or design changes since this hasn't been implemented yet. Thanks in advance to all who answer.

Oracle rewrite a procedure to be generic

I have a procedure, which is working well that other applications want to use.
As you can see table and column names are hardcoded into the procedure, which makes it difficult to share the code. Is there a way this can be rewritten so it could be shared. I want to avoid passing in more values if possible as it will make the code awkward and klunky.
Any suggestions would be greatly appreciated.
SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE table t(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
c CLOB,
create_date DATE DEFAULT SYSDATE
);
/
insert into t (c) values (
rpad('X',20,'X')
);
/
create or replace procedure lob_append( p_id in number, p_text in varchar2 )
as
l_clob clob;
l_text varchar2(32760);
l_system_date_time VARCHAR2(50);
begin
select c into l_clob from t where seq_num = p_id for update;
SELECT TO_CHAR (SYSDATE, 'MMDDYYYY HH24:MI:SS') into l_system_date_time from dual;
-- newline each time code is appended for clarity.
l_text := chr(10) || p_text || chr(10) || '['||l_system_date_time||']'||chr(10);
dbms_lob.writeappend( l_clob, length(l_text), l_text );
end;
/
exec lob_append(1, rpad('Z',20,'Z'));
exec lob_append(1, rpad('Y',10,'Y'));
select * from t;
/
Don't SELECT from the table in the procedure, instead pass the CLOB into the procedure; that way you do not need to use dynamic SQL in the procedure:
CREATE PROCEDURE lob_append(
p_clob IN OUT CLOB,
p_text IN VARCHAR2
)
AS
l_text varchar2(32760);
BEGIN
-- newline each time code is appended for clarity.
l_text := chr(10)
|| p_text || chr(10)
|| '['||TO_CHAR (SYSDATE, 'MMDDYYYY HH24:MI:SS')||']'||chr(10);
dbms_lob.writeappend(p_clob, length(l_text), l_text );
END;
/
Then, when you want to call it:
DECLARE
l_clob CLOB;
BEGIN
SELECT c INTO l_clob FROM t WHERE seq_num = 1 FOR UPDATE;
lob_append(l_clob, rpad('Z',20,'Z'));
END;
/
DECLARE
l_clob CLOB;
BEGIN
SELECT c INTO l_clob FROM t WHERE seq_num = 1 FOR UPDATE;
lob_append(l_clob, rpad('Y',10,'Y'));
END;
/
db<>fiddle here
This is how I understood the question.
Is there a way this can be rewritten so it could be shared.
Yes, by using dynamic SQL. You'd compose all statements you need into a varchar2 local variable and then run it using execute immediate. It means that you'd, actually, have to pass table/column names into the procedure so that you'd be able to use them in a "generic" way.
Pay attention to SQL injection, i.e. bad people might try to misuse that code. Read more about DBMS_ASSERT package.
I want to avoid passing in more values if possible as it will make the code awkward and klunky.
Well, that's right the opposite of what you'll have to do. If the procedure has to be "generic", you have to pass table/column names (as I already said), so that means more parameters than you have now.
Is it worth it? I don't like dynamic SQL. Although it seems that it "solves" some problems, it brings in another. Code is difficult to maintain and debug. Basically, there's no such thing as free lunch. There are benefits, and there are drawbacks.

How do we split a CLOB (with some lines with more than 32K characters) line by line via PLSQL?

I am trying to split a huge CLOB which contains lines with more than 32K characters.
I tried to use this
SELECT REGEXP_SUBSTR(file_cont, '[^'||chr(10)||']+', 1, LEVEL) AS substr
from data_tab where interface = 'Historical'
CONNECT BY LEVEL <= LENGTH(REGEXP_REPLACE(file_cont, '[^'||chr(10)||']+')) + 1
The table data_tab contains some files with pipe as a separator.
The column file_cont is a clob which contains the file we are interested in.
However, when I try to execute the above query, it looks like there is an infinite loop.
For information, the CLOB contains more than 600 lines.
What I want to do is to split the clob, line by line into distinct CLOB.
Do you know a query that can display this result without falling into an infinite loop?
EDIT : The file's size is 22MB.
Thank you in advance.
I have a special package for split and PCRE regular expressions:
https://github.com/xtender/XT_REGEXP
You can find this function in https://github.com/xtender/XT_REGEXP/blob/master/xt_regexp.pck
/**
* Clob simple split
*/
function clob_split_simple(p_clob in clob,p_delim in varchar2)
return clob_table pipelined is
row clob;
l_b number:=1;
l_e number:=1;
$IF DBMS_DB_VERSION.ver_le_11 $THEN
$ELSE
pragma UDF;
$END
begin
while l_e>0
loop
l_e:=instr(p_clob,p_delim,l_b);
pipe row(substr(p_clob,l_b,case when l_e>0 then l_e-l_b else length(p_clob)+length(p_delim)-l_b end));
l_b:=l_e+length(p_delim);
end loop;
end clob_split_simple;
So you can either use this pipelined function:
select *
from table(xt_regexp.clob_split_simple(:clob,chr(10));
or take this code as an example.
clob_table is just a table of clob:
https://github.com/xtender/XT_REGEXP/blob/master/types.sql
create or replace type clob_table as table of clob;
/
create or replace type date_table as table of date;
/
create or replace type number_table as table of number;
/
create or replace type varchar2_table as table of varchar2(4000);
/
create or replace type xml_table as table of xmltype;
/
Update: fixed a bug with long matches: dbms_lob.substr which returns varchar2, replaced with substr(clob) which return clob.
You can use a PL/SQL function to read the and split the value:
If you have the data type:
CREATE TYPE clob_table AS TABLE OF CLOB;
Then the function:
CREATE FUNCTION split_clob(
p_value IN CLOB,
p_delimiter IN VARCHAR2 DEFAULT ','
) RETURN clob_table PIPELINED
IS
v_start PLS_INTEGER;
v_next PLS_INTEGER;
v_len PLS_INTEGER;
BEGIN
v_start := 1;
LOOP
v_next := DBMS_LOB.INSTR( p_value, p_delimiter, v_start );
v_len := CASE v_next WHEN 0 THEN LENGTH( p_value ) + 1 ELSE v_next END - v_start;
PIPE ROW ( SUBSTR( p_value, v_start, v_len ) );
EXIT WHEN v_next = 0;
v_start := v_next + LENGTH(p_delimiter);
END LOOP;
END;
/
For the sample data:
CREATE TABLE table_name ( value CLOB );
DECLARE
v_value TABLE_NAME.VALUE%TYPE := EMPTY_CLOB();
BEGIN
FOR ch IN 65 .. 68 LOOP
FOR i IN 1 .. 10 LOOP
v_value := v_value || RPAD( CHR(ch), 4000, CHR(ch) );
END LOOP;
IF ch < 68 THEN
v_value := v_value || CHR(10);
END IF;
END LOOP;
INSERT INTO table_name ( value ) VALUES ( v_value );
END;
/
Then the output of:
SELECT SUBSTR( s.column_value, 1, 10 ) AS value,
LENGTH( s.column_value ) AS len
FROM table_name t
CROSS APPLY TABLE( split_clob( t.value, CHR(10) ) ) s
Is:
VALUE
LEN
AAAAAAAAAA
40000
BBBBBBBBBB
40000
CCCCCCCCCC
40000
DDDDDDDDDD
40000
db<>fiddle here

Modify multiple oracle triggers

I have a task that involves updating many triggers which are exactly the same query but applied to several different tables. Is there a way to update all these TRIGGERS using a FOR or similar statement? Actually what I need to do is modify the WHEN clause for all this triggers.
you can use dbms_metadat for this.
for example:
declare
type arr_tab is table of varchar2(30);
v_arr arr_tab;
v_trig clob;
begin
dbms_metadata.set_transform_param( DBMS_METADATA.SESSION_TRANSFORM,
'SQLTERMINATOR', FALSE );
v_arr := arr_tab('TEST_TRIG', 'TEST2_TRIG'); -- change these triggers.
for idx in 1..v_arr.count
loop
v_trig := dbms_metadata.get_ddl('TRIGGER',v_arr(idx), user);
execute immediate regexp_replace(regexp_replace(v_trig, 'ALTER TRIGGER.*', ''), 'WHEN ([^\)]*\))', 'WHEN (1=1)', 1, 1, 'mi');
end loop;
end;
/
the 'WHEN ([^\)]*\))', 'WHEN (1=1)' part replaces the WHEN clause with (in my case) WHEN (1=1).
You can use dba_triggers to extract the text of the trigger into CREATE or replace statements. But due to some of the columns being LONG datatype you will have trouble extracting them as VARCHAR2. This can be resolved by using Tom Kytes package which is lost somewhere on the oracle site. I include my own version which you may have to modify to meet your needs.
Run the select, insert your when clause and then run the create or replace statements.
This won't work due to the trigger_body being a long datatype
select 'CREATE OR REPLACE TRIGGER '|| description
||trigger_body
from dba_triggers
where owner = 'Your schema'
but this should work if your triggers are not more than 4000 characters
select 'CREATE OR REPLACE TRIGGER '|| description
|| ADMIN.LONG_HELP.SUBSTR_OF('select trigger_body from dba_triggers where trigger_name = :0',
1,4000,'0',dt.trigger_name)
from dba_triggers dt
where owner = 'YourSchema';
CREATE OR REPLACE PACKAGE ADMIN.LONG_HELP
/******************************************************************************
NAME: LONG_HELP
PURPOSE: Read fields of type long. (commonly found in data dictionary)
REVISIONS:
Ver Date Author Description
--------- ---------- --------------- ------------------------------------
1.0 10/27/2011 1. Created this package. based on Tom Kyte's column here
http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:839298816582
note that it only retrieves the first 4000 characters of any LONG column
USAGE in a WHERE
INSTR(
ADMIN.LONG_HELP.SUBSTR_OF('SELECT text from all_views where view_name =:o ',
1,4000,'o',m2.obj_name),m1.FK_ID) > 0
******************************************************************************/
--AUTHID CURRENT_USER
--removed to get around ORA-29470: Effective userid or roles are not the same as when cursor was parsed
--restrict usage to admin schema for Oracle 11g
AS
FUNCTION substr_of (p_query IN VARCHAR2,
p_from IN NUMBER,
p_for IN NUMBER,
p_name1 IN VARCHAR2 DEFAULT NULL ,
p_bind1 IN VARCHAR2 DEFAULT NULL ,
p_name2 IN VARCHAR2 DEFAULT NULL ,
p_bind2 IN VARCHAR2 DEFAULT NULL ,
p_name3 IN VARCHAR2 DEFAULT NULL ,
p_bind3 IN VARCHAR2 DEFAULT NULL ,
p_name4 IN VARCHAR2 DEFAULT NULL ,
p_bind4 IN VARCHAR2 DEFAULT NULL )
RETURN VARCHAR2;
END LONG_HELP;
/
CREATE OR REPLACE PACKAGE BODY ADMIN.LONG_HELP
AS
g_cursor NUMBER := DBMS_SQL.open_cursor;
g_query VARCHAR2 (32765);
PROCEDURE bind_variable (p_name IN VARCHAR2, p_value IN VARCHAR2)
IS
BEGIN
IF (p_name IS NOT NULL)
THEN
DBMS_SQL.bind_variable (g_cursor, p_name, p_value);
END IF;
END BIND_VARIABLE;
FUNCTION substr_of (p_query IN VARCHAR2,
p_from IN NUMBER,
p_for IN NUMBER,
p_name1 IN VARCHAR2 DEFAULT NULL ,
p_bind1 IN VARCHAR2 DEFAULT NULL ,
p_name2 IN VARCHAR2 DEFAULT NULL ,
p_bind2 IN VARCHAR2 DEFAULT NULL ,
p_name3 IN VARCHAR2 DEFAULT NULL ,
p_bind3 IN VARCHAR2 DEFAULT NULL ,
p_name4 IN VARCHAR2 DEFAULT NULL ,
p_bind4 IN VARCHAR2 DEFAULT NULL )
RETURN VARCHAR2
AS
/******************************************************************************
NAME: LONG_HELP.SUBSTR_OF
PURPOSE: CONVERT long data fields into VARCHAR2
WHOSE DATA IS CHANGED: none
WHAT USES THIS:
WHERE ARE THE RESOURCES NEEDED:
******************************************************************************/
l_buffer VARCHAR2 (4000);
l_buffer_len NUMBER;
BEGIN
IF (NVL (p_from, 0) <= 0)
THEN
raise_application_error (-20002,
'From must be >= 1 (positive numbers)');
END IF;
IF (NVL (p_for, 0) NOT BETWEEN 1 AND 4000)
THEN
raise_application_error (-20003, 'For must be between 1 and 4000');
END IF;
IF (p_query <> g_query OR g_query IS NULL)
THEN
IF (UPPER (TRIM (NVL (p_query, 'x'))) NOT LIKE 'SELECT%')
THEN
raise_application_error (-20001, 'This must be a select only');
END IF;
DBMS_SQL.parse (g_cursor, p_query, DBMS_SQL.native);
g_query := p_query;
END IF;
bind_variable (p_name1, p_bind1);
bind_variable (p_name2, p_bind2);
bind_variable (p_name3, p_bind3);
bind_variable (p_name4, p_bind4);
DBMS_SQL.define_column_long (g_cursor, 1);
IF (DBMS_SQL.execute_and_fetch (g_cursor) > 0)
THEN
DBMS_SQL.column_value_long (g_cursor,
1,
p_for,
p_from - 1,
l_buffer,
l_buffer_len);
END IF;
RETURN l_buffer;
END substr_of;
END LONG_HELP;
/

Returning dataset with PL/SQL and a variable table name

I'm trying to write a PL/SQL function to store a select statement with a variable table name (a bit weird i know but it is actually a good design decision). The following code does not work...but I'm not sure how to both take a variable table name (building the query) and return a dataset. Anyone have any experience in this? TIA.
CREATE OR REPLACE FUNCTION fn_netstat_all (casename in varchar2)
RETURN resultset_subtype
IS
dataset resultset_subtype;
v_sql varchar2(25000);
v_tablename varchar2(50);
begin
v_sql := 'SELECT * FROM ' || casename || '_netstat;';
OPEN dataset FOR
execute immediate v_sql;
return dataset;
end;
If your resultset_subtype is a ref_cursor (or just replace resultset_subtype with a ref_cursor) you could:
CREATE OR REPLACE
FUNCTION fn_netstat_all (
casename IN VARCHAR2
)
RETURN resultset_subtype
IS
dataset resultset_subtype;
BEGIN
OPEN dataset
FOR 'SELECT * FROM ' || casename || '_netstat';
RETURN dataset;
END fn_netstat_all;
FWIW, you might want to look into the DBMS_ASSERT package to wrap the casename variable to help protect against SQL Injection attacks in your dynamic SQL.
Hope it helps...
Below it is assumed all tables are similar. It's also possible to select a subset of colums that are similar in every table without using DBMS_SQL. I have also paid some attention to SQL injection mentioned by Ollie.
create table so9at (
id number(1),
data varchar2(5)
);
insert into so9at values (1, 'A-AAA');
insert into so9at values (2, 'A-BBB');
insert into so9at values (3, 'A-CCC');
create table so9bt (
id number(1),
data varchar2(5)
);
insert into so9bt values (5, 'B-AAA');
insert into so9bt values (6, 'B-BBB');
insert into so9bt values (7, 'B-CCC');
create table secret_identities (
cover_name varchar2(20),
real_name varchar2(20)
);
insert into secret_identities values ('Batman', 'Bruce Wayne');
insert into secret_identities values ('Superman', 'Clark Kent');
/* This is a semi-secure version immune to certain kind of SQL injections. Note
that it can be still used to find information about any table that ends with
't'. */
create or replace function cursor_of (p_table_id in varchar2)
return sys_refcursor as
v_cur sys_refcursor;
v_stmt constant varchar2(32767) := 'select * from ' || dbms_assert.qualified_sql_name(p_table_id || 't');
begin
open v_cur for v_stmt;
return v_cur;
end;
/
show errors
/* This is an unsecure version vulnerable to SQL injection. */
create or replace function vulnerable_cursor_of (p_table_id in varchar2)
return sys_refcursor as
v_cur sys_refcursor;
v_stmt constant varchar2(32767) := 'select * from ' || p_table_id || 't';
begin
open v_cur for v_stmt;
return v_cur;
end;
/
show errors
create or replace procedure print_values_of (p_cur in sys_refcursor) as
type rec_t is record (
id number,
data varchar2(32767)
);
v_rec rec_t;
begin
fetch p_cur into v_rec;
while p_cur%found loop
dbms_output.put_line('id = ' || v_rec.id || ' data = ' || v_rec.data);
fetch p_cur into v_rec;
end loop;
end;
/
show errors
declare
v_cur sys_refcursor;
begin
v_cur := cursor_of('so9a');
print_values_of(v_cur);
close v_cur;
v_cur := cursor_of('so9b');
print_values_of(v_cur);
close v_cur;
/* SQL injection vulnerability */
v_cur := vulnerable_cursor_of('secret_identities --');
dbms_output.put_line('Now we have a cursor that reveals all secret identities. Just see DBMS_SQL.DESCRIBE_COLUMNS ...');
close v_cur;
/* SQL injection made (mostly) harmless - will throw ORA-44004: invalid qualified SQL name */
v_cur := cursor_of('secret_identities --');
close v_cur;
end;
/

Resources