How to find the elapse time user was logged into database using trigger - oracle

I am trying to calculate the total time user logged into the database using a trigger
my table structure is seen below:
create table stats$user_log
(
user_id varchar2(30),
session_id number(8),
host varchar2(30),
logon_day date,
logon_time varchar2(10),
logoff_day date,
logoff_time varchar2(10),
elapsed_minutes varchar2(32)
);
My trigger for logon is as follows:
create or replace trigger
logon_audit_trigger
AFTER LOGON ON DATABASE
BEGIN
insert into stats$user_log values(
user,
sys_context('USERENV','SESSIONID'),
sys_context('USERENV','HOST'),
sysdate,
to_char(sysdate, 'hh24:mi:ss'),
null,
null,
null
);
END;
/
My trigger for logoff is as follows:
create or replace trigger
logoff_audit_trigger
BEFORE LOGOFF ON DATABASE
BEGIN
UPDATE
stats$user_log
set
logoff_day = sysdate,
logoff_time = to_char(sysdate, 'hh24:mi:ss'),
elapsed_minutes = round((logoff_day - logon_day)*1440,2)
WHERE
sys_context('USERENV','SESSIONID') = session_id;
END;
/
When the user logs out everything is captured except the elapse_minutes column it remains as null.
Can anyone tell me where i'm going wrong please and thanks

At the time you do the update, the logoff_day you refer to in the right-hand side of the set expression is still null, so the expression evaluates to null.
Any column values you refer to have to be the pre-update values, or changing the order that the columns are assigned within the set clause would change how the update worked, which at best be confusing. An update that sets a column based on its old value - e.g. set salary = salary * 1.1 - would be particularly problematic.
You can refer to sysdate a second time instead:
logoff_day = sysdate,
logoff_time = to_char(sysdate, 'hh24:mi:ss'),
elapsed_minutes = round((sysdate - logon_day)*1440,2)

If session auditing is enabled, the database already does this for you. Why create that for yourself? Check dba_audit_session for the results. You might need to talk to your dba / security staff to get access but it might be worth it.

Related

how to track and modify an in-flight oracle query before execution?

My Application is sending below query to Oracle.
SELECT * FROM EMPLOYEE WHERE DATE > (SYSDATE - 1) order by employee_id
I cannot change this query from the application. I am looking for a way for oracle to monitor the queries, change it to below query format and return the result.
SELECT * FROM EMPLOYEE WHERE DATE > (SYSDATE - 1) and Currently_employed = 'YES' order by employee_id
Thank You.
Subrat
If the application can connect to the database as a different user then the table owner, you could also implement a Virtual Private Database policy to modify the query in-flight. See here:
https://oracle-base.com/articles/8i/virtual-private-databases
You could also, if the application is connecting as a separate user, create a view as suggested by #ekochergin, and have a synonym "EMPLOYEE" in the application's user schema point to the view in the data owner's schema.
If the application is connecting as the data owner, your options are much more limited. Renaming the table and replacing it with a view as suggested would be the easiest option. If you've got a lot of money to invest in Oracle's Database Application Firewall appliance you could also modify the query in-flight with a security policy there.
You might needed to rename employee table to somewhat like "EMP_TABLE" and create a view named "EMPLOYEE" using
create view employee as select * from emp_table where currently_employed = 'YES';
Please test it against a test instance before implementing on live
Use the SQL Translation Framework if you only need to convert a small number of statements. If you need to modify many statements then you should look into the options described in the other answers, such as Virtual Private Database, views, or synonyms.
For this sample schema:
create table employee
(
employee_id number,
hire_date date,
currently_employed varchar2(3)
);
insert into employee values(1, sysdate, 'NO');
insert into employee values(1, sysdate, 'YES');
commit;
Create the following translator profile and then create the specific translation:
begin
dbms_sql_translator.create_profile('EMPLOYEE_TRANSLATOR_PROFILE');
dbms_sql_translator.register_sql_translation
(
profile_name => 'EMPLOYEE_TRANSLATOR_PROFILE',
sql_text => q'[SELECT * FROM EMPLOYEE WHERE HIRE_DATE > (SYSDATE - 1) order by employee_id]',
translated_text => q'[SELECT * FROM EMPLOYEE WHERE HIRE_DATE > (SYSDATE - 1) and Currently_employed = 'YES' order by employee_id]'
);
end;
/
The translation profile must be enabled in each session. Since you have no control over the application, you can create a logon profile that will automatically run the commands to enable the translation:
--Logon trigger that enables profiles.
--I'm not sure why, but you must create this trigger as SYS.
create or replace trigger sys.logon_trg
after logon on database
--Add your username here:
when (user in ('JHELLER'))
begin
execute immediate 'alter session set sql_translation_profile = jheller.employee_translator_profile';
execute immediate q'[alter session set events = '10601 trace name context forever, level 32']';
end;
/
Now, when the application runs the original query that would normally return two rows, it will run the second query that only returns one row:
SQL> SELECT * FROM EMPLOYEE WHERE HIRE_DATE > (SYSDATE - 1) order by employee_id;
EMPLOYEE_ID HIRE_DATE CUR
----------- --------- ---
1 12-FEB-21 YES
But be careful of tiny syntax changes that will prevent the translation. For example, if SELECT is changed to select, the query will not be replaced and will return two rows again:
SQL> select * FROM EMPLOYEE WHERE HIRE_DATE > (SYSDATE - 1) order by employee_id;
EMPLOYEE_ID HIRE_DATE CUR
----------- --------- ---
1 12-FEB-21 YES
1 12-FEB-21 NO

