I have a stored procedure that simply enables constraints of specific tables.
It has been working fine for quite a while, but all of sudden(today), I got ORA-08103 error.
What could be the cause of this error?
BEGIN
FOR c IN (
SELECT
c.owner,
c.table_name,
c.constraint_name
FROM user_constraints c
WHERE c.status = 'DISABLED'
AND c.table_name IN (
'TABLE_01', 'TABLE_02', 'TABLE_03', 'TABLE_04'
)) LOOP
EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' ENABLE CONSTRAINT ' || c.constraint_name;
END LOOP;
END;
[Update]
Disable constraints
Load bulk data
Enable constraints
These are steps I am following.
First, I disable the constrains of the tables, then load bulk data using SQLLoader and finally enable the disabled constraints, where I get ORA-08103 error.
ORA-08103 occurs when we try to run a DDL statement against an object which doesn't exist. Ah, but you say
they are always there. They will never be dropped
Database objects like tables have two identifiers in the data dictionary, the OBJECT_ID and the DATA_OBJECT_ID: we can see these in the ALL_OBJECTS view. The OBJECT_ID is constant for the lifetime of the table but the DATA_OBJECT_ID - the "dictionary object number of the segment that contains the object" - changes any time DDL is executed against the object. For instance, when a table is truncated or an index is rebuilt.
So to your situation: the ORA-08103 error indicates that the DATA_OBJECT_ID has changed since you ran the cursor. That is while you were running your procedure somebody else executed DDL against one of the tables, constraints or underlying indexes.
Probably this is an unfortunate coincidence and it won't happen the next time you run the procedure. But you can minimize the chances of another occurrence by changing the way you run the query:
declare
tabs dbms_debug_vc2coll := dbms_debug_vc2coll ('TABLE_01', 'TABLE_02', 'TABLE_03', 'TABLE_04');
BEGIN
for idx in 1..tabs.count() loop
FOR c IN (
SELECT
c.owner,
c.table_name,
c.constraint_name
FROM user_constraints c
WHERE c.table_name = tabs(idx)
AND c.status = 'DISABLED'
) LOOP
EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' ENABLE CONSTRAINT ' || c.constraint_name;
END LOOP;
END LOOP;
END;
Enabling constraints takes time (because of the need to validate them). So selecting tables one by one reduces the time you need the DATA_OBJECT_ID to remain fixed.
"How does your procedure above minimize the chance of the same error?"
Your cursor selects all four tables, and hence all four DATA_OBJECT_IDs. Suppose another session modifies TABLE_04 while you are enabling constraints on TABLE_01. When your procedure gets round to TABLE_04 the DATA_OBJECT_ID has changed and you'll get ORA-08103.
But if you were running my version of the code it wouldn't matter, because you would not select the DATA_OBJECT_ID for TABLE_04 until you were ready to process it. So you would get the changed DATA_OBJECT_ID (without knowing it was changed.
Related
Could you please help me in a unique situation I am in. I am receiving "ORA-30511: invalid DDL operation in system triggers" when dropping sequences and procedures during logoff trigger.
I need to delete tables, sequences and procedures of users before logoff event happens. I am writing the table details in DB_OBJECTS table upon create using a separate trigger. Below is my logoff trigger - could you please help me where I am doing wrong. Dropping tables is working fine in the below code. Only Dropping sequences and procedures is giving me "ORA-30511: invalid DDL operation in system triggers" error.
CREATE OR REPLACE TRIGGER DELETE_BEFORE_LOGOFF
BEFORE LOGOFF ON DATABASE
DECLARE
USER_ID NUMBER := SYS_CONTEXT('USERENV', 'SESSIONID');
BEGIN
FOR O IN (SELECT USER, OBJECT_NAME, OBJECT_TYPE
FROM DB_OBJECTS WHERE SID = USER_ID
AND USERNAME = USER AND SYSDATE > CREATED_DTTM) LOOP
IF O.OBJECT_TYPE = 'TABLE' THEN
EXECUTE IMMEDIATE 'DROP TABLE ' || O.USER || '.' || O.OBJECT_NAME || ' CASCADE CONSTRAINTS';
ELSIF O.OBJECT_TYPE = 'SEQUENCE' THEN
EXECUTE IMMEDIATE 'DROP SEQUENCE ' || O.USER || '.' || O.OBJECT_NAME;
ELSIF O.OBJECT_TYPE = 'PROCEDURE' THEN
EXECUTE IMMEDIATE 'DROP PROCEDURE ' || O.USER || '.' || O.OBJECT_NAME;
END IF;
END LOOP;
EXCEPTION WHEN NO_DATA_FOUND THEN NULL;
END;
/
That's a simple one.
Error code: ORA-30511
Description: invalid DDL operation in system triggers
Cause: An attempt was made to perform an invalid DDL operation in a system trigger. Most DDL operations currently are not supported in system triggers. The only currently supported DDL operations are table operations and ALTER/COMPILE operations.
Action: Remove invalid DDL operations in system triggers.
That's why only
Dropping tables is working fine
succeeded.
Therefore, you can't do that using trigger.
You asked (in a comment) how to drop these objects, then. Manually, as far as I can tell. Though, that's quite unusual - what if someone accidentally logs off? You'd drop everything they created. If you use that schema for educational purposes (for example, every student gets their own schema), then you could create a "clean-up" script you'd run once class is over. Something like this:
SET SERVEROUTPUT ON;
DECLARE
l_user VARCHAR2 (30) := 'SCOTT';
l_str VARCHAR2 (200);
BEGIN
IF USER = l_user
THEN
FOR cur_r IN (SELECT object_name, object_type
FROM user_objects
WHERE object_name NOT IN ('EMP',
'DEPT',
'BONUS',
'SALGRADE'))
LOOP
BEGIN
l_str :=
'drop '
|| cur_r.object_type
|| ' "'
|| cur_r.object_name
|| '"';
DBMS_OUTPUT.put_line (l_str);
EXECUTE IMMEDIATE l_str;
EXCEPTION
WHEN OTHERS
THEN
NULL;
END;
END LOOP;
END IF;
END;
/
PURGE RECYCLEBIN;
It is far from being perfect; I use it to clean up my Scott schema I use to answer questions on various sites so - once it becomes a mess, I run that PL/SQL code several times (because of possible foreign key constraint).
Other option is to keep a create user script(s) (along with all grant statements) and - once class is over - drop existing user and simply recreate it.
Or, if that user contains some pre-built tables, keep export file (I mean, result of data pump export) and import it after the user is dropped.
There are various options - I don't know whether I managed to guess correctly, but now you have something to think about.
I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).
I have fifty tables in a database, in that I need only six tables.
How can I delete remaining tables by one single query?
You can generate a list of DROP TABLE commands with the query below:
SELECT 'DROP TABLE ' || table_name || ';' FROM user_tables;
After that you remove your six tables you want to keep and execute the other commands. Or you add a WHERE table_name NOT IN (...) clause to the query.
Hope it helps.
Use something like this, as there is no direct command or way in oracle to do this
begin
for rec in (select table_name
from all_tables
where table_name like '%ABC_%'
)
loop
execute immediate 'drop table '||rec.table_name;
end loop;
end;
/
To expand on the answer,
for Oracle 10 versions and later, dropped tables are not deleted permanently, but moved to recycled bin. To truly delete tables need to add optional parameter PURGE.
Expanding on the accepted answer :
SELECT 'DROP TABLE ' || table_name || ' PURGE ;' DB_DEATH FROM user_tables;
First Run this query with table names that you want to keep.
SELECT CONCAT( 'DROP TABLE ', GROUP_CONCAT(table_name) , ';' )
AS statement FROM information_schema.tables
WHERE table_schema = 'mydatabase' AND table_name not in ('table1', 'table2', 'table3');
This query will give you DROP table query.
Select from the list on the left all the tables you want to drop. Drag and drop them in your worksheet. Select Object Name from the pop-up window
Press the Edit menu and select Replace.
Type in the find field the comma symbol ,
Type in the replace field the following text ;\ndrop table . Note that there is a space after the word table. Replace all.
Type drop table before your first table and ; after your last one.
You are ready to drop your tables.
DECLARE
TYPE bulk_array is table of ALL_TABLES.TABLE_NAME%type;
array bulk_array;
BEGIN
SELECT OWNER ||'.'|| TABLE_NAME BULK COLLECT
INTO array
FROM ALL_TABLES
WHERE TABLE_NAME LIKE '%%';--HERE U WILL PUT YOUR CONDITIONS.
FOR i IN array.FIRST..array.LAST LOOP
EXECUTE IMMEDIATE 'DROP TABLE '|| array(i) || ' PURGE'; --Specify PURGE if you want to drop the table and release the space associated
END LOOP;
END;
If the tables you want to keep are keep_tab1, keep_tab2, etc. or start with ASB..
use regexp_like. Here just to show an example with regexp_like
The loop continues even when an error occurs dropping one of the tables.You may
have to run this script multiple times as errors may occur when dropping a primary table without having dropped the referential tables first.
begin
for rec in (
select table_name from user_tables
where not
regexp_like(table_name, 'keep_tab1|keep_tab2|^ASB')
)
loop
begin
execute immediate 'drop table '||rec.table_name;
exception when others then
dbms_output.put_line(rec.table_name||':'||sqlerrm);
end;
end loop;
end;
I have an assignment for uni now where I need to write a database change script and then a rollback script. I should also do some simple checks whether the changes has been done or not. I have spent enormous time by writing the scripts because I am not skilled in plsql. The prodcut is here:
-- the check could be more extensive, e.g. checking the type of the column
declare
titleExists number;
begin
select count(*) into titleExists
from user_tab_columns
where table_name = 'TITLE'
and column_name = 'TITLE';
if titleExists > 0 then
execute immediate 'alter table title rename column title to name';
end if;
end;
/
declare
typeExists number;
begin
select count(*) into typeExists
from user_tab_columns
where table_name = 'TITLE'
and column_name = 'TYPE'
and data_type = 'CHAR';
if typeExists > 0 then
execute immediate 'alter table title add (new_type varchar2(12) check (new_type in (''business'', ''mod_cook'', ''psychology'', ''popular_comp'', ''trad_cook'')))';
execute immediate 'update title set new_type = trim(type)';
execute immediate 'alter table title modify (new_type not null)';
execute immediate 'alter table title drop column type';
execute immediate 'alter table title rename column new_type to type';
end if;
end;
/
The first part renames a column and the second part changes columns type and adds a check, basically turns a char column into an enum.
I would really like to know whehter I need to put every alteration in execute immediate block. Is there a simpler way of writing this?
There are a number of issues with the script, but keeping myself limited to the question whether to use execute immediate:
There is not really a simpler way of writing this. The PL/SQL is modifying the database it runs on, so adding for instance the update title occur as a hard-coded section in the PL/SQL is not possible (it would not compile).
Also, sometimes it is possible to merge multiple statements executed through execute immediate in one big PL/SQL being send over. But PL/SQL itself does not support the DDL statements, so you need some way of dynamic SQL.
Note also that each DDL does an implicit commit, so your update is always executed and committed by the following alter table statement.
I think you have done great being this your first PL/SQL assignment.
I have a procedure in which 100 tables have to be updated one by one. All tables have the same column to be updated. For improving the performance I am trying to use Execute Immediate with FORALL but I am getting a lot of compilation errors.
Is it syntactically possible to update 100 different tables inside a FORALL statement using Execute immediate.
My code looks something like this.
Declare
TYPE u IS TABLE OF VARCHAR2(240) INDEX BY BINARY_INTEGER;
Table_List u;
FOR somecursor IN (SELECT variable1, variable2 FROM SomeTable)
LOOP
BEGIN
Table_List(1) := 'table1';
Table_List(2) := 'table2';
......
......
table_list(100):= 'table100';
FORALL i IN Table_List.FIRST .. Table_List.LAST
EXECUTE IMMEDIATE 'UPDATE :1 SET column = :3 WHERE column = :2'
USING Table_List(i), somecursor.variable1, somecursor.variable2 ;
end loop;
I hope people can understand what I am trying to do through this code. If something is big time wrong please suggest me what exactly is the syntax and if it can be done in some other efficient way also.
Thanks a lot for all the help which comes my way.
(1) No, you can't use a bind variable for the table name.
(2) When you're using EXECUTE IMMEDIATE, this implies Dynamic SQL - but FORALL requires that only one statement to be executed. As soon as you specify a different table, you're talking about a different statement (regardless of whether the tables' structures happen to be equivalent or not).
You're going to have to do this in an ordinary FOR loop.
Just a guess, but I don't think you can use a bind variable as a table name. Have you tried:
EXECUTE IMMEDIATE 'UPDATE ' || Table_List(i) || ' SET column = :2 WHERE column = :3' ...