pl/sql mutation table on update - plsql

I know, what this question had been conceived many time, but I need your advice :)
Having 2 table:
Sale_income
| item |income |create_user |create_date |last_update_user | update_time|
|------|-------|------------|-------------------|-----------------|------------|
| 1 | 100 |duck |05-19-2016 |human |05-19-2016 |
| 2 | 250 |dog |05-19-2016 |human |05-19-2016 |
| 3 | 210 |cat |05-20-2016 |human |05-19-2016 |
Sale_income_audit
| item |change_id|last_inc|new_inc |user_update|update_date|operation |
|------|---------|--------|----------|-----------|-----------|------------|
| 1 | 1 |null |05-19-2016|duck |05-19-2016 |I |
| 2 | 2 |null |05-19-2016|dog |05-19-2016 |I |
| 3 | 33 |null |05-20-2016|cat |05-19-2016 |I |
The task is: when somebody doing insert, update or delete, trigger must insert a record above that takes place action into the table Sale_income_audit (type of operation - column 'operation). And at the same time must be update the table
Sale_income (last_update_user and update_time).
I done this so: create package with 3 variables:
create or replace package Sale_income_var as
v_old_income BINARY_INTEGER := null;
v_new_income BINARY_INTEGER := null;
v_item BINARY_INTEGER := null;
END Sale_income_var;
and 2 triggers
first
create or replace trigger audit_income_IUD
after insert or update or delete on Sale_income
for each row
begin
.
.
elsif updating then
Sale_income_var.v_old_income := :old.income;
Sale_income_var.v_new_income := :new.income;
if Sale_income_var.v_item is null then
Sale_income_var.v_item := :old.item;
DBMS_OUTPUT.PUT_LINE(Sale_income_var.v_item);
end if;
.
.
end if;
end audit_income_IUD;
second
create or replace trigger sale_income_au
after update of income on Sale_income
begin
update Sale_income set last_update_user = user, last_update_date = sysdate
where item = Sale_income_var.v_item;
INSERT into Sale_income_audit (item,
change_id,
last_income,
new_income,
user_update,
update_date,
operation)
VALUES (Sale_income_var.v_item,
auto_incr.NEXTVAL,
Sale_income_var.v_old_income,
Sale_income_var.v_new_income,
user,
sysdate,
'U');
Sale_income_var.v_item := null;
end sale_income_au;
It work, but I fell what it is wrong solution. Because updating block from 'general' trigger moved to another trigger and this magic with variables it`s not good to, I'm right?
How would you decide this task and what do you would change in my solution?
Thanks for help :)

It can be done using just one BEFORE trigger.
Before triggers are allowed to modify NEW values of table columns, and these modified values are stored in the table, so last_update_user and update_time columns can be updated in this way.
create or replace trigger audit_income_IUD
BEFORE insert or update or delete on Sale_income
for each row
declare
operation_type char;
begin
if updating or inserting then
:new.update_time := sysdate;
:new.last_update_user := user;
end if;
CASE
WHEN updating THEN operation_type := 'U';
WHEN inserting THEN operation_type := 'I';
WHEN deleting THEN operation_type := 'D';
END CASE;
INSERT into Sale_income_audit (item,
change_id,
last_income,
new_income,
user_update,
update_date,
operation)
VALUES (:old.item,
auto_incr.NEXTVAL,
:old.income,
:new.income,
user,
sysdate,
operation_type);
end;
/

An answer appeared simple. Need use after each row and add in block 'update'
:new.last_update_user := user;
:new.last_update_date := sysdate;
and we can be limited to one trigger with out package!

Related

Dynamic Column Update PLSQL trigger

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;

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.

Update in PL/SQL Block giving PLS-00103 error

I am trying to create a PL/SQL block to update a empty column, is_true, in a new table created_table on the basis of another table, table1. The id fields on the tables match and there are no rows in created_table that doesn't have a corresponding row in table1. I'm basing the update on the status column of table1, but instead of 'Yes' and 'No', I need it reflected as 'Y' and 'N':
created_table: expected results:
id | is_true | other columns id | is_true | other columns
---|---------|-------------- ---|---------|--------------
1 | null | ... 1 | 'Y' | ...
2 | null | ... 2 | 'N' | ...
table1:
id | status | other columns
---|--------|--------------
1 | 'Yes' | ...
2 | 'No' | ...
Since created_table is very large, I'm trying to update it using a PL/SQL procedure, so that in case of failure midway, I'll still have updated rows. The next run of the procedure can then pick up where it previously failed without processing already processed rows.
I've tried testing with this code block:
DECLARE
is_true varchar2 (5) created_table.is_true%type;
BEGIN
FOR status IN (SELECT a.status
from table1 a
left join created_table b
where and a.id=b.id )
LOOP
IF status = 'Yes' THEN
UPDATE created_table SET is_true= 'Y'
ELSE
UPDATE created_table SET is_true= 'N'
WHERE ROWNUM := status.ROWNUM
END IF;
DBMS_OUTPUT.PUT_LINE('Done');
END LOOP;
END;
But it's giving me errors:
PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following:
* & = - + ; < / > at in is mod remainder not rem
What can I do to make it work?
Your code had multiple errors:
DECLARE
-- is_true varchar2 (5) created_table.is_true%type; -- PLS-00103: Encountered the symbol "CREATED_TABLE" when expecting one of the following:
is_true created_table.is_true%type;
BEGIN
FOR status IN (SELECT a.status
from table1 a
-- left join created_table b -- ORA-00905: missing keyword
-- where and a.id=b.id )
left join created_table b on a.id = b.id)
LOOP
-- IF status = 'Yes' THEN -- PLS-00306: wrong number or types of arguments in call to '='
IF status.status = 'Yes' THEN
-- UPDATE created_table SET is_true= 'Y' -- ORA-00933: SQL command not properly ended
UPDATE created_table SET is_true= 'Y';
ELSE
UPDATE created_table SET is_true= 'N'
-- WHERE ROWNUM := status.ROWNUM -- ORA-00920: invalid relational operator and ORA-00933: SQL command not properly ended
WHERE ROWNUM = status.ROWNUM;
END IF;
DBMS_OUTPUT.PUT_LINE('Done');
END LOOP;
END; -- PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following:
/
The PLS-00103 is really just telling you that a / is missing. After that the remaining error is PLS-00302: component 'ROWNUM' must be declared. From "Oracle Database Online Documentation": rownum is a pseudocolumn.
Your SQL would update every row in the table setting is_true to 'Y' on the first encounter of status being 'Yes', since you didn't give it a WHERE clause. I'm assuming that is not your intent.
There was also no COMMIT in your PL/SQL block, so in effect it would have been the same as running a normal SQL.
Based on the situation you've described, I made some changes to the code block. This will provide with a limit for how many rows can be processed prior to a COMMIT. I set the limit to 5. You should change that to something appropriate. It will not pick up any rows that have empty values for is_true, so in effect it will only work on non-processed rows:
DECLARE
commit_counter PLS_INTEGER := 1; -- count commits
commit_limit PLS_INTEGER := 5; -- rows for commit limit
counter PLS_INTEGER := commit_limit;
BEGIN
FOR rec IN (SELECT a.status, a.id
FROM created_table b
JOIN table1 a ON a.id = b.id
WHERE b.is_true IS NULL) -- do not pick up processed rows
LOOP
IF rec.status = 'Yes' THEN
UPDATE created_table SET is_true = 'Y'
WHERE id = rec.id;
ELSE
UPDATE created_table SET is_true = 'N'
WHERE id = rec.id;
END IF;
counter := counter - 1;
IF counter < 1 THEN
counter := commit_limit; --reset counter
commit_counter := commit_counter + 1;
COMMIT;
END IF;
END LOOP;
COMMIT; -- all rows are processed;
DBMS_OUTPUT.PUT_LINE(commit_counter || ' COMMITS');
END;
/
This will update all the rows in one go, still only updating the "empty" rows:
UPDATE created_table b
SET is_true = (SELECT CASE a.status WHEN 'Yes' THEN 'Y'
ELSE 'N'
END
FROM table1 a
WHERE a.id = b.id)
WHERE b.is_true IS NULL;

Copy nested table's values in another tuple of same table

I have the following table in oracle 11g named parlamentari:
cf varchar(16)
nome varchar(20)
cognome varchar(20)
telefoni telefoni_NT
where telefoni_NT is a nested table of varchar2.
Now, I must to copyed the elements of the nested table of a tuple (sorgente) in another tuple (ricevente).
I try to write an example
Start situation
parlamentari
-------------------------------------
cf | nome | cognome | telefoni
-------------------------------------
1 | a | aa | VARCHAR(222,444)
2 | b | bb | VARCHAR(111)
Situation after procedure called
parlamentari
-------------------------------------
cf | nome | cognome | telefoni
-------------------------------------
1 | a | aa | VARCHAR(222,444)
2 | b | bb | VARCHAR(111, 222, 444)
I tried to written this procedure without success
create or replace procedure copia_telefoni2
(sorgente in parlamento2018.parlamentari.cf%type,
ricevente in parlamento2018.parlamentari.cf%type) as
cursor cur_out_tel is
select column_value as original_list
from parlamentari, table(telefoni)
where cf = sorgente
;
cursor in_parlamentare is
select column_value as copied_list
from parlamentari, table(telefoni)
where cf = destinazione;
begin
if (sorgente <> destinazione) then
for i in cur_out_tel loop
dbms_output.put_line(i.original_list);
insert into table(select telefoni from parlamentari where cf=destinazione) values (telefoni_nt(i.original_list));
end loop;
else
dbms_output.put_line('Errore! Sorgente e destinazione uguali');
end if;
end copia_telefoni2;
Copy nested table's values in another tuple of same table
As far i could understand your requirement, you don't need to insert in the table since the record already exists in your table. All you need is to update the existing record. Since your table column is a nested table,MULTISET UNION is something which would work in your situation. See below demo:
Tables:
CREATE OR REPLACE TYPE telefoni_nt IS TABLE OF VARCHAR2(100);
CREATE TABLE parlamentari (
cf VARCHAR(16),
nome VARCHAR(20),
cognome VARCHAR(20),
telefoni telefoni_nt
)
NESTED TABLE telefoni STORE AS nested_telefoni;
insert into parlamentari values('1','a','aa',telefoni_nt('VARCHAR(222,444)'));
insert into parlamentari values('2','b','bb',telefoni_nt('VARCHAR(111)'));
Output:
SQL> Select CF ,TELEFONI from parlamentari;
CF TELEFONI
-- ------
1 TELEFONI_NT('VARCHAR(222,444)')
2 TELEFONI_NT('VARCHAR(111)')
Procedure:
CREATE OR REPLACE PROCEDURE copia_telefoni2 (
sorgente IN parlamentari.cf%TYPE,
destinazione IN parlamentari.cf%TYPE
) AS
BEGIN
IF ( sorgente <> destinazione )
THEN
--Using MULTISET Operator to merge the destination column element with the source column elements.
UPDATE parlamentari
SET
telefoni = telefoni MULTISET UNION (SELECT telefoni
FROM parlamentari
WHERE cf = sorgente )
WHERE
cf = destinazione;
ELSE
dbms_output.put_line('Errore! Sorgente e destinazione uguali');
END IF;
COMMIT;
END copia_telefoni2;
/
Execution:
SQL> EXEC copia_telefoni2('1','2');
PL/SQL procedure successfully completed.
Result:
SQL> Select CF ,TELEFONI from parlamentari;
CF TELEFONI
-- ------
1 TELEFONI_NT('VARCHAR(222,444)')
2 TELEFONI_NT('VARCHAR(111)','VARCHAR(222,444)')
So you can see in the result that the two rows now have been merged.

Is there a way to reuse subqueries in the same query?

See Update at end of question for solution thanks to marked answer!
I'd like to treat a subquery as if it were an actual table that can be reused in the same query. Here's the setup SQL:
create table mydb.mytable
(
id integer not null,
fieldvalue varchar(100),
ts timestamp(6) not null
)
unique primary index (id, ts)
insert into mydb.mytable(0,'hello',current_timestamp - interval '1' minute);
insert into mydb.mytable(0,'hello',current_timestamp - interval '2' minute);
insert into mydb.mytable(0,'hello there',current_timestamp - interval '3' minute);
insert into mydb.mytable(0,'hello there, sir',current_timestamp - interval '4' minute);
insert into mydb.mytable(0,'hello there, sir',current_timestamp - interval '5' minute);
insert into mydb.mytable(0,'hello there, sir. how are you?',current_timestamp - interval '6' minute);
insert into mydb.mytable(1,'what up',current_timestamp - interval '1' minute);
insert into mydb.mytable(1,'what up',current_timestamp - interval '2' minute);
insert into mydb.mytable(1,'what up, mr man?',current_timestamp - interval '3' minute);
insert into mydb.mytable(1,'what up, duder?',current_timestamp - interval '4' minute);
insert into mydb.mytable(1,'what up, duder?',current_timestamp - interval '5' minute);
insert into mydb.mytable(1,'what up, duder?',current_timestamp - interval '6' minute);
What I want to do is return only rows where FieldValue differs from the previous row. This SQL does just that:
locking row for access
select id, fieldvalue, ts from
(
--locking row for access
select
id, fieldvalue,
min(fieldvalue) over
(
partition by id
order by ts, fieldvalue rows
between 1 preceding and 1 preceding
) fieldvalue2,
ts
from mydb.mytable
) x
where
hashrow(fieldvalue) <> hashrow(fieldvalue2)
order by id, ts desc
It returns:
+----+---------------------------------+----------------------------+
| id | fieldvalue | ts |
+----+---------------------------------+----------------------------+
| 0 | hello | 2015-05-06 10:13:34.160000 |
| 0 | hello there | 2015-05-06 10:12:34.350000 |
| 0 | hello there, sir | 2015-05-06 10:10:34.750000 |
| 0 | hello there, sir. how are you? | 2015-05-06 10:09:34.970000 |
| 1 | what up | 2015-05-06 10:13:35.470000 |
| 1 | what up, mr man? | 2015-05-06 10:12:35.690000 |
| 1 | what up, duder? | 2015-05-06 10:09:36.240000 |
+----+---------------------------------+----------------------------+
The next step is to return only the last row per ID. If I were to use this SQL to write the previous SELECT to a table...
create table mydb.reusetest as (above sql) with data;
...I could then do this do get the last row per ID:
locking row for access
select t1.* from mydb.reusetest t1,
(
select id, max(ts) ts from mydb.reusetest
group by id
) t2
where
t2.id = t1.id and
t2.ts = t1.ts
order by t1.id
It would return this:
+----+------------+----------------------------+
| id | fieldvalue | ts |
+----+------------+----------------------------+
| 0 | hello | 2015-05-06 10:13:34.160000 |
| 1 | what up | 2015-05-06 10:13:35.470000 |
+----+------------+----------------------------+
If I could reuse the subquery in my initial SELECT, I could achieve the same results. I could copy/paste the entire query SQL into another subquery to create a derived table, but this would just mean I'd need to change the SQL in two places if I ever needed to modify it.
Update
Thanks to Kristján, I was able to implement the WITH clause into my SQL like this for perfect results:
locking row for access
with items (id, fieldvalue, ts) as
(
select id, fieldvalue, ts from
(
select
id, fieldvalue,
min(fieldvalue) over
(
partition by id
order by ts, fieldvalue
rows between 1 preceding and 1 preceding
) fieldvalue2,
ts
from mydb.mytable
) x
where
hashrow(fieldvalue) <> hashrow(fieldvalue2)
)
select t1.* from items t1,
(
select id, max(ts) ts from items
group by id
) t2
where
t2.id = t1.id and
t2.ts = t1.ts
order by t1.id
Does WITH help? That lets you define a result set you can use multiple times in the SELECT.
From their example:
WITH orderable_items (product_id, quantity) AS
( SELECT stocked.product_id, stocked.quantity
FROM stocked, product
WHERE stocked.product_id = product.product_id
AND product.on_hand > 5
)
SELECT product_id, quantity
FROM orderable_items
WHERE quantity < 10;

Resources