Oracle Function to hardCode Column Name as Parameter - oracle

I have 2 tables with same set Column Name (52+ coulmns) . I need to write an Oracle function to compare whether any records get changed between these columns. EMP_ID is the primary Key
I'm trying to use the below function, but it is giving me incorrect result,
I'm calling the funcaiton like this:
get_data_change (emp_id, 'DEPT_NAME');
get_data_change (emp_id, 'PHONE_NUMBER');
Function I have created:
CREATE OR REPLACE function get_data_change (
in_emp_id varchar2, in_Column_Name varchar2)
return char is
v_data_changed char;
begin
select eid, in_Column_Name
into v_table1_eid, v_table1_Column_Value
from table 1
where eid=in_emp_id;
Select eid, in_Column_Name
into v_table2_eid, v_table2_Column_Value
from table 2
where eid = in_emp_id;
if ( v_table2_Column_Value != v_table1_Column_Value)
then
v_data_changed := 'Y'
else
v_data_changed :='N'
endif
return v_data_changed
end
end get_data_change;

in_Column_Name is a string variable to which you are assigning a literal string value such as 'DEPT_NAME'.
Therefore, your queries are interpreting this as a literal string value and returning the same thing into v_table1_Column_Value.
To do what you expect you need to use Dynamic SQL, something like:
EXECUTE IMMEDIATE 'select eid, ' || in_Column_Name
|| ' from table1 where eid=:P_emp_id'
into v_table1_eid, v_table1_Column_Value
using in_emp_id;
You need to be aware of the possibility of SQL Injection here - i.e. the value of in_Column_Name cannot be supplied by end-users.

Related

concatenate VArray to String and use in dynamic SQL - Oracle

I need to dynamically create a query and execute using Execute Immediate, I am facing the problem in appending the Vaaray variable. Getting Error
pls-00306 wrong number or types of arguments in call to ||
Vaaray //Its a type number
select ver_id bulk collect into Ver_Array from ( Select id from table)
No issue with below query as only id variable is used:
Execute Immediate 'Select ID, name , Date, time
from table
where id = ' || v_UC2_id
Error with below query
Execute Immediate 'Select ID, name , Date, time
from table
where id = ' || v_UC2_id
|| ' and ver_id in ( SELECT * FROM TABLE ( '
|| Ver_Array
|| ' )'
Tried to extract the query and concatenate in comma saperated values but the final result comes as String but field used in query is Number
Not sure how to handle this in dynamic query
The SQL you're writing is concatenating an array with a string, so you get an error.
You can do it like this:
create or replace type vat is varray(10) of number:
/
declare
ivat vat:=vat(1,2,3);
res number;
begin
execute immediate 'select sum(rn) from tarr where rn in (select column_value from table (:varrt))' into res using ivat;
dbms_output.put_line(res);
end;
/
Here I'm selecting just one row and value. If you have multiple rows and columns, then you better declare a cursor for this SQL, and loop thru it.

PL/SQL : Need to compare data for every field in a table in plsql

