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.
Related
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.
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;
/
Background
I'm trying to make a re-usable PL/SQL procedure to move data from one
database to another.
For this purpose, I'm using dynamic SQL.
The procedure executes perfectly if I use a REPLACE with placeholders.
However, for security reasons, I want to use bind variables.
Question
How can I make an entire PL/SQL code block dynamic (with bind
variables)? If I use a REPLACE instead of the bind variables, it works
fine.
How to replicate
To replicate this in your database, create the following procedure as it is:
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
l_cursor_limit pls_integer := 500;
l_values_list varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := q'[
declare
l_cur_limit pls_integer := :l_cursor_limit;
cursor c_get_to_be_moved is
select :i_table_name.*, :i_table_name.rowid
from :i_table_name;
type tab_to_be_moved is table of c_get_to_be_moved%rowtype;
l_to_be_moved tab_to_be_moved;
begin
open c_get_to_be_moved;
loop
fetch c_get_to_be_moved
bulk collect into l_to_be_moved limit l_cur_limit;
exit when l_to_be_moved.count = 0;
for i in 1.. l_to_be_moved.count loop
begin
insert into :i_table_name#:i_destination values (:l_values_list);
exception
when others then
dbms_output.put_line(sqlerrm);
l_to_be_moved.delete(i);
end;
end loop;
forall i in 1.. l_to_be_moved.count
delete
from :i_table_name
where rowid = l_to_be_moved(i).rowid;
for i in 1..l_to_be_moved.count loop
if (sql%bulk_rowcount(i) = 0) then
raise_application_error(-20001, 'Could not find ROWID to delete. Rolling back...');
end if;
end loop;
commit;
end loop;
close c_get_to_be_moved;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;]';
execute immediate l_sql using l_cursor_limit, i_table_name, i_destination, l_values_list;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;
/
And then you can execute the procedure with the following:
begin
move_data('MySchemaName', 'MyTableName', 'MyDatabaseLinkName');
end;
/
Due to many reasons(inability to generate an appropriate execution plan, security checking, etc.) Oracle does not allow identifiers binding (table names, schema names, column names and so on). So if it's really necessary, the only way is to hard code those identifiers after some sort of validation (to prevent SQL injection).
If I understand well, you could try a trick, by using a dynamic SQL inside a dynamic SQL.
setup:
create table tab100 as select level l from dual connect by level <= 100;
create table tab200 as select level l from dual connect by level <= 200;
create table tabDest as select * from tab100 where 1 = 2;
This will not work:
create or replace procedure testBind (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := 'insert into tabDest select * from :tableName';
execute immediate vSQL using pTableName;
end;
But this will do the trick:
create or replace procedure testBind2 (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := q'[declare
vTab varchar2(30) := :tableName;
vSQL2 varchar2(32000) := 'insert into tabDest select * from ' || vTab;
begin
execute immediate vSQL2;
end;
]';
execute immediate vSQL using pTableName;
end;
I think you can do it simpler.
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := 'insert into '||i_destination||'.'||i_table_name||' select * from '||i_schema_name||'.'||i_table_name;
execute immediate l_sql;
end;
If you are concerned about SQL-Injection, have a look at package DBMS_ASSERT. This PL/SQL package provides function to validate properties of input values.
I've been looking at the DBMS_SQL package in Oracle and trying to see if there is a way to create a view or something that users can select from to see the results of a dynamic SQL query.
I have a test procedure based on the documentation:
CREATE OR REPLACE PROCEDURE test_dyn_sql AS
cursor_name INTEGER;
rows_processed INTEGER;
l_query LONG;
BEGIN
l_query := 'SELECT SYSDATE AS the_date, ''ABC'' AS the_string, 1 AS the_int FROM dual';
cursor_name := dbms_sql.open_cursor;
DBMS_SQL.PARSE(cursor_name, l_query, DBMS_SQL.NATIVE);
rows_processed := DBMS_SQL.EXECUTE(cursor_name);
DBMS_SQL.CLOSE_CURSOR(cursor_name);
EXCEPTION
WHEN OTHERS THEN
DBMS_SQL.CLOSE_CURSOR(cursor_name);
DBMS_OUTPUT.put_line('ERROR');
END;
But this just executes the statement and does not return anything. What I'd like is a view so a user can just do a SELECT the_date FROM some_view and get the results. I will not know the names or number of columns in advance, so that's why I'm after a dynamic SQL solution.
" I will not know the names or number of columns in advance, so that's
why I'm after a dynamic SQL solution"
That's pretty hard to implement in SQL: SQL is all about the data structures, and it really expects the columns to exist up front. So you cannot build a VIEW on a mutable data structure.
You can implement a function which returns a ref cursor. This is a pointer to a data structure which can be interpreted by a client, say as a JDBC ResultSet.
Here's an example function which takes a table name and column name, assembles the query and returns its result set.
CREATE OR REPLACE FUNCTION test_dyn_sql
(tab_name in varchar2
, col_name in varchar2)
return sys_refcursor
AS
return_value sys_refcursor;
BEGIN
open return_value for
'SELECT SYSDATE AS the_date, '||col_name||' FROM '||tab_name;
return return_value;
END;
/
The output is not very elegant in SQL*Plus but you get the idea.
SQL> select test_dyn_sql ('EMP', 'NAME') from dual;
TEST_DYN_SQL('EMP','
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
THE_DATE NAME
--------- ------------------------------
03-APR-15 FOX IN SOCKS
03-APR-15 MR KNOX
03-APR-15 DAISY-HEAD MAYZIE
SQL>
I suggest you stick with Native Dynamic SQL (that is, execute immediate) as much as possible: as you can see, it's really simple compared to DBMS_SQL. Only reach for DBMS_SQL when you have some extremely complicated requirement.
I appreciate this may not be the solution form you're looking for. If that is the case please edit your question to provide more details about the problem you're trying to solve.
If you want your code to return the value of rows_processed variable use the code below which will return 0.
CREATE OR REPLACE FUNCTION test_dyn_sql(asd INTEGER) RETURN INTEGER AS
cursor_name INTEGER;
rows_processed INTEGER;
l_query LONG;
BEGIN
l_query := 'SELECT SYSDATE AS the_date, ''ABC'' AS the_string, 1 AS the_int FROM dual';
cursor_name := dbms_sql.open_cursor;
DBMS_SQL.PARSE(cursor_name, l_query, DBMS_SQL.NATIVE);
rows_processed := DBMS_SQL.EXECUTE(cursor_name);
DBMS_SQL.CLOSE_CURSOR(cursor_name);
RETURN rows_processed;
EXCEPTION
WHEN OTHERS THEN
DBMS_SQL.CLOSE_CURSOR(cursor_name);
DBMS_OUTPUT.put_line('ERROR');
RETURN 0;
END;
This is how you call the function from plsql
DECLARE
rows_precessed INTEGER;
BEGIN
rows_precessed := test_dyn_sqll(0);
DBMS_OUTPUT.put_line(rows_precessed);
END;
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;
/