In my previous post I wrote about a possible use-case for Polymorphic Table Functions. I also wrote that I had some ideas to implement extra functionality to make the function more generic.
After attending DOAG Konferentz und ausstellung 2018 and UKOUG Tech18 I finally came around to publishing my new version.
Lets start off with a table to hold my data, I chose to use the NATO phonetic alphabet for this:
create table t_alphabet (thecount number ,alphabet varchar2(4000) ) / begin insert into t_alphabet(alphabet, thecount) values ('Alfa;Bravo;Charlie;Delta;Echo;Foxtrot;Golf',7); insert into t_alphabet(alphabet, thecount) values ('Hotel;India;Juliett;Kilo;Lima;Mike;November',7); insert into t_alphabet(alphabet, thecount) values ('Oscar;Papa;Quebec;Romeo;Sierra;Tango;Uniform',7); insert into t_alphabet(alphabet, thecount) values ('Victor;Whiskey;;X-ray;Yankee;;Zulu',5); insert into t_alphabet(alphabet, thecount) values ('Alfa.Bravo.Charlie.Delta.Echo.Foxtrot.Golf',7); insert into t_alphabet(alphabet, thecount) values ('Hotel.India.Juliett.Kilo.Lima.Mike.November',7); insert into t_alphabet(alphabet, thecount) values ('Oscar.Papa.Quebec.Romeo.Sierra.Tango.Uniform',7); insert into t_alphabet(alphabet, thecount) values ('Victor.Whiskey..X-ray.Yankee..Zulu',5); commit; end; /
First of all, I learned from attending the presentations by Andrej Pashchenko: Polymorphic Table Functions in 18c: Einführung und Beispiele and Keith Laker: Patterns and Use Cases For Polymorphic Tables that there is no need for global (package) variables to have access to the parameters supplied. The more I can rely on Oracle to take care of the value of variables, the better I like it.
I won’t bore you with all the intermediate versions of the code, lets jump straight into the ‘final’ result.
The package:
create or replace package separated_ptf is function describe(tab in out dbms_tf.table_t ,cols in dbms_tf.columns_t default null ,coltosplit in varchar2 default null ,separator in varchar2 default ';') return dbms_tf.describe_t; procedure fetch_rows(coltosplit in varchar2 default null ,separator in varchar2 default ';'); end separated_ptf; /
The package body:
create or replace package body separated_ptf as function describe(tab in out dbms_tf.table_t ,cols in dbms_tf.columns_t default null ,coltosplit in varchar2 default null ,separator in varchar2 default ';') return dbms_tf.describe_t as -- metadata for column to add l_new_col dbms_tf.column_metadata_t; -- table of columns to add l_new_cols dbms_tf.columns_new_t; -- make sure the column to split is in the correct format (uppercase with doublequotes) l_coltosplit dbms_quoted_id := dbms_assert.enquote_name(str => coltosplit, capitalize => true); begin -- if the coltosplit parameter is null then if coltosplit is null then -- Mark the first column ReadOnly and don't display it anymore tab.column(1).for_read := true; tab.column(1).pass_through := false; else -- if the coltosplit parameter is not null then -- check every column from the source table for indx in tab.column.first .. tab.column.last loop -- if this is the column we want to split then if tab.column(indx).description.name = l_coltosplit then -- Mark this column ReadOnly and don't display it anymore tab.column(indx).for_read := true; tab.column(indx).pass_through := false; end if; end loop; end if; -- Add the new columns, as specified in the cols parameter for indx in 1 .. cols.count loop -- define metadata for column named cols(indx) -- that will default to a datatype of varchar2 with -- a length of 4000 l_new_col := dbms_tf.column_metadata_t(name => cols(indx)); -- add the new column to the list of columns new columns l_new_cols(l_new_cols.count + 1) := l_new_col; end loop; -- Instead of returning NULL we will RETURN a specific -- DESCRIBE_T that adds new columns return dbms_tf.describe_t(new_columns => l_new_cols); end; procedure fetch_rows(coltosplit in varchar2 default null ,separator in varchar2 default ';') is -- define a table type of varchar2 tables type colset is table of dbms_tf.tab_varchar2_t index by pls_integer; -- variable to hold the rowset as retrieved l_rowset dbms_tf.row_set_t; -- variable to hold the number of rows as retrieved l_rowcount pls_integer; -- variable to hold the number of put columns l_putcolcount pls_integer := dbms_tf.get_env().put_columns.count; -- variable to hold the new values l_newcolset colset; -- get the name of the column to be split from the get columns l_coltosplit dbms_quoted_id := trim('"' from dbms_tf.get_env().get_columns(1).name); begin -- dbms_tf.Trace(dbms_tf.Get_Env); -- fetch rows into a local rowset -- at this point the rows will have columns -- from the the table/view/query passed in dbms_tf.get_row_set(l_rowset, l_rowcount); -- for every row in the rowset... for rowindx in 1 .. l_rowcount loop -- for every column for colindx in 1 .. l_putcolcount loop -- split the row into separate values -- FUNCTION Row_To_Char(rowset Row_Set_t, -- rid PLS_INTEGER, -- format PLS_INTEGER default FORMAT_JSON) -- return VARCHAR2; -- splitting the regexp way: http://nuijten.blogspot.com/2009/07/splitting-comma-delimited-string-regexp.html l_newcolset(colindx)(rowindx) := trim(separator from regexp_substr(json_value(dbms_tf.row_to_char(l_rowset, rowindx), '$.' || l_coltosplit) ,'[^' || separator || ']*' || separator || '{0,1}' ,1 ,colindx)); end loop; -- every column end loop; -- every row in the rowset -- add the newly populated columns to the rowset for indx in 1 .. l_putcolcount loop dbms_tf.put_col(columnid => indx, collection => l_newcolset(indx)); end loop; end; end separated_ptf; /
The wrapper function:
create or replace function separated_fnc(p_tbl in table ,cols columns default null ,coltosplit in varchar2 default null ,separator in varchar2 default ';') return table pipelined row polymorphic using separated_ptf; /
Back to the improvements I suggested earlier.
Supporting duplicate separators:
Using the Regular Expression ‘[^;]+’ didn’t make this possible, because double ; (;;) would be regarded as one. So this had to be changed into ‘[^;]+;{0,1}’. This expression says (in my words): find all the characters which are not a ; followed by 0 or 1 ;. Since this will result in a string with a ; at the end I had to add the trim function around it.
Making the column to be split up a parameter
To find the column to be split I need to make the value look the same as the tab.column(indx).description.name value I can of course add quotes around the parameter myself, but I think it is better to use the built in sys.dbms_assert.enquote_name function to do this. If the value is not supplied or null I just (try to) split the first column.
Making the separator character a parameter
This seemed like an easy task, just replace every ; in my code by a variable, but when running a couple of tests I received an error which put me on the wrong path.
Calling the function using positioned parameters works like a charm, just as you would expect it:
select *
from separated_fnc(t_alphabet, columns(first, second, third, fourth, fifth, sixth, seventh),’alphabet’,’.’)
/
THECOUNT FIRST SECOND THIRD FOURTH FIFTH SIXTH SEVENTH ---------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- 7 Alfa;Bravo;Charlie;D elta;Echo;Foxtrot;Go lf 7 Hotel;India;Juliett; Kilo;Lima;Mike;Novem ber 7 Oscar;Papa;Quebec;Ro meo;Sierra;Tango;Uni form 5 Victor;Whiskey;X-ray ;Yankee;Zulu 7 Alfa Bravo Charlie Delta Echo Foxtrot Golf 7 Hotel India Juliett Kilo Lima Mike November 7 Oscar Papa Quebec Romeo Sierra Tango Uniform 5 Victor Whiskey X-ray Yankee Zulu 8 rows selected
But when I tried to use named parameters like this:
select * from separated_fnc(t_alphabet, columns(first, second, third, fourth, fifth, sixth, seventh),coltosplit => 'alphabet',separator => '.') /
it resulted in the following error:
ORA-62573: new column (FIRST) is not allowed with describe only polymorphic table function
After the DOAG conference I looked at this together with Chris Saxon, but we couldn’t find what is going on. So we contacted Keith Laker and he told me that it was bug in the version (18.3.0.0.0) of the database I am using and that it should be fixed in an upcoming (patch) release.
If you know about this behavior, I think it is quite a useful function (and with the upcoming fixes it will become even more useful).
If you have any suggestions or maybe ideas for other use cases for Polymorphic Table Functions, please don’t hesitate to use the comments.
Hi,
thanks for the suggestion and descriptions (particular the inline).
However ther is a question:
Why is in function describe(…the parameter “tab in out dbms_tf.table_t”.
defined as in/out although it is a function that returns a correct retrun type? “return dbms_tf.describe_t ”
It may look like picky, but should a function not only have exactly one return value?
I’m just curious – could there be a special reason for that?
Best regards
Andre
Hi Andre,
The first two parameters of the function are, for as far as I know, mandatory for the implementation of a PTF. I agree that a function should not have any OUT parameters, except the return value, but this is how Oracle has implemented this.
Kindest regards,
Patrick
I just tried to make the table parameter IN only, but it gave this error:
Errors for PACKAGE BODY DEMO.SEPARATED_PTF:
LINE/COL ERROR
——– ——————————————————————————————–
16/21 PLS-00363: expression ‘TAB.COLUMN(1).FOR_READ’ cannot be used as an assignment target
16/7 PL/SQL: Statement ignored
17/21 PLS-00363: expression ‘TAB.COLUMN(1).PASS_THROUGH’ cannot be used as an assignment target
17/7 PL/SQL: Statement ignored
25/28 PLS-00363: expression ‘TAB.COLUMN(INDX).FOR_READ’ cannot be used as an assignment target
25/11 PL/SQL: Statement ignored
26/28 PLS-00363: expression ‘TAB.COLUMN(INDX).PASS_THROUGH’ cannot be used as an assignment target
26/11 PL/SQL: Statement ignored
Makes sense, since you are changing the metadata of the columns.
Only first parameter is mandatory, as all current Polymorphic Table functions are non leaf table functions. The first parameter is IN/OUT as you are allowed to change the value of first parameter, like in your example:
tab.column(1).for_read := true;
tab.column(1).pass_through := false;
You can file a bug/SR for the query where it is giving error with a test case.
Thanks,in 19c,the bug you mentionned still exists, the positional call operates, the naming position not.
SQL> SELECT * FROM my_ptf_package.my_ptf(clients,COLUMNS(mail,id_cli,id_resa,moment),’reduction’,’ECO’);
PRENOM NOM INSCRIPT MILES REDUCTION
——————– ——————– ——– ———- ———-
Dany Lenquette 20/06/01 2000 1
Jean-Jacques Mercier 12/08/11 5000 2
Guy Blanchet 11/03/14 5000 3
SQL> SELECT * FROM my_ptf_package.my_ptf(tab=>clients,cols_to_discard=>COLUMNS(mail,id_cli,id_resa,moment),new_col_name=>’reduction’,new_col_val=>’ECO’);
*
ERREUR à la ligne 1 :
ORA-62573: la nouvelle colonne (REDUCTION) n’est pas autorisée avec la fonction
de table polymorphe de description uniquement