how do i access the procedure parameters inside the same procedure using a query
for example: see this procedure
procedure game(left in tab.left%type,right in tab.right%type,...)
is
--some local variables
begin
merge into tgt_table
using subquery --(here is what i need to use the parameters)
on some condition
when matched then
update the tgt table
when not matched then
insert the table;
end game;
In above procedure and in merge statement, i need a query such that it uses the parameters value as a table reference and using those values it either updates or inserts into the table based on the condition given.
Help me please. Thanks in advance
You would need to use dynamic SQL if your parameters define the table to use - something like:
procedure game(left in tab.left%type,right in tab.right%type,...)
is
--some local variables
l_sql long;
begin
l_sql := 'merge into tgt_table'
|| ' using ' || left
|| ' on some condition'
|| ' when matched then'
|| ' update ' || right
|| ' when not matched then'
|| ' insert the table';
execute immediate l_sql;
end game;
However, you have a lot more work left to do, since the condition, update and insert clauses all need to change according to the tables being used. I'm not convinced this procure will be particularly useful in fact.
Please read http://download.oracle.com/docs/cd/B14117_01/appdev.101/b10807/06_ora.htm#sthref777
Related
Quick Disclaimer:
First thing out of the way, I know the preferred way of handling dynamic SQL in Oracle now is the DBMS_SQL package but unfortunately my application team does not have the grants to execute these procs at the moment and I am hoping to get this quick workaround knocked out before our DBA team gets back to me. Also, this database is on Oracle 12c.
Script Goal: I recently developed a Stored Proc (let's call it Original) that uses values in a "control table" to make a large number of updates to certain columns in a database with many schemas and tables. This script I am struggling with now (let's call it Test) is meant to be a quick loop through those columns affected by Original so as to verify that everything worked expectedly. Ultimately, I want to output the top 5 results of each changed column and hand a spooled file to my testing team for validation.
The control_table used in both scripts has 4 columns and looks like this:
OWNER
TABLE_NAME
COLUMN_NAME
ALGORITHM
Schema1
TableA
ColumnA
Method1
Schema1
TableB
ColumnB
Method1
Schema2
TableC
ColumnC
Method2
An example of one of the tables that gets updated by Original (let's say for TableA above) would be:
OtherCol1
OtherCol2
ColumnA
OtherCol3
Ignored
Ignored
UpdatedData1
Ignored
Ignored
Ignored
UpdatedData2
Ignored
Ignored
Ignored
UpdatedData3
Ignored
Issue with Test script: I have the dynamic SQL - I believe - working as it needs and I have been trying to figure out how best to print the results of the EXECUTE IMMEDIATE command to output. In doing some reading, I found that BULK COLLECT INTO should allow me to store the results of the dynamic queries into a COLLECTION which I can then print with dbms_output. I have attempted to do this with both a TABLE and a VARRAY but in both cases when I print, I am finding that the data stored in my collection is the column header of my dynamic query instead of the query values! The only thing I can think that could be the problem is the combining of BULK COLLECT INTO with the USING command when I run the dynamic statement but I have seen nothing in the documentation to indicate that these two commands are incompatible and my Test procedure below compiles without issue (and even seems to run ok).
Test Script:
SET SERVEROUTPUT ON SIZE UNLIMITED;
DECLARE
l_script VARCHAR2(500);
l_errm VARCHAR2(64);
TYPE results IS VARRAY(5) OF VARCHAR2(250);
va_cols results; --Defining here with a VARRAY but I have also tried with a table
BEGIN
FOR c_col IN(
SELECT owner, table_name, column_name, algorithm FROM control_list)
LOOP
l_errm := NULL;
va_cols := NULL;
BEGIN
dbms_output.put_line('Column '|| c_col.column_name || ' of table ' || c_col.owner ||
'.' || c_col.table_name || ' used algorithm ' || c_col.algorithm);
l_script := 'SELECT :1 FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY';
dbms_output.put_line('Script sent to Exec Immediate: ' || l_script); --Print l_script for debugging
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols USING c_col.column_name, c_col.column_name;
dbms_output.put_line(va_cols(1));
dbms_output.put_line(va_cols(2));
dbms_output.put_line(va_cols(3));
dbms_output.put_line(va_cols(4));
dbms_output.put_line(va_cols(5));
EXCEPTION
WHEN OTHERS THEN
l_errm := SUBSTR(SQLERRM, 1, 64);
dbms_output.put_line(' ERROR: ' || l_errm || '. Skipping row');
CONTINUE;
END;
END LOOP;
END;
/
So my intended dbms_output of the script above is:
Column ColumnA of table Schema1.TableA used algorithm Method1
Script sent to Exec Immediate: SELECT :1 FROM SCHEMA1.TABLEA WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY
UpdatedData1
UpdatedData2
UpdatedData3
UpdatedData4
UpdatedData5
Instead, however, bizarrely, what I am getting when I run this is:
Column ColumnA of table Schema1.TableA used algorithm Method1
Script sent to Exec Immediate: SELECT :1 FROM SCHEMA1.TABLEA WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY
ColumnA
ColumnA
ColumnA
ColumnA
ColumnA
Has anyone seen this before and know what I am doing wrong? Thanks in advance!!
You can't use bind variables to change what columns you're referencing. You use bind variables to specify particular values at runtime. When you do
l_script := 'SELECT :1 FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY';
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols USING c_col.column_name, c_col.column_name;
you're telling Oracle that you want to select the literal string in the variable c_col.column_name. Not the column in the table by that name. Which is why every row returns that literal value.
You'd need to dynamically assemble the SQL statement with the column names, not try to use them as bind variables. So something like
l_script := 'SELECT ' || c_col.column_name ||
' FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE ' || c_col.column_name || ' IS NOT NULL FETCH FIRST 5 ROWS ONLY';
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols;
This is approximately what you want. I outer cursor over tables and column to inspect that generate the dynamic SQL.
Inner loop reading the column values from the previous query
DECLARE
TYPE CurTyp IS REF CURSOR;
v_cursor CurTyp;
v_value VARCHAR2(200);
v_stmt_str VARCHAR2(200);
BEGIN
FOR c IN (
SELECT table_name, column_name FROM control_list)
LOOP
dbms_output.put_line('tab: '||c.table_name);
v_stmt_str := 'SELECT '||c.column_name||' FROM '|| c.table_name;
OPEN v_cursor FOR v_stmt_str;
LOOP
FETCH v_cursor INTO v_value;
EXIT WHEN v_cursor%NOTFOUND;
dbms_output.put_line('col: '||c.column_name||' val: '||v_value);
END LOOP;
END LOOP;
CLOSE v_cursor;
END;
/
I have a stored procedure to truncate the table whose name is passed as a parameter to the procedure.
create or replace procedure delete_data_from_table(table_id VARCHAR2)
is
cursor table_cur is
SELECT table_name FROM all_tables WHERE table_name LIKE '%' || table_id || '%';
tab_name VARCHAR2(25);
BEGIN
open table_cur;
LOOP
FETCH table_cur into tab_name;
exit when table_cur%notfound;
EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || tab_name;
END LOOP;
close table_cur;
END;
I want to display the output of the execute immediate statement using DBMS_OUTPUT.PUT_LINE. Is it possible to do so?
Thanks in advance.
There is no native output from execute immediate or the statement you are running. When you truncate a table in a client it will usually report something like:
Table truncated.
or
Table MY_TABLE truncated.
or similar; but those messages are generated by the client running the command, not by the database.
You can just generate whatever message you want in your procedure, e.g.:
...
LOOP
FETCH table_cur into tab_name;
exit when table_cur%notfound;
EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || tab_name;
DBMS_OUTPUT.PUT_LINE('Table ' || tab_name || ' truncated.');
END LOOP;
...
If the truncate statement fails for any reason then it will throw an exception and it won't reach the dbms_output line.
For other statement types you can optionally use SQL%ROWCOUNT to report the number of rows inserted/updated/merged/deleted to mimic what your client might show for those, but that doesn't apply for truncation.
Bear in mind though that someone else running your code might not have display of those messages enabled.
I am a beginner with SQL and have a problem:
I have a DB with a big number of tables. Some of the tables have a column with the name "lab".
In this colums are values I need to be changed.
So I managed to get the names of the tables via
SELECT CNAME,TNAME FROM SYSTEM.COL WHERE CNAME = 'LAB';
And I know my update command
update TNAME set LAB='VALUE' WHERE LAB='OLDVALUE'
But I can not manage to connect both statements via a variable TNAME or something. I tried to use execute immediate, but that did me no good.
If its Oracle, something like this should do it:
BEGIN
FOR cur_tabs_cols IN ( SELECT CNAME,TNAME FROM SYSTEM.COL WHERE CNAME = 'LAB'; )
LOOP
EXECUTE IMMEDIATE 'UPDATE ' || cur_tabs_cols.TNAME || ' SET LAB = ''VALUE'' WHERE LAB = ''OLDVALUE''';
END LOOP;
COMMIT;
END;
You would need to write pl/sql for this.
The first thing is, please don't use SYSTEM.COL. Instead use the data dictionary view USER_TAB_COLS or USER_TAB_COLUMNS. (or ALL_TAB_COLS if in other schema)
EXECUTE IMMEDIATE would be what you want here.
BEGIN
FOR i IN (SELECT table_name
FROM user_tab_cols
WHERE column_name = 'LAB')
LOOP
EXECUTE IMMEDIATE
'UPDATE ' || i.table_name || ' set LAB = :value where LAB = :oldvalue'
USING 'value', 'oldvalue';
END LOOP;
END;
You can (and should) use a bind variable for value and oldvalue, just not for the table name.
Let's say that I have a tables tasks, projects, and work_items, all of which have a column fields containing a json object of custom values.
Now lets say I want to write a function to query an arbitrary table for its field names.
CREATE OR REPLACE FUNCTION getFieldNames(varchar) RETURNS varchar[] AS
$BODY$
DECLARE
fieldNames varchar[];
BEGIN
fieldNames := ARRAY(SELECT DISTINCT fieldName FROM
(EXECUTE 'SELECT json_object_keys(fields) AS fieldName FROM '
|| quote_ident($1)
) AS derivedFields
);
RETURN fieldNames;
END
$BODY$
LANGUAGE 'plpgsql';
This however errors out with:
ERROR: syntax error at or near "'SELECT json_object_keys(fields) AS fieldName FROM'"
LINE 8: (EXECUTE 'SELECT json_object_keys(fields) AS fieldNa..
The Nested select itself is sound as I verified by replacing the execute with
(SELECT json_object_keys(fields) AS fieldName
FROM tasks
)
and receiving correct results.
What is wrong with my code?
The EXECUTE statement does not return a relation that you can use as a sub-query. Instead, if it returns anything at all, it populates a variable or a single row through the INTO clause. The latter obviously does not match your requirement so you are stuck with the first. A more elegant solution is to move the EXECUTE statement outwards:
EXECUTE
'SELECT array_agg(DISTINCT fields) FROM ' ||
'(SELECT json_object_keys(fields) AS fields FROM ' || quote_ident($1) || ') AS x'
INTO fieldNames;
The worst issue is wrong position of EXECUTE statement. It is a PLpgSQL statement and then it cannot be placed in any SQL expression.
![enter image description here][1]I am creating a trigger that whenever I insert info into my table, I count the total row numbers and print the new added row. Here is my code:
Create or replace trigger TR_everyInsert
After INSERT On PERSONS
For each row
Declare
rowNumber int;
PERSON_NAME varchar(30);
gender varchar(30);
color varchar(30);
Begin
select PERSON_NAME,GENDER,COLOR
From PERSONS
Where PERSON_NAME=:new.PERSON_NAME;
select count(*) as rowNumber
from PERSONS;
if inserting then
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(rowNumber));
DBMS_OUTPUT.PUT_LINE ('New added info is ' || PERSON_NAME || 'with gender ' ||
GENDER || 'with color ' || color);
end if;
end;
/
However, I got compile error saying "into clause expected", what is the problem please?
First, you can't have a PL/SQL block that just executes a SELECT. You need to do something with the data. If you expect the query to return exactly 1 row, do a SELECT INTO. If you expect the query to return more than 1 row, you'd want to open a cursor that you'd iterate over.
Second, in a row-level trigger, you cannot generally query the table itself. You'll generally end up with a mutating table exception (there are some special cases where you can do this but that severely limits your flexibility going forward so it's something that should be avoided). To get row-level information, just use the various columns from your :new pseudo-record. To get the count, you'd realistically want to use a statement-level trigger. Depending on the Oracle version, you could create a compound trigger that has row- and statement-level components as well.
Third, t doesn't really make sense to have an IF inserting statement in a trigger that is only defined on the insert operation. It would only make sense to have that sort of statement if your trigger was defined on multiple operations (say INSERT OR UPDATE) and you wanted to do something different depending on which operation caused the trigger to fire.
Finally, you'll want your local variables to be named something that is distinct from the names of any columns. Most people adopt some sort of naming convention to disambiguate local variables, package global variables, and parameters from column names. I prefer prefixes l_, g_, and p_ for local variables, package global variables, and parameters which is a reasonably common convention in the Oracle community. You may prefer something else.
Something like
-- A row-level trigger prints out the data that is being inserted
Create or replace trigger TR_everyInsert_row
After INSERT On PERSONS
For each row
Begin
DBMS_OUTPUT.PUT_LINE ('New added info is ' || :new.PERSON_NAME ||
' with gender ' || :new.GENDER ||
' with color ' || :new.color);
end;
-- A statement-level trigger prints out the current row count
Create or replace trigger TR_everyInsert_stmt
After INSERT On PERSONS
Declare
l_cnt integer;
Begin
select count(*)
into l_cnt
from persons;
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(l_cnt) || ' rows.');
end;
The error message is pretty clear. You need to place the result of both your queries INTO the variables you declared:
Create or replace trigger TR_everyInsert
After INSERT On PERSONS
For each row
Declare
lv_rowNumber int;
lv-_PERSON_NAME varchar(30);
lv_gender varchar(30);
lv_color varchar(30);
Begin
select PERSON_NAME,GENDER,COLOR
into lv_person_name, lv_gender, lv_color
From PERSONS
Where PERSON_NAME=:new.PERSON_NAME;
select count(*) into lv_rowNumber
from PERSONS;
if inserting then
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(rowNumber));
DBMS_OUTPUT.PUT_LINE ('New added info is ' || PERSON_NAME || 'with gender ' ||
GENDER || 'with color ' || color);
end if;
end;
/
I would advice you to give your variables different names than your columns. It could make the code confusing to read...