Dynamic Column Update PLSQL trigger - plsql

I have two tables A and B
In table A there are two columns "Sequence Number" and "Content".
Name Null? Type
------- ----- ------------
SEQ_NO NUMBER(6)
CONTENT VARCHAR2(20)
In table B there are multiple statement columns like "Stmt_1", "Stmt_2", "Stmt_3" etc.
Name Null? Type
------ ----- ------------
STMT_1 VARCHAR2(20)
STMT_2 VARCHAR2(20)
STMT_3 VARCHAR2(20)
STMT_4 VARCHAR2(20)
I want to create a trigger on table A such that after every insert on table A, according to the "Sequence Number" value the corresponding column in table B gets updated.
For example: If table A has "Sequence Number" = 1 , then "Stmt_1" of table B gets updated to the value of "Content" column in table A.
If table A is given as
"SEQ_NO" "CONTENT"
1 "This is Content"
Then Table B should look like:
"STMT_1","STMT_2","STMT_3","STMT_4"
"This is Content","","",""
My approach is as follows:
create or replace trigger TestTrig
after insert on A for each row
begin
declare
temp varchar2(6);
begin
temp := concat("Stmt_",:new.seq_no);
update B
set temp = :new.content;
end;
end;
But I am getting an error in the update statement.
Does anyone know how to approach this problem?

