Passing column name as parameter in Oracle PL_SQL function - sql

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

Related

What is the analogous function to DBMS_XMLGEN.getxml(query) for json is the query is a variable and with oracle 19?

This question is not a duplicate of this question because the given answer works only if the query isn't a variable.
the following query is working but the result is saved in a xml file.
SELECT XMLTYPE.createXML (DBMS_XMLGEN.getxml ('select 2 as a from dual')) FROM DUAL;
It's working but I can use macro only in oracle>19. (because of the macro)
with FUNCTION f_test return varchar2 SQL_MACRO is
query VARCHAR2(100) := 'select 1 a from dual';
ret VARCHAR2(100) := chr(13) || query || chr(13);
BEGIN
RETURN ret;
END;
SELECT JSON_ARRAYagg( json_object(t.*) )
FROM f_test() t
code
I've tried to use dynamic sql with oracle 19
WITH
FUNCTION f
RETURN JSON_ARRAY
IS
query VARCHAR2 (100) := 'select 1 from dual';
l_str VARCHAR2 (1000);
l_cnt JSON_ARRAY;
BEGIN
l_str :=
'with from_dynamic_query as ('
|| query
|| ') SELECT JSON_ARRAYagg( json_object(*) ) from from_dynamic_query';
EXECUTE IMMEDIATE l_str
INTO l_cnt;
RETURN l_cnt;
END;
SELECT
FROM DUAL;
[Error] Execution (20: 8): ORA-06553: PLS-313: 'F' not declared in this scope
ORA-06552: PL/SQL: Item ignored
ORA-06553: PLS-488: 'JSON_ARRAY' must be a type
As I wrote in the comment, the issue may not be related to SQL_MACRO, but inability to process * in json_object (see db<>fiddle in 18c).
But this may also be worked out with Polymorphic Table Functions, which are available in 18c. You need to define new output calculated column with a row value serialized into JSON.
Below is the code example:
create package pkg_ser as
/*Package to implement PTF*/
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
;
procedure fetch_rows;
end pkg_ser;
/
create package body pkg_ser as
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
as
begin
/*Mark input columns as used for subsequent row processing*/
for i in 1..tab.column.count loop
tab.column(i).for_read := TRUE;
end loop;
/*Declare json output column*/
return dbms_tf.describe_t(
new_columns => dbms_tf.columns_new_t(
1 => dbms_tf.column_metadata_t(
name => 'JSONVAL',
type => dbms_tf.type_varchar2
)
)
);
end;
procedure fetch_rows
/*Process rowset and serialize each row in JSON*/
as
rowset dbms_tf.row_set_t;
num_rows pls_integer;
new_col dbms_tf.tab_varchar2_t;
begin
/*Get rows*/
dbms_tf.get_row_set(
rowset => rowset,
row_count => num_rows
);
for rn in 1..num_rows loop
/*Calculate new column value in the same row*/
new_col(rn) := dbms_tf.row_to_char(
rowset => rowset,
rid => num_rows,
format => dbms_tf.FORMAT_JSON
);
end loop;
/*Put column to output*/
dbms_tf.put_col(
columnid => 1,
collection => new_col
);
end;
end pkg_ser;
/
create function f_serialize_json(tab in table)
/*Function to serialize into JSON using PTF*/
return table pipelined
row polymorphic using pkg_ser;
/
with function f_local_exec (
query in clob
) return varchar2
as
ret varchar2(32000);
begin
/*Translate string to query using EXECUTE IMMEDIATE*/
execute immediate '
with a as (
' || query || '
)
select json_arrayagg(jsonval format json)
from f_serialize_json(a)
' into ret;
return ret;
end;
select f_local_exec(
'select level as id, mod(level, 3) as val from dual connect by level < 10'
) as jsonval
from dual
| JSONVAL |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0},{"ID":9, "VAL":0}] |
db<>fiddle here
It works if I replace json_arry with clob
WITH
FUNCTION f
RETURN clob
IS
query VARCHAR2 (100) := 'select 1 a from dual';
l_str VARCHAR2 (1000);
l_cnt clob;
BEGIN
l_str :=
'with from_dynamic_query as ('
|| query
|| ') SELECT JSON_ARRAYagg( json_object(*) ) from from_dynamic_query';
EXECUTE IMMEDIATE l_str
INTO l_cnt;
RETURN l_cnt;
END;
SELECT f()
FROM DUAL;

Dynamically select columns for json_table

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

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 ... |

variable inside execute immediate

I am using the following Procedure in pl sql
CREATE OR REPLACE "MY_PROCEDURE" (v_id number, v_name varchar2) AS
BEGIN
EXECUTE IMMEDIATE ' CREATE TABLE T_TEMPO ( t_id number , t_name varchar2(250) , t_value number )';
EXECUTE IMMEDIATE 'INSERT INTO T_TEMPO (t_id , t_name, t_value)
SELECT id , name , value
from TABLE_2
where TABLE_2.id = || v_id ||
AND TABLE_2.name = || v_name || ';
But this doesn't work I have a missing expression error, I wonder it can't evaluate the value of the variables v_id and v_name inside the execute immediate.
Can anyone help please?
You'll either need to quote the values (using quotation marks where necessary):
create or replace procedure my_procedure
( v_id number
, v_name varchar2 )
as
begin
execute immediate 'CREATE TABLE T_TEMPO (t_id number, t_name varchar2(250), t_value number )';
execute immediate 'INSERT INTO T_TEMPO (t_id, t_name, t_value)
SELECT id, name, value
FROM table_2
WHERE table_2.id = ' || v_id ||
' AND table_2.name = ''' || v_name || '''';
end my_procedure;
or (generally better) pass them as bind variables:
create or replace procedure my_procedure
( v_id number
, v_name varchar2 )
as
begin
execute immediate 'CREATE TABLE T_TEMPO (t_id number, t_name varchar2(250), t_value number )';
execute immediate 'INSERT INTO T_TEMPO (t_id, t_name, t_value)
SELECT id, name, value
FROM table_2
WHERE table_2.id = :id
AND table_2.name = :name' using v_id, v_name;
end my_procedure;
btw I would use the v prefix for variables and p for parameters.
Please use variables outside quotation:
EXECUTE IMMEDIATE ' CREATE TABLE T_TEMPO ( t_id number , t_name varchar2(250) , t_value number )';
EXECUTE IMMEDIATE 'INSERT INTO T_TEMPO (t_id , t_name, t_value)
SELECT id , name , value
from TABLE_2
where TABLE_2.id = ' || v_id ||
' AND TABLE_2.name = '''|| v_name || '''';