A few weeks ago I received a question about how to generate DDL for all the tables, procedures, functions, and packages of a set of schemas. Furthermore, the asker wanted each object’s script to be in a distinct file within a directory hierarchy.
Specifically, given a base directory, each schema should have its own subdirectory and within each of those, each object type would have a subdirectory of its own, and then files would be there for each object.
For example:
/base/SCHEMA1/tables/tab1 /base/SCHEMA1/tables/tab2 /base/SCHEMA1/functions/func1 /base/SCHEMA2/tables/tab1 /base/SCHEMA2/packages/pack1 etc.
In older versions of the database using utl_file_dir init parameter set to “*”, it would be relatively easy. Simply iterate through the objects, concatenate the schema, object type, and object name to create the path, open the file, and write the ddl. However, in 18c and above that parameter no longer exists. Even if it did, using “*” is bad practice as it creates a large security hole. So, while that would have been easier, pursuing that option is not recommended.
Instead, we’ll create a temporary directory object and then reassign its path as needed during iteration. That solves the problem of how to define a path for the UTL_FILE package but leaves another problem of how to create the directory hierarchy within the file system. UTL_FILE does not provide a “mkdir” functionality, nor do any of the other built in packages.
To solve this we’ll use a java stored procedure. This not only provides the needed functionality, it also uses the JVMs native fine-grained security so our final code will only be able to write within the allowed target directory tree. For the purposes here, if the directory already exists, we won’t do anything but we’ll still return a success. It’s not important how the path came into being as long as it exists when we need it.
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "DirectoryTools" AS import java.io.*; public class DirectoryTools{public static boolean CreateDirectory ( String p_path) throws IOException
{
File dir = new File(p_path);
// mkdir will create the directory if all parent directories already exist
// mkdirs will create the directory and all missing parent directories as well
if (dir.exists())
return true;
else
return dir.mkdirs();
}
} /
Then create a pl/sql api for the CreateDirectory method
CREATE OR REPLACE FUNCTION create_directory(p_path IN VARCHAR2) RETURN BOOLEAN IS LANGUAGE JAVA NAME 'DirectoryTools.CreateDirectory( java.lang.String ) return boolean' ; /
That gives a method to create the directory paths as needed for each schema and object type. To generate the scripts, we’ll use DBMS_METADATA. For this exercise just the object itself were needed, no dependent objects or grants required. To iterate through the objects we’ll need a cursor to filter for the desired object types and exclude special cases such as IOT over flows which are tables, but not objects that stand on their own. The last special requirement is the procedure should be able to accept a list of schemas. To accommodate that I’ll use a nested table type collection.
CREATE OR REPLACE TYPE vctab AS TABLE OF VARCHAR2(4000);
Putting all of these elements together we end up with a procedure like this:
CREATE OR REPLACE PROCEDURE extract_schemas(p_basepath IN VARCHAR2, p_schemas IN vctab) AUTHID CURRENT_USER IS v_prev_owner dba_objects.owner%TYPE; v_prev_type dba_objects.object_type%TYPE;v_dir_name dba_directories.directory_name%TYPE := 'TMPDIR_' || TO_CHAR(SYSTIMESTAMP, 'yyyymmdd_hh24missff6'); v_file UTL_FILE.file_type;
BEGIN FOR obj IN ( SELECT owner, object_type, object_name, DBMS_METADATA.get_ddl(object_type, object_name, owner) ddl_text FROM dba_objects WHERE owner MEMBER OF p_schemas AND object_type IN ('TABLE', 'VIEW', 'SEQUENCE', 'PACKAGE', 'PACKAGE BODY', 'PROCEDURE', 'FUNCTION') AND secondary = 'N' AND (object_type != 'TABLE' OR ((owner, object_name) IN (SELECT owner, table_name FROM dba_tables WHERE owner MEMBER OF p_schemas AND nested = 'NO' AND (iot_type IS NULL OR iot_type != 'IOT_OVERFLOW')))) ORDER BY owner, object_type, object_name) LOOP IF v_prev_owner IS NULL OR v_prev_owner != obj.owner OR v_prev_type IS NULL OR v_prev_type != obj.object_type THEN IF (create_directory(p_basepath || '/' || obj.owner || '/' || obj.object_type)) THEN EXECUTE IMMEDIATE 'create or replace directory ' || v_dir_name || ' as ''' || p_basepath || '/' || obj.owner || '/' || obj.object_type || ''''; v_prev_owner := obj.owner; v_prev_type := obj.object_type; ELSE -- Any errors causing the function to fail should be raised -- as java exceptions, so this is not expected to happen. raise_application_error(-20001, 'Failed to create directory path'); END IF; END IF; v_file := UTL_FILE.fopen(v_dir_name, obj.object_name, 'w', 32767; -- If the DDL will be larger then 32k, -- it can be split on new-line boundaries and written in pieces UTL_FILE.put_line(v_file, obj.ddl_text); UTL_FILE.fflush(v_file); UTL_FILE.fclose(v_file); END LOOP;EXECUTE IMMEDIATE 'drop directory ' || v_dir_name;
EXCEPTION WHEN OTHERS THEN EXECUTE IMMEDIATE 'drop directory ' || v_dir_name; RAISE; END; /
An example usage:
BEGIN extract_schemas('/oratmp', vctab('HR', 'SH', 'OE')); END;
This resulted in the following output files:
/oratmp/HR/PROCEDURE/ADD_JOB_HISTORY /oratmp/HR/PROCEDURE/SECURE_DML /oratmp/HR/SEQUENCE/DEPARTMENTS_SEQ /oratmp/HR/SEQUENCE/EMPLOYEES_SEQ /oratmp/HR/SEQUENCE/LOCATIONS_SEQ /oratmp/HR/TABLE/COUNTRIES /oratmp/HR/TABLE/DEPARTMENTS /oratmp/HR/TABLE/EMPLOYEES /oratmp/HR/TABLE/JOB_HISTORY /oratmp/HR/TABLE/JOBS /oratmp/HR/TABLE/LOCATIONS /oratmp/HR/TABLE/REGIONS /oratmp/HR/VIEW/EMP_DETAILS_VIEW /oratmp/OE/FUNCTION/GET_PHONE_NUMBER_F /oratmp/OE/SEQUENCE/ORDERS_SEQ /oratmp/OE/TABLE/CUSTOMERS /oratmp/OE/TABLE/INVENTORIES /oratmp/OE/TABLE/ORDER_ITEMS /oratmp/OE/TABLE/ORDERS /oratmp/OE/TABLE/PRODUCT_DESCRIPTIONS /oratmp/OE/TABLE/PRODUCT_INFORMATION /oratmp/OE/TABLE/PROMOTIONS /oratmp/OE/TABLE/WAREHOUSES /oratmp/OE/VIEW/ACCOUNT_MANAGERS /oratmp/OE/VIEW/BOMBAY_INVENTORY /oratmp/OE/VIEW/CUSTOMERS_VIEW /oratmp/OE/VIEW/OC_CORPORATE_CUSTOMERS /oratmp/OE/VIEW/OC_CUSTOMERS /oratmp/OE/VIEW/OC_INVENTORIES /oratmp/OE/VIEW/OC_ORDERS /oratmp/OE/VIEW/OC_PRODUCT_INFORMATION /oratmp/OE/VIEW/ORDERS_VIEW /oratmp/OE/VIEW/PRODUCT_PRICES /oratmp/OE/VIEW/PRODUCTS /oratmp/OE/VIEW/SYDNEY_INVENTORY /oratmp/OE/VIEW/TORONTO_INVENTORY /oratmp/SH/TABLE/CAL_MONTH_SALES_MV /oratmp/SH/TABLE/CHANNELS /oratmp/SH/TABLE/COSTS /oratmp/SH/TABLE/COUNTRIES /oratmp/SH/TABLE/CUSTOMERS /oratmp/SH/TABLE/FWEEK_PSCAT_SALES_MV /oratmp/SH/TABLE/PRODUCTS /oratmp/SH/TABLE/PROMOTIONS /oratmp/SH/TABLE/SALES /oratmp/SH/TABLE/SALES_TRANSACTIONS_EXT /oratmp/SH/TABLE/SUPPLEMENTARY_DEMOGRAPHICS /oratmp/SH/TABLE/TIMES /oratmp/SH/VIEW/PROFITS
The procedure above, as noted, does not include related objects such as indexes or grants. I also make the assumption the generated DDL will be less than 32K so it will fit within a VARCHAR2 value. If that isn’t true, I’ve noted with a comment where the code should be modified to split a large clob into smaller values consumable by UTL_FILE.
The last thing to consider are permissions. The procedure is defined with AUTHID CURRENT_USER so users that invoke it can write to whichever paths they have grants. Because of the recursive nature the write permission should be granted with the “-” syntax to the invoking user. For my id (SDS) to write to /oratmp and any subdirectories under it, the java grant looks like this:
DECLARE KEYNUM NUMBER; BEGIN SYS.DBMS_JAVA.GRANT_PERMISSION( grantee => 'SDS' ,permission_type => 'SYS:java.io.FilePermission' ,permission_name => '/oratmp/-' ,permission_action => 'write' ,key => KEYNUM ); END; /
The code above could be expanded to a richer functionality set but hopefully provides a solid base to build on. Comments and questions, as always, are welcome.