You need to use dynamic SQL (and ' is for string literals, " is for identifiers):
create or replace trigger TestTrig
after insert on A
for each row
DECLARE
temp varchar2(11);
begin
temp := 'Stmt_' || TO_CHAR(:new.seq_no, 'FM999990');
EXECUTE IMMEDIATE 'UPDATE B SET ' || temp || ' = :1' USING :NEW.content;
end;
/
You probably want to handle errors when seq_no is input as 5 and there is no STMT_5 column in table B:
create or replace trigger TestTrig
after insert on A
for each row
DECLARE
temp varchar2(11);
INVALID_IDENTIFIER EXCEPTION;
PRAGMA EXCEPTION_INIT(INVALID_IDENTIFIER, -904);
begin
temp := 'Stmt_' || TO_CHAR(:new.seq_no, 'FM999990');
EXECUTE IMMEDIATE 'UPDATE B SET ' || temp || ' = :1' USING :NEW.content;
EXCEPTION
WHEN INVALID_IDENTIFIER THEN
NULL;
end;
/
However
I would suggest that you do not want a table B or a trigger to update it and you want a VIEW instead:
CREATE VIEW B (stmt_1, stmt2, stmt3, stmt4) AS
SELECT *
FROM A
PIVOT (
MAX(content)
FOR seq_no IN (
1 AS stmt_1,
2 AS stmt_2,
3 AS stmt_3,
4 AS stmt_4
)
);
fiddle;

Related

How to insert data from R into Oracle table with identity column?

Assume I have a simple table in Oracle db
CREATE TABLE schema.d_test
(
id_record integer GENERATED AS IDENTITY START WITH 95000 NOT NULL,
DT DATE NOT NULL,
var varchar(50),
num float,
PRIMARY KEY (ID_RECORD)
)
And I have a dataframe in R
dt = c('2022-01-01', '2005-04-01', '2011-10-02')
var = c('sgdsg', 'hjhgjg', 'rurtur')
num = c(165, 1658.5, 8978.12354)
data = data.frame(dt, var, num)%>%
mutate(dt = as.Date(dt))
I'm trying to insert data into Oracle d_test table using the code
data %>%
dbWriteTable(
oracle_con,
value = .,
date = T,
'D_TEST',
append = T,
row.names=F,
overwrite = F
)
But the following error returned
Error in .oci.WriteTable(conn, name, value, row.names = row.names, overwrite = overwrite, :
Error in .oci.GetQuery(con, stmt, data = value) :
ORA-00947: not enough values
What's the problem?
How can I fix it?
Thank you.
This is pure Oracle (I don't know R).
Sample table:
SQL> create table test_so (id number generated always as identity not null, name varchar2(20));
Table created.
SQL> insert into test_so(name) values ('Name 1');
1 row created.
My initial idea was to suggest you to insert any value into the ID column, hoping that Oracle would discard it and generate its own value. However, that won't work.
SQL> insert into test_so (id, name) values (-100, 'Name 2');
insert into test_so (id, name) values (-100, 'Name 2')
*
ERROR at line 1:
ORA-32795: cannot insert into a generated always identity column
But, if you can afford recreating the table so that it doesn't automatically generate the ID column's value but use a "workaround" (we used anyway, as identity columns are relatively new in Oracle) - a sequence and a trigger - you might be able to "fix" it.
SQL> drop table test_so;
Table dropped.
SQL> create table test_so (id number not null, name varchar2(20));
Table created.
SQL> create sequence seq_so;
Sequence created.
SQL> create or replace trigger trg_bi_so
2 before insert on test_so
3 for each row
4 begin
5 :new.id := seq_so.nextval;
6 end;
7 /
Trigger created.
Inserting only name (Oracle will use a trigger to populate ID):
SQL> insert into test_so(name) values ('Name 1');
1 row created.
This is what you'll do in your code - provide dummy ID value, just to avoid
ORA-00947: not enough values
error you have now. Trigger will discard it and use sequence anyway:
SQL> insert into test_so (id, name) values (-100, 'Name 2');
1 row created.
SQL> select * from test_so;
ID NAME
---------- --------------------
1 Name 1
2 Name 2 --> this is a row which was supposed to have ID = -100
SQL>
The way you can handle this problem is to create table with GENERATED BY DEFAULT ON NULL AS IDENTITY like this
CREATE TABLE CM_RISK.d_test
(
id_record integer GENERATED BY DEFAULT ON NULL AS IDENTITY START WITH 5000 NOT NULL ,
DT date NOT NULL,
var varchar(50),
num float,
PRIMARY KEY (ID_RECORD)
)

Using cursors in loop in PL/SQL

I have table that contains 3 column.First column name is id , second column's name is parent_id and third one is expression.What i want to do is to search expression column for id.For example I send id value then if parent_id column has a value I want to send parent_id value and want to check expression column has 'E' or not.If It has null value and result has parent_id then I want to send parent_id value and again I want to check expression column has 'E' or not.If expression column has a value like that 'E', I updated variable resultValue as 1 and end loop.
my table A : It should return resultValue =1
id |parent_id|expression
123 |null | null
45 |123 | 'E'
22 |45 | null
my table B : It should return resultValue = 0
id |parent_id|expression
30 |null | null
20 |30 | null
10 |20 | null
my table C : It should return resultValue = 0
id |parent_id|expression
30 |null | null
20 |30 | null
10 |null | null
If first sending id(10) does not contain parent_id(table C) resultValue variable should be 0. If I find 'E' expression any parent row resultValue variable should return 1.
I created a code block with cursor.For the first time I used cursor.I am not sure using cursor with this kind of problem is a good idea or not.My code is running but to open cursor then to close cursor then again opening cursor it is good idea?
DECLARE
resultValue NUMBER := 0;
CURSOR c(v_id NUMBER )
IS
SELECT id_value, id_parent, expression FROM students WHERE id_value = v_id;
PROCEDURE print_overpaid
IS
id_value NUMBER;
id_parent NUMBER;
expression VARCHAR2(20);
BEGIN
LOOP
FETCH c INTO id_value, id_parent, expression;
EXIT
WHEN c%NOTFOUND;
IF id_parent IS NULL AND expression IS NULL THEN
EXIT;
END IF;
IF id_parent IS NOT NULL THEN
CLOSE c;
OPEN c(id_parent);
ELSIF id_parent <> NULL AND expression = 'X' OR id_parent IS NULL AND expression = 'X' THEN
resultValue := 1;
EXIT;
END IF;
END LOOP;
END print_overpaid;
BEGIN
OPEN c(22);
print_overpaid;
DBMS_OUTPUT.PUT_LINE(' My resultValue is : ' || resultValue);
CLOSE c;
END;
If I understood your description correctly, you are looking to see it the specified id of any row in the parentage contains 'E' in the column expression. You are correct that closing and reopening a cursor is not really a good idea. Although I do like your use of a nested procedure. However, it's not really necessary as this can be solved with a single query. The approach will be a recursive CTE that checks the target row for 'E' until a row contains it or the row does not have a parent.
with search_for_e(id, parent_id, e_cnt) as
( select id, parent_id, case when expression = 'E' then 1 else 0 end
from exp_tbl
where id = &id
union all
select t.id,t.parent_id, case when t.expression = 'E' then 1 else 0 end
from search_for_e s
join exp_tbl t on (t.id = s.parent_id)
where t.parent_id is not null
and s.e_cnt = 0
)
select max(e_cnt)
from search_for_e;
See fiddle here, it also contains an anonymous block implementation with nested function and one with cursor.

Return deleted rows in a cursor in PL/SQL

I have a requirement where I want to return the deleted records in a sys_refcursor. I can retrieve the data in cursor before the delete statement but is there any way to retrieve them after a delete statement? I mean process will be like at first delete and then open sys_refcursor for fetching deleted records.
There are probably a few options, but one option would be to use RETURNING and BULK COLLECT. Here is a simple example.
CREATE TABLE t (
a NUMBER,
b VARCHAR2(10),
c DATE
);
INSERT INTO t VALUES (1, 'a', SYSDATE);
INSERT INTO t VALUES (2, 'b', SYSDATE);
INSERT INTO t VALUES (3, 'c', SYSDATE);
INSERT INTO t VALUES (4, 'd', SYSDATE);
INSERT INTO t VALUES (5, 'e', SYSDATE);
CREATE OR REPLACE TYPE tt AS OBJECT (
a NUMBER,
b VARCHAR2(10),
c DATE
);
CREATE OR REPLACE TYPE tt_tab AS TABLE OF tt;
DECLARE
v_tt_tab tt_tab;
v_tt tt;
v_cur SYS_REFCURSOR;
BEGIN
DELETE FROM t
WHERE a < 4
RETURNING tt(a, b, c) BULK COLLECT INTO v_tt_tab;
OPEN v_cur FOR
SELECT tt(a,
b,
c)
FROM TABLE(v_tt_tab);
LOOP
FETCH v_cur
INTO v_tt;
EXIT WHEN v_cur%NOTFOUND;
dbms_output.put_line(v_tt.a || ' ' || v_tt.b || ' ' || v_tt.c);
END LOOP;
CLOSE v_cur;
END;
/
/*
1 a 07-OCT-20
2 b 07-OCT-20
3 c 07-OCT-20
*/
By creating an object that matches the data we want to keep and a table of that object we can return that into a collection and then easily turn it back into a cursor using the TABLE function.
You need to be careful with how many rows you delete as you don't want to run out of memory. I would be cautious with this approach if you delete more than a few hundred rows.
Another suggestion might be to use a GTT, INSERT the rows you plan to delete and then delete from the first table using the GTT as your "key".
CREATE GLOBAL TEMPORARY TABLE t_gtt (
a NUMBER,
b VARCHAR2(10),
c DATE
);
DECLARE
v_tt_tab tt_tab;
v_tt tt;
v_cur SYS_REFCURSOR;
BEGIN
INSERT INTO t_gtt
SELECT *
FROM t
WHERE a < 4;
DELETE FROM t
WHERE EXISTS (SELECT NULL
FROM t_gtt
WHERE t_gtt.a = t.a);
OPEN v_cur FOR
SELECT tt(a,
b,
c)
FROM t_gtt;
LOOP
FETCH v_cur
INTO v_tt;
EXIT WHEN v_cur%NOTFOUND;
dbms_output.put_line(v_tt.a || ' ' || v_tt.b || ' ' || v_tt.c);
END LOOP;
CLOSE v_cur;
END;
/
This option is maybe better if you are planning to delete a large amount of rows. I used my tt object again, but you really don't need it. It just made looping to dump the contents of the SYS_REFCURSOR easier.

SQLite trigger to update a column on editing updates the column for all records in table

Hypothetical table x, with columns (all of type text):
name
sname
sqlmodded
I'd like to create a trigger to set the value of sqlmodded to '1' if any column in the record is updated.
My trigger is as follows:
CREATE TRIGGER trigger1
AFTER UPDATE
ON x
FOR EACH ROW
WHEN old.sqlmodded IS NULL
BEGIN
UPDATE x
SET sqlmodded = '1';
END;
When I run an update statement to change a value of name or sname, the trigger kicks in but alters sqlmodded for all records in the table.
Here's code to reproduce:
CREATE TABLE "x"(
"NAME" Text,
"SNAME" Text,
"sqlmodded" Text );
CREATE TRIGGER "trigger1"
AFTER UPDATE
ON "x"
FOR EACH ROW
WHEN old.sqlmodded is null
BEGIN UPDATE x SET sqlmodded = '1'; END;
Now insert a few records:
INSERT INTO x
(
name
, sname
) VALUES
(
"Joe"
, "Bloggs"
)
;
INSERT INTO x
(
name
, sname
) VALUES
(
"Joline"
, "Bloggs"
)
;
Now run an update:
UPDATE x
SET
name = "Justine"
WHERE name = "Joline"
;
If you view the resulting two records both records will have had sqlmodded set to 1.
The trigger runs this command:
UPDATE x
SET sqlmodded = '1';
This statement indeed updates all rows in the table. This is how SQL works.
If you want to update only a specific row, you have to tell the database which row that would be:
UPDATE x
SET sqlmodded = '1'
WHERE rowid = NEW.rowid;
Here's the working code:
CREATE TRIGGER sqlmods
AFTER UPDATE
ON x
FOR EACH ROW
WHEN old.sqlmodded IS NULL
BEGIN
UPDATE x
SET sqlmodded = TRUE
WHERE rowid = NEW.rowid;
END;

How to create a dynamic where clause in oracle

My requirement is to create a procedure or SQL query in which where clause should be created in run time depending on the paramters provided by the user.
Example if user provides data for three columns then where clause should have filter conditions for these three columns only to select the data from database table, like wise if user provides data for 4 columns then where caluse should have 4 columns.
I don't see any very specific solution to your question but it can be done using putting OR in where clause with different set of user input. See below:
Created procedure: Here my employee table has emp_id,name and salary columns. Now am assuming user sometimes passes emp_id and emp_name alone.
CREATE OR REPLACE PROCEDURE dynmc_selec (id NUMBER DEFAULT 0,
name VARCHAR2 DEFAULT 'A',
salary NUMBER DEFAULT 0,
emp_output IN OUT SYS_REFCURSOR)
AS
var VARCHAR2 (100);
BEGIN
--You need to make several combinations in where clause like ( emp_id , emp_name , salary ) OR ( emp_id , emp_name ) OR (emp_name , salary ) and use it in where clause with 'OR'.
--Also its needed that all the columns of the table is in where clause. If user doesnot pass anything then defualt value will be passed.
var :=
'select emp_id from employee where ( emp_id ='
|| id
|| ' and emp_name = '''
|| name
|| ''' ) OR emp_sal = '
|| salary;
DBMS_OUTPUT.put_line (var);
EXECUTE IMMEDIATE var;
OPEN emp_output FOR var;
EXCEPTION
WHEN OTHERS
THEN
NULL;
END;
Calling it:
declare
a SYS_REFCURSOR;
v_emp_id employee.emp_id%type;
begin
--passing emp_id and name only
dynmc_selec(id=>1,name=>'KING',emp_output=>a);
loop
FETCH a INTO v_emp_id;
EXIT WHEN a%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp_id );
end loop;
end;
Output:
select emp_id from employee where ( emp_id =1 and emp_name = 'KING' ) OR emp_sal = 0
1
select * from test_table
where
(1 = nvl2(:l_date, 0, 1) or created_at > :l_date)
and (1 = nvl2(:l_no, 0,1) or no = :l_no);
With Oracle nvl2 function:
When the parameter l_date is null, then 1 = nvl2(l_date, 0, 1) evaluates to true and the filter by created_at is not taking place.
When the parameter l_date is not null, then 1 = nvl2(l_date, 0, 1) evaluates to false and the filter by created_at is taking place.
The same thing happens with the parameter l_no.

Resources