Non-leaf Polymorphic Table Functions (PTF)

Adding Columns

Applies to: Oracle 18c, Tested against: Oracle 18.1

Adding a column to the output of a polymorphic table function is a two step process.

  1. in describe() define the new column
  2. in fetch_rows() populate the new column, this can be performed via either:

    • dbms_tf.put_col()
    • dbms_tf.put_row_set()

(Note: This is the first lesson in the tutorial to use a fetch_rows() procedure.)

For step 1 we need to learn and use the dbms_tf.describe_t data type. We can visualize dbms_tf.describe_t like this.

describe_t.new_columns
  describe_t.new_columns(i)
    describe_t.new_columns(i).name
    describe_t.new_columns(i).name_len
    describe_t.new_columns(i).type
    describe_t.new_columns(i).max_len
    describe_t.new_columns(i).precision
    describe_t.new_columns(i).scale
    describe_t.new_columns(i).charsetid
    describe_t.new_columns(i).charsetform
    describe_t.new_columns(i).collation
describe_t.cstore_chr
  describe_t.cstore_chr(a)
describe_t.cstore_num
  describe_t.cstore_num(a)
describe_t.cstore_bol
  describe_t.cstore_bol(a)
describe_t.cstore_dat
  describe_t.cstore_dat(a)
describe_t.method_names
  describe_t.method_names(a)

For step 2 we need to learn and use the tab_varchar2_t data type (or tab_number_t, tab_date_t, etc.) to populate our new columns via put_col().

TYPE TAB_VARCHAR2_T IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;

Alternately we can use the dbms_tf.row_set_t data type to populate the new column via put_row_set(). We can visualize dbms_tf.row_set_t like this.

row_set_t
  row_set_t(i).description
    row_set_t(i).description.name
    row_set_t(i).description.name_len
    row_set_t(i).description.type
    row_set_t(i).description.max_len
    row_set_t(i).description.precision
    row_set_t(i).description.scale
    row_set_t(i).description.charsetid
    row_set_t(i).description.charsetform
    row_set_t(i).description.collation
  row_set_t(i).tab_varchar2(j)
  row_set_t(i).tab_number(j)
  row_set_t(i).tab_date(j)
  row_set_t(i).tab_binary_float(j)
  row_set_t(i).tab_binary_double(j)
  row_set_t(i).tab_raw(j)
  row_set_t(i).tab_char(j)
  row_set_t(i).tab_clob(j)
  row_set_t(i).tab_blob(j)
  row_set_t(i).tab_timestamp(j)
  row_set_t(i).tab_timestamp_tz(j)
  row_set_t(i).tab_interval_ym(j)
  row_set_t(i).tab_interval_ds(j)
  row_set_t(i).tab_timestamp_ltz(j)
  row_set_t(i).tab_rowid(j)

Note that the "description" record contains a description of any FOR_READ=TRUE columns retrieved via get_row_set(). fetch_rows() should only read this information, not write to it. Writing to it has no effect on the end result.

To better understand how row_set_t works take a look at what a DEPT row set might look like.

row_set_t( 1 ).description.name      = 'DEPTNO' ;
row_set_t( 1 ).description.type      = dbms_tf.type_number ;
row_set_t( 1 ).description.precision = 2 ;
row_set_t( 1 ).description.scale     = 0 ;
row_set_t( 1 ).tab_number            = ( 10, 20, 30, 40 ) ;

row_set_t( 2 ).description.name    = 'DNAME' ;
row_set_t( 2 ).description.type    = dbms_tf.type_varchar2 ;
row_set_t( 2 ).description.max_len = 14 ;
row_set_t( 2 ).tab_varchar2        = ( 'ACCOUNTING', 'RESEARCH', 'SALES', 'OPERATIONS' )

