Can execute immediate be used with JSON_TABLE - oracle

I need to convert JSON into data table (key value columns) in Oracle 12c v12.1.0.2
So for example there is a JSON string like
{"ID": 10, "Description": "TestJSON", "status":"New"}
I need this converted to :
Column1 Column2
------------------------------------
ID 10
Description TestJSON
status New
Now my JSON string could change the number of attributes and hence I require to keep the conversion dynamic.
I tried using execute immediate :
set serveroutput on;
declare
sqlsmt VARCHAR2(200);
t3 varchar2(50);
begin
sqlsmt := 'SELECT * '||
'FROM json_table( ( select jsonstr from mytable where ID= 10) , ''$[*]'' '||
'COLUMNS ( :t1 PATH ''$.''|| '':t2'' ))';
execute immediate sqlsmt into t3 using 'desc' , '$.Description' ;
DBMS_OUTPUT.PUT_LINE( 'Output Variable: ' || t3);
END;
However, I get the following error:
ORA-00904: : invalid identifier
ORA-06512: at line 8
00904. 00000 - "%s: invalid identifier"
Please help. I have Oracle 12c V1. But I really need to pull columns dynamically from JSON.

There are a couple of things that can help with dynamic SQL (assuming you really need to use it). The first is to use dbms_output to show the generated statement before you try to execute it; so in your case:
...
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3;
--using 'descr' , '$.Description' ;
DBMS_OUTPUT.PUT_LINE( 'Output Variable: ' || t3);
END;
/
with your code that shows:
SELECT * FROM json_table( ( select jsonstr from mytable where ID= 10) , '$[*]' COLUMNS ( :t1 PATH '$.'|| ':t2' ))
The most obvious issue there is in '$.'|| ':t2', where :t2 shouldn't be in quotes; that isn't causing the error but would stop it being bound to your variable as you expect as it's a literal value. You also have the $. part in that bit and in your variable value, but again it isn't getting that far.
In common with all dynamic SQL, you can only supply values for variables in the using clause. You're trying to pass the column name as a bind variable, which isn't allowed; so it's trying to use :t1 as the output column name, not desc; and :t1 isn't a valid name. (Neither is desc as that's a reserved word - but either gets the same error.) So, you have to concatenate the column name in rather than binding it.
It looks like you would be able to use :t2 for the path though; but you you can't do that either, not as a dynamic SQL restriction but as a SQL/JSON one - if you got that far, with a valid variable value, you'd still get "ORA-40454: path expression not a literal". You have to concatenate the path into the statement too.
Finally the $[*] doesn't allow you to match the Description... which leads to the second hint about dynamic SQL; get a static query working properly first, then make that dynamic.
So putting that together, you could do:
declare
sqlsmt varchar2(200);
t1 varchar2(30) := 'descr';
t2 varchar2(30) := 'Description';
t3 varchar2(50);
begin
sqlsmt := 'SELECT * '||
'FROM json_table( ( select jsonstr from mytable where ID= 10) , ''$'' '||
'COLUMNS ( ' || t1 || ' PATH ''$.' || t2 || '''))';
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3;
dbms_output.put_line( 'Output Variable: ' || t3);
end;
/
which with your example data outputs:
SELECT * FROM json_table( ( select jsonstr from mytable where ID= 10) , '$' COLUMNS ( descr PATH '$.Description'))
Output Variable: TestJSON
It's a bit odd that the only thing you are allowed to pass as a variable, the 10, is hard-coded. But I get this is an experiment.
You could also write the statement as:
select j.*
from mytable t
cross join json_table ( t.jsonstr, '$' columns ( descr path '$.Description' )) j
where t.id = 10;
which you could do dynamically as:
declare
sqlsmt varchar2(200);
id number := 10;
t1 varchar2(30) := 'descr';
t2 varchar2(30) := 'Description';
t3 varchar2(50);
begin
sqlsmt := 'select j.*'
|| ' from mytable t'
|| q'^ cross join json_table ( t.jsonstr, '$' columns ( ^'
|| t1
|| q'^ path '$.^'
|| t2
|| q'^' )) j^'
|| ' where t.id = :id';
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3 using id;
dbms_output.put_line( 'Output Variable: ' || t3);
end;
/
I've used the alternative quoting mechanism to avoid having to double-up the quotes within the statement, but that's optional. With the same data that outputs:
select j.* from mytable t cross join json_table ( t.jsonstr, '$' columns ( descr path '$.Description' )) j where t.id = :id
Output Variable: TestJSON
db<>fiddle

Related

Step out of a query to get where clauses from a separate table, within a stored procedure

I have a procedure which I'm using to output row counts to a .csv file but some of the where clauses I may want to use are contained in a table. How can I use them to create conditions for the counts?
I've attempted using concatenation pipes to select against the table that holds the where clauses but I'm confused about syntax and where they should go and I believe this is where I need the most help.
These are the columns in the table that contains some of the where clauses I ultimately want to use in the procedure.
SCHEMA, DATABASE, FULL_TABLE, DRIVER_TABLE, MAND_JOIN
And the values may be such as:
PROD, DB1, RLTSHP, BOB.R_ID, A.AR_ID = B.AR_ID
The procedure I have written is as follows:
create or replace procedure PROJECT is
--variables
l_dblink varchar2(100) := 'DB1';
ROW_COUNT number;
file_handle UTL_FILE.file_type;
BEGIN
utl_file.put_line(file_handle, 'OWNER,TABLE_NAME,ROW_COUNT');
--main loop
for rws in (select /*+parallel */ owner, table_name
from dba_tables#DB1 a
where table_name in (select table_name
from meta_table
where driver_table is not null
and additional_joins is null)
and a.owner in (select distinct schema
from meta_table c)
order by table_name)
loop
execute immediate 'select count(*) from ' ||rws.owner||'.'||rws.table_name || '#' || l_dblink into ROW_COUNT;
utl_file.put_line(file_handle,
rws.OWNER || ',' ||
rws.TABLE_NAME || ',' ||
ROW_COUNT);
end loop;
END PROJECT;
/
However, instead of the simple select count(*) reflected in the above, I want to find a way to include data in the meta_table to construct "where" clauses that use table joins to limit the output so that I'm not counting all rows, but rows that meet the criteria in the join I've constructed.
For example, so that the actual count that gets executed will be something like this:
select count(*)
from PROD.RLTSHP#DB1 b,
BOB.R_ID#DB1 a
where A.AR_ID = B.AR_ID;
Essentially I would be constructing the query using the entries in the meta_table. I think I can do this with concat's / pipes but I'm not sure exactly how to.
Can you help?
You need to extend your simple statement to assemble the join criteria as well. The one catch is that you must give the tables aliases which match the aliases used in additional_joins i.e. B for FULL and A for DRIVER. These have to be standard for all rows in your META_TABLE otherwise you will generate invalid SQL.
create or replace procedure PROJECT is
l_dblink varchar2(100) := 'DB1';
ROW_COUNT number;
file_handle UTL_FILE.file_type;
v_sql varchar2(32767);
BEGIN
utl_file.put_line(file_handle, 'OWNER,TABLE_NAME,ROW_COUNT');
<< main_loop >>
for rws in (select mt.*
from dba_tables#DB1 db
join meta_table mt
on mt.driver_table = db.table_name
and mt.owner = db.owner
where mt.db_link = l_dblink
order by mt.table_name)
loop
-- simple query
v_sql := 'select count(*) from ' || rws.owner||'.'||rws.driver_table || '#' || l_dblink;
-- join query
if rws.additional_joins is not null
and rws.full_table is not null then
v_sql := v_sql|| ' b, '|| rws.full_table ||'#'||l_dblink|| ' a where ' ||rws.additional_joins;
end if;
-- uncomment this for debugging
--dbms_output.put_line(v_sql);
execute immediate v_sql into ROW_COUNT;
utl_file.put(file_handle,
rws.OWNER || ',' ||
rws.TABLE_NAME || ',' ||
utl_file.put_line(file_handle, ROW_COUNT);
end loop main_loop;
END PROJECT;
/
Notes
We have to use a variable to assemble the statement because the final SQL is conditional on the contents of a row. This enables efficient debugging because we have something we can display. Dynamic SQL is hard, because it turns compilation errors into runtime errors. Diagnosis is difficult when we can't see the actual executed code.
I have tweaked your driving query to make the joins safer.
The column names you used in the code are not consist with the column names you used for the table structure. So there may be naming bugs which you'll need to fix for yourself.
I have retained the Old Skool implicit join syntax. I was tempted to generate ANSI 92 SQL (inner join ... on) but it's not clear that the additional_joins will contain only join criteria.
Pro tip. Instead of commenting your loops - --main loop - use an actual PL/SQL label - <<main_loop>> so you can link the matching end loop statement, as I have done in this code.
Improvements you may want to add:
validate that FULL_TABLE exists in target database
include FULL_TABLE in UTL_FILE output
validate that columns referenced in ADDITIONAL_JOIN are valid (using DBA_TAB_COLUMNS, but it's trickier because you will have to parse the column names from the text)
worry about whether the content of ADDITIONAL_JOIN is a valid and complete join condition
First of all I don't recommend to use PARALLEL hint. It can kill your db if you will have a lot of queries with PARALLEL hints.
I assume that columns MAND_JOIN means, that always we have value there.
create or replace procedure PROJECT is
lc_sql_template CONSTANT varchar2(4000) :=
'select count(*) ' || CHR(10) ||
' from #TableOwner.#TableName#DB1 b' || CHR(10) ||
' inner join #FullTableName#DB1 a ON #JoinCodition';
lv_row_count number;
lv_file_handle UTL_FILE.file_type;
lv_sql varchar2(32767);
BEGIN
utl_file.put_line(lv_file_handle, 'OWNER,TABLE_NAME,ROW_COUNT');
for rws in (select mt.*
from dba_tables#DB1 db
inner join meta_table mt
on mt.driver_table = db.table_name
and mt.owner = db.owner
where mt.driver_table is not null
and mt.additional_joins is null
order by mt.table_name)
loop
lv_sql := lc_sql_template;
lv_sql := replace(lv_sql, '#TableOwner' , rws.owner);
lv_sql := replace(lv_sql, '#TableName' , rws.driver_table);
lv_sql := replace(lv_sql, '#FullTableName' , rws.full_table);
lv_sql := replace(lv_sql, '#JoinCodition' , rws.mand_join);
$if $$DevMode = true $then -- I even recommand to log this all the time
your_log_package.info(lv_sql);
$end
execute immediate lv_sql into lv_row_count;
utl_file.put(lv_file_handle, rws.OWNER || ',' || rws.TABLE_NAME || ',' || lv_row_count);
end loop main_loop;
exception
when others then
your_log_package.error(lv_sql);
raise;
end PROJECT;

How to return a record set from a Function in Oracle

I am trying to get the record data using function by passing values
please find the below
CREATE TABLE "TEST"
( "TEST_ID" NUMBER(9,0) NOT NULL ENABLE,
"TEST_DESC" VARCHAR2(30 BYTE),
"TEST_DATE" DATE
);
create or replace TYPE TEST_OBJ_TYPE IS OBJECT
(
TEST_ID NUMBER(9),
TEST_DESC VARCHAR(30),
dates date
);
create or replace TYPE TEST_TABTYPE AS TABLE OF TEST_OBJ_TYPE;
Using the above object and table type created the function as follows
create or replace FUNCTION GET_ROWS(dates date)RETURN TEST_TABTYPE
AS
V_Test_Tabtype Test_TabType;
table_name varchar2(30);
q1 varchar2(300);
BEGIN
table_name :='Test';
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC)FROM' || '
(SELECT TEST_ID, TEST_DESC FROM ' || table_name || ' where
TEST_DATE = '''||dates||''' ) A';
dbms_output.put_line(q1);
EXECUTE IMMEDIATE q1 BULK COLLECT INTO V_Test_TabType ;
RETURN V_Test_TabType;
EXCEPTION
WHEN OTHERS THEN
v_Test_TabType.DELETE;
RETURN v_Test_TabType;
END;
When I execute this the SQL is printing correctly but not giving the record value.
Error as follows:
select (GET_ROWS('01-08-18')) from dual
Error report -
ORA-02315: incorrect number of arguments for default constructor
ORA-06512: at "AMTEL_MIS.GET_ROWS", line 13
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM (SELECT TEST_ID, TEST_DESC FROM Test where TEST_DATE = '01-08-18' ) A
Please assist me further
Thanks in advance
Your type TEST_OBJ_TYPE is defined with three attributes: TEST_ID, TEST_DESC, DATES. However, your query populates the constructor with just two columns:
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM
You're missing a value for DATES and that's why Oracle hurls ORA-02315.
I have tried as per your suggestion but it's is giving me an error
ORA-00904: "A"."DATES": invalid identifier
Because of the convoluted way your function is written you need to include TEST_DATE (or dates) in both the subquery and the object constructor:
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC,A.TEST_DATE)FROM' || ' -- here!
(SELECT TEST_ID, TEST_DESC, TEST_DATE FROM ' -- and here!
|| table_name || ' where TEST_DATE = '''||dates||''' ) A';
If you do that your code will work. Here is a LiveSQL demo of your code with the fix. (Free Oracle login required).
As it seems likely that you will want to pass in the table name so here is a version of your code which does that:
create or replace function get_rows(dates date, p_table_name in varchar2)
return test_tabtype
as
v_test_tabtype test_tabtype;
q1 varchar2(300);
begin
q1 := 'select test_obj_type(a.test_id, a.test_desc,a.test_date) from'
|| '(select test_id, test_desc, test_date from '
|| p_table_name
|| ' where test_date = :1 ) a';
dbms_output.put_line(q1);
execute immediate q1
bulk collect into v_test_tabtype
using dates ;
return v_test_tabtype;
exception
when others then
v_test_tabtype.delete;
return v_test_tabtype;
end;
Note how much easier it is to understand the code when it is laid out with consistent use of case and regular indentation. Readability is a feature!

Dynamic Function- Oracle

I am trying to create a function by giving 2 variables as inputs. These 2 variables are used dynamically in the function. Am using a select statement in the beginning of the loop to find the primary key column in a particular table. This column is assign to a variable value1. Am using this value1 variable as a sequence variable.
create FUNCTION Test(schemaname in varchar2, tablename in varchar2)
return number
IS cnpParmId NUMBER;
good VARCHAR(1) := 'F';
exist VARCHAR(1) := 'F';
value1 varchar2(500);
begin
good := 'F';
exist := 'F';
loop
SELECT cols.column_name into value1
FROM all_constraints cons, all_cons_columns cols
WHERE cols.TABLE_NAME= 'tablename'
And cols.OWNER='schemaname'
And cons.constraint_type = 'P'
AND cons.constraint_name = cols.constraint_name
AND cons.owner = cols.owner
ORDER BY cols.table_name, cols.position;
select schemaname.value1_seq.nextval into cnpParmId from dual;
begin
select 'T' into good from dual
where cnpParmId not in
(select value1 from schemaname.tablename);
exception when NO_DATA_FOUND then good := 'F';
end;
exit when good = 'T';
end loop;
return cnpParmId;
end;
/
Test(XYZ,ABC);
but am getting the following errors:
Error(21,11): PLS-00487: Invalid reference to variable 'SCHEMANAME'
Error(21,22): PL/SQL: ORA-02289: sequence does not exist
Error(23,7): PL/SQL: SQL Statement ignored
Error(25,38): PL/SQL: ORA-00942: table or view does not exist
When you introduce variables as table or column names you need to do a dynamic query using EXECUTE IMMEDIATE.
Your sequence query should be:
execute immediate 'select ' || schemaname || '.nextval from dual' into cnpParmId;
And your "get the return value" query should be:
execute immediate
'select ''T'' from dual where cnpParmId not in ' ||
'(select value1 from ' || schemaname || '.' || tablename || ')' into good;
Also note that your query to get value1 is looking for the literal values schemaname and tablename:
SELECT cols.column_name into value1
FROM all_constraints cons, all_cons_columns cols
WHERE cols.TABLE_NAME= 'tablename'
And cols.OWNER='schemaname'
... and so on
You can use variables to represent values, so just get rid of the single quotes around the variable names:
SELECT cols.column_name into value1
FROM all_constraints cons, all_cons_columns cols
WHERE cols.TABLE_NAME= tablename
And cols.OWNER=schemaname
... and so on

Alter Table Add Column in PL SQL loop

I am trying to insert columns in a table using a for loop that iterates over a cursor. The code is:
declare
cursor Months_ET is
SELECT distinct to_char(FEE_CD_ACT_SUM_ACCTG_DA, 'MON-YY') as "Month_U"
FROM EXPORT_TABLE
WHERE EXPORT_TABLE.FEE_CD_ACT_SUM_ACCTG_DA >= to_date('10/01/2013','mm/dd/yyyy')
AND EXPORT_TABLE.FEE_CD_ACT_SUM_ACCTG_DA < to_date('10/01/2014', 'mm/dd/yyyy');
n integer := 1;
begin
for mon in Months_ET loop
dbms_output.put_line(mon."Month_U");
execute immediate 'Alter table "Fee_CT" add('|| mon."Month_U" ||' varchar(20))';
n := n +1;
end loop;
end;
The cursor in the beginning jsut gets a list of unique month names which the dbms_output.put_line prints out as:
SEP-14
AUG-14
JUL-14
So I know the variable is not empty.
So using those results I want to add three columns for each month- yr. However I get an invalid datatype error. I have also tried altering to the for loop to concatenate the table name outside of the quotes like this:
for mon in Months_ET loop
--Month_List(n) := mon."Month_U";
dbms_output.put_line(mon."Month_U");
execute immediate 'Alter table' ||"Fee_CT" || 'add('|| mon."Month_U" ||' varchar(20))';
n := n +1;
But I get a message that "Table,View Or Sequence reference 'Fee_CT' not allowed in this context." Not sure what I am doing wrong. The actual data is much larger and covers a wider time frame so using multiple alter table statements isn't realistic. plus the underlying data will be changing, so I need to be able to change the column names with the underlying data.
Your table name and column names use non-standard characters - lower case letters, dashes. This is a really bad idea, because it means having to wrap every reference in double-quotes. Every person who has to use your schema will curse you whenever they have to fix a PLS-00357, ORA-00903 or ORA-00904 exception because they forgot to double-quote an identifier. Look, it's even caught you out :)
But if you really want to persist, the statement you need is:
execute immediate 'Alter table "Fee_CT" add("'|| mon."Month_U" ||"' varchar(20))';
The table name is part of the boilerplate text not a variable. You need to wrap the non-standard column name in double-quotes. Make sure the boilerplate has spaces around the key-words.
Above all, remember that a syntax error in dynamic SQL throws a runtime error, not a compilation error. Use logging or DBMS_OUTPUT to review the assembled statements.
Not sure why you want to create columns dynamically.But it is possible though:
Errors :
1.Column names can only have '_'(underscore),no other special character. ie.,AUG-15 --> AUG_15
When using Alias names for further processing use SUBQUERY (Month_U )
Quotes should be properly used.
space between keywords/variable in execute statement.
create table Table_A
(id integer,
date1 date
);
-- Table created.
begin
insert into table_A values (1,trunc(sysdate) );
insert into table_A values (2,trunc(sysdate+100) );
end;
select to_char(date1, 'MON-YY') as "Month_U" from table_A;
--AUG-15
--DEC-15
Declare
cursor months_ET is select month_u from
( select to_char(date1, 'MON_YY') AS Month_U from table_A) DUAL;
sql_stmnt varchar2(400) ;
table_name varchar2(20) := 'Table_A';
column_name varchar2(20) := 'New_col1';
data_type varchar2(20) := 'date' ; -- you can change to varchar2
Begin
FOR MON in months_ET
LOOP
sql_stmnt := ' alter table ' || table_name || ' add( ' || MON.MONTH_U
|| ' ' || data_type || ' ) ' ;
dbms_output.put_line(sql_stmnt );
Execute immediate sql_stmnt ;
END LOOP;
End;
OUTPUT:
alter table Table_A add( AUG_15 date )
alter table Table_A add( DEC_15 date )
Table altered.
Always use a DBMS_OUTPUT.PUT_LINE to test your execute immediate statement.
Give space between keywords/variables.
Use single quotes
Now Check this Example:
create table Table_A(id integer);
-- Table created.
Declare
sql_stmnt varchar2(400) ;
table_name varchar2(20) := 'Table_A';
column_name varchar2(20) := 'New_col1';
data_type varchar2(20) := 'varchar2(20)' ;
Begin
sql_stmnt := ' alter table ' || table_name || ' add( ' || Column_name || ' ' || data_type || ' ) ' ;
execute immediate sql_stmnt ;
dbms_output.put_line(sql_stmnt );
End;
-- alter table Table_A add( New_col1 varchar2(20) )
-- Table altered.
Desc Table_A;
Column Data Type Length Precision Scale
ID NUMBER 22 - 0 - - -
NEW_COL1 VARCHAR2 20 - - - - -

How to trim all columns in all rows in all tables of type string?

In Oracle 10g, is there a way to do the following in PL/SQL?
for each table in database
for each row in table
for each column in row
if column is of type 'varchar2'
column = trim(column)
Thanks!
Of course, doing large-scale dynamic updates is potentially dangerous and time-consuming. But here's how you can generate the commands you want. This is for a single schema, and will just build the commands and output them. You could copy them into a script and review them before running. Or, you could change dbms_output.put_line( ... ) to EXECUTE IMMEDIATE ... to have this script execute all the statements as they are generated.
SET SERVEROUTPUT ON
BEGIN
FOR c IN
(SELECT t.table_name, c.column_name
FROM user_tables t, user_tab_columns c
WHERE c.table_name = t.table_name
AND data_type='VARCHAR2')
LOOP
dbms_output.put_line(
'UPDATE '||c.table_name||
' SET '||c.column_name||' = TRIM('||c.column_name||') WHERE '||
c.column_name||' <> TRIM('||c.column_name||') OR ('||
c.column_name||' IS NOT NULL AND TRIM('||c.column_name||') IS NULL)'
);
END LOOP;
END;
Presumably you want to do this for every column in a schema, not in the database. Trying to do this to the dictionary tables would be a bad idea...
declare
v_schema varchar2(30) := 'YOUR_SCHEMA_NAME';
cursor cur_tables (p_schema_name varchar2) is
select owner, table_name, column_name
from all_tables at,
inner join all_tab_columns atc
on at.owner = atc.owner
and at.table_name = atc.table_name
where atc.data_type = 'VARCHAR2'
and at.owner = p_schema;
begin
for r_table in cur_tables loop
execute immediate 'update ' || r.owner || '.' || r.table_name
|| ' set ' || r.column_name || ' = trim(' || r.column_name ||');';
end loop;
end;
This will only work for fields that are VARCHAR2s in the first place. If your database contains CHAR fields, then you're out of luck, because CHAR fields are always padded to their maximum length.

Resources