Skip to main content

Oracle APEX - Dynamic Reporting

Oracle APEX • PL/SQL • Dynamic Reporting


Have you ever needed to build a dashboard where users select a report from a dropdown list, and the page dynamically renders the query results? A common design pattern is to centralize your report queries in a database table:

REPORTS_TABLE (QUERY_ID, REPORT_NAME, QUERIES, CREATE_BY, CREATE_DATE)

However, if you've tried implementing this with an Interactive Report or Interactive Grid, you likely ran into database metadata conflicts or compilation errors when switching between reports.

In this post, we will explore why standard reports struggle with dynamic structures and provide a step-by-step guide to building a fully dynamic report viewer using Classic Reports and Generic Columns in Oracle APEX.

The Challenge: Fixed Metadata vs. Dynamic Columns

Oracle APEX Interactive Reports (IR) and Interactive Grids (IG) are highly interactive tools that allow end-users to sort, filter, and group data. To provide this functionality, APEX wraps your query in outer SQL statements. This requires APEX to know the exact column names, data types, and column counts at page design time.

If your stored queries return different columns (e.g., Query A returns 3 columns, Query B returns 8 columns), IR and IG will fail with ORA-00904: invalid identifier errors.

The Solution: Classic Reports combined with the Generic Columns option. This configuration maps runtime query columns to generic placeholders (COL01, COL02 ... COL60), allowing the page to render any valid SQL query on the fly.

Step-by-Step Implementation Guide

Step 1: Create the Select List Item