What is the right PL/SQL for updating rows without a need to reinsert it?

I new at using PL/SQL and I want the following:
I have this table on Oracle SQLcl
create table Child (
id varchar not null,
name varchar not null,
gender varchar not null,
YearOfBirth number(4) not null,
YearsOfAge number(4) null,
CONSTRAINT Pk primary key (id)
);
And I want a PL/SQL (preferred anonymous) that update field of "YearsOfAge" by minusing 2020 from the "YearOfBirth" field. I could do that but my problem is that the table won't be updated until I insert the PL/SQL block again. So whenever I insert a new row, I have to insert my PL/SQL block again. I want to get the table updated whenever I insert/update a row, without a need to insert this block following a new row.
To be clearer, I just want to insert SL/SQL block one time after creating the table, then get the table's "YearsOfAge" updated whenever I insert/update/delete a row. So when I write "select * from Child;" I need to see the "YearsOfAge" with the new value that computed from subtracting 2020 from "YearOf Birth".
My current PL/SQL is below:
begin
IF INSERTING THEN
update Child set YearsOfAge = 2020 - YearOfBirth;
ELSIF DELETEING THEN
update Child set YearsOfAge = 2020 - YearOfBirth;
ELSE
update Child set YearsOfAge = 2020 - YearOfBirth;
END IF;
END;
/
If you really need to store the age this way, some options are virtual columns, views, and triggers.
Virtual Column
With a virtual column, Oracle will automatically perform the calculation on the fly.
SQL> create table Child
2 (
3 id number not null,
4 name varchar2(10) not null,
5 gender varchar2(10) not null,
6 YearOfBirth number(4) not null,
7 YearsOfAge number generated always as (2020 - yearOfBirth) null,
8 constraint pk_child primary key (id)
9 );
Table created.
SQL> insert into child(id, name, gender, yearOfBirth) values(1, 'A', 'female' , 1990);
1 row created.
SQL> insert into child(id, name, gender, yearOfBirth) values(2, 'B', 'male' , 2000);
1 row created.
SQL> insert into child(id, name, gender, yearOfBirth) values(3, 'C', 'non-binary', 2010);
1 row created.
SQL> select * from child;
ID NAME GENDER YEAROFBIRTH YEARSOFAGE
---------- ---------- ---------- ----------- ----------
1 A female 1990 30
2 B male 2000 20
3 C non-binary 2010 10
View
One downside of virtual columns is that they cannot use functions like SYSDATE, so the year has to be hard-coded. With a view, the expression can reference SYSDATE and will always be up-to-date:
create or replace view child_view as
select id, name, gender, yearOfBirth, extract(year from sysdate) - yearOfBirth yearsOfAge
from child;
Trigger (Warning)
You can also use a trigger to create the value when a row is inserted or updated:
create or replace trigger child_trg
before update or insert on child
for each row
begin
if updating('YEAROFBIRTH') or inserting then
:new.yearsOfAge := extract(year from sysdate) - :new.yearOfBirth;
end if;
end;
/
But in practice, triggers are a pain to maintain. Which leads to the question: why do you want to store this information in the first place?
Good database design should minimize the amount of redundant data. There are always exceptions, but you should have a good reason for those exceptions, like an especially complicated calculation that you don't want others to get wrong, you can't create a PL/SQL function because of an unusual security constraint, etc. Calculating something as trivial as the age may cause more problems than it solves.

oracle trigger after update on specific field followed by insert

