I have a stored procedure which runs many queries and obtains a lot of values which it stores in variables. I want this procedure to be able to be able to insert the query results into a table which is given by the user. I would store the given table name as a varchar parameter but how can I insert into this table?
At compile time, Oracle is saying that the table does not exist.
Presumably it's telling you that a table with the name of the variable you're using doesn't exist, which of course it won't. The actual table may or may not exist at compile time; since it's flexible it's probably safer to assume it might not. Either way you don't know what it will be so you need to use dynamic SQL to achieve this.
As mentioned in a comment on another answer, you have to be careful about SQL injection. Normally you'd want to use bind variables in the dynamic SQL, but you can't use binds for object names, so you have to concatenate it. Hopefully you're using 11g, which includes the dbms_assert package.
Here's a simple example:
create or replace procedure p42 (table_name varchar2) as
begin
execute immediate 'insert into '
|| dbms_assert.qualified_sql_name(table_name)
|| ' select * from dual';
end;
/
I can then create a table after the procedure already exists, and successfully call the procedure:
create table t42 (dummy varchar2(1));
exec p42('t42');
select * from t42;
DUMMY
-----
X
Your real query will obviously be more complicated, and should use bind variables for any filter values you're passing in along with the target table name.
The advantage of the dbms_assert call is that it will error if something illegal is passed in, and if something nasty is passed:
exec p42('t42 select ''Y'' from dual union all');
ORA-44004: invalid qualified SQL name
ORA-06512: at "SYS.DBMS_ASSERT", line 207
ORA-06512: at "STACKOVERFLOW.P42", line 3
ORA-06512: at line 1
If the procedure simply concatenated the passed value:
execute immediate 'insert into ' || table_name || ' select * from dual';
... then that same call would insert two rows into the table:
exec p42('t42 select ''Y'' from dual union all');
select * from t42;
DUMMY
-----
Y
X
Which is something to worry about if the data integrity is at all important to you. If you can't use dbms_assert then you could try to check that the passed name actually exists in all_tables and doesn't contain anything like a union etc. but it's much safer to acknowledge that you won't think of all the possible attacks and let the built-in function do the hard work for you.
Try something like this:
exec ('insert into ' + #tblname + ' (col) values (123)')
Related
I am trying to understand what steps I need to undertake in order to move a stored procedure from one schema and into another. The schema that this is currently sitting in is going to be made redundant and I have been asked to move all tables and procedures. I have no trouble with tables but never done anything with procedures hence want to make sure I don't miss anything out.
What I have currently done is look through the procedure and made a list of what its actually doing i.e. dropping/creating and inserting data into tables.
After this I wasn't sure if it was just a case of copying the procedure code and then creating a new procedure on the new schema with the same code and then compiling it.
I would really appreciate it if somebody could advise if I am missing anything in the steps that I am undertaking just to ensure I don't mess things up.
There is no way to "move" an object from one schema to another.
The only practible way I see here is copying the source code and then executing it in the new schema. As #pmdba wrote as comment, you should watch out for schema names like "MYSCHEMA"."TABLENAME" and other references.
If you got too much to copy you may consider writing a block where you automatically read the data of the old schema and create it automatically in the new one.
You can get the data of (nearly) everything with, i.e. procedures:
select * from all_source where owner = 'OLDSCHEMANAME' and type = 'PROCEDURE';
and use it like this:
begin
....
select listagg(text, '') within group (order by line) into proc_code
from all_source
where owner = 'OLDSCHEMANAME'
and type = 'PROCEDURE'
group by name;
execute immediate 'create or replace ' || proc_code; -- perhaps you need to remove the last ';' here
...
end;
Please note that this code is only meant as hint and doesn't need to be taken exactly that way. Also, you may still get errors due to non existing objects, wrong schema references etc..
To get the ddl of a table one may use select dbms_metadata.get_ddl('TABLE','Table_name','Schema_Name') from dual;.
By googling dbms_metadata.get_ddl you might get more info on the DBMS_METADATA-package and how to use it correctly.
As already stated, there is no mechanism to copy one object ( procedure , function or package, etc ) to another schema. One alternative is using all_source, but I prefer DBMS_METADATA because allows you to transfer all dependencies, like for example privileges. Imagine I need to copy a procedure but I need to keep the privileges, with this package I can get everything.
Example
SQL> create procedure myschema1.my_procedure ( p1 number )
2 as
3 var1 number := p1;
4 begin
5 select 1 into var1 from dual;
6 end;
7 /
Procedure created.
SQL> grant execute on myschema1.my_procedure to myuser ;
Grant succeeded.
Now, let's imagine we want to copy the procedure and its privileges to another schema
SQL> set long 99999999 set lines 200 pages 400
SQL> select dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1') from dual ;
DBMS_METADATA.GET_DDL('PROCEDURE','MY_PROCEDURE','MYSCHEMA1')
--------------------------------------------------------------------------------
CREATE OR REPLACE EDITIONABLE PROCEDURE "MYSCHEMA1"."MY_PROCEDURE" ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
But, imagine you don't want quotation and neither the editionable argument
SQL> select
replace(dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1','11.2.0'),'"','') as ddl from dual ;
DDL
--------------------------------------------------------------------------------
CREATE OR REPLACE PROCEDURE MYSCHEMA1.MY_PROCEDURE ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
Then to get the final command with the new schema owner, we use regexp_replace to replace the first occurrence
SQL> select regexp_replace(replace(dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1','11.2.0'),'"',''),'MYSCHEMA1','MYSCHEMA2',1,1)
2 as ddl from dual ;
DDL
--------------------------------------------------------------------------------
CREATE OR REPLACE PROCEDURE MYSCHEMA2.MY_PROCEDURE ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
Finally, we can get all privileges by
SQL> select dbms_metadata.get_dependent_ddl( 'OBJECT_GRANT' , 'MY_PROCEDURE' , 'MYSCHEMA1' ) from dual ;
DBMS_METADATA.GET_DEPENDENT_DDL('OBJECT_GRANT','MY_PROCEDURE','MYSCHEMA1')
--------------------------------------------------------------------------------
GRANT EXECUTE ON "MYSCHEMA1"."MY_PROCEDURE" TO "MYUSER"
Remember to apply at session level before to start some settings to enhance dbms_metadata output:
begin
DBMS_METADATA.set_transform_param (DBMS_METADATA.session_transform, 'SQLTERMINATOR', true);
DBMS_METADATA.set_transform_param (DBMS_METADATA.session_transform, 'PRETTY', true);
end;
I want to call an existing procedure and store its table-typed OUT parameters to new physical tables, without having to repeat the definitions of the output types when creating the new tables. For example, if the procedure were
CREATE PROCEDURE MYPROC
(IN X INTEGER, OUT Y TABLE(A INTEGER, B DOUBLE, C NVARCHAR(25)))
LANGUAGE SQLSCRIPT AS BEGIN
...
END;
I would want to create a physical table for the output without repeating the (A INTEGER, B DOUBLE, C NVARCHAR(25)) part.
If I already had a table with the structure I want my result to have, I could CREATE TABLE MY_OUTPUT LIKE EXISTING_TABLE, but I don't.
If I already had a named type defined for the procedure's output type, I could create my table based on that type, but I don't.
If it were a subquery instead of a procedure output parameter, I could CREATE TABLE MY_OUTPUT AS (<subquery>), but it's not a subquery, and I don't know how to express it as a subquery. Also, there could be multiple output parameters, and I don't know how you'd make this work with multiple output parameters.
In my specific case, the functions come from the SAP HANA Predictive Analysis Library, so I don't have the option of changing how the functions are defined. Additionally, I suspect that PAL's unusually flexible handling of parameter types might prevent me from using solutions that would work for ordinary SQLScript procedures, but I'm still interested in solutions that would work for regular procedures, even if they fail on PAL.
Is there a way to do this?
It's possible, with limitations, to do this by using a SQLScript anonymous block:
DO BEGIN
CALL MYPROC(5, Y);
CREATE TABLE BLAH AS (SELECT * FROM :Y);
END;
We store the output to a table variable in the anonymous block, then create a physical table with data taken from the table variable. This even works with PAL! It's a lot of typing, though.
The limitation I've found is that the body of an anonymous block can't refer to local temporary tables created outside the anonymous block, so it's awkward to pass local temporary tables to the procedure this way. It's possible to do it anyway by passing the local temporary table as a parameter to the anonymous block itself, but that requires writing out the type of the local temporary table, and we were trying to avoid writing table types manually.
As far as I understand, you want to use your database tables as output parameter types.
In my default schema, I have a database table named CITY
I can create a stored procedure as follows using the table as output parameter type
CREATE PROCEDURE MyCityList (
OUT CITYLIST CITY
)
LANGUAGE SQLSCRIPT
AS
BEGIN
CITYLIST = SELECT * FROM CITY;
END;
After procedure is created, you can execute it as follows
do
begin
declare myList CITY;
call MyCityList(:myList);
select * from :myList;
end;
Here is the result where the output data is in a database table format, namely as CITY table
I hope this answers your question,
Update after first comment
If the scenario is the opposite as mentioned in the first comment, you can query system view PROCEDURE_PARAMETER_COLUMNS and create dynamic SQL statements that will generate tables with definitions in procedure table type parameters
Here is the SQL query
select
parameter_name,
'CREATE Column Table ' ||
procedure_name || '_'
|| parameter_name || ' ( ' ||
string_agg(
column_name || ' ' ||
data_type_name ||
case when data_type_name = 'INTEGER' then '' else
'(' || length || ')'
end
, ','
) || ' );'
from PROCEDURE_PARAMETER_COLUMNS
where
schema_name = 'A00077387'
group by procedure_name, parameter_name
You need to replace the WHERE clause according to your case.
Each line will have such an output
CREATE Column Table LISTCITIESBYCOUNTRYID_CITYLIST ( CITYID INTEGER,NAME NVARCHAR(40) );
The format for table name is concatenation of procedure name and parameter name
One last note, some data types integer, decimal, etc requires special code like excluding length or adding of scale , etc. Some are not handled in this SQL.
I'll try to enhance the query soon and publish an update
I created an Oracle proc where I create a dynamic sql statement based on the parameters supplied to the proc.
I've done some testing and it appears that I can't perform sql injection.
Is there anything additional I should be safe guarding against?
SELECT 'UPDATE ' || p_table || ' SET MY_FIELD = ''' || p_Value || ''' ' || p_Where
INTO query_string
FROM DUAL;
EDIT:
Scenarios that I've tried.
1. WHERE SOME_VAL IN ('AAA','BBB') - This works
2. WHERE SOME_VAL IN ('AAA','BBB') OR SOME_VAL2 = '123' - This works.
3. WHERE SOME_VAL IN ('AAA','BBB'); DROP TABLE TEST_TABLE; - This errors out.
4. WHERE SOME_VAL IN ('AAA','BBB') OR (DELETE FROM TEST_TABLE) - This errors out.
It depends on how and by whom your procedure is being invoked. Usually you need to worry about SQL injection for something that is open to large number of users in production. And that should not be the case for any database procedure. If your database procedure is accessible by large number of users, then you have potential for malicious use by someone.
In your case, you can mitigate this risk by creating mapping of parameters to hide actual schema object names and some validation.
For example change parameter p_table to table_name as input parameter. Then using case statement map to actual table name. I am giving you example of table name here because you should really restrict who can access which table from db.
CREATE OR REPLACE PROCEDURE test_proc(table_name IN VARCHAR)
IS
p_table varchar2(100);
BEGIN
CASE table_name
WHEN 'A' THEN p_table:='db_table_a';
WHEN 'B' THEN p_table:='db_table_b';
ELSE RAISE 'Invalid table name parameter';
END CASE;
SELECT 'UPDATE ' || p_table || ' SET MY_FIELD = ''' || p_Value || ''' '
|| p_Where
INTO query_string
FROM DUAL;
END;
You should do similar mapping and validation for other parameters too.
SQL injection always opens Pandora's box.
You should always assume a user can break out of a dynamic SQL statement. With full SQL access you should then assume a user can find a way to escalate privileges and own your database. (Depending on how paranoid you are, it might be safe to assume privilege escalation is impossible as long as your database and schemas are constantly patched and thoroughly hardened. In practice the vast majority of Oracle databases are not sufficiently patched and hardened.)
Below are a few simple examples that should scare you. And you should also assume that there are many hackers who are more clever than I am and have better attacks.
Sample Schema
First let's create a simple table with some data for a realistic test.
drop table test1;
create table test1(my_field varchar2(100), some_val varchar2(100));
insert into test1 values('A', 'AAA');
commit;
Obviously Dangerous Function
Are all of the existing functions safe?
create or replace function dangerous_function return number is
pragma autonomous_transaction;
begin
delete from test1;
commit;
return 1;
end;
/
If not, what is stopping the user from calling it like this?
--Safe static part:
update test1
set my_field = 'b'
--Dangerous dynamic part:
where some_val IN ('AAA')
and 1 = (select dangerous_function from dual)
Luckily creating an autonomous function is unusual and you can probably check the code. But can you guarantee the application will not create one in the future?
Custom Function in SQL
Even if there are no objects a clever user can turn your UPDATE into other DML:
--Safe static part:
update --+ WITH_PLSQL
test1
set my_field = 'b'
--Dangerous dynamic part:
where some_val IN ('AAA')
and 1 = (
with function dangerous_function return number is
pragma autonomous_transaction;
begin
delete from test1;
commit;
return 1;
end;
select dangerous_function from dual
);
I did cheat a little, the above code only works for me with the --+ WITH_PLSQL hint. Without that hint the code throws the error ORA-32034: unsupported use of WITH clause. But that's only a version limitation that might be lifted in the future. Or there might be some clever way to work around it, sometimes hints can break out of their part of the query and reference other sections.
Why Risk It?
Maybe there is a safe way to do it. But why risk it? Everybody in the IT world understands SQL injection bugs now. If you mess up and cause an exploit there will be no sympathy for you.
For example, I have some table "Test" which has one column "my_date" . What I am trying to do is adding record to a table using some variable:
query_date := "SELECT sysdate FROM dual";
EXECUTE IMMEDIATE ('insert into test values query_date');
I need to insert the record to the table in this exact way by constructing string and executing query, however I get error. Is it possible to do so?
You can either get the result of the first query into a (date) variable and then use that:
SELECT sysdate into query_date FROM dual;
insert into test (my_date) values (query_date)
-- or if you really want dynamic SQL, with a bind variable
EXECUTE IMMEDIATE 'insert into test (my_date) values (:query_date)' using query_date;
Or reading your question literally, use the first string as part of the second string by concatenating it:
query_date := "SELECT sysdate FROM dual";
EXECUTE IMMEDIATE 'insert into test (my_date) ' || query_date;
If you printed out the second statement instead of executing it you'd see:
insert into test (my_date) SELECT sysdate FROM dual
... which is valid SQL. This will work if the query_string is more complicated or is itself being constructed dynamically. But if the number of column expressions in the query_string select list also varies, you will have to construct the column list dynamically too, otherwise you'll have too many or too few columns for the insert.
Exactly how you do that depends on how you're constructing the query string - essentially as you add an expression to the query string, you'd also add a column name to a separate list, and end up with:
EXECUTE IMMEDIATE 'insert into test (' || column_list ' ||) ' || query_string);
where column_list is built up as say col1, col2 and query_string as select x.col1, y.col2 from ....
There isn't an obvious reason to use dynamic SQL in what you've shown. Or, if you are really using sysdate, any need for separate query to get that, as you can just do:
insert into test (my_date) values (sysdate)
... so I assume your real scenario is really more complicated. But note that you don't use the values keyword with an insert ... select ... pattern. You can with a single column and a subquery but it's not a good idea even then, and doesn't work if you have multiple columns in the subquery.
Why you need an EXECUTE IMMEDIATE for Insert statement ?
As long as the base table where you are inserting the values remain same we dont need to do EXIMM. Now the query_date ? Just do a traditional looping or variable thing.
I want to call create table/ alter table command from a procedure. Is it possible?
My requirement is to change the datatype of a column in all tables. So, I am just getting the column name from user_tab_cols. Now I want to create a temp table which requires create statement .. but i am unable to use that within a proc.
Can anyone please help me out?
I presume from the reference to USER_TAB_COLUMNS that this is Oracle. ALTER and CREATE statements are DDL, which we cannot execute directly in PL/SQL. However, there are a couple of ways around this restriction: EXECUTE IMMEDIATE and DBMS_UTILITY.EXEC_DDL(). I will use EXECUTE IMMEDIATE in the following example.
begin
for lrec in ( select table_name from user_tab_columns
where column_name = 'UNIVERSAL_COLUMN_NAME')
loop
execute immediate 'alter table '||lrec.table_name||
' modify UNIVERSAL_COLUMN_NAME varchar2(255)';
end loop;
end;
Note that the usual restrictions apply: the new datatype has to be compatible with the existing datatype (unless the column is empty), and things are trickier with some specilaized datatypes like CLOBs.
edit
I haven't addressed the CREATE TABLE statement. The principle is the same, it is just longer to type out. Besides, I am not entirely clear how it applies to your prior requirement to change the datatype of those columns.
you can generate the query as string and execute it with 'exec' keyword.