DDL History in Oracle: A BAU Solution

In this blog post, you’ll learn step-by-step how to set up an Oracle database environment for a demo application showcasing the DDL History feature of BAU.

What is BAU?

Beg APEX Utilities (BAU) is a suite of tools designed to streamline and standardize your Oracle APEX development workflow. The DDL History is just one of the powerful features present in our product!

What is BAU DDL History?

Leveraging a database-level trigger, BAU DDL History automatically keeps track of DDL changes to your database objects. Whenever functions, packages, procedures, triggers, types, or views are modified or dropped, our trigger captures the previous state of the object along with detailed auditing information.

To prevent unwanted changes during development, BAU DDL History trigger also includes an object locking mechanism. BAU Object Lock allows developers to lock specific database objects, ensuring that only selected users can modify them until the lock is released or inactive. This adds an extra layer of safety and coordination for teams working in shared environments.

Now, let's get to work! 🚀


Database setup

Step 1: Connect as SYS on your database

sql /nolog
connect sys/"YourSYSPassword"@your_host/your_service AS SYSDBA
💡
Remember to replace placeholders with your actual credentials.

Step 2: Prepare the database

Execute the following commands to ensure a clean environment for the BAU DDL History Demo:

CREATE USER bau IDENTIFIED BY "YourSecurePassword" DEFAULT TABLESPACE SYSAUX QUOTA UNLIMITED ON SYSAUX;
GRANT CONNECT, RESOURCE TO bau;
GRANT SELECT ON dba_users TO bau;
GRANT SELECT ON dba_objects TO bau;
💡
Remember to replace the password placeholder with a real one.

This creates the BAU user and grants the necessary permissions.

Step 3: Create BAU objects

connect bau/"YourSecurePassword"@your_host/your_service
💡
Remember to replace placeholders with your actual credentials.

Run the following scripts to create the necessary tables, sequences, and triggers:

CREATE SEQUENCE BAU_DDL_HISTORY_ID_SEQ;

CREATE TABLE BAU_DDL_HISTORY (
    ID                INTEGER         DEFAULT BAU_DDL_HISTORY_ID_SEQ.NEXTVAL PRIMARY KEY,
    EVENT             VARCHAR2(20),
    EVENT_TIMESTAMP   TIMESTAMP(6)    DEFAULT CURRENT_TIMESTAMP,
    EVENT_USER        VARCHAR2(256),
    MACHINE           VARCHAR2(256),
    OSUSER            VARCHAR2(256),
    OBJECT_OWNER      VARCHAR2(256),
    OBJECT_TYPE       VARCHAR2(256),
    OBJECT_NAME       VARCHAR2(256),
    OBJECT_DDL        CLOB
);

CREATE SEQUENCE BAU_OBJECT_LOCK_ID_SEQ;

CREATE TABLE BAU_OBJECT_LOCK (
    ID                INTEGER         DEFAULT BAU_OBJECT_LOCK_ID_SEQ.NEXTVAL PRIMARY KEY,
    ACTIVE            CHAR(1)         DEFAULT 'Y' NOT NULL,
    OBJECT_OWNER      VARCHAR2(256)   NOT NULL,
    OBJECT_TYPE       VARCHAR2(256)   NOT NULL,
    OBJECT_NAME       VARCHAR2(256)   NOT NULL,
    LOCKED_BY         VARCHAR2(256)   NOT NULL,
    ALLOWED_SCHEMAS   VARCHAR2(256),
    CREATED           DATE            NOT NULL,
    CREATEDBY         VARCHAR2(256)   NOT NULL,
    UPDATED           DATE,
    UPDATEDBY         VARCHAR2(256),
    CONSTRAINT BAU_OBJECT_LOCK_ACTIVE_CK CHECK (ACTIVE IN ('Y','N')),
    CONSTRAINT BAU_OBJECT_LOCK_UK UNIQUE (OBJECT_OWNER, OBJECT_TYPE, OBJECT_NAME)
);

CREATE OR REPLACE TRIGGER TRG_BAU_OBJECT_LOCK_BIU
BEFORE INSERT OR UPDATE ON BAU_OBJECT_LOCK
FOR EACH ROW
BEGIN
    IF INSERTING THEN
        IF :NEW.ID IS NULL THEN
            :NEW.ID := BAU_OBJECT_LOCK_ID_SEQ.NEXTVAL;
        END IF;

        :NEW.CREATED := SYSDATE;
        :NEW.CREATEDBY := NVL(V('APP_USER'), USER);
    END IF;

    IF UPDATING THEN
        :NEW.UPDATED := SYSDATE;
        :NEW.UPDATEDBY := NVL(V('APP_USER'), USER);
    END IF;
END;
/