I'm creating a trigger that will insert a record into another table based on an after update and certain text being referenced in the update..
CREATE OR REPLACE TRIGGER NCATSPROD_PM.DEACTIVATE_USERS
AFTER UPDATE OF "CHANGEBY_CHAR" ON NCATSPROD_PM.PM_USER_DATA
REFERENCING NEW AS "BICSUPP"
BEGIN
INSERT INTO NCATSPROD_PM.DEACTIVATED_USERS (USERNAME, EMAIL, DEACTIVATED_DATE)
VALUES (SELECT USER_ID, EMAIL, CHANGE_DATE FROM ncatsprod_pm.pm_user_data);
END;
When i try to compile this i'm getting an error
Error(1,3): PL/SQL: SQL Statement ignored
Error(1,90): PL/SQL: ORA-00936: missing expression
A little unsure where i'm going wrong with this? Go to source on the error takes me to the AFTER UPDATE line but looking at examples i don't see what's up. I tried using SQL Developer's trigger wizard and that gave me the same result
The REFERENCING clause is just used to change the names of the built-in OLD and NEW records. OLD contains the values of the row before it was updated, and NEW holds the values after it was updated. Here's an example of how you might use them.
create table PM_USER_DATA (user_id varchar2(20), email varchar2(20), change_date date, changeby_char varchar2(20));
create table DEACTIVATED_USERS (username varchar2(20), email varchar2(20), deactivated_date date);
insert into pm_user_data values ('test', 'test#test.com', sysdate, 'SOMEUSER');
create or replace trigger deactivate_users
after update of changeby_char on pm_user_data
referencing old as o1 new as n1
for each row
begin
if :n1.changeby_char = 'BICSUPP' then
insert into deactivated_users (username, email, deactivated_date)
values (:n1.user_id, :n1.email, :n1.change_date);
end if;
end;
/
update pm_user_data set changeby_char = 'BICSUPP';
select * from deactivated_users;
/* output:
USERNAME EMAIL DEACTIVATED_DATE
-------------------- -------------------- ----------------
test test#test.com 10-OCT-17
1 row selected.
*/

ORA-02437: "primary key violated" - why can't I see duplicate ID in SQL Developer?

