There’s a hesitation to publish this example, because publishing it may be interpreted as an endorsement of this approach and it certainly isn’t. Over the years there have been plenty of articles describing the long term pain that typically comes from generic data models. Here’s a few to whet your appetite.
https://www.red-gate.com/simple-talk/opinion/opinion-pieces/bad-carma/
https://asktom.oracle.com/pls/asktom/asktom.search?tag=query-on-design
But I’m posting this example because it serves as a nice tutorial for DBMS_SQL, and also, there is perhaps the suggestion that the requestor is moving away from a generic data model to a more well structured one. We’ll go with the benefit of the doubt here
The incoming data for this example was the “classic” generic data model where the table name, column name and column values were not defined in the data dictionary but as values within a table
SQL> CREATE TABLE data_table
2 (
3 row_seq int,
4 table_name VARCHAR2(30),
5 column_name VARCHAR2(30),
6 column_value VARCHAR2(30)
7 );
Table created.
SQL>
SQL>
SQL>
SQL> insert into data_table values ( 1,'TEST_TAB','ORDER_NUMBER','ORD1001');
1 row created.
SQL> insert into data_table values ( 1,'TEST_TAB','CUST_NAME','CUST1001');
1 row created.
SQL> insert into data_table values ( 1,'TEST_TAB','INVOICE_NUMBER','INV001');
1 row created.
SQL> insert into data_table values ( 1,'TEST_TAB','ORDER_DATE','04/11/2018 15:14:00');
1 row created.
SQL> insert into data_table values ( 1,'TEST_TAB','AMOUNT','1001');
1 row created.
SQL>
SQL> insert into data_table values ( 2,'TEST_TAB','ORDER_NUMBER','ORD1002');
1 row created.
SQL> insert into data_table values ( 2,'TEST_TAB','CUST_NAME','CUST1002');
1 row created.
SQL> insert into data_table values ( 2,'TEST_TAB','INVOICE_NUMBER','INV002');
1 row created.
SQL> insert into data_table values ( 2,'TEST_TAB','ORDER_DATE','02/11/2018 15:14:00');
1 row created.
SQL> insert into data_table values ( 2,'TEST_TAB','AMOUNT','1002');
1 row created.
SQL>
SQL> commit;
Commit complete.
SQL> select * from data_table;
ROW_SEQ TABLE_NAME COLUMN_NAME COLUMN_VALUE
---------- ------------------------------ ------------------------------ ------------------------------
1 TEST_TAB ORDER_NUMBER ORD1001
1 TEST_TAB CUST_NAME CUST1001
1 TEST_TAB INVOICE_NUMBER INV001
1 TEST_TAB ORDER_DATE 04/11/2018 15:14:00
1 TEST_TAB AMOUNT 1001
2 TEST_TAB ORDER_NUMBER ORD1002
2 TEST_TAB CUST_NAME CUST1002
2 TEST_TAB INVOICE_NUMBER INV002
2 TEST_TAB ORDER_DATE 02/11/2018 15:14:00
2 TEST_TAB AMOUNT 1002
10 rows selected.
The task here was to take those values and convert into INSERT statements, so with the sample data above, the aim is to insert those rows into a table called TEST_TAB. Given that the DML must generated entirely from metadata, we can use DBMS_SQL to handle it. Constructing the DML is easier than you might think due to some handy analytic SQL functions plus the ever useful LISTAGG.
SQL> select
2 row_number() over
3 ( partition by table_name, row_seq order by column_name ) as seq,
4 count(*) over
5 ( partition by table_name, row_seq ) as col_cnt,
6 listagg(column_name,',') within group
7 ( order by column_name ) over ( partition by table_name, row_seq ) as cols,
8 listagg(':'||column_name,',') within group
9 ( order by column_name ) over ( partition by table_name, row_seq ) as bindcols,
10 column_value
11 from data_table
12 order by table_name, row_seq, column_name
13 @pr
==============================
SEQ : 1
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : 1001
==============================
SEQ : 2
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : CUST1001
==============================
SEQ : 3
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : INV001
==============================
SEQ : 4
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : 04/11/2018 15:14:00
==============================
SEQ : 5
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : ORD1001
==============================
SEQ : 1
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : 1002
==============================
SEQ : 2
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : CUST1002
==============================
SEQ : 3
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : INV002
==============================
SEQ : 4
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : 02/11/2018 15:14:00
==============================
SEQ : 5
COL_CNT : 5
COLS : AMOUNT,CUST_NAME,INVOICE_NUMBER,ORDER_DATE,ORDER_NUMBER
BINDCOLS : :AMOUNT,:CUST_NAME,:INVOICE_NUMBER,:ORDER_DATE,:ORDER_NUMBER
COLUMN_VALUE : ORD1002
Now that we have each data value, plus all the components for an INSERT statement (including binding values not using any literals), we’re good to go:
SQL> create table test_tab (
2 order_number varchar2(10),
3 cust_name varchar2(10),
4 invoice_number varchar2(10),
5 order_date varchar2(30),
6 amount varchar2(10)
7 );
Table created.
SQL> declare
2 l_sql varchar2(32000);
3 l_cur pls_integer := dbms_sql.open_cursor;
4 l_execute pls_integer;
5 begin
6 for i in (
7 select table_name,
8 column_name,
9 row_number() over ( partition by table_name, row_seq order by column_name ) as seq,
10 count(*) over ( partition by table_name, row_seq ) as col_cnt,
11 listagg(column_name,',') within group ( order by column_name ) over ( partition by table_name, row_seq ) as cols,
12 listagg(':'||column_name,',') within group ( order by column_name ) over ( partition by table_name, row_seq ) as bindcols,
13 column_value
14 from data_table
15 order by table_name, row_seq, column_name
16 ) loop
17 if i.seq = 1 then
18 l_sql := 'insert into '||i.table_name||'('||i.cols||') values ('||i.bindcols||')';
19 dbms_sql.parse(l_cur,l_sql,dbms_sql.native);
20 end if;
21 dbms_sql.bind_variable(l_cur,i.column_name,i.column_value);
22 if i.seq = i.col_cnt then
23 l_execute := dbms_sql.execute(l_cur);
24 end if;
25 end loop;
26 dbms_sql.close_cursor(l_cur);
27 end;
28 /
PL/SQL procedure successfully completed.
SQL>
SQL> select * from test_tab;
ORDER_NUMB CUST_NAME INVOICE_NU ORDER_DATE AMOUNT
---------- ---------- ---------- ------------------------------ ----------
ORD1001 CUST1001 INV001 04/11/2018 15:14:00 1001
ORD1002 CUST1002 INV002 02/11/2018 15:14:00 1002
2 rows selected.
SQL>