This sets up the core tables for tracking DDL events and object locking.

Step 5: Create DDL capture functionality

connect sys/"YourSYSPassword"@your_host/your_service AS SYSDBA
💡
Remember to replace placeholders with your actual credentials.

Create this PL/SQL function to retrieve DDL from database objects:

CREATE OR REPLACE FUNCTION BAU_GET_DDL (
    P_OWNER         VARCHAR2,
    P_OBJECT_TYPE   VARCHAR2,
    P_OBJECT_NAME   VARCHAR2
) RETURN CLOB IS
    v_source CLOB := '';
BEGIN
    -- If object is a VIEW, retrieve its text from DBA_VIEWS
    IF P_OBJECT_TYPE = 'VIEW' THEN
        BEGIN
            SELECT
                TEXT_VC
            INTO
                v_source
            FROM
                DBA_VIEWS
            WHERE
                VIEW_NAME = P_OBJECT_NAME
                AND OWNER = P_OWNER;
        EXCEPTION
        WHEN NO_DATA_FOUND THEN
            NULL;
        END;
    ELSE
        -- For other object types, retrieve source code from DBA_SOURCE
        FOR R IN (
            SELECT
                TEXT
            FROM
                DBA_SOURCE
            WHERE
                NAME = P_OBJECT_NAME
                AND OWNER = P_OWNER
                AND TYPE = P_OBJECT_TYPE
            ORDER BY 
                LINE
        ) LOOP
            -- Append each line of the object's source code to the variable
            v_source := v_source || R.TEXT;
        END LOOP;
    END IF;

    -- Return the complete source code
    RETURN v_source;
END;

Finally, create a database-level trigger to log changes on objects:

CREATE OR REPLACE TRIGGER TRG_BAU_DDL_HISTORY
BEFORE CREATE OR ALTER OR DROP ON DATABASE
WHEN (ORA_DICT_OBJ_TYPE IN ('FUNCTION', 'PACKAGE', 'PACKAGE BODY', 'PROCEDURE', 'TRIGGER', 'TYPE', 'TYPE BODY', 'VIEW'))
DECLARE
    v_machine   VARCHAR2(256);
    v_osuser    VARCHAR2(256);
    v_locked_by VARCHAR2(256);
    v_ddl       CLOB;
    v_ddl_body  CLOB;
    v_event     VARCHAR2(256);

    -- Cursor to verify if the object belongs to an Oracle-maintained user.
    CURSOR c_oracle_maintained_user IS
    SELECT 
        USERNAME 
    FROM 
        DBA_USERS
    WHERE 
        USERNAME = ORA_DICT_OBJ_OWNER
        AND (ORACLE_MAINTAINED = 'Y' OR USERNAME LIKE 'ORDS_%');

    -- Cursor to check if the object is currently locked by BAU.
    CURSOR c_bau_object_locked IS
    SELECT 
        LOCKED_BY 
    FROM 
        BAU.BAU_OBJECT_LOCK
    WHERE 
        OBJECT_OWNER = ORA_DICT_OBJ_OWNER
        AND OBJECT_TYPE = REGEXP_SUBSTR(ORA_DICT_OBJ_TYPE, '^\S+')  -- Matches both PACKAGE and PACKAGE BODY
        AND OBJECT_NAME = ORA_DICT_OBJ_NAME
        AND USER NOT IN (SELECT COLUMN_VALUE FROM TABLE(APEX_STRING.SPLIT(ALLOWED_SCHEMAS, ':')))
        AND ACTIVE = 'Y';