I would receive an error:
ORA-02437: cannot validate (%s.%s) - primary key violated
Cause: attempted to validate a primary key with duplicate values or null values
I found it was because I have a stored procedure that increments the ID, but it had failed to do so when it re-ran and had an error related to one of my datatypes. I found I now had a duplicate ID in my database table. All this made sense and I was able to easily rectify it with a DELETE FROM MyTable WHERE ID = x, where x was the offending duplicate ID. The problem I have is the only way I was able to even find the IDs that were duplicated is in the first place is because I did a SELECT * FROM MyTable WHERE ID = x -- where x was one greater than the last ID I could actually see. I found it just by an educated guess. So:
Why can't I see these duplicate IDs when I open the table in Oracle SQL Developer? It only shows the last row as the ID before the duplicates. I don't think it is because of my primary key constraint, since the first line in my stored procedure is to remove that (and put it back, at the end - probably when I got my error), and it was not present when I looked at my table.
Is there some way to make these last IDs that got inserted into the table visible, so I wouldn't have to guess or assume that the duplicate IDs are "hiding" as one greater than the last ID I have in my table, in the future? There is a commit; in my stored procedure, so they should have appeared -- unless, of course, the procedure got hung up before it could run that line of code (highly probable).
Stored procedure that runs:
create or replace
PROCEDURE PRC_MYTABLE_INTAKE(
, EMPLOYEE_ID IN NVARCHAR2
, TITLE_POSITION IN NVARCHAR2
, CREATED_DATE IN DATE
, LAST_MODIFIED IN DATE
) AS
myid integer := 0;
appid integer := 0;
BEGIN
-- disable PK constraint so it can be updated
EXECUTE IMMEDIATE 'ALTER TABLE MYTABLE DROP CONSTRAINT MYTABLE_PK';
COMMIT;
-- assign ID to myid
SELECT ID INTO myid FROM MYTABLE WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE);
-- increment
myid := myid + 1;
-- assign APPLICATION_ID to appid
SELECT APPLICATION_ID INTO appid FROM MYTABLE WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE);
-- increment
appid := appid + 1;
-- use these ids to insert with
INSERT INTO MYTABLE (ID, APPLICATION_ID,
, EMPLOYEE_ID
, TITLE_POSITION
, CREATED_DATE
, LAST_MODIFIED
) VALUES(myid, appid,
, EMPLOYEE_ID
, TITLE_POSITION
, CREATED_DATE
, LAST_MODIFIED
);
COMMIT;
-- re-enable the PK constraint
EXECUTE IMMEDIATE 'ALTER TABLE PASS ADD CONSTRAINT MYTABLE_PK PRIMARY KEY (ID)';
COMMIT;
END;
Here's one problem:
SELECT ID
INTO myid
FROM MYTABLE
WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE)
There is no correlation between ID and ROWID, so you're not getting the maximum current ID, you're just getting the one that happens to be on the row that is furthest from the start of a datafile with a high number.
The code you need is:
SELECT COALESCE(MAX(ID),0)
FROM MYTABLE;
Or better yet, just use a sequence.
No idea why you're dropping the PK either.
Furthermore, when you issue the query:
SELECT APPLICATION_ID INTO appid ...
... that could be for a different row than the one you already got the id for, because a change could have been committed to the table.
Of course another issue is that you can't run two instances of this procedure at the same time either.
For David Aldridge, since he wants to look at code instead of the real reason I posted my question, run this ---
CREATE TABLE YOURSCHEMA.TESTING
(
TEST_ID NVARCHAR2(100) NOT NULL
, TEST_TYPE NVARCHAR2(100) NOT NULL
, CONSTRAINT TEST_PK PRIMARY KEY
(
TEST_ID
)
ENABLE
);
create or replace
PROCEDURE PRC_TESTING_INSERT(
TEST_TYPE IN NVARCHAR2
) AS
testid integer := 0;
BEGIN
-- disable PK constraint so it can be updated
EXECUTE IMMEDIATE 'ALTER TABLE TESTING DROP CONSTRAINT TEST_PK';
COMMIT;
-- assign TEST_ID to testid
SELECT TEST_ID INTO testid FROM TESTING WHERE ROWID IN (SELECT MAX(ROWID) FROM TESTING);
-- increment
testid := testid + 1;
-- use this id to insert with
INSERT INTO TESTING (TEST_ID, TEST_TYPE) VALUES(testid, TEST_TYPE);
COMMIT;
-- re-enable the PK constraint
EXECUTE IMMEDIATE 'ALTER TABLE TESTING ADD CONSTRAINT TEST_PK PRIMARY KEY (TEST_ID)';
COMMIT;
END;
SET serveroutput on;
DECLARE
test_type varchar(100);
BEGIN
test_type := 'dude';
YOURSCHEMA.PRC_TESTING_INSERT(test_type);
-- to verify the variable got set and procedure ran, could do:
--dbms_output.enable;
--dbms_output.put_line(test_type);
END;
Now, because there is no data in the table, the stored procedure will fail with ORA-06512: no data found. If you then try and run it again, you will get ORA-02443: cannot drop constraint - nonexistent constraint, because the EXECUTE IMMEDIATE 'ALTER TABLE TESTING DROP CONSTRAINT TEST_PK'; successfully dropped it, and the procedure never ran the command at the end to re-add it. This is what made me think I needed the commits, but even without them, it still will not complete the whole procedure.
To prove that the procedure DOES run, if given proper data, run this after creating the table, but before creating/running the stored procedure:
INSERT INTO TESTING (TEST_ID, TEST_TYPE)
VALUES ('1', 'hi');
And if you run the proc from a new table (not one with its constraint dropped), it will run fine.
Since mathguy didn't post this as the answer, though I'll credit him for the information...
Answer to why I can't see the duplicates is because the COMMIT does not occur in the procedure when it failed due to a datatype mismatch (which we found was actually in the application's code that sent the variable's values into this procedure, not in the stored procedure, itself). (It's also why I'll mark down anyone that says you don't have to add so many COMMIT lines in this procedure.) The commands were run in the session of the user that starts it - in my case, another session of the same DB user I was logged in with, but started from my application, instead of my SQL Developer session. It also explains why I could do a COMMIT, myself, but it did not affect the application's session - I could not commit any actions ran from another session. Had I ran a COMMIT as an OracleCommand and did an .ExecuteNonQuery on my OracleConnection right after the failure within the catch of my application, I would have seen the rows in SQL Developer without having to do a special query.
So, in short, the only way to see the items was with a direct query using WHERE ID =, find the last ID and increment it, and put it in the query.

Oracle trigger insert to other table then modify the original table

