Creating a dynamic script from dynamic SQL - oracle

I've searched a lot of Q-A's but I haven't found quite what I'm looking for. I'm looking to create a procedure that generates a transport script (but does NOT execute it) for two different lookup tables in a dynamic way.
I've been able to create the sql select statement using:
DECLARE
testertable VARCHAR2(320) := 'LOOKUP_TABLE';
testerid VARCHAR2(320) := '999';
testerfield VARCHAR2(320) := 'id_no';
type nameArray IS TABLE OF VARCHAR2(60);
type typeArray IS TABLE OF VARCHAR2(60);
data_nameA nameArray := nameArray();
data_typeA typeArray := typeArray();
i NUMBER;
CURSOR c_curse IS SELECT column_name, data_type
FROM user_tab_columns
WHERE table_name = testertable;
i := 0;
FOR n IN c_curse LOOP
i := i + 1;
data_nameA.EXTEND(1);
data_typeA.EXTEND(1);
data_nameA(i) := n.column_name;
data_typeA(i) := n.data_type;
END LOOP;
From there, I'm able to create the select statement. Then I want to use that statement to open a query, and populate insert statements, using data_typeA(i) as a reference to make sure quotes are input as needed.
This is part of a larger transport function that I'm not allowed to modify, my task is to expand it to include these two lookup tables, which may or may not exist for each transported system.
Hopeful output would be a CLOB or other text field like this:
-- transport for lookup 'LOOKUP_TABLE'
Insert into LOOKUP_TABLE VALUES (value1, 'value2', value3), (value4, 'value5', value6), (value7, 'value8', value9);
Thank you.

Related

Create insert record dynamically by changing pk of existing record for passed in table