BEGIN
    -- Skip logging for Oracle-maintained objects.
    OPEN c_oracle_maintained_user;
    FETCH c_oracle_maintained_user INTO v_locked_by;
    IF c_oracle_maintained_user%FOUND THEN
        CLOSE c_oracle_maintained_user;
        RETURN;
    END IF;
    CLOSE c_oracle_maintained_user;

    -- Prevent DDL operation if the object is locked by BAU.
    OPEN c_bau_object_locked;
    FETCH c_bau_object_locked INTO v_locked_by;
    IF c_bau_object_locked%FOUND THEN
        CLOSE c_bau_object_locked;
        RAISE_APPLICATION_ERROR(-20001, 'BAU - Object Locked by: ' || v_locked_by);
    END IF;
    CLOSE c_bau_object_locked;

    -- Retrieve current session information.
    SELECT 
        MACHINE, OSUSER
    INTO 
        v_machine, v_osuser
    FROM 
        SYS.V_$SESSION
    WHERE 
        SID = SYS_CONTEXT('USERENV', 'SID');

    -- Clean up any inactive locks upon object drop.
    IF ORA_SYSEVENT = 'DROP' THEN
        DELETE FROM 
            BAU.BAU_OBJECT_LOCK
        WHERE 
            OBJECT_OWNER = ORA_DICT_OBJ_OWNER 
            AND OBJECT_TYPE = REGEXP_SUBSTR(ORA_DICT_OBJ_TYPE, '^\S+')
            AND OBJECT_NAME = ORA_DICT_OBJ_NAME
            AND ACTIVE = 'N';
    END IF;

    -- Fetch current DDL definition of the object, if it already exists.
    v_ddl := BAU_GET_DDL (
        P_OWNER       => ORA_DICT_OBJ_OWNER,
        P_OBJECT_TYPE => ORA_DICT_OBJ_TYPE,
        P_OBJECT_NAME => ORA_DICT_OBJ_NAME
    );

    -- Define CREATE OR REPLACE custom event.
    IF ORA_SYSEVENT = 'CREATE' AND v_ddl IS NOT NULL THEN 
        v_event := 'CREATE OR REPLACE';
    ELSE 
        v_event := ORA_SYSEVENT;
    END IF;

    -- If DROP of a PACKAGE or TYPE, also fetch the BODY.
    IF ORA_SYSEVENT = 'DROP' AND ORA_DICT_OBJ_TYPE IN ('PACKAGE', 'TYPE') THEN
        v_ddl_body := BAU_GET_DDL (
            P_OWNER       => ORA_DICT_OBJ_OWNER,
            P_OBJECT_TYPE => ORA_DICT_OBJ_TYPE || ' BODY',
            P_OBJECT_NAME => ORA_DICT_OBJ_NAME
        );

        IF v_ddl_body IS NOT NULL THEN
            v_ddl := v_ddl || CHR(13) || v_ddl_body;
        END IF;
    END IF;

    -- Insert DDL event into BAU history log, distinguishing CREATE vs CREATE OR REPLACE.
    INSERT INTO BAU.BAU_DDL_HISTORY (
        EVENT,
        EVENT_TIMESTAMP,
        EVENT_USER,
        MACHINE,
        OSUSER,
        OBJECT_OWNER,
        OBJECT_TYPE,
        OBJECT_NAME,
        OBJECT_DDL
    ) VALUES (
        v_event,
        SYSTIMESTAMP,
        USER,
        v_machine,
        v_osuser,
        ORA_DICT_OBJ_OWNER,
        ORA_DICT_OBJ_TYPE,
        ORA_DICT_OBJ_NAME,
        v_ddl
    );
END;

Oracle APEX setup

Step 1: Create a workspace for the BAU schema

First we will need to sign in into your APEX instance internal workspace:

On the internal homescreen, click on the top right Create Workspace green button:

Set Re-use existing schema as Yes and choose BAU as the Schema Name:

Set up the workspace admin, choose a strong password! 🔑

On the internal homescreen, select the Create User option on the right tab Workspace Tasks:

Set up the workspace developer user, choose a strong password! 🔑

Click on the top right Create User green button:

Sign out of the internal workspace:

Sign in into our new BAU workspace, with the developer user:

Step 2: Import and use BAU DDL History Demo application

After signing in on our new workspace, we need to import the BAU DDL History Demo. Under App Builder on the top bar, select the Import option:

Select or drag-and-drop the BAU DDL History Demo file on the file dropzone:

Install Application!

Run Application!

Sign in into BAU DDL History Demo with the BAU workspace developer credentials:

🚀
Now you’re ready to explore one of BAU’s powerful features, improving your Oracle database object management and enhancing your productivity! Perform some CREATE, CREATE OR REPLACE, ALTER or DROP operations on database objects to see BAU’s DDL History feature in action.

Important

1. This feature is intended for use in development or staging environments, where tracking object changes and managing collaborative edits is critical. We do not recommend enabling this feature in production environments due to the overhead of DDL event logging and object locking.
2. If you encounter any unexpected errors while compiling objects — other than the expected lock warning — it’s possible that the DDL History trigger was not created correctly. In that case: Reconnect as SYS, disable or drop the TRG_BAU_DDL_HISTORY and then recreate it carefully ensuring it was copied and pasted completely with no syntax errors.

Contact us!

Explore all the advantages BAU (Beg APEX Utilities) brings to your Oracle APEX development.
To learn more about BAU, schedule a demo, or discuss how our solutions can meet your company’s unique needs, visit our website at www.begcloud.com or reach out directly at <atendimento@begcloud.com>.

0
Subscribe to my newsletter

Read articles from Lázzaro Daudt da Costa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lázzaro Daudt da Costa
Lázzaro Daudt da Costa

Passionate about building solutions with Oracle APEX, Cloud Solutions integrations, AI and efficient PL/SQL development. I share learnings, and insights here to help fellow developers optimize their solutions. 🚀👨‍💻