I have theses two tables:
TABLE ASSET_ENTRY_NOTE (
ID NUMBER NOT NULL, --PK
ASSETMDL_ID NUMBER NOT NULL, --FK
DEPT_ID NUMBER NOT NULL, --FK
LOCATION NVARCHAR2(100) NOT NULL,
ASSET_ID NUMBER, --FK TO ASSETS
ACCOUNT_ID NUMBER NOT NULL, --FK
TOTAL_DPRC_DURATION FLOAT(126) NOT NULL,
TOTAL_PROD_HRS FLOAT(126),
AMORTIZATION_PRCNTG FLOAT(126),
ACQUIRE_DATE DATE NOT NULL,
DESCRIPTION NVARCHAR2(200) NOT NULL,
APPRFLAG NUMBER DEFAULT 0 NOT NULL,
WRK_HRS FLOAT(126),
)
TABLE ASSETS (
ID NUMBER NOT NULL, --PK
ASSETMDL_ID NUMBER NOT NULL, --FK
DEPT_ID NUMBER NOT NULL,
LOCATION NVARCHAR2(100) NOT NULL, --FK
ACCOUNT_ID NUMBER NOT NULL,
ACQUIRE_DATE DATE NOT NULL,
TOTAL_DPRC_DURATION FLOAT(126),
BALANCE_CLOSING_DATE DATE,
SELL_VAL FLOAT(126),
RPLCMNT_DISCOUNT FLOAT(126),
DESCRIPTION NVARCHAR2(200) NOT NULL,
)
Note that there's a one to one relationship between the two tables (i.e. ASSET_ENTRY_NOTE.ASSET_ID is Unique.
When the ASSETS_ENTRY_NOTE.APPRFLAG is updated to 1 I have this trigger that:
gets a new primary key sequence for the ASSETS table.
insert data from ASSETS_ENTRY_NOTE to ASSETS.
updates the column ASSETS_ENTRY_NOTE.ASSET_ID to the same value as the primary key value on the sequence.
This is the latest try for my trigger:
CREATE OR REPLACE TRIGGER ENTRYNT_ASSET_TRIG
after UPDATE OF APPRFLAG ON ASSET_ENTRY_NOTE
for each row
when (new.apprflag = 1)
declare
v_asset_id number;
BEGIN
SELECT assets_PK_SEQ.NEXTVAL INTO v_asset_id
FROM DUAL d;
insert into assets (ID,
assets.assetmdl_id,
assets.dept_id,
assets.location,
assets.account_id,
assets.acquire_date,
assets.total_dprc_duration,
assets.description
)
values (v_asset_id,
assetmdl_id,
dept_id,
location,
account_id,
acquire_date,
total_dprc_duration,
description
);
update ASSET_ENTRY_NOTE set asset_id = v_asset_id where ;
END;
The thing is, I know that ASSET_ENTRY_NOTE is a mutating table and the last UPDATE statement is not allowed here, But nothing else is working for me.
What I've already tried:
creating a statement-level trigger to update one value only.
using before instead of after but that's incorrect because I need the values just to insert into the ASSETS.
using a cursor to go through each value changed but I had exact fetch error.
creating a procedure that handles inserting and updating.
Any help would be appreciated.
The design seems quite strange to me, but to answer the question about the trigger:
To change the asset_entry_note row in the trigger, you need a before update trigger. In there you can just assign the value to the asset_id column.
Your insert statement is also wrong. You can table-qualify column names in the column list of an insert statement. And the values clause needs to use the values from the inserted row. You are referencing the target table's columns which is not allowed).
You also don't need a select statement to obtain the sequence value.
Putting all that together, your trigger should look something like this:
CREATE OR REPLACE TRIGGER ENTRYNT_ASSET_TRIG
BEFORE UPDATE OF APPRFLAG ON ASSET_ENTRY_NOTE
for each row
when (new.apprflag = 1)
declare
v_asset_id number;
BEGIN
v_asset_id := assets_PK_SEQ.NEXTVAL;
insert into assets
(ID,
assetmdl_id,
dept_id,
location,
account_id,
acquire_date,
total_dprc_duration,
description)
values
(v_asset_id,
new.assetmdl_id, -- reference the inserted row here!
new.dept_id,
new.location,
new.account_id,
new.acquire_date,
new.total_dprc_duration,
new.description);
new.asset_id := v_asset_id;
END;
/
You have to change the design of the application to have only one table with sign to indicate the membership of a particular entity.
Another way is to create 'after statement' trigger to update all affected rows in ASSET_ENTRY_NOTE with proper values. These rows is to be collected in, for example, package collection in row-level trigger.
I fixed it and it worked:
changed to before.
edited the update statement to an assignment of new so that the last line would become :new.asset_id := v_asset_id ;

Resources