Search code examples
sqldatabaseoraclefunction

Pass column name as parameter in Oracle DB function


I have a problem related with Oracle DB function. I don;t know how to pass column_name as parameter. Below I present current function without parameters and it's working fine:

At the beginning i'm creating two types:

CREATE OR REPLACE NONEDITIONABLE TYPE metric_record AS OBJECT (
    period_start VARCHAR2(255),
    data_01_avg NUMBER,
    data_02_avg NUMBER,
    data_03_avg NUMBER,
    data_04_avg NUMBER,
    group_name VARCHAR2(255)
);

CREATE OR REPLACE NONEDITIONABLE TYPE metric_table AS TABLE OF metric_record;

And the function:

create or replace NONEDITIONABLE FUNCTION get_metrics_summary RETURN metric_table IS
    s_metrics metric_table;
BEGIN
    SELECT metric_record(
        TO_CHAR(period_start, 'YYYY-MM-DD HH24:MI'),
        AVG(data_01_avg),
        AVG(data_02_avg),
        AVG(data_03_avg),
        AVG(data_04_avg),
        group_name)
    BULK COLLECT INTO s_metrics
    FROM (
        SELECT
            TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS') AS period_start,
            TO_CHAR(TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96, 'YYYY-MM-DD HH24:MI:SS') AS time_start,
            ROUND(AVG(t.data_01),9) AS data_01_avg,
            ROUND(AVG(t.data_02),2) AS data_02_avg,
            ROUND(AVG(t.data_03),9) AS data_03_avg,
            ROUND(AVG(t.data_04),2) AS data_04_avg,
            m.HARDCODED_COLUMN_NAME AS group_name
        FROM METRICS t
        INNER JOIN METADATA m ON t.metadata_id = m.id
        WHERE m.HARDCODED_COLUMN_NAME = 2
        GROUP BY TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS'),
                TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96,
                m.HARDCODED_COLUMN_NAME
    )
    GROUP BY period_start, group_name
    ORDER BY period_start;

    RETURN s_metrics;
END get_metrics_summary;

above function is working fine, but as You can see I have hardcoded column. I wan't to pass 'HARDCODED_COLUMN_NAME' as parameter, like this:

...get_metrics_summary(COL_NAME)...

and then use it in my query in each place where I need it (3 places):

...m.COL_NAME AS group_name... ...m.COL_NAME = 2... ...m.COL_NAME...

Beside of that as You can see I have hardcoded value for my column, but as column name it should be passed as parameter: ...m.COL_NAME = VALUE... ...get_metrics_summary(COL_NAME, VALUE)...

I was trying for some time how to figure it out but I don't know how, please help :)

I'm expecting updated, fixed, working, rebuilded query or different solution how can I retrieve my data. Additional info: i'll make a call to trigger this function and retrieve data from my app.

I was trying to find on stack overflow and use EXECUTE IMMEDIATE but was not working.

I have implemented EXECUTE IMMEDIATE:

CREATE OR REPLACE NONEDITIONABLE FUNCTION function1 RETURN metric_table IS
    v_sql VARCHAR2(8000);
    s_metrics metric_table;
BEGIN
    v_sql := 'SELECT metric_record(
        TO_CHAR(period_start, ''YYYY-MM-DD HH24:MI''),
        AVG(data_01_avg),
        AVG(data_02_avg),
        AVG(data_03_avg),
        AVG(data_04_avg),
        group_name)
    BULK COLLECT INTO s_metrics
    FROM (
        SELECT
            TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, ''SSSSS''))/900)*900, ''SSSSS'') AS period_start,
            TO_CHAR(TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, ''SSSSS''))/900)/96, ''YYYY-MM-DD HH24:MI:SS'') AS time_start,
            ROUND(AVG(t.data_01),9) AS data_01_avg,
            ROUND(AVG(t.data_02),2) AS data_02_avg,
            ROUND(AVG(t.data_03),9) AS data_03_avg,
            ROUND(AVG(t.data_04),2) AS data_04_avg,
            t.METADATA_ID AS group_name
        FROM METRICS t
        GROUP BY TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, ''SSSSS''))/900)*900, ''SSSSS''),
                TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, ''SSSSS''))/900)/96,
                t.METADATA_ID
    )
    GROUP BY period_start, group_name
    ORDER BY period_start';

    EXECUTE IMMEDIATE v_sql INTO s_metrics;
    RETURN s_metrics;
END function1;

but now I have an error

select * from table(function1());
select function1() from dual;

ORA-03001: niezaimplementowana funkcja (unimplemented function)
ORA-06512: przy "SYSTEM.FUNCTION1", linia 30 (by "SYSTEM.FUNCTION1", line 30)
03001. 00000 -  "unimplemented feature"
*Cause:    This feature is not implemented.
*Action:   None.


