PL/SQL: How to pass a tablename to a dynamic SQL? - oracle

I am new to PL/SQL and I try to have the table name of a SELECT dynamically set by a parameter.
This is working fine.
DECLARE
FUNCTION foo (pat VARCHAR) RETURN NUMBER IS
tabname VARCHAR (100) := 'my_table';
n NUMBER := -1;
sqlcmd VARCHAR (100) := 'SELECT COUNT(*) FROM ' || tabname || ' WHERE bezeichnung LIKE :1';
BEGIN
EXECUTE IMMEDIATE sqlcmd INTO n USING pat;
RETURN n;
END foo;
BEGIN
dbms_output.put_line (foo ('bla%'));
END;
If I try to have tabname set by a parameter as it is with pat then it fails with ther error :
invalid table name
DECLARE
FUNCTION defval (pat VARCHAR, offs NUMBER) RETURN NUMBER IS
tabname VARCHAR (100) := 'A_KGL_EIGENSCHAFTEN';
n NUMBER := -1;
sqlcmd VARCHAR (100) := 'SELECT COUNT(*) FROM :1 WHERE bezeichnung LIKE :2';
BEGIN
EXECUTE IMMEDIATE sqlcmd INTO n USING tabname, pat;
dbms_output.put_line ('tabname: ' || tabname);
dbms_output.put_line ('n: ' || n);
RETURN n;
END defval;
BEGIN
dbms_output.put_line (defval ('LPG.GAX.%.DBE', 2));
END;
How can I set the table name by this bound parameters?

If you are looking to prevent the sql injection using concate then you can use the sys.DBMS_ASSERT.SQL_OBJECT_NAME(p_table_name)
So your string variable should look like this:
sqlcmd VARCHAR (100) := 'SELECT COUNT(*) FROM '
|| sys.DBMS_ASSERT.SQL_OBJECT_NAME(tabname)
|| ' WHERE bezeichnung LIKE :1';
You can learn more about DBMS_ASSERT from oracle documentation.

You cannot use bind arguments to pass the names of schema objects to a dynamic SQL statement.
From the Oracle documentation : https://docs.oracle.com/cd/B12037_01/appdev.101/b10807/13_elems017.htm
So yes, your 2nd query is bound to fail. Your concat in the first one seem the good way to go.

Related

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;

PL/SQL ORA-00905: missing keyword for Set Value

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;

To Dynamic SQL or Not

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.

Dynamically assigning variables oracle sql

I have a table attribute_config with below columns:
table_name column_name key
Let us say is has below 2 rows
account accountphone accountnum
customer customernumber customerid
Key can be only accountnum or customerid.
I have to write code which will accept (i_accountnum,i_customerid) and;
fetch the respective values from columns mentioned in column_name in tables mentioned in table_name using the key in where condition.
For ex: select accountphone from account where accountnum = i_accountnum
select customernumber from customer where customerid = i_customerid
the complete query should be formed dynamically, whether to pass i_accountnum or i_customerid in the query also needs to be decided dynamically. if key - accountnum, i_accountnum will be passed to where condition.
I have been trying on these lines so far, this is not working, i know it is wrong.
declare
v_accountnum varchar2(20);
v_customerid varchar2(20);
v_attribute_value varchar2(20);
v_stmt varchar2(255);
begin
Account_Num := 'TestCustomer'; -- input to the function
v_customer_ref := 'TestAccount'; -- input to the function
for i in (Select * from attribute_config) loop
v_stmt := 'select ' || i.column_name || ' from ' || i.table_name ||' where ' || i.key|| ' = v_' || i.key;
execute immediate v_Stmt into v_attribute_value;
end loop;
end;
This will fix your code, but I do not see any advantage of using dynamic query when your code should accept 2 parameters(i_accountnum,i_customerid) - which is already static situation and fetch the relevant values, perhaps only in learning purposes.
declare
procedure fecth_values(i_accountnum account.accountnum%type,
i_customerid customer.customerid%type) return varchar2 is
v_attribute_value varchar2(20);
begin
for i in (select * from attribute_config) loop
execute immediate 'select ' || i.column_name || ' from ' ||
i.table_name || ' where ' || i.key || ' = ' || case when i.key = 'accountnum' then i_accountnum when i.key = 'customerid' then i_customerid end;
into v_attribute_value;
dbms_output.put_line(v_attribute_value);
end loop;
return null;
end;
begin
fecth_values(1, 1);
end;
Your where clause was wrong the i.key should be compared against the inputed values, not the 'v_' || i.key, which is undeclared when you execute your stmt.

Update statement error in SQL procedure

I am writing a stored procedure to update data in a table where I will pass a string with the new data (col1='new values', col2='new 2 values'). But when I am compiling my stored procedure , i am getting an error :- "missing equal sign".
Even i tried doing it in a different way (commented code in proc) but that is also giving an error.
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(update_values IN VCHAR2,myid IN INT)
sqlStmt VARCHAR2(1024);
BEGIN
UPDATE MY_TEST_TABLE SET update_values WHERE (TEST_Id = myid);
--sqlStmt := 'UPDATE MY_TEST_TABLE SET ' || update_values || ' WHERE TEST_Id = ' ||myid ;
-- EXECUTE sqlStmt;
END;
Try this (untested):
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(update_values IN VARCHAR2, myid IN NUMBER) AS
sqlStmt VARCHAR2(1024);
BEGIN
sqlStmt := 'UPDATE MY_TEST_TABLE SET ' || update_values || ' WHERE TEST_Id = ' || myid;
EXECUTE IMMEDIATE sqlStmt;
END;
/
The datatype of your first parameter should be VARCHAR2 (maybe just a typo in your post)
Syntax of simple update statement in Oracle is:
Update <table_name>
set <column_name> = some_value
where <conditions..>
You update statement is missing = some_value part that you need to provide.
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(P_update_values IN CHARVAR2, p_myid IN INT)
BEGIN
UPDATE MY_TEST_TABLE
SET col1 = p_update_values
WHERE TEST_Id = p_myid;
END;
/
Using Dynamic SQL, although not required in this case:
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(p_update_values IN VARCHAR2, p_myid IN NUMBER) AS
sqlStmt VARCHAR2(1024);
BEGIN
sqlStmt := 'UPDATE MY_TEST_TABLE SET col1 = :a WHERE TEST_Id = :b';
EXECUTE IMMEDIATE sqlStmt USING p_update_values, p_myid;
END;
/
Things to be noted:
1) Always use meaningful and different names that are other than column names for the parameters.
2) Always use bind variables, :a and :b in above examples, to avoid SQL Injections and improve overall performance if you are going to call this procedure multiple times.

Resources