Oracle prefix data to a clob - oracle

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.

Related

How to process comma separated input in stored procedure

I need to write one procedure in which there will be table name as an input parameter (Should have only one table at a time) and column names (should have multiple column names comma separated) and column values(should have multiple column names comma separated).
My Attempt:
CREATE OR REPLACE PROCEDURE sp_test_insert(
p_table_name IN VARCHAR2,
p_column_name IN VARCHAR2,-- It can have multiple column names separated by comma
p_column_value IN VARCHAR2-- It can have multiple column names separated by
)
AS
lv_str VARCHAR2(4000);
BEGIN
lv_str := 'INSERT INTO '||p_table_name||'should have multiple column names' ||
'VALUES('||'should have multiple column names' ||')';
EXECUTE IMMEDIATE lv_str;
END;
Tool used: SQL Developer(18c)
I am stuck on how to handle multiple column names and column values inside the procedure body. How will I define an array and proceed accordingly?
Don't. You are setting yourself up to have a procedure that is vulnerable to SQL injection attacks.
If you really want to (please don't):
CREATE OR REPLACE PROCEDURE sp_test_insert(
p_table_name IN VARCHAR2,
p_column_name IN VARCHAR2,
p_column_value IN VARCHAR2
)
AS
lv_str VARCHAR2(4000);
BEGIN
lv_str := 'INSERT INTO '||p_table_name||' (' || p_column_name || ') VALUES(' || p_column_value ||')';
EXECUTE IMMEDIATE lv_str;
END;
/
Then you can do:
BEGIN
sp_test_insert(
'my_table',
'col1, col2, col3',
q'['a', DATE '2022-05-31', 42]'
);
END;
/
But you can also do:
BEGIN
sp_test_insert(
'my_table',
'col1, col2, col3',
q'['a', (SELECT DATE '1970-01-01' FROM secret_table WHERE username = 'Admin' AND password_hash = 'abcgefg1234'), 42]'
);
END;
/
Don't make your application vulnerable to SQL injection attacks; avoid dynamic SQL if you can help it!
If you want to make it more resistant to SQL injection attacks then you can use the DBMS_ASSERT package:
CREATE OR REPLACE PROCEDURE sp_test_insert(
p_table_name IN VARCHAR2,
p_column_name IN SYS.ODCIVARCHAR2LIST,
p_column_value IN SYS.ODCIVARCHAR2LIST
)
AS
lv_str VARCHAR2(4000);
BEGIN
lv_str := 'INSERT INTO '
|| DBMS_ASSERT.SQL_OBJECT_NAME(
DBMS_ASSERT.ENQUOTE_NAME(p_table_name, FALSE)
)
||' ('
|| DBMS_ASSERT.ENQUOTE_NAME(p_column_name(1), FALSE);
FOR i IN 2 .. p_column_name.COUNT LOOP
lv_str := lv_str || ', '
|| DBMS_ASSERT.ENQUOTE_NAME(p_column_name(i), FALSE);
END LOOP;
lv_str := lv_str || ') VALUES('
|| DBMS_ASSERT.ENQUOTE_LITERAL(p_column_value(1));
FOR i IN 2 .. p_column_name.COUNT LOOP
lv_str := lv_str || ', '
|| DBMS_ASSERT.ENQUOTE_LITERAL(p_column_value(i));
END LOOP;
lv_str := lv_str || ')';
EXECUTE IMMEDIATE lv_str;
END;
/
Then:
BEGIN
sp_test_insert(
'MY_TABLE',
SYS.ODCIVARCHAR2LIST( 'COL1', 'COL2', 'COL3'),
SYS.ODCIVARCHAR2LIST( 'a', '31-MAY-2022', '42')
);
END;
/
However, the values are now all passed into the dynamic SQL statement as strings which makes it more difficult to pass data to DATE, TIMESTAMP or INTERVAL (etc.) columns as it relies on implicit data-type conversions.
You should still avoid dynamic SQL if possible.
db<>fiddle here

Oracle appending to a clob

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;
/

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.

Is there a fast PLSQL function for returning a comma-delimited list of column names for a given schema.table?

I'm trying to set up some simple utilities in a PL/SQL environment. Eventually, I'd expect the hardcoded MYTABLE to be replaced by a bind variable.
I've started with the following, which returns an error:
DECLARE
TYPE colNames_typ IS TABLE OF all_tab_cols.column_name%type index by PLS_INTEGER;
v_ReturnVal colNames_typ;
v_sql VARCHAR2(32000);
BEGIN
v_sql :='SELECT column_name FROM all_tab_cols WHERE table_name = ''MYTABLE'' ' ;
EXECUTE IMMEDIATE (v_sql)
INTO v_returnVal;
-- Convert assoc array to a comma delimited list.
END;
The error returned:
PLS-00597: expression 'V_RETURNVAL' in the INTO list is of wrong type
I cant think of a more 'right' type than a table of entries with the exact same variable type as the source.
Any help would be awesome!
Thanks
Is there a fast PLSQL function for returning a comma-delimited list of column names for a given schema.table?
Use LISTAGG:
DECLARE
v_owner ALL_TAB_COLUMNS.OWNER%TYPE := 'SCHEMA_NAME';
v_table_name ALL_TAB_COLUMNS.TABLE_NAME%TYPE := 'TEST_DATA';
v_columns VARCHAR2(32000);
BEGIN
SELECT LISTAGG( '"' || column_name || '"', ',' )
WITHIN GROUP ( ORDER BY COLUMN_ID )
INTO v_columns
FROM all_tab_columns
WHERE owner = v_owner
AND table_name = v_table_name;
DBMS_OUTPUT.PUT_LINE( v_columns );
END;
/
(Note: you also need to pass the owner or, if you have two tables with identical names in different schemas then, you will get columns for both.)
(Note 2: I am assuming you want a list of column names to put into a dynamic query; if so, then you want to surround the column identifiers with double-quotes. If you don't and a column identifier is case-sensitive then you will get an incorrect name as Oracle will implicitly convert unquoted identifiers to upper case when it parses them in a query. If you don't want the quotes then use SELECT LISTAGG( column_name, ',' ).)
Which, if you have the table:
CREATE TABLE test_data (
A NUMBER,
B DATE,
C VARCHAR2(20),
E TIMESTAMP,
Z INTERVAL DAY TO SECOND,
Q CHAR(5)
);
Outputs:
"A","B","C","E","Z","Q"
db<>fiddle here
Not sure if this is what is being asked:
create or replace function get_cols_string(target_owner all_tab_cols.owner%type, target_table_name all_tab_cols.table_name%type) return varchar2 is
outputString varchar2(32767);
oneMore boolean := false;
BEGIN
for current_col in
(SELECT column_name FROM all_tab_cols
WHERE owner = target_owner and table_name = target_table_name) loop
if(oneMore) then
outputString := outputString || ', ';
end if;
outputString := outputString || current_col.column_name;
oneMore := TRUE;
end loop;
return outputString;
END;
/
Rem Test the above with simple cases
create table tab1 (c1 number);
create table tab2 (c1 number, c2 number);
set serveroutput on
declare
owner_name varchar2(32767) := 'SYS';
table_name varchar2(32767) := 'TAB1';
begin
dbms_output.put_line('For: ' || owner_name || '.' || table_name);
dbms_output.put_line(get_cols_string(owner_name, table_name));
end;
/
declare
owner_name varchar2(32767) := 'SYS';
table_name varchar2(32767) := 'TAB2';
begin
dbms_output.put_line('For: ' || owner_name || '.' || table_name);
dbms_output.put_line(get_cols_string(owner_name, table_name));
end;
/
declare
owner_name varchar2(32767) := 'SYS';
table_name varchar2(32767) := 'ALL_TAB_COLS';
begin
dbms_output.put_line('For: ' || owner_name || '.' || table_name);
dbms_output.put_line(get_cols_string(owner_name, table_name));
end;
/
You asked "is there a reason why the previous approach failed" - well yes. The error stems from Oracle being a very strict typing thus making your assumption that there is not "a more 'right' type than a table of entries with the exact same variable type as the source" false. A collection (table) of type_a is not a variable type_a. You attempted to store a variable of type_a into a collection of type_a, thus giving you the wrong type exception.
Now that does not mean you were actually far off. You wanted to store a collection of type_a variables, returned by the select, into a collection of type_a. You can do that, you just need to let Oracle know. You accomplish it with BULK COLLECT INTO. The following shows that process and creates your CSV of column names.
Note: #MTO posted the superior solution, this just shows you how your original could have been accomplished. Still it is a useful technique to keep in your bag of tricks.
declare
type colnames_typ is table of all_tab_cols.column_name%type;
k_comma constant varchar2(1) := ',';
v_returnval colnames_typ;
v_sql varchar2(32000);
v_sep varchar2(1) := ' ';
v_csv_names varchar2(512);
begin
v_sql := 'select column_name from all_tab_cols where table_name = ''MYTABLE'' order by column_id';
execute immediate (v_sql)
bulk collect into v_returnval;
-- Convert assoc array to a comma delimited list.
for indx in 1 .. v_returnval.count
loop
v_csv_names := v_csv_names || v_sep || v_returnval(indx);
v_sep :=k_comma;
end loop;
v_csv_names := trim(v_csv_names);
dbms_output.put_line(v_csv_names);
end;

Can someone help me find the error in this procedure?

I would like to know where is the mistake in this procedure
set serveroutput on
create or replace procedure insert_table(column_name in varchar2, dat in varchar2)
as
table_name varchar2(100):= column_name || '_' || dat;
sql_create varchar2(100) := 'create table '||table_name||'(FIRSTNAME varchar2(100))';
sql_str varchar2(100);
CURSOR c_emp is
select FIRSTNAME
from employees
where column_name = dat;
begin
execute immediate sql_create;
for r_reg in c_emp
loop
sql_str:='INSERT INTO '||table_name||'('''||r_reg.firstname||''')';
execute immediate sql_str;
end loop;
end;
/
execute insert_table('CITY','London');
Edit:
Ok i add the correction mentioned below in the syntax error, but how can I do so that the parameter of the column name can be taken at the cursor, because for now it is of type varchar and 'CITY' should be a row name.
You will need to use the dynamic query in the cursor as follows. Also, you missed VALUES keyword in the INSERT statement which I have added (Please see inline comment in the codes)
Oracle sample data creation:
SQL> CREATE TABLE "EMPLOYEES" (
2 "ID" NUMBER,
3 "FIRSTNAME" VARCHAR2(100 BYTE),
4 "CITY" VARCHAR2(100 BYTE),
5 PRIMARY KEY ( "ID" ) USING INDEX ENABLE
6 );
Table created.
SQL>
Now, Let's create your procedure
SQL> CREATE OR REPLACE PROCEDURE INSERT_TABLE (
2 COLUMN_NAME IN VARCHAR2,
3 DAT IN VARCHAR2
4 ) AS
5
6 TABLE_NAME VARCHAR2(100) := COLUMN_NAME || '_' || DAT;
7 SQL_CREATE VARCHAR2(100) := 'create table ' || TABLE_NAME || '(FIRSTNAME varchar2(100))';
8 C_EMP SYS_REFCURSOR; -- declaration of cursor
9 LV_FNAME EMPLOYEES.FIRSTNAME%TYPE; -- to store the each value from cursor
10 BEGIN
11 EXECUTE IMMEDIATE SQL_CREATE;
12 OPEN C_EMP FOR 'SELECT FIRSTNAME
13 FROM EMPLOYEES
14 WHERE ' || COLUMN_NAME || ' = ''' || DAT || '''';
15 -- above statement used dynamic query in cursor
16 LOOP
17 FETCH C_EMP INTO LV_FNAME;
18 EXIT WHEN C_EMP%NOTFOUND;
19 EXECUTE IMMEDIATE 'INSERT INTO ' || TABLE_NAME || ' VALUES (''' || LV_FNAME || ''')'; -- added VALUES keyword in INSERT statement.
20 END LOOP;
21
22 COMMIT;
23 END INSERT_TABLE;
24 /
Let's execute it and see the result now.
SQL>
SQL> EXECUTE INSERT_TABLE('CITY', 'London');
PL/SQL procedure successfully completed.
SQL>
SQL>
SQL> SELECT * FROM CITY_LONDON;
FIRSTNAME
--------------------------------------------------------------------------------
TEJASH
SQL>
Ohh Yes, It created the desired table and also data is populated correctly.
Cheers!!
You are missing quotes. This
sql_str:='INSERT INTO '||table_name||'('||r_reg.firstname||');';
should be
sql_str:='INSERT INTO '||table_name||'('''||r_reg.firstname||''');';
This will turn INSERT INTO CITY_London (Peter); into INSERT INTO CITY_London ('Peter');.
And according to the docs (e.g. https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/dynamic.htm):
When constructing a single SQL statement in a dynamic string, do not include a semicolon (;) at the end inside the quotation mark.
So the two SQL strings for EXECUTE IMMEDIATE should be:
sql_create varchar2(100) := 'create table ' || table_name || '(FIRSTNAME varchar2(100))';
and
sql_str := 'INSERT INTO ' || table_name || '(''' || r_reg.firstname || ''')';
It is the answer for your last question. You can pass parameters to the cursor like there:
declare
CURSOR c_emp(p_param varchar2) is
select p_param val
from dual;
begin
for r_reg in c_emp('b')
loop
dbms_output.put_line(r_reg.val);
end loop;
end;

Resources