Solution

  • Do not use EXECUTE IMMEDIATE, instead whitelist the possible values in a CASE expression:

    CREATE NONEDITIONABLE FUNCTION get_metrics_summary(
      i_col_name IN VARCHAR2
    )
    RETURN metric_table
    IS
      s_metrics metric_table;
    BEGIN
      SELECT metric_record(
               TO_CHAR(period_start, 'YYYY-MM-DD HH24:MI'),
               AVG(data_01_avg),
               AVG(data_02_avg),
               AVG(data_03_avg),
               AVG(data_04_avg),
               group_name
             )
      BULK COLLECT INTO s_metrics
      FROM (
        SELECT TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS') AS period_start,
               TO_CHAR(TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96, 'YYYY-MM-DD HH24:MI:SS') AS time_start,
               ROUND(AVG(t.data_01),9) AS data_01_avg,
               ROUND(AVG(t.data_02),2) AS data_02_avg,
               ROUND(AVG(t.data_03),9) AS data_03_avg,
               ROUND(AVG(t.data_04),2) AS data_04_avg,
               CASE i_col_name
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END AS group_name
        FROM   METRICS t
               INNER JOIN METADATA m
               ON t.metadata_id = m.id
        WHERE  CASE i_col_name
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END = 2
        GROUP BY
               TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS'),
               TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96,
               CASE i_col_name
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END
      )
      GROUP BY period_start, group_name
      ORDER BY period_start;
    
      RETURN s_metrics;
    END get_metrics_summary;
    

    For passing multiple key-value pairs to the function, something like:

    CREATE NONEDITIONABLE FUNCTION get_metrics_summary(
      i_col_name1 IN VARCHAR2,
      i_value1    IN VARCAHR2,
      i_col_name2 IN VARCHAR2 DEFAULT NULL,
      i_value2    IN VARCAHR2 DEFAULT NULL,
      i_col_name3 IN VARCHAR2 DEFAULT NULL,
      i_value3    IN VARCAHR2 DEFAULT NULL
    )
    RETURN metric_table
    IS
      s_metrics metric_table;
    BEGIN
      SELECT metric_record(
               TO_CHAR(period_start, 'YYYY-MM-DD HH24:MI'),
               AVG(data_01_avg),
               AVG(data_02_avg),
               AVG(data_03_avg),
               AVG(data_04_avg),
               group_name
             )
      BULK COLLECT INTO s_metrics
      FROM (
        SELECT TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS') AS period_start,
               TO_CHAR(TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96, 'YYYY-MM-DD HH24:MI:SS') AS time_start,
               ROUND(AVG(t.data_01),9) AS data_01_avg,
               ROUND(AVG(t.data_02),2) AS data_02_avg,
               ROUND(AVG(t.data_03),9) AS data_03_avg,
               ROUND(AVG(t.data_04),2) AS data_04_avg,
               CASE i_col_name1
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END AS group_name
        FROM   METRICS t
               INNER JOIN METADATA m
               ON t.metadata_id = m.id
        WHERE  CASE 
               WHEN i_col_name1 = 'COLUMN1' AND m.column1 = i_value1 THEN 1
               WHEN i_col_name1 = 'COLUMN2' AND m.column2 = i_value1 THEN 1
               WHEN i_col_name1 = 'COLUMN3' AND m.column3 = i_value1 THEN 1
               WHEN i_col_name1 = 'COLUMN4' AND m.column4 = i_value1 THEN 1
               -- No default, expect the 1st key-value pair to always be present
               END = 1
        AND    CASE 
               WHEN i_col_name2 = 'COLUMN1' AND m.column1 = i_value2 THEN 1
               WHEN i_col_name2 = 'COLUMN2' AND m.column2 = i_value2 THEN 1
               WHEN i_col_name2 = 'COLUMN3' AND m.column3 = i_value2 THEN 1
               WHEN i_col_name2 = 'COLUMN4' AND m.column4 = i_value2 THEN 1
               WHEN i_col_name2 IS NULL THEN 1 -- Always match if not set
               END = 1
        AND    CASE 
               WHEN i_col_name3 = 'COLUMN1' AND m.column1 = i_value3 THEN 1
               WHEN i_col_name3 = 'COLUMN2' AND m.column2 = i_value3 THEN 1
               WHEN i_col_name3 = 'COLUMN3' AND m.column3 = i_value3 THEN 1
               WHEN i_col_name3 = 'COLUMN4' AND m.column4 = i_value3 THEN 1
               WHEN i_col_name3 IS NULL THEN 1 -- Always match if not set
               END = 1
        GROUP BY
               TO_DATE(FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)*900, 'SSSSS'),
               TRUNC(t.TIMESTAMP) + FLOOR(TO_NUMBER(TO_CHAR(t.TIMESTAMP, 'SSSSS'))/900)/96,
               CASE i_col_name1
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END,
               CASE i_col_name2
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END,
               CASE i_col_name3
               WHEN 'COLUMN1' THEN m.column1
               WHEN 'COLUMN2' THEN m.column2
               WHEN 'COLUMN3' THEN m.column3
               WHEN 'COLUMN4' THEN m.column4
               END
      )
      GROUP BY period_start, group_name
      ORDER BY period_start;
    
      RETURN s_metrics;
    END get_metrics_summary;