Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
PL/SQL Office Hours session begins soon…
Database Triggers:
You Say Stop, I Say Go
with
Chris Saxon, @ChrisRSaxon & @SQLDaily
https://www.youtube.com/c/TheMagicofSQL
https://blogs.oracle.com/sql
Steven Feuerstein, @sfonplsql
https://www.youtube.com/channel/PracticallyPerfectPLSQL
http://stevenfeuersteinonplsql.blogspot.com/
And
special
guests!
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Welcome to Ask TOM Office Hours!
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Database Triggers
Some people say you should NEVER use them.
Some people hate using the word "NEVER."
Let's talk about it.
We'll hear from Toon Koppelaars, Jacek Gebal and Chris
Saxon. But we also want to hear from you.
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Database Triggers
Broadly two different types of triggers:
• System triggers (database events like logon triggers,
DDL triggers, etc.)
• DML triggers (triggers on tables and views): simple,
compound, instead of
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
CREATE OR REPLACE TRIGGER stop_drop_trg
BEFORE DROP ON hr.SCHEMA
BEGIN
RAISE_APPLICATION_ERROR (
num => -20999,
msg => 'Cannot drop object');
END;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
System
trigger
example
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Focusing In On DML Triggers
For the rest of this session, we will concentrate on
DML triggers – triggers created on tables or views.
They are the source of controversy and likely impact
on production applications.
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Some thoughts from
Jacek Gabel (Fidelity, utPLSQL)
"Triggers are great. They can also
become a maintenance hell."
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Some thoughts from
Toon Koppelaars on….
"Triggers Considered Harmful,
Considered Harmful"
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Mostly Agree With #1 Influencer of Past Times
• http://tkyte.blogspot.com/2006/09/classic-example-of-why-i-despise.html
– “Triggers are so abused - and used so inappropriately, I'd rather live without them.”
– “There are no times triggers cannot be avoided. They are purely a convenience that is
overused, abused and improperly used.”
• http://tkyte.blogspot.com/2007/03/dreaded-others-then-null-strikes-
again.html
– [On why he doesn’t like triggers] “Because I hate being surprised or tricked. And
triggers are all about trickery and surprises.”
9
Still available at web.archive.org
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Tom's Legacy
• Default response at community.oracle.com when someone poses
question related to triggers:
“Triggers should be avoided as much as possible“
“Don’t use them, they are bad”
“Triggers are considered harmful”
10
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Four Use-Cases of Database Triggers
1. Modify :NEW columns
2. Execute changes outside transaction
3. Execute changes inside transaction
4. Query tables
11
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Modify :NEW Columns
• Done in row-triggers
– :NEW.CREATED := SYSDATE
– :NEW.CREATOR := USER
– :NEW.ID := SEQ.NEXTVAL
– …
• What happens if I supply these column values?
– They get overwritten
– Maybe application error would be more appropriate?
12
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Execute Changes Outside Transaction
• Examples
– Call REST service
– Talk to some endpoint over tcp/ip
– Send email
– Write to file
– Execute autonomous transaction
• What happens if my transaction rolls back?
– All of the above is outside your transaction and irreversable
13
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Execute Changes Inside Transaction
• Examples
– Insert audit row in audit table
– Maintain redundant data in other table(s)
– Propagate more changes to other parts of database design
• Semantics of your insert, update or delete has changed significantly
14
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Query Tables
• Very common use-case is to mandate cross-row constraints via
triggers
– These require queries
– E.g. check at most one president in EMPLOYEES table
– Mutating table error
– Serialization
• More complex than you think to get this right
– SQL assertions
15
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Four Use-Cases of Database Triggers
1. Modify :NEW columns
2. Execute changes outside transaction
3. Execute changes inside transaction
4. Query tables
16
“Surprises and Trickery”
“Surprises and Trickery”
“Surprises and Trickery”
Too complex to do it right
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
CREATE OR REPLACE TRIGGER dg_workouts_bir
BEFORE INSERT ON dg_workouts FOR EACH ROW
DECLARE
BEGIN
IF :new.workout_id IS NULL
THEN
:new.workout_id := dg_workouts_seq.NEXTVAL;
END IF;
:new.created_on := SYSDATE;
:new.created_by := qdb_config.apex_user ();
:new.changed_on := SYSDATE;
:new.changed_by := qdb_config.apex_user ();
END;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
What
do you
think of
this
trigger?
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
CREATE TABLE dg_workouts
(
workout_id INTEGER NOT NULL
NUMBER GENERATED ALWAYS AS IDENTITY,
...
created_by VARCHAR2 (132 BYTE) NOT NULL,
changed_by VARCHAR2 (132 BYTE) NOT NULL,
created_on DATE NOT NULL DEFAULT SYSDATE,
changed_on DATE NOT NULL DEFAULT SYSDATE
)
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Fix the
table
DDL
or
Switch
to API
call.
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
CREATE OR REPLACE TRIGGER dg_workouts_bir
BEFORE INSERT ON dg_workouts FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
/* Change another table */
UPDATE ...;
/* Send email */
UTL_SMTP...;
COMMIT;
END;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
What
do you
think of
this
trigger?
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Trigger Restarts
“Our trigger fired twice!” We had a
one-row table with a BEFORE FOR
EACH ROW trigger on it. We
updated one row, yet the trigger
fired two times.
Tom: "If you have a trigger that does
anything non-transactional, trigger restarts
could be a fairly serious issue."
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
CREATE OR REPLACE PACKAGE BODY dg_workout_mgr
IS
PROCEDURE update_workout (...) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
UPDATE dg_workouts ...;
/* Change other table */
UPDATE ...;
/* Send email */
UTL_SMTP...;
COMMIT;
END;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
What to
do?
Move logic
to PL/SQL
API, restrict
direct
access to
table
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
SQL> create or replace
2 trigger TRG_AUDIT_EMPLOYEES
3 after insert or update or delete on EMPLOYEES
4 for each row
5 declare
6 l_operation varchar2(1) :=
7 case when updating then 'U'
8 when deleting then 'D'
9 else 'I' end;
10 begin
11 if updating or inserting then
12 insert into AUDIT_EMPLOYEES
13 (aud_who, aud_when ...
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
What
about
this
trigger?
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | Pixabay
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
update ex_rates
set exchange_rate = 1
where to_currency_code = 'USD'
and from_current_code = 'GBP';
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
"Sell one million
dollars!"
Ryan McGuire / Gratisography
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
update ex_rates
set exchange_rate = 1.29
where to_currency_code = 'USD'
and from_current_code = 'GBP';
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
"Buy one million
dollars!"
Ryan McGuire / Gratisography
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
"We need
auditing!"
Ryan McGuire / Gratisography
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
select from_currency_code,
to_currency_code,
exchange_rate
from ex_rates
versions between timestamp
date'2018-01-01' and sysdate;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Limited
by undo
Flashback Data
Archive
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
create flashback archive change_history
tablespace users retention 7 year;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
alter table ex_rates
flashback archive change_history;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
select from_currency_code,
to_currency_code,
exchange_rate,
versions_operation,
versions_starttime,
versions_endtime
from ex_rates
versions between timestamp
date'2018-01-01' and sysdate;
blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
FREE from
11.2.0.4
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
FROM_CODE TO_CODE EX_RATE OP STARTTIME ENDTIME
GBP USD 1.3 <null> <null> 17-SEP-2018 08.58.38
GBP USD 1.34 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.38
GBP USD 1.29 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.38
GBP USD 1 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.41
GBP USD 1.29 U 17-SEP-2018 08.58.41 17-SEP-2018 08.58.41
GBP USD 1.31 U 17-SEP-2018 08.58.41 <null>
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
Now it's over to you!
Copyright © 2019, Oracle and/or its affiliates. All rights reserved. |
asktom.oracle.com
#AskTOMOfficeHours
Ryan McGuire / Gratisography

AskTOM Office Hours on Database Triggers

  • 1.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | PL/SQL Office Hours session begins soon… Database Triggers: You Say Stop, I Say Go with Chris Saxon, @ChrisRSaxon & @SQLDaily https://www.youtube.com/c/TheMagicofSQL https://blogs.oracle.com/sql Steven Feuerstein, @sfonplsql https://www.youtube.com/channel/PracticallyPerfectPLSQL http://stevenfeuersteinonplsql.blogspot.com/ And special guests!
  • 2.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Welcome to Ask TOM Office Hours!
  • 3.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon Database Triggers Some people say you should NEVER use them. Some people hate using the word "NEVER." Let's talk about it. We'll hear from Toon Koppelaars, Jacek Gebal and Chris Saxon. But we also want to hear from you.
  • 4.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon Database Triggers Broadly two different types of triggers: • System triggers (database events like logon triggers, DDL triggers, etc.) • DML triggers (triggers on tables and views): simple, compound, instead of
  • 5.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | CREATE OR REPLACE TRIGGER stop_drop_trg BEFORE DROP ON hr.SCHEMA BEGIN RAISE_APPLICATION_ERROR ( num => -20999, msg => 'Cannot drop object'); END; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon System trigger example
  • 6.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon Focusing In On DML Triggers For the rest of this session, we will concentrate on DML triggers – triggers created on tables or views. They are the source of controversy and likely impact on production applications.
  • 7.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Some thoughts from Jacek Gabel (Fidelity, utPLSQL) "Triggers are great. They can also become a maintenance hell."
  • 8.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Some thoughts from Toon Koppelaars on…. "Triggers Considered Harmful, Considered Harmful"
  • 9.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Mostly Agree With #1 Influencer of Past Times • http://tkyte.blogspot.com/2006/09/classic-example-of-why-i-despise.html – “Triggers are so abused - and used so inappropriately, I'd rather live without them.” – “There are no times triggers cannot be avoided. They are purely a convenience that is overused, abused and improperly used.” • http://tkyte.blogspot.com/2007/03/dreaded-others-then-null-strikes- again.html – [On why he doesn’t like triggers] “Because I hate being surprised or tricked. And triggers are all about trickery and surprises.” 9 Still available at web.archive.org
  • 10.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Tom's Legacy • Default response at community.oracle.com when someone poses question related to triggers: “Triggers should be avoided as much as possible“ “Don’t use them, they are bad” “Triggers are considered harmful” 10
  • 11.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Four Use-Cases of Database Triggers 1. Modify :NEW columns 2. Execute changes outside transaction 3. Execute changes inside transaction 4. Query tables 11
  • 12.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Modify :NEW Columns • Done in row-triggers – :NEW.CREATED := SYSDATE – :NEW.CREATOR := USER – :NEW.ID := SEQ.NEXTVAL – … • What happens if I supply these column values? – They get overwritten – Maybe application error would be more appropriate? 12
  • 13.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Execute Changes Outside Transaction • Examples – Call REST service – Talk to some endpoint over tcp/ip – Send email – Write to file – Execute autonomous transaction • What happens if my transaction rolls back? – All of the above is outside your transaction and irreversable 13
  • 14.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Execute Changes Inside Transaction • Examples – Insert audit row in audit table – Maintain redundant data in other table(s) – Propagate more changes to other parts of database design • Semantics of your insert, update or delete has changed significantly 14
  • 15.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Query Tables • Very common use-case is to mandate cross-row constraints via triggers – These require queries – E.g. check at most one president in EMPLOYEES table – Mutating table error – Serialization • More complex than you think to get this right – SQL assertions 15
  • 16.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Four Use-Cases of Database Triggers 1. Modify :NEW columns 2. Execute changes outside transaction 3. Execute changes inside transaction 4. Query tables 16 “Surprises and Trickery” “Surprises and Trickery” “Surprises and Trickery” Too complex to do it right
  • 17.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | CREATE OR REPLACE TRIGGER dg_workouts_bir BEFORE INSERT ON dg_workouts FOR EACH ROW DECLARE BEGIN IF :new.workout_id IS NULL THEN :new.workout_id := dg_workouts_seq.NEXTVAL; END IF; :new.created_on := SYSDATE; :new.created_by := qdb_config.apex_user (); :new.changed_on := SYSDATE; :new.changed_by := qdb_config.apex_user (); END; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon What do you think of this trigger?
  • 18.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | CREATE TABLE dg_workouts ( workout_id INTEGER NOT NULL NUMBER GENERATED ALWAYS AS IDENTITY, ... created_by VARCHAR2 (132 BYTE) NOT NULL, changed_by VARCHAR2 (132 BYTE) NOT NULL, created_on DATE NOT NULL DEFAULT SYSDATE, changed_on DATE NOT NULL DEFAULT SYSDATE ) blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon Fix the table DDL or Switch to API call.
  • 19.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | CREATE OR REPLACE TRIGGER dg_workouts_bir BEFORE INSERT ON dg_workouts FOR EACH ROW DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN /* Change another table */ UPDATE ...; /* Send email */ UTL_SMTP...; COMMIT; END; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon What do you think of this trigger?
  • 20.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Trigger Restarts “Our trigger fired twice!” We had a one-row table with a BEFORE FOR EACH ROW trigger on it. We updated one row, yet the trigger fired two times. Tom: "If you have a trigger that does anything non-transactional, trigger restarts could be a fairly serious issue."
  • 21.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | CREATE OR REPLACE PACKAGE BODY dg_workout_mgr IS PROCEDURE update_workout (...) IS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN UPDATE dg_workouts ...; /* Change other table */ UPDATE ...; /* Send email */ UTL_SMTP...; COMMIT; END; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon What to do? Move logic to PL/SQL API, restrict direct access to table
  • 22.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | SQL> create or replace 2 trigger TRG_AUDIT_EMPLOYEES 3 after insert or update or delete on EMPLOYEES 4 for each row 5 declare 6 l_operation varchar2(1) := 7 case when updating then 'U' 8 when deleting then 'D' 9 else 'I' end; 10 begin 11 if updating or inserting then 12 insert into AUDIT_EMPLOYEES 13 (aud_who, aud_when ... blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon What about this trigger?
  • 23.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Pixabay
  • 24.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | update ex_rates set exchange_rate = 1 where to_currency_code = 'USD' and from_current_code = 'GBP'; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
  • 25.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon "Sell one million dollars!" Ryan McGuire / Gratisography
  • 26.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | update ex_rates set exchange_rate = 1.29 where to_currency_code = 'USD' and from_current_code = 'GBP'; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
  • 27.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon "Buy one million dollars!" Ryan McGuire / Gratisography
  • 28.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | "We need auditing!" Ryan McGuire / Gratisography
  • 29.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. |
  • 30.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | select from_currency_code, to_currency_code, exchange_rate from ex_rates versions between timestamp date'2018-01-01' and sysdate; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon Limited by undo
  • 31.
  • 32.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | create flashback archive change_history tablespace users retention 7 year; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
  • 33.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | alter table ex_rates flashback archive change_history; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon
  • 34.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | select from_currency_code, to_currency_code, exchange_rate, versions_operation, versions_starttime, versions_endtime from ex_rates versions between timestamp date'2018-01-01' and sysdate; blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon FREE from 11.2.0.4
  • 35.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | blogs.oracle.com/sql www.youtube.com/c/TheMagicOfSQL @ChrisRSaxon FROM_CODE TO_CODE EX_RATE OP STARTTIME ENDTIME GBP USD 1.3 <null> <null> 17-SEP-2018 08.58.38 GBP USD 1.34 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.38 GBP USD 1.29 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.38 GBP USD 1 U 17-SEP-2018 08.58.38 17-SEP-2018 08.58.41 GBP USD 1.29 U 17-SEP-2018 08.58.41 17-SEP-2018 08.58.41 GBP USD 1.31 U 17-SEP-2018 08.58.41 <null>
  • 36.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | Now it's over to you!
  • 37.
    Copyright © 2019,Oracle and/or its affiliates. All rights reserved. | asktom.oracle.com #AskTOMOfficeHours Ryan McGuire / Gratisography