I need to create a procedure which will take collection as an input and compare the data with staging table data row by row for every field (approx 50 columns).
Business logic :
whenever a staging table column value will mismatch with the corresponding collection variable value then i need to update 'FAIL' into staging table STATUS column and reason into REASON column for that row.
If matched then need to update 'SUCCESS' in STATUS column.
Payload will be approx 500 rows in each call.
I have created below sample script:
PKG Specification :
CREATE OR REPLACE
PACKAGE process_data
IS
TYPE pass_data_rec
IS
record
(
p_eid employee.eid%type,
p_ename employee.ename%type,
p_salary employee.salary%type,
p_dept employee.dept%type
);
type p_data_tab IS TABLE OF pass_data_rec INDEX BY binary_integer;
PROCEDURE comp_data(inpt_data IN p_data_tab);
END;
PKG Body:
CREATE OR REPLACE
PACKAGE body process_data
IS
PROCEDURE comp_data (inpt_data IN p_data_tab)
IS
status VARCHAR2(10);
reason VARCHAR2(1000);
cnt1 NUMBER;
v_eid employee_copy.eid%type;
v_ename employee_copy.ename%type;
BEGIN
FOR i IN 1..inpt_data.count
LOOP
SELECT ec1.eid,ec1.ename,COUNT(*) over () INTO v_eid,v_ename,cnt1
FROM employee_copy ec1
WHERE ec1.eid = inpt_data(i).p_eid;
IF cnt1 > 0 THEN
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
UPDATE employee_copy SET status = 'SUCCESS' WHERE eid = inpt_data(i).p_eid;
ELSE
UPDATE employee_copy SET status = 'FAIL' WHERE eid = inpt_data(i).p_eid;
END IF;
ELSE
NULL;
END IF;
END LOOP;
COMMIT;
status :='success';
EXCEPTION
WHEN OTHERS THEN
status:= 'fail';
--reason:=sqlerrm;
END;
END;
But in this approach i have below mentioned issues.
Need to declare all local variables for each column value.
Need to compare all variable data using 'and' operator. Not sure whether it is correct way or not because if there are 50 columns then if condition will become very heavy.
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
Need to update REASON column when any column data mismatched (first mismatched column name) for that row, in this approach i am not able to achieve.
Please suggest any other good way to achieve this requirement.
Edit :
There is only one table at my end i.e target table. Input will come from any other source as collection object.
REVISED Answer
You could load the the records into t temp table, but unless you want additional processing it's not necessary. AFAIK there is no way to identify the offending column (first one only) without slugging through column-by-column. However, your other concern having to declare a variable is not necessary. You can declare a single variable defined as %rowtype which gives you access to each column by name.
Looping through an array of data to find the occasional error is just bad (imho) with SQL available to eliminate the good ones in one fell swoop. And it's available here. Even though your input is a array we can use as a table by using the TABLE operator, which allows an array (collection) as though it were a database table. So the MINUS operator can till be employed. The following routine will set the appropriate status and identify the first miss matched column for each entry in the input array. It reverts to your original definition in package spec, but replaces the comp_data procedure.
create or replace package body process_data
is
procedure comp_data (inpt_data in p_data_tab)
is
-- define local array to hold status and reason for ecah.
type status_reason_r is record
( eid employee_copy.eid%type
, status employee_copy.status%type
, reason employee_copy.reason%type
);
type status_reason_t is
table of status_reason_r
index by pls_integer;
status_reason status_reason_t := status_reason_t();
-- define error array to contain the eid for each that have a mismatched column
type error_eids_t is table of employee_copy.eid%type ;
error_eids error_eids_t;
current_matched_indx pls_integer;
/*
Helper function to identify 1st mismatched column in error row.
Here is where we slug our way through each column to find the first column
value mismatch. Note: There is actually validate the column sequence, but
for purpose here we'll proceed in the input data type definition.
*/
function identify_mismatch_column(matched_indx_in pls_integer)
return varchar2
is
employee_copy_row employee_copy%rowtype;
mismatched_column employee_copy.reason%type;
begin
select *
into employee_copy_row
from employee_copy
where employee_copy.eid = inpt_data(matched_indx_in).p_eid;
-- now begins the task of finding the mismatched column.
if employee_copy_row.ename != inpt_data(matched_indx_in).p_ename
then
mismatched_column := 'employee_copy.ename';
elsif employee_copy_row.salary != inpt_data(matched_indx_in).p_salary
then
mismatched_column := 'employee_copy.salary';
elsif employee_copy_row.dept != inpt_data(matched_indx_in).p_dept
then
mismatched_column := 'employee_copy.dept';
-- elsif continue until ALL columns tested
end if;
return mismatched_column;
exception
-- NO_DATA_FOUND is the one error that cannot actually be reported in the customer_copy table.
-- It occurs when an eid exista in the input data but does not exist in customer_copy.
when NO_DATA_FOUND
then
dbms_output.put_line( 'Employee (eid)='
|| inpt_data(matched_indx_in).p_eid
|| ' does not exist in employee_copy table.'
);
return 'employee_copy.eid ID is NOT in table';
end identify_mismatch_column;
/*
Helper function to find specified eid in the initial inpt_data array
Since the resulting array of mismatching eid derive from a select without sort
there is no guarantee the index values actually match. Nor can we sort to build
the error array, as there is no way to know the order of eid in the initial array.
The following helper identifies the index value in the input array for the specified
eid in error.
*/
function match_indx(eid_in employee_copy.eid%type)
return pls_integer
is
l_at pls_integer := 1;
l_searching boolean := true;
begin
while l_at <= inpt_data.count
loop
exit when eid_in = inpt_data(l_at).p_eid;
l_at := l_at + 1;
end loop;
if l_at > inpt_data.count
then
raise_application_error( -20199, 'Internal error: Find index for ' || eid_in ||' not found');
end if;
return l_at;
end match_indx;
-- Main
begin
-- initialize status table for each input enter
-- additionally this results is a status_reason table in a 1:1 with the input array.
for i in 1..inpt_data.count
loop
status_reason(i).eid := inpt_data(i).p_eid;
status_reason(i).status :='SUCCESS';
end loop;
/*
We can assume the majority of data in the input array is valid meaning the columns match.
We'll eliminate all value rows by selecting each and then MINUSing those that do match on
each column. To accomplish this cast the input with TABLE function allowing it's use in SQL.
Following produces an array of eids that have at least 1 column mismatch.
*/
select p_eid
bulk collect into error_eids
from (select p_eid, p_ename, p_salary, p_dept from TABLE(inpt_data)
minus
select eid, ename, salary, dept from employee_copy
) exs;
/*
The error_eids array now contains the eid for each miss matched data item.
Mark the status as failed, then begin the long hard process of identifying
the first column causing the mismatch.
The following loop used the nested functions to slug the way through.
This keeps the main line logic clear.
*/
for i in 1 .. error_eids.count -- if all inpt_data rows match then count is 0, we bypass the enttire loop
loop
current_matched_indx := match_indx(error_eids(i));
status_reason(current_matched_indx).status := 'FAIL';
status_reason(current_matched_indx).reason := identify_mismatch_column(current_matched_indx);
end loop;
-- update employee_copy with appropriate status for each row in the input data.
-- Except for any cid that is in the error eid table but doesn't exist in the customer_copy table.
forall i in inpt_data.first .. inpt_data.last
update employee_copy
set status = status_reason(i).status
, reason = status_reason(i).reason
where eid = inpt_data(i).p_eid;
end comp_data;
end process_data;
There are a couple other techniques used you may want to look into if you are not familiar with them:
Nested Functions. There are 2 functions defined and used in the procedure.
Bulk Processing. That is Bulk Collect and Forall.
Good Luck.
ORIGINAL Answer
It is NOT necessary to compare each column nor build a string by concatenating. As you indicated comparing 50 columns becomes pretty heavy. So let the DBMS do most of the lifting. Using the MINUS operator does exactly what you need.
... the MINUS operator, which returns only unique rows returned by the
first query but not by the second.
Using that this task needs only 2 Updates: 1 to mark "fail", and 1 to mark "success". So try:
create table e( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
);
create table stage ( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
, status varchar2(20)
, reason varchar2(20)
);
-- create package spec and body
create or replace package process_data
is
procedure comp_data;
end process_data;
create or replace package body process_data
is
package body process_data
procedure comp_data
is
begin
update stage
set status='failed'
, reason='No matching e row'
where e_id in ( select e_id
from (select e_id, col1, col2 from stage
except
select e_id, col1, col2 from e
) exs
);
update stage
set status='success'
where status is null;
end comp_data;
end process_data;
-- test
-- populate tables
insert into e(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any') from dual union all
select (3,'ok', 'best ever') from dual union all
select (4,'xx','zzzzzz') from dual;
insert into stage(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any more') from dual union all
select (4,'yy', 'zzzzzz') from dual union all
select (5,'no e','nnnnn') from dual;
-- run procedure
begin
process_data.comp_date;
end;
-- check results
select * from stage;
Don't ask. Yes, you to must list every column you wish compared in each of the queries involved in the MINUS operation.
I know the documentation link is old (10gR2), but actually finding Oracle documentation is a royal pain. But the MINUS operator still functions the same in 19c;

How does Oracle Insert Into work when order of values is not defined?

I came across some code that looks like this. I understand that it will return the auto-generated id, but what I don't understand is when I pass cursor data when I call this function, how does it identify what values are to be inserted in which columns when the column order is not defined?
FUNCTION INSERT_ROW(DATA IN OWNER.TABLE%ROWTYPE)
RETURN OWNER.TABLE.ID%TYPE
IS
l_ID OWNER.MY_TABLE.ID%TYPE;
l_Data OWNER.MY_TABLE%ROWTYPE := DATA;
BEGIN
INSERT INTO OWNER.MY_TABLE
VALUES l_Data
RETURNING ID INTO l_ID;
I tried to look up many examples and I only come across ones where the values are defined in order like this
INSERT INTO my_table (val2, val3, val4) VALUES (2, 3, 4) RETURNING val1
INTO val1;
The order of columns in a table in Oracle IS defined. Take a look at the ALL_TAB_COLUMNS view - there's a COLUMN_ID column which defines the order of columns within the table. If a field list is not given in a SELECT (i.e. SELECT * FROM MY_TABLE) the columns from MY_TABLE will be returned in ALL_TAB_COLUMNS.COLUMN_ID order. This is also the same way columns are ordered in a %ROWTYPE variable, and it's the way that an INSERT which doesn't have a field list specified expects fields to be ordered.
The insert values statement in your code is a PL/SQL extension to the standard insert values clause that has parentheses. This is a page from the 12.2 manual about this topic:
https://docs.oracle.com/en/database/oracle/oracle-database/12.2/lnpls/INSERT-statement-extension.html#GUID-D81224C4-06DE-4635-A850-41D29D4A8E1B
The OWNER.TABLE%ROWTYPE data type defines a record with the same columns as the table and in the same order. You are just passing the data into the function in that format and passing it into a variable and then into the insert statement.
The main purpose of the RETURNING clause is to obtain the value of a derived column, a value which is generated during the insert process. Usually this is a technical primary key derived from a sequence, or since 12c an IDENTITY column.
So for instance:
create table my_table (
val1 number generated as identity primary key
, val2 varchar2(16)
, val3 varchar2(16)
, val4 date)
/
declare
id number;
begin
INSERT INTO my_table (val2, val3, val4)
VALUES ('one', 'test', sysdate)
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
This is why the examples you found specify columns in the INSERT projection: the value of the primary key is generated automatically, so there's no point in us assigning it a value in our code.
Now your function uses a record type in its insert statement. We can't do that with IDENTITY columns. This variant ...
declare
lrec my_table%rowtype;
id number;
begin
lrec.val2 := 'two';
lrec.val3 := 'test again';
lrec.val4 := sysdate;
INSERT INTO my_table
VALUES lrec
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
... will hurl
ORA-32795: cannot insert into a generated always identity column
But we can use a %rowtype with the old-fashioned sequence and trigger combo:
create table my_table (
val1 number primary key
, val2 varchar2(16)
, val3 varchar2(16)
, val4 date)
/
create sequence my_seq start with 42;
create or replace trigger my_trg
before insert on my_table for each row
begin
:new.val1 := my_seq.nextval;
end;
/
declare
lrec my_table%rowtype;
id number;
begin
lrec.val1 := 1;
lrec.val2 := 'three';
lrec.val3 := 'test again';
lrec.val4 := sysdate;
INSERT INTO my_table
VALUES lrec
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
Here is a LiveSQL demo (free Oracle OTN account required, alas). If you run it you will see that the trigger overrides the assigned value and the val1 column has the value from the sequence.

Oracle SQL Concat troubled

I have this:
CREATE or replace TYPE type_movie AS object(
idmovie numeric(6),
title varchar(50),
genere varchar(20),
year numeric(4),
refprojec ref type_projec,
MEMBER FUNCTION getProjec1 return numeric
);
And
CREATE or replace TYPE type_projec AS object(
idmovie numeric(6),
date date,
hour char(5),
refmovie ref type_movie,
MEMBER FUNCTION getData return varchar
);
create table tmovie of type_movie;
create table tprojec of type_projec;
Then, the problem:
create or replace TYPE BODY type_projec AS
MEMBER FUNCTION getData return varchar is
all varchar;
BEGIN
SELECT concat(to_char(t.idmovie) || t.title || t.genere || to_char(t.year)) INTO all
FROM tmovie t
WHERE t.refmovie.idmovie=self.idmovie;
return all;
END;
END;
I want to concatenate all the selected items to return a unique varchar of all of them.
It seems that there is an error in the line SELECT concat(to_char(t.idmovie) || t.title || t.genere || to_char(t.year)) INTO all
But supposedly all seems correct?
Any possible answers?
"But supposedly all seems correct?"
Up to a point.
ALL is an Oracle reserved word, so it's a poor choice of variable name. Use something else, even l_all, instead.
CONCAT() takes two arguments; you supply only one. As your are using the || concatenation operator you don't need to call CONCAT(). This will work
SELECT to_char(t.idmovie) || t.title || t.genere || to_char(t.year) INTO l_all
Also, the syntax for referencing the type is wrong. This will compile ...
WHERE t.idmovie=self.idmovie;
... but it may not be what you want to implement.
ALL and DATE are reserved words - you want to use something else instead of those names. You do not need to use CONCAT(str1,str2) (and you are only supplying a single argument) since you are using the || concatenation operator.
You can just use DEREF():
create or replace TYPE BODY type_projec AS
MEMBER FUNCTION getData return varchar is
str varchar(4000);
BEGIN
SELECT DEREF( self.refmovie ).idmovie
|| DEREF( self.refmovie ).title
|| DEREF( self.refmovie ).genere
|| DEREF( self.refmovie ).year
INTO str
FROM DUAL;
return str;
END;
END;
/

execute query defined in column as String

Hello again I need some help,
I have table where in column "query" is defined query statement. I would like to run it and as output get the result.For example:
Create table table1
(
ID Number,
Query Varchar2(400)
)
insert into table1(id,query) values (1,'select name from table2 where table2.id=table1.id and table2.type = variable');
create table table2
(ID number,
Name varchar2(400),
Type Varchar2(400)
)
insert into table2 values (1,'Mathew','M');
insert into table2 values (1,'Thomas','G');
insert into table2 values (2,'Jerry','P');
For now query :
'select name from table2 where table2.id=table1.id and table2.type = variable'
should return only "Mathew" (assuming variable as 'M' - procedure variable input)
As procedure input I want to have variable which I will replace somehow in query statement.
Could you give me some tips how to handle with that?
------------Edit
I did stmh like that:
create or replace procedure queryrun
(var1 varchar2) as
str VARCHAR2(200);
BEGIN
execute immediate 'select replace(query,''variable'','''||var1||''') from table1' into str;
dbms_output.put_line('Value is '||str);
END;
But as result it present query... no result of select statement...
You are only selecting your query, not running it; and you're replacing the string "'variable'" - including the single quotes - with your value, but your original query string doesn't have the single quotes around it - so nothing matches.
You should not really substitue a hard-coded value anyway. Change your stored query to include a bind variable placeholder instead:
insert into table1(id,query)
values (1,'select name from table2 where table2.id=table1.id and table2.type = :variable');
Although that query is invalid anyway - you don't have table1 defined in the from clause or a join clause. When you have a valid query you can run standalone, use that, but with a bind variable (denoted by the leading colon).
But let's assume you have a valid query string in your table, that will only return one row, say:
insert into table1(id,query)
values (1,'select name from table2 where type = :variable');
Your procedure then needs a local variable to hold that query string. You select your query into that using static SQL, and then use dynamic SQL via execute immediate to run the query from that string, and provide the bind value with the using clause. The result goes into another local variable, which you are already doing.
So a simple version might look like this:
create or replace procedure queryrun (p_var1 varchar2) as
l_query table1.query%type;
l_name table2.name%type;
begin
select query into l_query from table1 where id = 1;
execute immediate query into l_name using p_var1;
dbms_output.put_line('Value is ' || l_name);
end;
This is obviously rather contrived. If you have multiple queries in your table, and perhaps pass a second ID variable into the procedure to choose which one to run, they would all have to take a single bind variable, and would have to all be able to put the result into the same type and size of result variable. You're also restricted to queries that return exactly one row. You can adapt and extend this of course, but hopefully this will get you started.
You can have bind variable and use plsql execute immediate.
Examples:
http://www.dba-oracle.com/t_oracle_execute_immediate.htm

Resources