I want to pass a table name and schema into a procedure, and have it generate insert, update and delete statements for the particular table. This is part of an automated testing solution (in a development environment) in which I need to test some change data capture. I want to make this dynamic as it is going to be need to be done for lots of different tables over a long period of time, and I need to call it via a REST request through ORDS, so don't want to have to make an endpoint for every table.
Update and delete are fairly easy, however I am struggling with the insert statement. Some of the tables being passed in have hundreds of columns with various constraints, fks etc. so I think it makes sense to just manipulate an existing record by changing only the primary key. I need to be able to modify the primary key to a new value known to me beforehand (e.g. '-1').
Ideally I would create a dynamic rowtype, and select into where rownum = 1, then loop round the primary keys found from all_constraints, and update the rowtype.pk with my new value, before inserting this into the table. Essentially the same as this but without knowing the table in advance.
e.g. rough idea
PROCEDURE manipulate_records(p_owner in varchar2, p_table in varchar2)
IS
cursor c_pk is
select column_name
from all_cons_columns
where owner = p_owner
and constraint_name in (select constraint_name
from all_constraints
where table_name = p_table
and constraint_type = 'P');
l_row tbl_passed_in%ROWTYPE --(I know this isn't possible but ideally)
BEGIN
-- dynamic sql or refcursor to collect a record
select * into tbl_passed_in from tablename where rownum = 1;
-- now loop through pks and reassign their values to my known value
for i in c_pk loop
...if matches then reassign;
...
end loop;
-- now insert the record into the table passed in
END manipulate_records;
I have searched around but haven't found any examples which fit this exact use case, where an unknown column needs to be modified and insert into a table.
Depending on how complex your procedure is, you might be able to store it as a template in a CLOB. Then pull it in, replace table and owner, then compile it.
DECLARE
prc_Template VARCHAR2(4000);
vc_Owner VARCHAR2(0008);
vc_Table VARCHAR2(0008);
BEGIN
vc_Table := 'DUAL';
vc_Owner := 'SYS';
-- Pull code into prc_Template from CLOB, but this demonstrates the concept
prc_Template := 'CREATE OR REPLACE PROCEDURE xyz AS r_Dual <Owner>.<Table>%ROWTYPE; BEGIN NULL; END;';
prc_Template := REPLACE(prc_Template,'<Owner>',vc_Owner);
prc_Template := REPLACE(prc_Template,'<Table>',vc_Table);
-- Create the procedure
EXECUTE IMMEDIATE prc_Template;
END;
Then you have the appropriate ROWTYPE available:
CREATE OR REPLACE PROCEDURE xyz AS r_Dual SYS.DUAL%ROWTYPE; BEGIN NULL; END;
But you can't create the procedure and run it in the same code block.

DBMS_SQL.EXECUTE not giving output when the SQL have DBMS_XMLGEN.GETXML

I am generating a dynamic SQL which generates XML data, using SELECT DBMS_XMLGEN.GETXML, as the output have special characters,
which are not supported in XML. I tried and tested SQL in separately, It is working as expected.
I am not able to fetch the data, when I call it using Dynamic SQL.
I have used DBMS_SQL.DESC_TAB2 as it is treating whole SQL starting DBMS_XMLGEN.GETXML as one column.
--Snippet
Declaration
SelectCursorId NUMBER; --For Dynamic SQL binding
RowProcessed INTEGER; --For Dynamic SQL binding
MstrSeqNbr NUMBER := 1;
ColumnCount NUMBER; --For Dynamic SQL binding
RecordSqrNbr NUMBER := 0; --For Dynamic SQL binding
ColumnDescTbl DBMS_SQL.DESC_TAB2; --dbms_sql.desc_tab2
ColumnValue VARCHAR2(4000); --For Dynamic SQL binding
DymanicSQLCols VARCHAR2(4000); -- For debugging purpose, columns returned
SelectSQL VARCHAR2(6000);
BEGIN
--Snippet
SelectSQL := 'SELECT DBMS_XMLGEN.GETXML('SELECT MRQ.BatchNBR AS Batch_NUMBER,
MRQ.BatchRUNSEQNBR AS Batch_RUN_INSTANCE,
MRQ.BatchRUNDATE AS RUN_DATE,
MRQ.SPDATE AS POST_DATE,
MRQ.BatchNAME AS Batch_NAME,
MRQ.RPTNAME AS DATABASE_NAME,
MRQ.EFFDATE AS RUN_TIME,
MRQ.BatchSTARTDATE AS ELAPSED_TIME ,
CURSOR ( SELECT MRI.MSTREPORTRECSEQNBR AS RECORD_SEQUENCE_NUMBER,
MRI.RTTEXT1VC100 AS COUNTRY_NAME,
MRI.RTTEXT2VC100 AS CURRENCY_USED,
MRI.RTTEXT1VC50 AS COUNTRY_SHORT_CODE,
MRI.RTNUM1P0 AS ISO_CURRENCY_CODE
FROM MasterReporting MRI
WHERE BatchNbr = MR.BatchNbr
AND BatchRUNSEQNBR = MR.BatchRUNINSTANCE
ORDER BY MSTREPORTRECSEQNBR )Record
FROM BatchRUNHIST MR , MastRptSeqDtl MRQ
WHERE MR.BatchNbr = 100
AND MR.BatchRUNINSTANCE IN( 67)
AND MRQ.BatchRUNSEQNBR = MR.BatchRUNINSTANCE
AND MR.BatchRunStatCD =''COMPL''
')
FROM DUAL ';
SelectCursorId := DBMS_SQL.OPEN_CURSOR; --Pass
DBMS_SQL.PARSE ( SelectCursorId, SelectSQL, DBMS_SQL.NATIVE); --Pass
DBMS_SQL.DESCRIBE_COLUMNS2( SelectCursorId, ColumnCount, ColumnDescTbl); --Pass
** However ColumnDescTbl(1).col_name is only giving following, not sure if this is the issue
DBMS_XMLGEN.GETXML('SELECT MRQ.BatchNBRASBatch_NUMBER, MRQ.BatchRUNSEQNBRASBatch_RUN_INSTANCE, MRQ.BatchRUNDATEASRUN_DATE, MRQ.SPDATEASPOST_DATE, MRQ.BatchNAMEASBatch_NAME, MRQ.RPTNAMEASDATABAS
Next step also passes
For k in 1..ColumnCount LOOP
DBMS_SQL.DEFINE_COLUMN(SelectCursorId, k, ColumnValue, 4000); --Pass
END LOOP;
RowProcessed := DBMS_SQL.EXECUTE(SelectCursorId);
--Passes but gives 0 as output,
--Whereas, running the SQL separately gives you one row of XML data.
Minimum One row of XML Data should be returned.
From the documentation for dbms_sql.execute:
The return value is only valid for INSERT, UPDATE, and DELETE statements; for other types of statements, including DDL, the return value is undefined and must be ignored.
As your statement is a select the RowProcessed return value has no meaning; it isn't significant that you're seeing zero there.
You need to do a fetch_rows step after that, or change it to execute_and_fetch. Either way you then need to process the fetched data, of course.
(The col_name looks like a non-issue, it's just an automatically generated name/alias. If you change your dynamic statement to do ...) AS my_col_alias FROM DUAL then the col_name will be reported as MY_COL_ALIAS.)

Insert into nested table plsql

I'm a newbie with pl sql and I'm facing some problems with inserting into nested tables (I'm using these just to test a procedure).
So my code is:
insert into t_prenotazioni
(nro_cliente, data_disponibilita)
values
(righe.nro_cliente, v_data_disponibilita);
where t_prenotazioni is a table of a type defined by me, righe.nro_cliente is a value that I get from a cursor and v_data_disponibilita is a variable.
The error that I get is:
PLS-00330 invalid use of type name or subtype
You are probably trying to do something like:
declare
type type_prenotazioni is record(nro_cliente number, data_disponibilita date);
type prenotazioni is table of type_prenotazioni;
vPrenotazioni prenotazioni;
begin
vPrenotazioni := new prenotazioni();
vPrenotazioni.extend(1);
vPrenotazioni(1).nro_cliente := 10;
vPrenotazioni(1).data_disponibilita := sysdate;
--
for i in vPrenotazioni.first .. vPrenotazioni.last loop
dbms_output.put_line(vPrenotazioni(i).nro_cliente || ' - ' ||
to_char(vPrenotazioni(i).data_disponibilita, 'dd/mm/yyyy')
);
end loop;
end;
I would stronlgy recommend having a look at the Oracle documentation to improve your knowledge; this is only a simple, small example, but there are many many different things you may want to do.

Re-using bind variables in Oracle PL/SQL

I have a hefty SQL statement with unions where code keeps getting re-used. I was hoping to find out if there is a way to re-use a single bind variable without repeating the variable to for "USING" multiple times.
The code below returns "not all variables bound" until I change the "USING" line to "USING VAR1,VAR2,VAR1;"
I was hoping to avoid that as I'm referring to :1 in both instances - any ideas?
declare
var1 number :=1;
var2 number :=2;
begin
execute immediate '
select * from user_objects
where
rownum = :1
OR rownum = :2
OR rownum = :1 '
using var1,var2;
end;
/
EDIT: For additional info, I am using dynamic SQL as I also generate a bundle of where conditions.
I'm not great with SQL arrays (I am using a cursor in my code but I think that will overcomplicate the issue) but the pseudocode is:
v_where varchar2(100) :='';
FOR i in ('CAT','HAT','MAT') LOOP
v_where := v_where || ' OR OBJECT_NAME LIKE ''%' || i.string ||'%''
END;
v_where := ltrim(v_where, ' OR');
And then modifying the SQL above to something like :
execute immediate '
select * from user_objects
where
rownum = :1
OR rownum = :2
OR rownum = :1 AND ('||V_WHERE||')'
using var1,var2;
There are some options you might consider, although they may require changes, either to how you execute your SQL statement or to your SQL statement itself.
Use DBMS_SQL instead of EXECUTE IMMEDIATE -- DBMS_SQL (see http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_sql.htm) is harder to use than EXECUTE IMMEDIATE, but gives you more control over the process -- including the ability (through DBMS_SQL.BIND_VARIABLE and DBMS_SQL.BIND_ARRAY) to bind by name instead of by position.
Use EXECUTE IMMEDIATE with a WITH clause -- You might be able restructure your query to use WITH clause that gathers your bind variables in subquery at the beginning, and then joins to the subquery (instead of referencing the bind variables directly) whenever it needs them. It might look something like this
with your_parameters as
(select :1 as p1, :2 as p2 from dual)
select *
from your_table, your_parameters
where your_table.some_column1 = your_parameters.p1
and your_table.some_column2 <= your_parameters.p1
and your_table.some_column3 = your_parameters.p2
This could affect the performance of your query, but it might be an acceptable compromise.
Don't use dynamic SQL -- Of course, if you don't need dynamic SQL, you don't need to use EXECUTE IMMEDIATE, so the "bind only by position" limitiation does not apply. Are you sure you really need to use dynamic SQL?
EDIT: If you're using dynamic SQL because you have a variable number of OR conditions like you posted in your edit, you might be able to avoid using dynamic SQL by doing one of the following:
If the OR criteria come from a table (or query) -- Join to that table (or query) instead of using a list of OR criteria. For example, if CAT, HAT, and MAT are listed in a column named YOUR_CRITERIA in a table named YOUR_CRITERIA_TABLE you might add YOUR_CRITERIA_TABLE to the FROM clause and replace the OBJECT_NAME LIKE '%CAT% OR OBJECT_NAME LIKE '%MAT% OR OBJECT_NAME LIKE '%HAT% OR OBJECT_NAME LIKE '%MAT% in the WHERE clause with something like OBJECT_NAME LIKE '%' || YOUR_CRITERIA_TABLE.YOUR_CRITERIA || '%'.
Otherwise, you might put the criteria in a global temporary table -- If your criteria don't come from a table (or query), you could (once, at design time, not at run time) create a global temporary table to hold them, and then at run time, insert the criteria into the global temporary table and then join to it as described in item 1.
Or, you might put the criteria in an nested table -- This is like item 2, except uses a nested table (one created using CREATE TYPE...IS TABLE OF) instead of a global temporary table. You could create or own nested table type, or use a built-in one like SYS.ODCIVARCHAR2LIST. In PL/SQL, you would populate an variable of this type, and then use it like a "real" table like in item 1.
An example of item 3 might look something like:
DECLARE
tblCriteria SYS.ODCIVARCHAR2LIST;
BEGIN
tblCriteria := SYS.ODCIVARCHAR2LIST();
-- In "real" code you might populate the nested table in a loop.
-- This example populates it explicitly so that it will compile. For the
-- purpose of the example, we could have populated the nested table in
-- a single statement:
-- tblCriteria := SYS.ODCIVARCHAR2LIST('CAT', 'HAT', 'MAT');
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'CAT';
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'HAT';
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'MAT';
FOR rec IN
(
SELECT
USER_OBJECTS.*
FROM
USER_OBJECTS,
TABLE(tblCriteria) YOUR_NESTED_TABLE
WHERE
USER_OBJECTS.OBJECT_NAME LIKE '%' || YOUR_NESTED_TABLE.COLUMN_VALUE || '%'
)
LOOP
-- Do something. For example, print out the object name.
DBMS_OUTPUT.PUT_LINE(rec.OBJECT_NAME);
END LOOP;
END;
No, unfortunately, the bind variables for EXECUTE IMMEDIATE must be provided in the same order they appear in the statement, and the bind variable names are ignored. So you'll just have to have :1, :2 and :3 in your statement.

oracle: efficient way to configure columns in an output report

I'm designing an HTML report that effectively extracts columns from a single table
The number of columns in this table is quite large, and I would like some way to configure the application to say which columns to display. Note: This is not a per-user setting.
Lets say I have the main table:
MAIN_TABLE
id
first_name
last_name
weight
height
attribute_4
attribute_5
attribute_6
...
attribute_99
I was thinking of a table like
MAIN_TABLE_DISPLAY
column_name
display
Or perhaps
MAIN_TABLE_DISPLAY
display_id
display_first_name
display_last_name
display_weight
display_height
display_attribute_4
display_attribute_5
display_attribute_6
...
display_attribute_99
But I would like to perform an efficient join.
Any suggestions?
Thanks.
Dynamic column inclusion/exclusion == dynamic SQL.
This solution might give you some ideas.
http://tkyte.blogspot.com/2006/01/i-like-online-communities.html - he passes a ref_cursor to a function that returns a CLOB that is fully formatted HTML table with full resultset of that ref_cursor. All in less than 100 lines.
Have you thought about using a view? Your application can pull it's data from there, and to change which columns you're showing, change the view. You would have to change the presentation side of things to account for the different columns also.
As jva said, you'll need to use dynamic SQL. Something like this (with the appropriate bug fixes) should do it:
type column_table is table of varchar2(30);
function table_as_html(table_name varchar2, columns column_table) return clob is
sql_query varchar2(32767);
sql_cursor sql_refcursor;
html_row clob;
html_text clob;
begin
sql_query := 'select ''<tr>';
for column in 1 .. columns.count loop
sql_query := sql_query||'||''<td>''||'||columns(column)||'||''</td>'''
end loop;
sql_query := sql_query||'||''</tr>'' from '||table_name;
open sql_cursor for sql_query;
loop
fetch sql_cursor into html_row;
exit when sql_cursor%notfound;
html_text := html_text||html_row||chr(10);
end loop;
close sql_cursor;
return html_text;
end;

Resources