First, create a Select List item on your page (we'll name it P1_REPORT_SELECT) to let users choose which report to display.

  • Type: Select List
  • List of Values > Type: SQL Query
  • SQL Query:
select report_name as display_value,
       query_id    as return_value
  from reports_table
 order by report_name;

Step 2: Create the Classic Report Region

Create a new region on your page where the dynamic data will be displayed.

  • Title: Dynamic Report Viewer
  • Type: Classic Report
  • Source > Location: Local Database
  • Source > Type: PL/SQL Function Body returning SQL Query
  • PL/SQL Function Body:
declare
    l_query varchar2(32767);
begin
    -- Return a placeholder message if no report is selected
    if :P1_REPORT_SELECT is null then
        return 'select ''Please select a report from the list above.'' as message from dual';
    end if;

    -- Fetch the dynamic query text
    select queries
      into l_query
      from reports_table
     where query_id = :P1_REPORT_SELECT;

    return l_query;
exception
    when no_data_found then
        return 'select ''Error: Selected report query not found.'' as error_message from dual';
    when others then
        return 'select ''' || sqlerrm || ''' as error_message from dual';
end;

*Note: Remember to add P1_REPORT_SELECT to Page Items to Submit.

Step 3: Configure Generic Columns

  1. Navigate to the Classic Report's Region tab in the Page Designer.
  2. Scroll down to the Source section.
  3. Set Use Generic Column Names to Yes.
  4. Set Amount of Generic Columns to the maximum number of columns you expect your largest query to return (e.g., 60).

Step 4: Generate Dynamic Column Headers

Since APEX maps the columns to generic names like COL01, your table headers will display as "Col 1", "Col 2". To show the real headers, use `DBMS_SQL` to pull dynamic metadata:

  1. Go to the Classic Report Attributes tab > Heading section.
  2. Set Type to PL/SQL Function Body.
  3. Enter this PL/SQL block:
declare
    l_query       varchar2(32767);
    l_cursor      integer;
    l_col_cnt     integer;
    l_desc_tab    dbms_sql.desc_tab;
    l_headers     varchar2(4000);
begin
    if :P1_REPORT_SELECT is null then
        return 'Message';
    end if;

    -- Fetch query text
    select queries
      into l_query
      from reports_table
     where query_id = :P1_REPORT_SELECT;

    -- Open cursor and describe the query columns
    l_cursor := dbms_sql.open_cursor;
    dbms_sql.parse(l_cursor, l_query, dbms_sql.native);
    dbms_sql.describe_columns(l_cursor, l_col_cnt, l_desc_tab);
    dbms_sql.close_cursor(l_cursor);

    -- Concatenate column names separated by colons (:)
    for i in 1..l_col_cnt loop
        if i > 1 then
            l_headers := l_headers || ':';
        end if;
        l_headers := l_headers || l_desc_tab(i).col_name;
    end loop;

    return l_headers;
exception
    when others then
        if dbms_sql.is_open(l_cursor) then
            dbms_sql.close_cursor(l_cursor);
        end if;
        return 'Error';
end;

Step 5: Refresh the Report via AJAX

Make the UI responsive with a Dynamic Action so the report updates instantly when a new selection is made.

  1. Right-click on P1_REPORT_SELECT and select Create Dynamic Action.
  2. Name: Refresh Report
  3. Event: Change
  4. True Action > Action: Refresh
  5. True Action > Selection Type: Region
  6. True Action > Region: [Select your Classic Report Region]

Report Type Comparison Matrix

Here is a comparison of how different report types in Oracle APEX handle dynamic SQL structures:

Feature / Capability Classic Report (Generic) Interactive Report (IR) Interactive Grid (IG)
Supports Dynamic Columns? Yes No No
Dynamic Column Headers? Yes (via PL/SQL) No No
End-User Filters & Sorting? Manual Coding Only Yes Yes
Data Editing (DML)? No No Yes

Security Warnings & Best Practices

WARNING: Executing raw queries stored in a database table introduces a significant SQL Injection risk. If unauthorized users gain access to modify your reports config table, they can write destructive database queries.

To keep your APEX application secure, implement these best practices:

  • Authorization Schemes: Restrict the admin screen where queries are edited or created to high-privilege Developer/Admin roles only.
  • SQL Parsing Validation: Add a database trigger or validation block to verify that the query text only starts with a SELECT token (and does not contain DDL operations like DROP or ALTER).
  • Read-only DB Schema: Where possible, have the APEX parsing schema execute queries in a read-only context.

Conclusion

By using Classic Reports and the Generic Columns feature, APEX developers can bypass database compile-time design constraints and build highly flexible, dynamic dashboard tools. This minimizes page count, keeping your workspace clean and centralized.

Comments

Popular posts from this blog

APEX - Tip: Fix Floating Label Issue

Oracle APEX's Universal Theme provides a modern and clean user experience through features like floating (above) labels for page items.  These floating labels work seamlessly when users manually enter data, automatically moving the label above the field on focus or input.  However, a common UI issue appears when page item values are set Dynamically the label and the value overlap, resulting in a broken and confusing user interface. once the user focuses the affected item even once, the label immediately corrects itself and displays properly. When an issue is reported, several values are populated based on a single user input, causing the UI to appear misaligned and confusing for the end user. Here, I'll share a few tips to fix this issue. For example, employee details are populated based on the Employee name. In this case, the first True Action is used to set the values, and in the second True Action, paste the following code setTimeout(function () {   $("#P29_EMAIL,#P29_...

Oracle APEX UI Tip: Display Page Title Next to the APEX Logo

In most Oracle APEX applications, every page has a Page Title displayed at the top. While useful, this title occupies vertical space, especially in apps where screen real estate matters (dashboards, reports, dense forms). So the goal is simple: Show the page title near the APEX logo instead of consuming page content space. This keeps the UI clean, professional, and consistent across all pages. Instead of placing the page title inside the page body:         ✅ Fetch the current page title dynamically         ✅ Display it right after the APEX logo         ✅ Do it globally, so it works for every page All of this is achieved using:         ✅ Global Page (Page 0)         ✅ One Dynamic Action         ✅ PL/SQL + JavaScript Simple, effective, and reusable. 1️⃣ Create a Global Page Item On Page 0 (Global Page), create a hidden item:      P0_PAGE_TITLE This item wi...

Building a Custom Debug Package for Oracle APEX Using PL/SQL

While developing Oracle APEX applications, debugging page processes and backend PL/SQL logic can be challenging—especially when values are lost between processes or execution flow is unclear.  Although DBMS_OUTPUT is useful, it doesn’t work well inside APEX runtime. To solve this, I built a custom PL/SQL debug Package that logs execution flow and variable values into a database table.  This approach helps trace exactly where the code reached, what values were passed, and whether a block executed or not - even inside page-level processes and packaged procedures Why a Custom Debug Package? Works seamlessly inside Oracle APEX page processes Persists debug information even after session ends Helps trace execution flow Captures runtime values Can be turned ON/OFF dynamically Does not interrupt business logic The Package consists of:- Debug Table                         -  Stores debug messages Sequence ...