row_set_t( 3 ).description.name    = 'LOC' ;
row_set_t( 3 ).description.type    = dbms_tf.type_varchar2 ;
row_set_t( 3 ).description.max_len = 13 ;
row_set_t( 3 ).tab_varchar2        = ( 'NEW YORK', 'DALLAS', 'CHICAGO', 'BOSTON' )  

PUT_COL()

Let's look at an example using the PUT_COL() method.

create or replace package ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t ) return dbms_tf.describe_t ;

  procedure fetch_rows ;

  function augment ( p_table table)
    return table pipelined row polymorphic using ptf_demo ;

  end ptf_demo ;
/

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    --
    -- step (1) define the new column
    --

    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_new_column dbms_tf.tab_varchar2_t ;

  begin

    --
    -- step (2) populate the new column
    --

    v_new_column(1) := 'hello' ;
    v_new_column(2) := 'world' ;

    dbms_tf.put_col( 1, v_new_column );

    end fetch_rows ;

  end ptf_demo ;
/

select *
from   ptf_demo.augment( scott.dept ) ;
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK   hello
        20 RESEARCH   DALLAS     world
        30 SALES      CHICAGO
        40 OPERATIONS BOSTON

PUT_ROW_SET()

Now let's look at a similar example which uses the PUT_ROW_SET() method. We only need to change the package body for this example. The package specification remains the same as above.

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    --
    -- step (1) define the new column
    --

    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_new_columns dbms_tf.row_set_t;

  begin

    --
    -- step (2) populate the new column
    --

    v_new_columns(1).tab_varchar2(1) := 'hello' ;
    v_new_columns(1).tab_varchar2(2) := 'world' ;

    dbms_tf.put_row_set( v_new_columns );

    end fetch_rows ;

  end ptf_demo ;
/

select *
from   ptf_demo.augment( scott.dept );
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK   hello
        20 RESEARCH   DALLAS     world
        30 SALES      CHICAGO
        40 OPERATIONS BOSTON

DBMS_TF.ROW_SET_T.DESCRIPTION

Above we claimed that updating the description record has no effect on the output. Let's confirm this with a test.

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_new_columns dbms_tf.row_set_t;

  begin

    v_new_columns(1).description.name := 'A_DIFFERENT_COLUMN_NAME' ;

    dbms_tf.put_row_set( v_new_columns );

    end fetch_rows ;

  end ptf_demo ;
/

select *
from   ptf_demo.augment( scott.dept ) ;
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK
        20 RESEARCH   DALLAS
        30 SALES      CHICAGO
        40 OPERATIONS BOSTON

Note how the column name remained set to the value defined in describe(), i.e. "THE_NEW_COLUMN". The column name set in fetch_rows(), "A_DIFFERENT_COLUMN_NAME", was ignored.

NO_DATA_FOUND Exceptions

Be sure to avoid or properly handle NO_DATA_FOUND exceptions in fetch_rows(). When executing a PL/SQL procedure or function, e.g. fetch_rows(), the SQL engine does not treat NO_DATA_FOUND exceptions as errors. Instead it simply stops processing when it encounters NO_DATA_FOUND, see AskTOM "NO_DATA_FOUND in Functions").

This is what happens when fetch_rows() raises NO_DATA_FOUND.

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_new_columns dbms_tf.row_set_t;

    v_some_string varchar2(100);

  begin

    v_new_columns( 1 ).tab_varchar2(1) := 'hello world' ;

    --
    -- let's accidentally raise a NO_DATA_FOUND exception by referencing
    -- a non-existent array index
    --
    v_some_string := v_new_columns( 99 ).tab_varchar2(1) ;

    dbms_tf.put_row_set( v_new_columns );

    end fetch_rows ;

  end ptf_demo ;
/

select *
from   ptf_demo.augment( scott.dept ) ;
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK
        20 RESEARCH   DALLAS
        30 SALES      CHICAGO
        40 OPERATIONS BOSTON

Note how no error was raised. Note also that put_row_set() failed to execute, i.e. "hello world" did not appear in the output.

