Dynamic query building yields no results and crashes process - sql

I am trying to run a SQL query within a PL/SQL script that takes dynamic parameters and should return a specific result. For that I tried the EXECUTE IMMEDIATE command. Unfortunately it crashes always my variable is a varchar. If it is a number instead it runs perfetcly.
Here is my tried code:
plsql_request := 'SELECT :x FROM TEST_TABLE WHERE ID = :y';
EXECUTE IMMEDIATE plsql_request INTO plsql_result USING variable_1, '2837123 | hsiae';
While testing it crashes always in this constelation.
Second approch:
plsql_request := 'SELECT ' || variable_1 || ' FROM TEST_TABLE WHERE ID = ''' || variable_2 || '''';
EXECUTE IMMEDIATE plsql_request ;
I do not get any results at all, hence I think the query-building using dynamic variables in combination with varchars fails.
Any help apreciated,
Filip.

You cannot use a bind variable for an object literal like a column name, table name, etc.. so you must use the second method with || variable_1 || to splice in your dynamic column name. However, you should use real bind variables :y, USING, etc.. for your predicate value in order to not generate an excessive number of child cursors.
So, :
plsql_request := 'SELECT '||variable_1||' FROM TEST_TABLE WHERE ID = :y';
EXECUTE IMMEDIATE plsql_request INTO plsql_result USING '2837123 | hsiae';

Related

Oracle - BULK COLLECT INTO VARRAY used with Bind Variables only collecting column headers

Quick Disclaimer:
First thing out of the way, I know the preferred way of handling dynamic SQL in Oracle now is the DBMS_SQL package but unfortunately my application team does not have the grants to execute these procs at the moment and I am hoping to get this quick workaround knocked out before our DBA team gets back to me. Also, this database is on Oracle 12c.
Script Goal: I recently developed a Stored Proc (let's call it Original) that uses values in a "control table" to make a large number of updates to certain columns in a database with many schemas and tables. This script I am struggling with now (let's call it Test) is meant to be a quick loop through those columns affected by Original so as to verify that everything worked expectedly. Ultimately, I want to output the top 5 results of each changed column and hand a spooled file to my testing team for validation.
The control_table used in both scripts has 4 columns and looks like this:
OWNER
TABLE_NAME
COLUMN_NAME
ALGORITHM
Schema1
TableA
ColumnA
Method1
Schema1
TableB
ColumnB
Method1
Schema2
TableC
ColumnC
Method2
An example of one of the tables that gets updated by Original (let's say for TableA above) would be:
OtherCol1
OtherCol2
ColumnA
OtherCol3
Ignored
Ignored
UpdatedData1
Ignored
Ignored
Ignored
UpdatedData2
Ignored
Ignored
Ignored
UpdatedData3
Ignored
Issue with Test script: I have the dynamic SQL - I believe - working as it needs and I have been trying to figure out how best to print the results of the EXECUTE IMMEDIATE command to output. In doing some reading, I found that BULK COLLECT INTO should allow me to store the results of the dynamic queries into a COLLECTION which I can then print with dbms_output. I have attempted to do this with both a TABLE and a VARRAY but in both cases when I print, I am finding that the data stored in my collection is the column header of my dynamic query instead of the query values! The only thing I can think that could be the problem is the combining of BULK COLLECT INTO with the USING command when I run the dynamic statement but I have seen nothing in the documentation to indicate that these two commands are incompatible and my Test procedure below compiles without issue (and even seems to run ok).
Test Script:
SET SERVEROUTPUT ON SIZE UNLIMITED;
DECLARE
l_script VARCHAR2(500);
l_errm VARCHAR2(64);
TYPE results IS VARRAY(5) OF VARCHAR2(250);
va_cols results; --Defining here with a VARRAY but I have also tried with a table
BEGIN
FOR c_col IN(
SELECT owner, table_name, column_name, algorithm FROM control_list)
LOOP
l_errm := NULL;
va_cols := NULL;
BEGIN
dbms_output.put_line('Column '|| c_col.column_name || ' of table ' || c_col.owner ||
'.' || c_col.table_name || ' used algorithm ' || c_col.algorithm);
l_script := 'SELECT :1 FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY';
dbms_output.put_line('Script sent to Exec Immediate: ' || l_script); --Print l_script for debugging
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols USING c_col.column_name, c_col.column_name;
dbms_output.put_line(va_cols(1));
dbms_output.put_line(va_cols(2));
dbms_output.put_line(va_cols(3));
dbms_output.put_line(va_cols(4));
dbms_output.put_line(va_cols(5));
EXCEPTION
WHEN OTHERS THEN
l_errm := SUBSTR(SQLERRM, 1, 64);
dbms_output.put_line(' ERROR: ' || l_errm || '. Skipping row');
CONTINUE;
END;
END LOOP;
END;
/
So my intended dbms_output of the script above is:
Column ColumnA of table Schema1.TableA used algorithm Method1
Script sent to Exec Immediate: SELECT :1 FROM SCHEMA1.TABLEA WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY
UpdatedData1
UpdatedData2
UpdatedData3
UpdatedData4
UpdatedData5
Instead, however, bizarrely, what I am getting when I run this is:
Column ColumnA of table Schema1.TableA used algorithm Method1
Script sent to Exec Immediate: SELECT :1 FROM SCHEMA1.TABLEA WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY
ColumnA
ColumnA
ColumnA
ColumnA
ColumnA
Has anyone seen this before and know what I am doing wrong? Thanks in advance!!
You can't use bind variables to change what columns you're referencing. You use bind variables to specify particular values at runtime. When you do
l_script := 'SELECT :1 FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE :2 IS NOT NULL FETCH FIRST 5 ROWS ONLY';
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols USING c_col.column_name, c_col.column_name;
you're telling Oracle that you want to select the literal string in the variable c_col.column_name. Not the column in the table by that name. Which is why every row returns that literal value.
You'd need to dynamically assemble the SQL statement with the column names, not try to use them as bind variables. So something like
l_script := 'SELECT ' || c_col.column_name ||
' FROM ' || c_col.owner || '.' || c_col.table_name ||
' WHERE ' || c_col.column_name || ' IS NOT NULL FETCH FIRST 5 ROWS ONLY';
EXECUTE IMMEDIATE l_script BULK COLLECT INTO va_cols;
This is approximately what you want. I outer cursor over tables and column to inspect that generate the dynamic SQL.
Inner loop reading the column values from the previous query
DECLARE
TYPE CurTyp IS REF CURSOR;
v_cursor CurTyp;
v_value VARCHAR2(200);
v_stmt_str VARCHAR2(200);
BEGIN
FOR c IN (
SELECT table_name, column_name FROM control_list)
LOOP
dbms_output.put_line('tab: '||c.table_name);
v_stmt_str := 'SELECT '||c.column_name||' FROM '|| c.table_name;
OPEN v_cursor FOR v_stmt_str;
LOOP
FETCH v_cursor INTO v_value;
EXIT WHEN v_cursor%NOTFOUND;
dbms_output.put_line('col: '||c.column_name||' val: '||v_value);
END LOOP;
END LOOP;
CLOSE v_cursor;
END;
/

How to convert every Table from a specific User to JSON Format using the "trick" provided by SQL Developer

I want to convert all tables from a specific user to JSON (or XML) Format. I've read about a "trick" mentioned by SQL Developer.
In other words, I already started to create a Procedure with two parameters:
p_format: The format (in my case it will be "json")
p_user: The username
As IDE I use Oracle SQL Developer and my database is an Oracle XE Database.
At first the procedure loops though all tables of the given user and in the loop, it should execute the following:
SELECT /*p_format*/ * FROM p_user || '.' || table
Unfortunately, I cannot use this SELECT Statement as mentioned above. I need to use the command EXECUTE IMMEDIATE <Statement>.
The next problem I faced was the following: I wanted to output the result of the EXECUTE IMMEDIATE command. Therefore I used the command EXECUTE IMMEDIATE <Statement> INTO <Variable>. After compiling the procedure and executing it, I stumpled across the following Error:
"inconsistent datatypes: expected %s got %s"
This is my code of the procedure:
CREATE OR REPLACE PROCEDURE EXPORT_TABLE_TO_FORMAT_FROM(p_format VARCHAR2, p_user VARCHAR2) IS
/***************************************************************************
Author:
Class:
School:
Date:
Function - EXPORT_TABLE_TO_JSON_FROM(p_user):
Displays the data of every table from a given User as JSON
Parameter: p_user ... User
***************************************************************************/
v_tableData VARCHAR2(32767);
v_sqlStatement VARCHAR2(200);
BEGIN
FOR tablerec IN (SELECT *
FROM ALL_TABLES
WHERE OWNER = p_user)
LOOP
v_sqlStatement := 'SELECT /*' || p_format || '*/ * FROM ' || p_user || '.' || tablerec.TABLE_NAME;
EXECUTE IMMEDIATE v_sqlStatement INTO v_tableData;
DBMS_OUTPUT.PUT_LINE (v_sqlStatement);
END LOOP;
END;
You can see that I loop though all tables of a given user and created a sql statement with p_format and p_user and with tablerec.TABLE_NAME.
The desired result should look exactly like that:
{"results":[{"columns":[{"name":"COUNTRY_ID","type":"CHAR"},
{"name":"COUNTRY_NAME","type":"VARCHAR2"},{"name":"REGION_ID","type":"NUMBER"}],"items":
[
{"country_id":"AR","country_name":"Argentina","region_id":2},
{"country_id":"AU","country_name":"Australia","region_id":3},
{"country_id":"BE","country_name":"Belgium","region_id":1},
{"country_id":"BR","country_name":"Brazil","region_id":2},
{"country_id":"CA","country_name":"Canada","region_id":2},
{"country_id":"CH","country_name":"Switzerland","region_id":1},
{"country_id":"CN","country_name":"China","region_id":3},
{"country_id":"DE","country_name":"Germany","region_id":1},
{"country_id":"DK","country_name":"Denmark","region_id":1},
{"country_id":"EG","country_name":"Egypt","region_id":4},
{"country_id":"FR","country_name":"France","region_id":1},
{"country_id":"IL","country_name":"Israel","region_id":4},
{"country_id":"IN","country_name":"India","region_id":3},
{"country_id":"IT","country_name":"Italy","region_id":1},
{"country_id":"JP","country_name":"Japan","region_id":3},
{"country_id":"KW","country_name":"Kuwait","region_id":4},
{"country_id":"ML","country_name":"Malaysia","region_id":3},
{"country_id":"MX","country_name":"Mexico","region_id":2},
{"country_id":"NG","country_name":"Nigeria","region_id":4},
{"country_id":"NL","country_name":"Netherlands","region_id":1},
{"country_id":"SG","country_name":"Singapore","region_id":3},
{"country_id":"UK","country_name":"United Kingdom","region_id":1},
{"country_id":"US","country_name":"United States of America","region_id":2},
{"country_id":"ZM","country_name":"Zambia","region_id":4},
{"country_id":"ZW","country_name":"Zimbabwe","region_id":4}]}]}
The JSON hint is specific to SQL Developer and SQLcl, not the database directly. So you need to run the entire thing within these tools.
Easiest way to do that is to have your script write a script that you can then run, eg
spool /tmp/get_all_json.sql
select 'select /*json*/ * from '||table_name||';'
from user_tables;
spool off
#/tmp/get_all_json.sql

Dynamic SQL with variable amount of parameters

I know the following works (this would be part of the code inside a stored procedure):
sql_string := 'INSERT INTO my_table (data_id, data_col) SELECT SYS_GUID(),''{"json_id1":'' || (:value_1 - val_from_view_1 + :i - 1) || ''}'' FROM my_view WHERE col_1 <= :value_2';
FOR i IN 1..N LOOP
EXECUTE IMMEDIATE sql_string
USING IN OUT input_value_1, i, input_value_2;
END LOOP;
My question is, since that sql_string comes as an input parameter to my stored procedure, how do I deal with a variable number of parameters for the USING clause? That is, how do I cover the above case, a case where the sql_string is such that I have something like this:
EXECUTE IMMEDIATE sql_string
USING IN OUT input_value_1, i, input_value_2, ..., input_value_N;
and everything in between?
PS: I'm not married to EXECUTE IMMEDIATE, so if there's a better way to do this, feel free to suggest it.

Executing SQL via Oracle Stored Procedure passing variables

I created the following Stored Procedure in Oracle:
create or replace
PROCEDURE APPUSERCT
(
PROJNAME IN VARCHAR2
, WHEREDATE IN VARCHAR2 ,
cnt OUT long
) AS
BEGIN
select count(Distinct UPPER(field1)) into cnt from bs_log where application_name=PROJNAME and field1 is not null and log_type='info' || WHEREDATE;
END APPUSERCT;
In my php page WHEREDATE is set as:
$whereDate=" and (TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')>= TO_DATE('".$startDate."','mm/dd/yyyy') AND TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')<= TO_DATE('".$endDate."','mm/dd/yyyy'))";
I then Bind the parameters and values and call the SP:
$sql = 'BEGIN APPUSERCT(:projName,:whereDate,:cnt); END;';
$result = oci_parse($dbconn, $sql);
oci_bind_by_name($result,':cnt',$totalRowCount,32);
oci_bind_by_name($result,':projName',$projName,32);
oci_bind_by_name($result,':whereDate',$whereDate,200);
oci_execute($result);
If no date range is entered on the php page by the user, then the WHEREDATE is blank in the SP and the SQL runs without any range or any additional syntax appended to the end of it in the Store Procedure (SP).
When the user enters a date range on the php page then the WHEREDATE param becomes:
and (TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')>=TO_DATE('05/01/2015','mm/dd/yyyy') AND TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')<=TO_DATE('05/07/2015','mm/dd/yyyy'))
and it is appended to the end of the SQL in the SP. But whenever this happens my php page always returns 0 count from the SQL executed. No error, just 0 count.
If I try running the SQL directly in Oracle it's:
select count(Distinct UPPER(field1)) as cnt from bs_log where application_name='Myweather' and field1 is not null and log_type='info' and (TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')>= TO_DATE('05/01/2015','mm/dd/yyyy') AND TO_DATE(created_on,'mm/dd/yyyy HH24:MI:SS')<= TO_DATE('05/07/2015','mm/dd/yyyy'))
and I get results. I get a count back. But when calling it through the procedure I get 0. Can anyone see why?
Thanks!
Well, a couple of problems show up.
First, I suggest that you change the declaration of the cnt parameter from LONG to NUMBER. LONG is not a numeric type in Oracle; instead, it's a type of LOB whose use is deprecated. If you really want to return a LOB use either BLOB or CLOB, whichever is appropriate.
Second, you can't pass in a character string containing part of a WHERE clause (the WHEREDATE parameter) and concatenate it to the end of an SQL statement. In this case you were contatenating the text in WHEREDATE to the string literal info, which I suspect is not what you had in mind. You should probably be using dynamic SQL, in a manner similar to the following:
create or replace PROCEDURE APPUSERCT(PROJNAME IN VARCHAR2,
WHEREDATE IN VARCHAR2,
cnt OUT NUMBER)
AS
strSql_text VARCHAR2(32767) := 'select count(Distinct UPPER(field1)) ' ||
' from bs_log where application_name=''' || PROJNAME ||
''' and field1 is not null and ' ||
' log_type=''info'' ' || WHEREDATE;
csr SYS_REFCURSOR;
BEGIN
OPEN csr FOR strSql_text;
FETCH csr INTO cnt;
CLOSE csr;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('APPUSERCT - exception : ' || SQLCODE || ' (' ||
SQLERRM || ')');
RAISE;
END APPUSERCT;
So you build up the SQL statement as a text string, including your addition to the WHERE clause, then use the OPEN statement to open a cursor for that statement. Then the cursor is fetched, placing the result into cnt, then the cursor is closed, and finally the routine exits. I've included a default EXCEPTION handler as well - always a good idea in any routine.

dynamic sql selection in nested select

Let's say that I have a tables tasks, projects, and work_items, all of which have a column fields containing a json object of custom values.
Now lets say I want to write a function to query an arbitrary table for its field names.
CREATE OR REPLACE FUNCTION getFieldNames(varchar) RETURNS varchar[] AS
$BODY$
DECLARE
fieldNames varchar[];
BEGIN
fieldNames := ARRAY(SELECT DISTINCT fieldName FROM
(EXECUTE 'SELECT json_object_keys(fields) AS fieldName FROM '
|| quote_ident($1)
) AS derivedFields
);
RETURN fieldNames;
END
$BODY$
LANGUAGE 'plpgsql';
This however errors out with:
ERROR: syntax error at or near "'SELECT json_object_keys(fields) AS fieldName FROM'"
LINE 8: (EXECUTE 'SELECT json_object_keys(fields) AS fieldNa..
The Nested select itself is sound as I verified by replacing the execute with
(SELECT json_object_keys(fields) AS fieldName
FROM tasks
)
and receiving correct results.
What is wrong with my code?
The EXECUTE statement does not return a relation that you can use as a sub-query. Instead, if it returns anything at all, it populates a variable or a single row through the INTO clause. The latter obviously does not match your requirement so you are stuck with the first. A more elegant solution is to move the EXECUTE statement outwards:
EXECUTE
'SELECT array_agg(DISTINCT fields) FROM ' ||
'(SELECT json_object_keys(fields) AS fields FROM ' || quote_ident($1) || ') AS x'
INTO fieldNames;
The worst issue is wrong position of EXECUTE statement. It is a PLpgSQL statement and then it cannot be placed in any SQL expression.