19.2. DDL Triggers
Oracle allows you to define triggers that will fire when Data Definition Language (DDL) statements are executed. Simply put, DDL is any SQL statement used to create or modify a database object such as a table or an index. Here are some examples of DDL statements:
CREATE TABLE ALTER INDEX DROP TRIGGER
Each of these statements results in the creation, alteration, or removal of a database object.
The syntax for creating these triggers is remarkably similar to that of DML triggers, except that the firing events differ, and they are not applied to individual tables.
The INSTEAD OF CREATE TABLE trigger, described at the end of this section, allows the default behavior of a CREATE TABLE event to be manipulated and is a somewhat idiosyncratic DDL trigger. Not all of the aspects of syntax and usage described in the following subsections apply to this trigger type.
19.2.1. Creating a DDL Trigger
To create (or replace) a DDL trigger, use the syntax shown here:
1 CREATE [OR REPLACE] TRIGGER trigger name 2 {BEFORE | AFTER } {DDL event} ON {DATABASE | SCHEMA} 3 [WHEN (...)] 4 DECLARE 5 Variable declarations 6 BEGIN 7 ... some code... 8 END;
The following table summarizes what is happening in this code:
Line(s) | Description |
---|
1 | Specifies that a trigger is to be created with the name supplied. Specifying OR REPLACE is optional. If the trigger exists, and REPLACE is not specified, then good old Oracle error 4081 will appear stating just that. | 2 | This line has a lot to say. It defines whether the trigger will fire before, after, or instead of the particular DDL event as well as whether it will fire for all operations within the database or just within the current schema. Note that the INSTEAD OF option is available only in Oracle9i Release 1 and higher. | 3 | An optional WHEN clause that allows you to specify logic to avoid unnecessary execution of the trigger. | 4-7 | These lines simply demonstrate the PL/SQL contents of the trigger. |
Here's an example of a somewhat uninformed town crier trigger that announces the creation of all objects:
/* File on web: uninformed_town_crier.sql */ SQL> CREATE OR REPLACE TRIGGER town_crier 2 AFTER CREATE ON SCHEMA 3 BEGIN 4 DBMS_OUTPUT.PUT_LINE('I believe you have created something!'); 5 END; 6 / Trigger created.
SQL> SET SERVEROUTPUT ON SQL> CREATE TABLE a_table 2 (col1 NUMBER); Table created.
SQL> CREATE INDEX an_index ON a_table(col1); Index created.
SQL> CREATE FUNCTION a_function RETURN BOOLEAN AS 2 BEGIN 3 RETURN(TRUE); 4 END; 5 / Function created.
SQL> /*-- flush the DBMS_OUTPUT buffer */ SQL> BEGIN NULL; END; 2 / I believe you have created something! I believe you have created something! I believe you have created something!
PL/SQL procedure successfully completed.
| Text displayed using the DBMS_OUTPUT built-in package within DDL triggers will not display until you successfully execute a PL/SQL block, even if that block does nothing. |
|
Over time, this town crier would be ignored due to a lack of information, always proudly announcing that something had been created but never providing any details. Thankfully, there is a lot more information available to DDL triggers, allowing for a much more nuanced treatment, as shown in this version:
/* File on web: informed_town_crier.sql */ SQL> CREATE OR REPLACE TRIGGER town_crier 2 AFTER CREATE ON SCHEMA 3 BEGIN 4 -- use event attributes to provide more info 5 DBMS_OUTPUT.PUT_LINE('I believe you have created a ' || 6 ORA_DICT_OBJ_TYPE || ' called ' || 7 ORA_DICT_OBJ_NAME); 8 END; 9 / Trigger created.
SQL> SET SERVEROUTPUT ON SQL> CREATE TABLE a_table 2 (col1 NUMBER); Table created.
SQL> CREATE INDEX an_index ON a_table(col1); Index created.
SQL> CREATE FUNCTION a_function RETURN BOOLEAN AS 2 BEGIN 3 RETURN(TRUE); 4 END; 5 / Function created.
SQL> /*-- flush the DBMS_OUTPUT buffer */ SQL> BEGIN NULL; END; / I believe you have created a TABLE called A_TABLE I believe you have created a INDEX called AN_INDEX I believe you have created a FUNCTION called A_FUNCTION
PL/SQL procedure successfully completed.
Much more attention will be paid now that the town crier is more forthcoming. The above examples touch upon two important aspects of DDL triggers: the specific events to which they can be applied and the event attributes available within the triggers.
19.2.2. Available Events
Table 19-1 lists the DDL events for which triggers can be coded. Each event can have a BEFORE and an AFTER trigger.
Table 19-1. Available DDL eventsDDL event | Fires when... |
---|
ALTER | Any database object is altered using the SQL ALTER command | ANALYZE | Any database object is analyzed using the SQL ANALYZE command | ASSOCIATE STATISTICS | Statistics are associated with a database object | AUDIT | Auditing is turned on using the SQL AUDIT command | COMMENT | Comments are applied to a database object | CREATE | Any database object is created using the SQL CREATE command | DDL | Any of the events listed here occur | DISASSOCIATE STATISTICS | Statistics are disassociated from a database object | DROP | Any database object is dropped using the SQL DROP command | GRANT | Privileges are granted using the SQL GRANT command | NOAUDIT | Auditing is turned off using the SQL NOAUDIT command | RENAME | A database object is renamed using the SQL RENAME command | REVOKE | Privileges are revoked using the SQL REVOKE command | TRUNCATE | A table is truncated using the SQL TRUNCATE command |
As with DML triggers, these DDL triggers fire when the event to which they are attached occurs within the specified database or schema. There is no limit to the number of trigger types that can exist in the database or schema.
19.2.3. Available Attributes
Oracle provides a set of functions (defined in the DBMS_STANDARD package) that provide information about what fired the DDL trigger and other information about the trigger state (e.g., the name of the table being dropped). Table 19-2 displays these trigger attribute functions. The following sections offer some examples of usage.
Table 19-2. DDL trigger event and attribute functionsName | Description |
---|
ORA_CLIENT_IP_ADDRESS | Returns the IP address of the client. | ORA_DATABASE_NAME | Returns the name of the database. | ORA_DES_ENCRYPTED_PASSWORD | Returns the DES-encrypted password of the current user. | ORA_DICT_OBJ_NAME | Returns the name of the database object affected by the firing DDL. | ORA_DICT_OBJ_NAME_LIST | Returns the count of objects affected. It also returns a complete list of objects affected in the NAME_LIST parameter, which is a collection of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_DICT_OBJ_OWNER | Returns the owner of the database object affected by the firing DDL. | ORA_DICT_OBJ_OWNER_LIST | Returns the count of objects affected. It also returns a complete list of object owners affected in the NAME_LIST parameter, which is a collection of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_DICT_OBJ_TYPE | Returns the type of database object affected by the firing DDL (e.g., TABLE or INDEX). | ORA_GRANTEE | Returns the count of grantees. The USER_LIST argument contains the full list of grantees, which is a collection of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_INSTANCE_NUM | Returns the number of the database instance. | ORA_IS_ALTER_COLUMN | Returns TRUE if the specified COLUMN_NAME argument is being altered, or FALSE if not. | ORA_IS_CREATING_NESTED_TABLE | Returns TRUE if a nested table is being created, or FALSE if not. | ORA_IS_DROP_COLUMN | Returns TRUE if the specified COLUMN_NAME argument is indeed being dropped, or FALSE if not. | ORA_LOGIN_USER | Returns the name of the Oracle user for which the trigger fired. | ORA_PARTITION_POS | Returns the position in the SQL command where a partitioning clause could be correctly added. | ORA_PRIVILEGE_LIST | Returns the number of privileges being granted or revoked. The PRIVILEGE_LIST argument contains the full list of privileges affected, which is a collection of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_REVOKEE | Returns the count of revokees. The USER_LIST argument contains the full list of revokees, which is a collection of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_SQL_TXT | Returns the number of lines in the SQL statement firing the trigger. The SQL_TXT argument returns each line of the statement, which is an argument of type DBMS_STANDARD.ORA_NAME_LIST_T. | ORA_SYSEVENT | Returns the type of event that caused the DDL trigger to fire (e.g., CREATE, DROP, or ALTER). | ORA_WITH_GRANT_OPTION | Returns TRUE if privileges were granted with the GRANT option, or FALSE if not. |
Note the following about the event and attribute functions:
The datatype ORA_NAME_LIST_T is defined in the DBMS_STANDARD package as: TYPE ora_name_list_t IS TABLE OF VARCHAR2(64);
In other words, this is a nested table of strings, each of which can contain up to 64 characters. The DDL trigger event and attribute functions are also defined in the DBMS_STANDARD package. Oracle creates a standalone function (which adds the "ORA_" prefix to the function name) for each of the packaged functions by executing the $ORACLE_HOME/rdbms/dbmstrig.sql script during database creation. In some releases of Oracle, there are errors in this script that cause the standalone functions to not be visible or executable. If you feel that these elements have not been properly defined, you should ask your DBA to check the script for problems and make the necessary corrections. The USER_SOURCE data dictionary view does not get updated until after both BEFORE and AFTER DDL triggers are fired. In other words, you cannot use these functions to provide a "before and after" version control system built entirely within the database and based on database triggers.
19.2.4. Working with Events and Attributes
The best way to demonstrate the possibilities offered by DDL trigger events and attributes is with a series of examples .
Here is a trigger that prevents any and all database objects from being created:
CREATE OR REPLACE TRIGGER no_create AFTER CREATE ON SCHEMA BEGIN RAISE_APPLICATION_ERROR ( -20000, 'ERROR : Objects cannot be created in the production database.' ); END;
After installing this trigger, attempts at creating anything meet with failure:
SQL> CREATE TABLE demo (col1 NUMBER); * ERROR at line 1: ORA-20000: Objects cannot be created in the production database.
That is a rather terse and uninformative error message. There was a failure, but what failed? Wouldn't it be nice to have a little more information in the error message, such as the object I was attempting to create?
/* File on web: no_create.sql */ CREATE OR REPLACE TRIGGER no_create AFTER CREATE ON SCHEMA BEGIN RAISE_APPLICATION_ERROR (-20000, 'Cannot create the ' || ORA_DICT_OBJ_TYPE || ' named ' || ORA_DICT_OBJ_NAME || ' as requested by ' || ORA_DICT_OBJ_OWNER || ' in production.'); END;
With this trigger installed, an attempt to create my table now offers much more diagnostic information:
SQL> CREATE TABLE demo (col1 NUMBER); * ERROR at line 1: ORA-20000: Cannot create the TABLE named DEMO as requested by SCOTT in production
I could even place this logic within a BEFORE DDL trigger and take advantage of the ORA_SYSEVENT attribute to respond to specific events:
CREATE OR REPLACE TRIGGER no_create BEFORE DDL ON SCHEMA BEGIN IF ORA_SYSEVENT = 'CREATE' THEN RAISE_APPLICATION_ERROR (-20000, 'Cannot create the ' || ORA_DICT_OBJ_TYPE || ' named ' || ORA_DICT_OBJ_NAME || ' as requested by ' || ORA_DICT_OBJ_OWNER); ELSIF ORA_SYSEVENT = 'DROP' THEN -- Logic for DROP operations ... END IF; END;
19.2.4.1 What column did I touch?
I can use the ORA_IS_ALTER_COLUMN function to decipher which column was altered by an ALTER TABLE statement. Here is one example:
/* File on web: preserve_app_cols.sql */ CREATE OR REPLACE TRIGGER preserve_app_cols AFTER ALTER ON SCHEMA DECLARE -- Cursor to get columns in a table CURSOR curs_get_columns (cp_owner VARCHAR2, cp_table VARCHAR2) IS SELECT column_name FROM all_tab_columns WHERE owner = cp_owner AND table_name = cp_table; BEGIN -- if it was a table that was altered... IF ora_dict_obj_type = 'TABLE' THEN -- for every column in the table... FOR v_column_rec IN curs_get_columns ( ora_dict_obj_owner, ora_dict_obj_name ) LOOP -- Is the current column one that was altered? IF ORA_IS_ALTER_COLUMN (v_column_rec.column_name) THEN -- Reject change to "core application" column IF mycheck.is_application_column ( ora_dict_obj_owner, ora_dict_obj_name, v_column_rec.column_name ) THEN RAISE_APPLICATION_ERROR ( 'FAIL', 'Cannot alter core application attributes' ); END IF; -- table/column is core END IF; -- current column was altered END LOOP; -- every column in the table END IF; -- table was altered END;
Attempts to change core application attributes will now be stopped.
Remember that this logic will not work when the trigger is fired for the addition of new columns. That column information is not yet visible in the data dictionary when the DDL trigger fires.
I can check for attempts to drop specific columns as follows:
IF ORA_IS_DROP_COLUMN ('COL2') THEN do something! ELSE do something else! END IF;
| The ORA_IS_DROP_COLUMN and ORA_IS_ALTER_COLUMN functions are blissfully unaware of the table to which the column is attached; they work on column name alone. |
|
19.2.4.2 Lists returned by attribute functions
Some of the attribute functions return two pieces of data: a list of items and a count of items. For example, the ORA_GRANTEE function returns a list and a count of users that were granted a privilege, and the ORA_PRIVILEGE_LIST function returns a list and a count of privileges granted. These two functions are perfect for use in AFTER GRANT triggers. The what_privs.sql file available on the book's web site offers an extended example of how to use both of these functions. Below is just a portion of the total code:
/* File on web: what_privs.sql */ CREATE OR REPLACE TRIGGER what_privs AFTER GRANT ON SCHEMA DECLARE v_grant_type VARCHAR2 (30); v_num_grantees BINARY_INTEGER; v_grantee_list ora_name_list_t; v_num_privs BINARY_INTEGER; v_priv_list ora_name_list_t; BEGIN -- Retrieve information about grant type and then the lists. v_grant_type := ORA_DICT_OBJ_TYPE; v_num_grantees := ORA_GRANTEE (v_grantee_list); v_num_privs := ORA_PRIVILEGE_LIST (v_priv_list);
IF v_grant_type = 'ROLE PRIVILEGE' THEN DBMS_OUTPUT.put_line ( 'The following roles/privileges were granted');
-- For each element in the list, display the privilege. FOR counter IN 1 .. v_num_privs LOOP DBMS_OUTPUT.put_line ('Privilege ' || v_priv_list (counter)); END LOOP;
This trigger is great for detailing what privileges and objects are affected by grant operations, as shown below. In a more sophisticated implementation, you might consider storing this information in database tables so that you have a detailed history of changes that have occurred.
SQL> GRANT DBA TO book WITH ADMIN OPTION; Grant succeeded.
SQL> EXEC DBMS_OUTPUT.PUT_LINE('Flush buffer'); The following roles/privileges were granted Privilege UNLIMITED TABLESPACE Privilege DBA Grant Recipient BOOK Flush buffer
SQL> GRANT SELECT ON x TO system WITH GRANT OPTION; Grant succeeded.
SQL> EXEC DBMS_OUTPUT.PUT_LINE('Flush buffer'); The following object privileges were granted Privilege SELECT On X with grant option Grant Recipient SYSTEM Flush buffer
19.2.5. Dropping the Undroppable
We have shown that one use for DDL triggers is preventing a particular type of DDL on a particular object or type of object. But what if we create a trigger that prevents DROP DDL and then attempt to drop the trigger itself? Will we be left with a trigger that is essentially undroppable? Fortunately, Oracle has thought of this scenario, as you can see here:
SQL> CREATE OR REPLACE TRIGGER undroppable 2 BEFORE DROP ON SCHEMA 3 BEGIN 4 RAISE_APPLICATION_ERROR(-20000,'You cannot drop me! I am invincible!'); 5 END;
SQL> DROP TABLE employee; * ERROR at line 1: ORA-20000: You cannot drop me! I am invincible!
SQL> DROP TRIGGER undroppable; Trigger dropped.
19.2.6. The INSTEAD OF CREATE Trigger
Oracle provides the INSTEAD OF CREATE trigger to allow you to automatically partition a table. To do so, the trigger must trap the SQL statement being executed, insert the partition clause into it, and then execute it using the ORA_SQL_TXT function. The following trigger demonstrates these steps.
CREATE TRIGGER io_create INSTEAD OF CREATE ON DATABASE WHEN (ORA_DICT_OBJ_TYPE = 'TABLE') DECLARE v_sql VARCHAR2 (32767); -- sql to be built v_sql_t ora_name_list_t; -- table of sql BEGIN -- get the SQL statement being executed FOR counter IN 1 .. ora_sql_txt (v_sql_t) LOOP v_sql := v_sql || v_sql_t (counter); END LOOP; -- Determine the partition clause and add it. -- We will call the my_partition function v_sql := SUBSTR (v_sql, 1, ora_partition_pos) || magic_partition_function || SUBSTR (v_sql, ora_partition_pos + 1);
/* Prepend table name with login username. | Replace CRLFs with spaces. | Requires an explicit CREATE ANY TABLE privilege, | unless you switch to AUTHID CURRENT_USER. */ v_sql := REPLACE (UPPER (REPLACE (v_sql, CHR (10), ' ')) , 'CREATE TABLE ' , 'CREATE TABLE ' || ora_login_user || '.' ); -- now execute the SQL EXECUTE IMMEDIATE v_sql; END;
Now tables will be partitioned automatically, as determined by the logic in the my_partition function.
Oracle offers several partitioning options (e.g., range, hash) and logical partitioning choices (e.g., by primary key, by unique key). You must decide which of these you want to utilize in your partitioning function.
If you do not include the WHEN clause shown above, you will find that attempts to create objects that are not tables will fail with this error:
ORA-00604: error occurred at recursive SQL level 1 ORA-30511: invalid DDL operation in system triggers
Further, if you try to create an INSTEAD OF trigger for any other DDL operation besides CREATE, you will receive this compilation error:
ORA-30513: cannot create system triggers of INSTEAD OF type
| INSTEAD OF triggers for DML operations (insert, update, and delete) are addressed later in this chapter. These triggers share some syntax with the INSTEAD OF CREATE trigger for tables, but that is the extent of their similarity, |
|
|
No comments:
Post a Comment