I searched for this error, but since it's very vague, I could not find something similar to understand where is the problem. This code is actually for an Oracle Apex application. I actually have bind variables instead of numbers 1 and 84 (which I confirm are correct values within my tables), but still got same error.
After declaring the variables, it selects a string that will be the name of a column within another table and put it V_COLUMN.
Then i dynamically build a query to get the value of this column and put it into V_VALUE and finally I return a value (which is then shown in a form textfield). Unfortunately it returns the ORA 00905.
When I tried to run the sql commands separately using known values, it runs. So I think there must be some syntax problem somewhere in the dynamic sql. Thanks for any assistance.
DECLARE
V_COLUMN VARCHAR2(50) := 'UNKNOWN';
V_VALUE VARCHAR2(50) := 0;
V_SQL VARCHAR2(500);
BEGIN
SELECT SUB_CAT_ABBREV INTO V_COLUMN FROM SUB_CATEGORY WHERE SUB_CATEGORY_ID = 1;
V_SQL := 'SELECT ' || V_COLUMN || ' INTO V_VALUE FROM PLANNED_EFFORTS WHERE PLAN_ID = 84';
EXECUTE IMMEDIATE V_SQL;
RETURN V_VALUE;
EXCEPTION
WHEN no_data_found THEN
RETURN 'No Data Found Error';
WHEN too_many_rows then
RETURN 'Too many rows';
WHEN OTHERS THEN
RETURN 'Other Error';
END;
Just get rid off your INTO clause from your dynamic SQL statement:
V_SQL := 'SELECT ' || V_COLUMN || ' FROM PLANNED_EFFORTS WHERE PLAN_ID = 84';
EXECUTE IMMEDIATE V_SQL
INTO V_VALUE
Moreover, if you expect more then one value you can use BULK COLLECT INTO and return values into some collection type:
V_SQL := 'SELECT ' || V_COLUMN || ' FROM PLANNED_EFFORTS WHERE PLAN_ID = 84;
EXECUTE IMMEDIATE V_SQL
BULK COLLECT INTO V_VALUES
where V_VALUES can be declared as:
TYPE TABLE_OF_VARCHAR2 IS TABLE OF VARCHAR2(50);
V_VALUES TABLE_OF_VARCHAR2;
and accessed in the loop as follows:
for i in V_VALUES.FIRST .. V_VALUES.LAST LOOP
-- V_VALUES(i)
END LOOP;
Related
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;
When doing an update, we want to do a concurrency check on the ORA_ROWSCN (Oracle db BTW). So I was thinking of creating a function, where you pass in the ora_rowscn you have, table name, column name and an ID. The function would check would do a select statement based on the table, column and id you've passed in and if the returned ora_rowscn is different from the one you passed in, return a true or false is it's the same.
I do validity check on the table_name and column_name passed in to make sure they exists first.
FUNCTION ConcurrencyCheck (
pi_orarowscn_in IN NUMBER,
pi_table_name IN VARCHAR2,
pi_column_name IN VARCHAR2,
pi_id IN NUMBER
)
RETURN BOOLEAN IS
r_data_out_of_date BOOLEAN := false;
ln_orarowscn_current NUMBER := 0;
lv_sql VARCHAR2(300) := '';
lv_column VARCHAR(20) := '';
lv_table VARCHAR(20) := '';
BEGIN
SELECT table_name INTO lv_table from ALL_TABLES WHERE TABLE_NAME = pi_table_name;
IF lv_table = '' THEN
RAISE NO_DATA_FOUND;
ELSE
SELECT column_name INTO lv_column from USER_TAB_COLUMNS WHERE TABLE_NAME = pi_table_name AND COLUMN_NAME = pi_column_name;
IF lv_column = '' THEN
RAISE NO_DATA_FOUND;
ELSE
lv_sql := 'select ORA_ROWSCN from ' || pi_table_name || ' where ' || pi_column_name || ' = ' || pi_id || '';
EXECUTE IMMEDIATE lv_sql INTO ln_orarowscn_current;
IF ln_orarowscn_current <> pi_orarowscn_in THEN
r_data_out_of_date := true;
END IF;
END IF;
END IF;
RETURN r_data_out_of_date;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE;
WHEN OTHERS THEN
RAISE;
END ConcurrencyCheck;
I do not like the dynamic SQL that I have there. I would much rather (not have dynamic SQL but... ) have it like the following:
lv_sql := 'select ORA_ROWSCN from :table_name where :column_name = :id';
EXECUTE IMMEDIATE lv_sql INTO ln_orarowscn_current USING pi_table_name, pi_column_name, pi_id;
but I keep getting an SQL error saying the table is wrong
The other solution is to create a sub-function for ALL the tables that would return the ORA_ROWSCN for each and a IF ELSE in the main function to call each.
I'm looking for best practice here. Is this a time where dynamic SQL is acceptable? Or should I go the "long" route and create a ton of functions/procedures for each table?
Thank you!
As for the best practices, in these cases it's recommended to use the sys.dbms_assert package.
FUNCTION ConcurrencyCheck(
pi_orarowscn_in IN NUMBER,
pi_table_name IN VARCHAR2,
pi_column_name IN VARCHAR2,
pi_id IN NUMBER
)
RETURN BOOLEAN IS
ln_orarowscn_current NUMBER := 0;
lv_sql VARCHAR2(300) := '';
BEGIN
lv_sql := '
select X.ora_rowscn
from '||sys.dbms_assert.sql_object_name(pi_table_name)||' X
where X.'||sys.dbms_assert.simple_sql_name(pi_column_name)||' = :pi_id
';
execute immediate lv_sql
into ln_orarowscn_current
using in pi_id;
return ln_orarowscn_current <> pi_orarowscn_in;
END ConcurrencyCheck;
Note: If I were in your place, I'd give myself the initial hard time of implementing a metadata-driven code generator for the package of one-check-per-one-table functions that would be, as you may have guessed, static PL/SQL code – giving you all those nice compile-time syntax/semantic checks.
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 have a table A which has column A which holds table names as values.
All these tables have a common column C. I need maximum value of this column for each table.
I tried this using dynamic SQL but I'm getting errors. Please suggest.
DECLARE
query1 VARCHAR2(100);
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
Cursor cursor_a IS
SELECT a FROM A;
BEGIN
Open cursor_a;
LOOP
Fetch cursor_a INTO c_table2;
EXIT WHEN cursor_a%notfound;
query1 := 'SELECT max(object_ref) AS "c_obj" FROM c_table' ;
EXECUTE IMMEDIATE query1;
dbms_output.put_line('Maximum value: '|| c_table || c_obj);
END LOOP;
Close cursor_a;
END;
Dynamic SQL can't see your PL/SQL variable: you need to pass it a string which can be executed in the scope of the SQL engine. So you need to concatenate the table name with the statement's boilerplate text:
query1 := 'SELECT max(c) FROM ' || variable_name;
You also need to return the result of the query into a variable.
Here is how it works (I've stripped out some of the unnecessary code from your example):
DECLARE
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
BEGIN
for lrec in ( select a as tab_name from A )
LOOP
EXECUTE IMMEDIATE 'SELECT max(object_ref) FROM ' || lrec.tab_name
into c_obj ;
dbms_output.put_line('Maximum value: '|| lrec.tab_name
|| '='|| c_obj);
END LOOP;
END;
There is some miss match in veriables that you had used i.e.
declared as "c_table" but accessing as "c_table2"
Each table common column name is "C" but accessing as "object_ref"
In dynamic query use INTO keyword to store the value to your varibale
Suggestions
Use concat() function to prepare the query dynamically i.e. something like:
SET #SQL := CONCAT('SELECT max(c) INTO ', c_obj, ' FROM ',c_table);
Steps of implementing dynamic query is:
SET #SQL = <your dynamic query>
PREPARE stmt FROM #SQL;
EXECUTE stmt;
Sample code:
DECLARE
query1 VARCHAR2(100);
c_table VARCHAR2(40);
c_obj VARCHAR2(20);
CURSOR cursor_a IS
SELECT a FROM A;
BEGIN
OPEN cursor_a;
LOOP
FETCH cursor_a INTO c_table;
EXIT WHEN cursor_a%notfound;
SET #SQL := CONCAT('SELECT max(object_ref) AS c_obj INTO ', c_obj, ' FROM ',c_table);
PREPARE stmt FROM #SQL;
EXECUTE stmt;
dbms_output.put_line('Maximum value: '|| c_table || c_obj);
END LOOP;
CLOSE cursor_a;
END;
I am confused as to the meaning of something I just read in the book Oracle PL/SQL Recipes.
In recipe 8-12, it states:
It is not possible to simply pass a NULL value using native dynamic
SQL. At least, you cannot pass a NULL as a literal.
What that suggests to me is that something like the following (A):
DECLARE
v_string varchar2(255);
v_count natural;
BEGIN
v_string := 'SELECT count(*) INTO :count FROM item WHERE color IS NOT NULL';
EXECUTE IMMEDIATE v_string INTO v_count;
dbms_output.put_line('Count: ' || v_count);
END;
Would not work properly. It suggests (B):
Passing an uninitialized variable via the EXECUTE IMMEDIATE statement
will have the same effect as substituting a NULL value for a bind
variable.
The book's example code:
DECLARE
TYPE cur_type IS REF CURSOR;
cur cur_type;
null_value CHAR(1);
sql_string VARCHAR2(150);
emp_rec employees%ROWTYPE;
BEGIN
sql_string := 'SELECT * ' ||
'FROM EMPLOYEES ' ||
'WHERE MANAGER_ID IS :null_val';
OPEN cur FOR sql_string USING null_value;
LOOP
FETCH cur INTO emp_rec;
DBMS_OUTPUT.PUT_LINE(emp_rec.first_name || ' ' || emp_rec.last_name ||
' - ' || emp_rec.email);
EXIT WHEN cur%NOTFOUND;
END LOOP;
CLOSE cur;
END;
/
So my confusion is this: method (A) seems to behave just fine for me, whether I run the query dynamically or not, it is properly selecting records without NULL (or with them, if I invert the conditional).
So what's the deal with this? I'm running Oracle 11g release 2.
What you're not allowed to write is :
OPEN cur FOR sql_string USING NULL;
when you could write :
OPEN cur FOR sql_string USING 'A string';
There's nothing more to it.