Dynamically select columns for json_table - sql

I have this query:
DECLARE
rc sys_refcursor;
j_keys varchar2(2000);
query_s varchar2(20000);
BEGIN
j_keys := '(
SELECT
listagg(distinct k.COLUMN_VALUE || '' varchar(256) PATH ''$.'' || k.COLUMN_VALUE, '', '') as j_cols
FROM (select json_response as json_value from SOME_TABLE where param=''some_param'') t
CROSS APPLY JSON_TABLE(
t.json_value,
''$[*]''
COLUMNS (
idx FOR ORDINALITY,
json_obj VARCHAR2(4000) FORMAT JSON PATH ''$''
)
) jt
CROSS APPLY get_keys( jt.json_obj ) k
)';
query_s := 'SELECT * FROM json_table((select json_response from SOME_TABLE where param=''some_param''), ''$[*]''
COLUMNS
' || j_keys || ')';
open rc for query_s;
dbms_sql.return_result(rc);
END;
It's a nasty query, meant to test the possibility of dynamically selecting columns for the json_table (and then parse any json-string in the selected clob - named json_response in SOME_TABLE)
Not entirely sure my syntax is set correct, but currently it complains about:
ORA-00904: invalid identifier
on line 22 (the "open rc for '...' line)

You want to run the first query rather than creating a string literal containing the text of the query and then put the output from the first query into the second query string:
DECLARE
rc sys_refcursor;
j_keys varchar2(2000);
query_s varchar2(20000);
BEGIN
SELECT listagg(
k.COLUMN_VALUE || ' varchar(256) PATH ''$.' || k.COLUMN_VALUE || '''',
','
)
INTO j_keys
FROM ( SELECT JSON_QUERY( json_value, '$[1]' RETURNING CLOB) AS json_obj
FROM table_name
)t
CROSS APPLY get_keys( t.json_obj ) k;
query_s := 'SELECT jt.*
FROM table_name t
CROSS APPLY JSON_TABLE(
t.json_value,
''$[*]''
COLUMNS
' || j_keys || ') jt';
open rc for query_s;
DECLARE
col1 VARCHAR2(50);
col2 VARCHAR2(50);
col3 VARCHAR2(50);
BEGIN
LOOP
FETCH rc INTO col1, col2, col3;
EXIT WHEN rc%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( col1 || ', ' || col2 || ', ' || col3 );
END LOOP;
END;
-- or
-- dbms_sql.return_result(rc);
END;
/
Which, given this setup:
CREATE TABLE table_name (
id NUMBER
GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
json_value CLOB
CHECK( json_value IS JSON )
);
INSERT INTO table_name ( json_value ) VALUES (
'[{"column1":"value1","column2":"value2","column3":"value3"},
{"column1":"value4","column2":"value5","column3":"value6"},
{"column3":"value9","column1":"value7","column2":"value8"}]'
);
CREATE FUNCTION get_keys(
value IN CLOB
) RETURN SYS.ODCIVARCHAR2LIST PIPELINED
IS
js JSON_OBJECT_T := JSON_OBJECT_T( value );
keys JSON_KEY_LIST;
BEGIN
keys := js.get_keys();
FOR i in 1 .. keys.COUNT LOOP
PIPE ROW ( keys(i) );
END LOOP;
END;
/
CREATE FUNCTION get_value(
value IN CLOB,
path IN VARCHAR2
) RETURN VARCHAR2
IS
js JSON_OBJECT_T := JSON_OBJECT_T( value );
BEGIN
RETURN js.get_string( path );
END;
/
Outputs:
value1, value2, value3
value4, value5, value6
value7, value8, value9
db<>fiddle here

Related

Passing column name as parameter in Oracle PL_SQL function

I am trying to pass two input parameters, column name and table name . Then the function should output a string value as defined below :
create or replace function get_id5(in_col_name IN VARCHAR2,in_tbl_name IN VARCHAR2)
return VARCHAR2
is
/*in_col_nm varchar(32) := in_col_name;
/*in_tbl_nm varchar(64) := in_tbl_name; */
integer_part NUMBER ;
integer_part_str VARCHAR2(32) ;
string_part VARCHAR2(32) ;
full_id VARCHAR2(32) ;
out_id VARCHAR(32) ;
BEGIN
/*select MAX(in_col_nm) INTO full_id FROM in_tbl_nm ; */
execute immediate 'select MAX('||in_col_name||') FROM' || in_tbl_name ||'INTO' || full_id;
/*select regexp_replace(full_id , '[^0-9]', '') INTO integer_part_str , regexp_replace(full_id , '[^a-z and ^A-Z]', '') INTO string_part from dual ; */
integer_part_str := regexp_replace(full_id , '[^0-9]', '') ;
string_part := regexp_replace(full_id , '[^a-z and ^A-Z]', '') ;
integer_part := TO_NUMBER(integer_part_str);
integer_part := integer_part + 1 ;
integer_part_str := TO_CHAR(integer_part) ;
out_id := string_part + integer_part_str ;
return out_id;
END;
I have a table named BRANDS in Database and the max value for column BRAND_ID is 'Brand05'. The expected output is 'Brand06'.
However when i run:
select get_id5('BRAND_ID' , 'BRANDS') from dual;
or
DECLARE
a VARCHAR(32) ;
BEGIN
a := get_id5('BRAND_ID' , 'BRANDS');
END;
I am getting the below error:
ORA-00923: FROM keyword not found where expected
You need spaces between the FROM and the table_name and the INTO should be outside of the SQL text:
execute immediate 'select MAX('||in_col_name||') FROM ' || in_tbl_name INTO full_id;
You could also use DBMS_ASSERT:
execute immediate 'select MAX('||DBMS_ASSERT.SIMPLE_SQL_NAME(in_col_name)||') FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(in_tbl_name) INTO full_id;
Then you need to use || as the string concatenation operator (rather than +).
Your function could be:
create or replace function get_id5(
in_col_name IN VARCHAR2,
in_tbl_name IN VARCHAR2
) RETURN VARCHAR2
IS
full_id VARCHAR2(32);
BEGIN
execute immediate 'select MAX('||DBMS_ASSERT.SIMPLE_SQL_NAME(in_col_name)||') '
|| 'FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(in_tbl_name)
INTO full_id;
return regexp_replace(full_id , '\d+', '')
|| TO_CHAR(regexp_replace(full_id , '\D+', '') + 1);
END;
/
For the sample data:
CREATE TABLE brands (brand_id) AS
SELECT 'BRAND5' FROM DUAL;
Then:
select get_id5('BRAND_ID' , 'BRANDS') AS id from dual;
Outputs:
ID
BRAND6
db<>fiddle here
A better solution
To generate the number, use a SEQUENCE or, from Oracle 12, an IDENTITY column and, if you must have a string prefix then use a virtual column to generate it.
CREATE TABLE brands(
ID NUMBER
GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
brand_id VARCHAR2(10)
GENERATED ALWAYS
AS (CAST('BRAND' || TO_CHAR(id, 'FM000') AS VARCHAR2(10)))
);
BEGIN
INSERT INTO brands (id) VALUES (DEFAULT);
INSERT INTO brands (id) VALUES (DEFAULT);
INSERT INTO brands (id) VALUES (DEFAULT);
END;
/
SELECT * FROM brands;
Outputs:
ID
BRAND_ID
1
BRAND001
2
BRAND002
3
BRAND003
db<>fiddle here

How to display column name and column comment, when try DML

I'm try use DML operations on table, when insert or update. I need to show column name and column comment when the operation failed. For example code:
CREATE TABLE test_test(col1 VARCHAR2(10), col2 VARCHAR2(100) not null);
DECLARE
ex_insert_null EXCEPTION;
PRAGMA EXCEPTION_INIT(ex_insert_null, -1400);
ex_value_too_large EXCEPTION;
PRAGMA EXCEPTION_INIT(ex_value_too_large, -12899);
BEGIN
INSERT INTO test_test
(col1
,col2)
SELECT CASE
WHEN LEVEL = 8 THEN
(LEVEL + 1) || 'qqqqqqqqqqqq'
ELSE
(LEVEL + 2) || 'qqq'
END AS col1
,CASE
WHEN LEVEL = 7 THEN
NULL
ELSE
(LEVEL + 3) || 'wwwwwww'
END AS col2
FROM dual
CONNECT BY LEVEL <= 10;
COMMIT;
EXCEPTION
WHEN ex_insert_null THEN
ROLLBACK;
dbms_output.put_line('ex_insert_null at ' || ' ' /* || column_name || ' ' || column_comment */);
WHEN ex_value_too_large THEN
ROLLBACK;
dbms_output.put_line('ex_value_too_large at ' || ' ' /* || column_name || ' ' || column_comment */);
END;
/
As APC has pointed out, you could use "existing Oracle exceptions" eg if you had something like ...
procedure insert_( col1 varchar2, col2 varchar2 )
is
v_errorcode varchar2(64) ;
v_errormsg varchar2(128) ;
begin
insert into t ( c1, c2 ) values ( col1, col2 ) ;
exception
when others then
if sqlcode = -1400 or sqlcode = -12899 then
v_errorcode := sqlcode;
v_errormsg := substr( sqlerrm, 1, 128 );
dbms_output.put_line( v_errorcode || ' ' || v_errormsg ) ;
raise;
end if;
end insert_ ;
... you could get error messages such as these:
-1400 ORA-01400: cannot insert NULL into ("MYSCHEMA"."T"."C2")
-12899 ORA-12899: value too large for column "MYSCHEMA"."T"."C1" (actual: 13, maximum: 10)
If this is enough information for you, fine. However, you also want to see the COMMENTS for the columns. Although we could get the column names from the SQLERRM strings, it may be more reliable to use user-defined exceptions (as you have hinted).
As a starting point, the following DDL and PACKAGE code may be of use for you. ( see also: dbfiddle here )
Tables:
drop table t cascade constraints ;
drop table errorlog cascade constraints ;
create table t (
c1 varchar2(10)
, c2 varchar2(64) not null
) ;
comment on column t.c1 is 'this is the column comment for c1';
comment on column t.c2 is 'this is the column comment for c2';
create table errorlog (
when_ timestamp
, msg varchar2(4000)
) ;
Package spec
create or replace package P is
-- insert into T, throwing exceptions
procedure insert_( col1 varchar2, col2 varchar2 );
-- use your example SELECT, call the insert_ procedure
procedure insert_test ;
-- retrieve the column comments from user_col_comments
function fetch_comment( table_ varchar2, col_ varchar2 ) return varchar2 ;
end P ;
/
Package body
create or replace package body P is
procedure insert_( col1 varchar2, col2 varchar2 )
is
ex_value_too_large exception ; -- T.c1: varchar2(10)
ex_insert_null exception ; -- T.c2: cannot be null
v_errorcol varchar2(32) := '' ;
v_comment varchar2(128) := '' ;
v_tablename constant varchar2(32) := upper('T') ;
begin
if length( col1 ) > 10 then
v_errorcol := upper('C1') ;
raise ex_value_too_large ;
end if;
if col2 is null then
v_errorcol := upper('C2') ;
raise ex_insert_null ;
end if ;
insert into t ( c1, c2 ) values ( col1, col2 ) ;
exception
when ex_value_too_large then
dbms_output.put_line( ' ex_value_too_large # '
|| v_errorcol || ' (' || fetch_comment( v_tablename, v_errorcol ) || ')' );
when ex_insert_null then
dbms_output.put_line( ' ex_insert_null # '
|| v_errorcol || ' (' || fetch_comment( v_tablename, v_errorcol ) || ')' );
when others then
raise ;
end insert_ ;
procedure insert_test
is
begin
for rec_ in (
select
case
when level = 8 then ( level + 1 ) || 'qqqqqqqqqqqq'
else ( level + 2 ) || 'qqq'
end as col1
, case
when level = 7 then null
else ( level + 3 ) || 'wwwwwww'
end as col2
from dual
connect by level <= 10
) loop
insert_( rec_.col1, rec_.col2 ) ;
end loop;
commit;
end insert_test;
function fetch_comment( table_ varchar2, col_ varchar2 ) return varchar2
is
v_comment varchar2(4000) ; -- same datatype as in user_tab_comments
begin
select comments into v_comment
from user_col_comments
where table_name = table_
and column_name = col_ ;
return v_comment ;
end fetch_comment ;
end P ;
/
For testing the package code, execute the following anonymous block:
begin
P.insert_test ;
end;
/
-- output
ex_insert_null # C2 (this is the column comment for c2)
ex_value_too_large # C1 (this is the column comment for c1)
-- Table T contains:
SQL> select * from T;
C1 C2
3qqq 4wwwwwww
4qqq 5wwwwwww
5qqq 6wwwwwww
6qqq 7wwwwwww
7qqq 8wwwwwww
8qqq 9wwwwwww
11qqq 12wwwwwww
12qqq 13wwwwwww
In the dbfiddle, all output will be written to T and ERRORLOG, respectively. You can also use dbms_output.put_line (which is commented out in the dbfiddle) if needed. Notice that the cursor for loop in the insert_test procedure is inefficient (we could use BULK operations). Also, you need to decide where and how the exceptions are handled. As mentioned, this example is just a starting point - which will probably need lots of refinements.

How to have XMLForest returning all columns of a table

I have a table with a lot of columns (e.g : Column1, Column 2, Column 3, Column 4, ...)
I'd like to use XMLElement and XMLForest function to generate an XML with each column being a tag.
I'm only able to do this by manually adding each column in the XMLForest :
e.g :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.Column1,
TABLE.Column2,
TABLE.Column2,
...)
)
FROM ...
Results :
<ParentTag> <Column1>Value1</Column1> <Column2>Value2</Column2> ...</ParentTag>
However i'd like to avoid typing each column as their number could increase in the future.
How can i do something like this ? :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.*)
)
FROM ...
You can use a PLSQL procedure to get your requirement done. Here in the PLSQL procedure, it would accept a Tablename and then generate the XMLForest and show the result. See below:
-- Creating a type of XMLTYPE
CREATE OR REPLACE TYPE Outpt IS TABLE OF XMLTYPE;
/
--Procedure with In parameter as Tablename and out parameter as resultset
CREATE OR REPLACE PROCEDURE XM_FOREST (tabnm VARCHAR2, v_out IN OUT Outpt)
AS
var VARCHAR2 (4000);
v_sql VARCHAR2 (4000);
BEGIN
FOR i IN (SELECT cname
FROM col
WHERE tname = tabnm)
LOOP
var := var || ',' || i.cname;
END LOOP;
var := LTRIM (var, ',');
v_sql :=
'select XMLElement("ParentTag",XMLForest('
|| var
|| ' ) ) from '
|| tabnm;
EXECUTE IMMEDIATE v_sql BULK COLLECT INTO v_out;
END;
--------------
--Execution
DECLARE
var_out Outpt := Outpt ();
LCLOB CLOB;
BEGIN
var_out.EXTEND;
XM_FOREST (tabnm => 'EMPLOYEE', v_out => var_out);
FOR i IN 1 .. var_out.COUNT
LOOP
LCLOB := var_out (i).getCLOBVAL ();
DBMS_OUTPUT.put_line (LCLOB);
END LOOP;
END;
------
--Result
SQL> /
<ParentTag><EMPLOYEE_ID>1</EMPLOYEE_ID><FIRST_NAME>XXX</FIRST_NAME></ParentTag>
<ParentTag><EMPLOYEE_ID>2</EMPLOYEE_ID><FIRST_NAME>YYY</FIRST_NAME></ParentTag>
PL/SQL procedure successfully completed.
How can i do something like this ? :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.*)
)
FROM ...
You cannot, you will have to type out all the names individually.
You could generate the query using dynamic SQL
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name (
id NUMBER,
a NUMBER,
b NUMBER,
c NUMBER,
d NUMBER
);
Query 1:
SELECT '
SELECT XMLElement(
"ParentTag",
XMLForest( '
|| LISTAGG( '"' || column_name || '"', ',' )
WITHIN GROUP ( ORDER BY Column_id )
||' ) ) FROM ...' AS query
FROM user_tab_columns
WHERE table_name = 'TABLE_NAME'
Results:
| QUERY |
|-------------------------------------------------------|
| SELECT XMLElement( |
| "ParentTag", |
| XMLForest( "ID","A","B","C","D" ) ) FROM ... |

Update column data to null without writing each column name in sql

I have a table XX_LOCATION with 20 columns. Out of this I want data in only 4 columns in rest 16 I want to update the column values to null.
How can 1 update statement be used for this i.e. I don't want to write each column name of 16 columns in the update statement. Is there any other way out?
Ok, I admit. I am having a little bit of fun with this one.
BEGIN
FOR eachrec IN (SELECT column_name
FROM user_tab_cols a
WHERE table_name = 'MYTABLE'
AND column_name NOT IN ('COL1', 'COL2', 'COL3'
, 'COL4'))
LOOP
execute immediate 'update XX_LOCATION set ' || eachrec.column_name || ' = null';
END LOOP;
END;
Here is another possibility, this one with only a single execute immediate:
DECLARE
l_cmd VARCHAR2 (2000);
l_comma VARCHAR2 (1);
BEGIN
l_cmd := 'update xx_location set ';
FOR eachrec IN (SELECT column_name
FROM user_tab_cols a
WHERE table_name = 'MYTABLE'
AND column_name NOT IN ('COL1', 'COL2', 'COL3'
, 'COL4'))
LOOP
l_cmd := l_cmd || l_comma || eachrec.column_name || ' = null';
l_comma := ',';
END LOOP;
execute immediate l_cmd;
END;
And finally, a completely automated solution to exclude N columns, without having to type the non-specified column names:
CREATE TYPE column_tt IS TABLE OF VARCHAR2 (30);
CREATE OR REPLACE PROCEDURE nullify_columns (
p_owner IN all_tab_cols.owner%TYPE
, p_table IN all_tab_cols.table_name%TYPE
, p_exclude_columns IN OUT NOCOPY column_tt
)
AS
l_cmd VARCHAR2 (2000);
l_comma VARCHAR2 (1);
BEGIN
l_cmd := 'update ' || p_owner || '.' || p_table || ' set ';
FOR eachrec IN (SELECT column_name
FROM all_tab_cols a
WHERE owner = p_owner
AND table_name = p_table)
LOOP
IF NOT eachrec.column_name MEMBER OF p_exclude_columns
THEN
l_cmd := l_cmd || l_comma || eachrec.column_name || ' = null';
l_comma := ',';
END IF;
END LOOP;
EXECUTE IMMEDIATE l_cmd;
END;
CREATE TABLE totally_bogus_dude
(
col1 VARCHAR2 (10)
, col2 VARCHAR2 (10)
, col3 VARCHAR2 (10)
, col4 VARCHAR2 (10)
, col5 VARCHAR2 (10)
);
DECLARE
l_exclude column_tt := column_tt ();
BEGIN
l_exclude.EXTEND;
l_exclude (l_exclude.COUNT) := 'COL1';
l_exclude.EXTEND;
l_exclude (l_exclude.COUNT) := 'COL2';
l_exclude.EXTEND;
l_exclude (l_exclude.COUNT) := 'COL3';
l_exclude.EXTEND;
l_exclude (l_exclude.COUNT) := 'COL4';
FOR eachrec IN (SELECT COLUMN_VALUE
FROM TABLE (l_exclude))
LOOP
DBMS_OUTPUT.put_line (eachrec.COLUMN_VALUE);
END LOOP;
nullify_columns (p_owner => USER, p_table => 'TOTALLY_BOGUS_DUDE', p_exclude_columns => l_exclude);
END;

oracle xmltable with columns from another table

with oracle xmltable
SELECT u.*
FROM table1
, XMLTable('/abc/def[contract = $count]'
PASSING xmlcol, 1 as "count"
COLUMNS contract integer path 'contract',
oper VARCHAR2(20) PATH 'oper' ) u
This is normally what we do.
Now I need to have "COLUMNS" in above query selected from another tables column for Xpath
something like
{
SELECT u.*
FROM table1
, XMLTable('/abc/def[contract = $count]'
PASSING xmlcol, 1 as "count"
COLUMNS (select xpath from xpath_metadeta )) u
}
Please let me know if this is possible and how?
One option that comes to my mind is dynamic sql and ref cursor.
Something like this:
DECLARE
columnParameters SYS.ODCIVARCHAR2LIST :=
SYS.ODCIVARCHAR2LIST(
'TITLE VARCHAR2(1000) PATH ''title''',
'SUMMARY CLOB PATH ''summary''',
'UPDATED VARCHAR2(20) PATH ''updated''',
'PUBLISHED VARCHAR2(20) PATH ''published''',
'LINK VARCHAR2(1000) PATH ''link/#href'''
);
ref_cursor SYS_REFCURSOR;
cursor_id NUMBER;
table_description DBMS_SQL.DESC_TAB;
column_count NUMBER;
string_value VARCHAR2(4000);
clob_value CLOB;
FUNCTION DYNAMIC_XMLTABLE(xml_columns SYS.ODCIVARCHAR2LIST) RETURN SYS_REFCURSOR
IS
result SYS_REFCURSOR;
statementText VARCHAR2(32000) := Q'|SELECT * FROM
XMLTABLE(
XMLNAMESPACES (DEFAULT 'http://www.w3.org/2005/Atom'),
'for $entry in /feed/entry return $entry'
PASSING
HTTPURITYPE('http://stackoverflow.com/feeds/tag?tagnames=oracle&sort=newest').getxml()
COLUMNS
{column_definition}
)|';
BEGIN
SELECT REPLACE(statementText, '{column_definition}', LISTAGG(COLUMN_VALUE, ', ') WITHIN GROUP (ORDER BY ROWNUM)) INTO statementText FROM TABLE(xml_columns);
DBMS_OUTPUT.PUT_LINE('Statement: ' || CHR(10) || statementText);
OPEN result FOR statementText;
RETURN result;
END;
BEGIN
DBMS_OUTPUT.ENABLE(NULL);
ref_cursor := dynamic_xmltable(columnParameters);
cursor_id := DBMS_SQL.TO_CURSOR_NUMBER(ref_cursor);
DBMS_SQL.DESCRIBE_COLUMNS(cursor_id, column_count, table_description);
FOR i IN 1..column_count LOOP
IF table_description(i).col_type = 1 THEN
DBMS_SQL.DEFINE_COLUMN(cursor_id, i, string_value, 4000);
ELSIF table_description(i).col_type = 112 THEN
DBMS_SQL.DEFINE_COLUMN(cursor_id, i, clob_value);
END IF;
END LOOP;
WHILE DBMS_SQL.FETCH_ROWS(cursor_id) > 0 LOOP
FOR i IN 1..column_count LOOP
DBMS_OUTPUT.PUT_LINE(table_description(i).col_name || ': datatype=' || table_description(i).col_type);
IF (table_description(i).col_type = 1) THEN
BEGIN
DBMS_SQL.COLUMN_VALUE(cursor_id, i, string_value);
DBMS_OUTPUT.PUT_LINE('Value: ' || string_value);
END;
ELSIF (table_description(i).col_type = 112) THEN
BEGIN
DBMS_SQL.COLUMN_VALUE(cursor_id, i, clob_value);
DBMS_OUTPUT.PUT_LINE('Value: ' || clob_value);
END;
-- add other data types
END IF;
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(cursor_id);
END;
I depends how the cursor is consumed. It's much simple if by an application, a bit more difficult if using PL/SQL.