Referencing Input Table Data

To reference input table data when setting a new output column value we must do two things.

  1. in describe() identify the input columns we wish to reference by setting their FOR_READ attribute to TRUE
  2. in fetch_rows() retrieve the FOR_READ=TRUE columns using either:

    • dbms_tf.get_col()
    • dbms_tf.get_row_set()

Let's see an example using the get_col() method.

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    --
    -- step (1) identify the input columns we wish to reference by setting FOR_READ to TRUE
    -- (column #3 in DEPT is the LOC column)
    --

    p_input_table_metadata.column(3).for_read := true ;

    --
    -- define new columns
    --
    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_loc         dbms_tf.tab_varchar2_t ;
    v_new_column  dbms_tf.tab_varchar2_t ;

    i pls_integer ;

  begin

    --
    -- step (2) retrieve the FOR_READ=TRUE columns
    --

    dbms_tf.get_col( 1, v_loc );

    --
    -- loop through the for_read rows and set new column values
    --

    i := v_loc.first ;

    while i is not null loop

      v_new_column(i) := lower( v_loc(i) ) ;

      i := v_loc.next(i) ;

      end loop ;

    --
    -- output our new values
    --
    dbms_tf.put_col( 1, v_new_column );

    end fetch_rows;

  end;
/

select *
from   ptf_demo.augment( scott.dept ) ;
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK   new york
        20 RESEARCH   DALLAS     dallas
        30 SALES      CHICAGO    chicago
        40 OPERATIONS BOSTON     boston

Here is an exmaple using the get_row_set() method.

create or replace package body ptf_demo as

  function describe( p_input_table_metadata in out dbms_tf.table_t )
    return dbms_tf.describe_t
  as

    v_new_column_desc dbms_tf.describe_t ;

  begin

    --
    -- step (1) identify the input columns we wish to reference by setting FOR_READ to TRUE
    --

    p_input_table_metadata.column(2).for_read := true ; -- (1) DNAME
    p_input_table_metadata.column(3).for_read := true ; -- (2) LOC

    --
    -- define new columns
    --
    v_new_column_desc.new_columns(1).name     := 'THE_NEW_COLUMN' ;
    v_new_column_desc.new_columns(1).type     := dbms_tf.type_varchar2 ;
    v_new_column_desc.new_columns(1).max_len  := 100 ;

    return v_new_column_desc ;

    end describe ;


  procedure fetch_rows as

    v_for_read_columns dbms_tf.row_set_t;
    v_new_columns      dbms_tf.row_set_t;

    i pls_integer ;

  begin

    --
    -- step (2) retrieve the FOR_READ=TRUE columns
    --

    dbms_tf.get_row_set( rowset => v_for_read_columns );

    --
    -- loop through the for_read rows and set new column values
    --

    i := v_for_read_columns(1).tab_varchar2.first ;

    while i is not null loop

      v_new_columns(1).tab_varchar2(i) :=
        v_for_read_columns(1).tab_varchar2(i) || ' (' ||
        v_for_read_columns(2).tab_varchar2(i) || ')' ;

      i := v_for_read_columns(1).tab_varchar2.next(i) ;

      end loop ;

    --
    -- output our new values
    --
    dbms_tf.put_row_set( v_new_columns );

    end fetch_rows;

  end;
/

select *
from   ptf_demo.augment( scott.dept ) ;
    DEPTNO DNAME      LOC        THE_NEW_COLUMN
---------- ---------- ---------- ----------------------------------------------------------------------------------------------------
        10 ACCOUNTING NEW YORK   ACCOUNTING (NEW YORK)
        20 RESEARCH   DALLAS     RESEARCH (DALLAS)
        30 SALES      CHICAGO    SALES (CHICAGO)
        40 OPERATIONS BOSTON     OPERATIONS (BOSTON)



Revision Notes

Date Category Note
2018-06-23 Created Added to site