Skip to content

Initial Oracle Database support to the SQL Explain panel - #2448

Open
luzfcb wants to merge 1 commit into
django-commons:mainfrom
luzfcb:initial_oracle_support
Open

Initial Oracle Database support to the SQL Explain panel#2448
luzfcb wants to merge 1 commit into
django-commons:mainfrom
luzfcb:initial_oracle_support

Conversation

@luzfcb

@luzfcb luzfcb commented Aug 14, 2026

Copy link
Copy Markdown

Problem:

django-debug-toolbar returns an HTTP 500 error when attempting to use the Explain panel with Oracle.

Click here to display the source of error when using with Oracle
The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/debug_toolbar/decorators.py", line 34, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/debug_toolbar/decorators.py", line 46, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/debug_toolbar/panels/sql/views.py", line 61, in sql_explain
    result, headers = form.explain()
                      ^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/debug_toolbar/panels/sql/forms.py", line 83, in explain
    cursor.execute(f"EXPLAIN {sql}", params)
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/utils.py", line 122, in execute
    return super().execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/utils.py", line 79, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/utils.py", line 92, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/utils.py", line 100, in _execute
    with self.db.wrap_database_errors:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/django/db/backends/oracle/base.py", line 633, in execute
    return self.cursor.execute(query, self._param_generator(params))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/luzfcb/.virtualenvs/myprojectenv-3.12/lib/python3.12/site-packages/oracledb/cursor.py", line 708, in execute
    impl.execute(self)
  File "src/oracledb/impl/thin/cursor.pyx", line 277, in oracledb.thin_impl.ThinCursorImpl.execute
  File "src/oracledb/impl/thin/protocol.pyx", line 482, in oracledb.thin_impl.Protocol._process_single_message
  File "src/oracledb/impl/thin/protocol.pyx", line 483, in oracledb.thin_impl.Protocol._process_single_message
  File "src/oracledb/impl/thin/protocol.pyx", line 475, in oracledb.thin_impl.Protocol._process_message
  File "src/oracledb/impl/thin/messages/base.pyx", line 102, in oracledb.thin_impl.Message._check_and_raise_exception
django.db.utils.DatabaseError: ORA-00905: missing keyword
Help: https://docs.oracle.com/error-help/db/ora-00905/

Description

This PR solves the issue by introducing initial support for Oracle Database to django-debug-toolbar's SQL Explain panel, closing the 14-year-old issue #227.

Unlike databases that support single-step integrated query analysis (such as PostgreSQL's "EXPLAIN ANALYZE"), Oracle requires a multi-step orchestration workflow to execute an explain plan and format its output securely without session pollution.

Key Features & Implementations:

  1. Oracle SQL Explain Orchestration:

    • Introduced OracleExplainPlanHelper to encapsulate and run the multi-step EXPLAIN PLAN and DBMS_XPLAN.DISPLAY lifecycle.
    • Clean up temporary rows from PLAN_TABLE at the end of execution.
    • Suppress Query Block Registry (QBR) XML output on legacy Oracle connections (< 21) to prevent memory exhaustion and infinite recursion bugs on some markdown parsers of some LLM cli.
    • Include a schema health audit fallback that queries ALL_TABLES and ALL_INDEXES metadata to format and append table and index statistics (sizes, status, performance counters) as aligned ASCII tables.
  2. Query Tracking and Toolbar Integration:

    • Updated debug_toolbar/panels/sql/forms.py to hook SQLSelectForm and delegate select-explain execution to the new Oracle helper.
    • Updated debug_toolbar/panels/sql/tracking.py to filter out toolbar queries using vendor-specific quoting (quote_name).
  3. Infrastructure & CI/CD:

    • Expanded docker-compose.yml to provision local PostgreSQL, MariaDB, and Oracle Database Free containers for multi-backend testing.
    • Integrated Oracle Database test job in .github/workflows/test.yml and tox.ini with isolated test/coverage databases.
  4. Testing:

    • Added tests/panels/test_sql_oracle.py with rigorous unit and mocking coverage.
    • Updated integration tests for synchronous and asynchronous contexts.
**Click here to display how Explain will looks like on Oracle**

Screenshot

sql_explain_oracle

A real report

Plan hash value: 843816195
 
-----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                               | Name                        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                        |                             |    10 | 54340 |    15   (7)| 00:00:01 |
|*  1 |  VIEW                                   |                             |    10 | 54340 |    15   (7)| 00:00:01 |
|*  2 |   WINDOW SORT PUSHED RANK               |                             |   105 | 43680 |    15   (7)| 00:00:01 |
|*  3 |    HASH JOIN RIGHT OUTER                |                             |   105 | 43680 |    14   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL                   | DJANGO_CONTENT_TYPE         |    31 |  1271 |     4   (0)| 00:00:01 |
|   5 |     NESTED LOOPS                        |                             |   105 | 39375 |    10   (0)| 00:00:01 |
|   6 |      TABLE ACCESS BY INDEX ROWID        | AUTH_USER                   |     1 |   166 |     2   (0)| 00:00:01 |
|*  7 |       INDEX UNIQUE SCAN                 | SYS_C0032863                |     1 |       |     1   (0)| 00:00:01 |
|   8 |      TABLE ACCESS BY INDEX ROWID BATCHED| DJANGO_ADMIN_LOG            |   105 | 21945 |     8   (0)| 00:00:01 |
|*  9 |       INDEX RANGE SCAN                  | DJANGO_ADM_USER_ID_C564EBA6 |   105 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------------
 
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
 
   1 - SEL$2B60952E / from$_subquery$_006@SEL$4
   2 - SEL$2B60952E
   4 - SEL$2B60952E / DJANGO_CONTENT_TYPE@SEL$2
   6 - SEL$2B60952E / AUTH_USER@SEL$1
   7 - SEL$2B60952E / AUTH_USER@SEL$1
   8 - SEL$2B60952E / DJANGO_ADMIN_LOG@SEL$1
   9 - SEL$2B60952E / DJANGO_ADMIN_LOG@SEL$1
 
Outline Data
-------------
 
  /*+
      BEGIN_OUTLINE_DATA
      SWAP_JOIN_INPUTS(@"SEL$2B60952E" "DJANGO_CONTENT_TYPE"@"SEL$2")
      USE_HASH(@"SEL$2B60952E" "DJANGO_CONTENT_TYPE"@"SEL$2")
      USE_NL(@"SEL$2B60952E" "DJANGO_ADMIN_LOG"@"SEL$1")
      LEADING(@"SEL$2B60952E" "AUTH_USER"@"SEL$1" "DJANGO_ADMIN_LOG"@"SEL$1" "DJANGO_CONTENT_TYPE"@"SEL$2")
      FULL(@"SEL$2B60952E" "DJANGO_CONTENT_TYPE"@"SEL$2")
      BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$2B60952E" "DJANGO_ADMIN_LOG"@"SEL$1")
      INDEX_RS_ASC(@"SEL$2B60952E" "DJANGO_ADMIN_LOG"@"SEL$1" ("DJANGO_ADMIN_LOG"."USER_ID"))
      INDEX_RS_ASC(@"SEL$2B60952E" "AUTH_USER"@"SEL$1" ("AUTH_USER"."ID"))
      NO_ACCESS(@"SEL$4" "from$_subquery$_006"@"SEL$4")
      OUTLINE(@"SEL$2")
      OUTLINE(@"SEL$1")
      ANSI_REARCH(@"SEL$2")
      OUTLINE(@"SEL$1A566D0B")
      OUTLINE(@"SEL$3")
      MERGE(@"SEL$1" >"SEL$1A566D0B")
      OUTLINE(@"SEL$8B0CE372")
      ANSI_REARCH(@"SEL$3")
      OUTLINE(@"SEL$F52A8B21")
      OUTLINE_LEAF(@"SEL$4")
      MERGE(@"SEL$8B0CE372" >"SEL$F52A8B21")
      OUTLINE_LEAF(@"SEL$2B60952E")
      ALL_ROWS
      DB_VERSION('19.1.0')
      OPTIMIZER_FEATURES_ENABLE('19.1.0')
      IGNORE_OPTIM_EMBEDDED_HINTS
      END_OUTLINE_DATA
  */
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("from$_subquery$_006"."rowlimit_$$_rownumber"<=10)
   2 - filter(ROW_NUMBER() OVER ( ORDER BY INTERNAL_FUNCTION("DJANGO_ADMIN_LOG"."ACTION_TIME") DESC )<=10)
   3 - access("DJANGO_ADMIN_LOG"."CONTENT_TYPE_ID"="DJANGO_CONTENT_TYPE"."ID"(+))
   7 - access("AUTH_USER"."ID"=:ARG0)
   9 - access("DJANGO_ADMIN_LOG"."USER_ID"=:ARG0)
 
Column Projection Information (identified by operation id):
-----------------------------------------------------------
 
   1 - "from$_subquery$_006"."ITEM_0"[NUMBER,22], "from$_subquery$_006"."ITEM_1"[TIMESTAMP,11], 
       "from$_subquery$_006"."ITEM_2"[NUMBER,22], "from$_subquery$_006"."ITEM_3"[NUMBER,22], 
       "from$_subquery$_006"."ITEM_4"[LOB,4000], "from$_subquery$_006"."ITEM_5"[NVARCHAR2,400], 
       "from$_subquery$_006"."ITEM_6"[NUMBER,22], "from$_subquery$_006"."ITEM_7"[LOB,4000], 
       "from$_subquery$_006"."ITEM_8"[NUMBER,22], "from$_subquery$_006"."ITEM_9"[NVARCHAR2,256], 
       "from$_subquery$_006"."ITEM_10"[TIMESTAMP,11], "from$_subquery$_006"."ITEM_11"[NUMBER,22], 
       "from$_subquery$_006"."ITEM_12"[NVARCHAR2,300], "from$_subquery$_006"."ITEM_13"[NVARCHAR2,300], 
       "from$_subquery$_006"."ITEM_14"[NVARCHAR2,300], "from$_subquery$_006"."ITEM_15"[NVARCHAR2,508], 
       "from$_subquery$_006"."ITEM_16"[NUMBER,22], "from$_subquery$_006"."ITEM_17"[NUMBER,22], 
       "from$_subquery$_006"."ITEM_18"[TIMESTAMP,11], "from$_subquery$_006"."ITEM_19"[NUMBER,22], 
       "from$_subquery$_006"."ITEM_20"[NVARCHAR2,200], "from$_subquery$_006"."ITEM_21"[NVARCHAR2,200], 
       "from$_subquery$_006"."rowlimit_$$_rownumber"[NUMBER,22]
   2 - (#keys=1) INTERNAL_FUNCTION("DJANGO_ADMIN_LOG"."ACTION_TIME")[11], 
       "DJANGO_CONTENT_TYPE"."ID"[NUMBER,22], "DJANGO_ADMIN_LOG"."CONTENT_TYPE_ID"[NUMBER,22], 
       "DJANGO_CONTENT_TYPE"."MODEL"[NVARCHAR2,200], "DJANGO_CONTENT_TYPE"."APP_LABEL"[NVARCHAR2,200], 
       "AUTH_USER".ROWID[ROWID,10], "AUTH_USER"."ID"[NUMBER,22], "AUTH_USER"."PASSWORD"[NVARCHAR2,256], 
       "AUTH_USER"."LAST_LOGIN"[TIMESTAMP,11], "AUTH_USER"."IS_SUPERUSER"[NUMBER,22], 
       "AUTH_USER"."USERNAME"[NVARCHAR2,300], "AUTH_USER"."FIRST_NAME"[NVARCHAR2,300], 
       "AUTH_USER"."LAST_NAME"[NVARCHAR2,300], "AUTH_USER"."EMAIL"[NVARCHAR2,508], "AUTH_USER"."IS_STAFF"[NUMBER,22], 
       "AUTH_USER"."IS_ACTIVE"[NUMBER,22], "AUTH_USER"."DATE_JOINED"[TIMESTAMP,11], 
       "DJANGO_ADMIN_LOG".ROWID[ROWID,10], "DJANGO_ADMIN_LOG"."ID"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."USER_ID"[NUMBER,22], "DJANGO_ADMIN_LOG"."OBJECT_ID"[LOB,4000], 
       "DJANGO_ADMIN_LOG"."OBJECT_REPR"[NVARCHAR2,400], "DJANGO_ADMIN_LOG"."ACTION_FLAG"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."CHANGE_MESSAGE"[LOB,4000], ROW_NUMBER() OVER ( ORDER BY 
       INTERNAL_FUNCTION("DJANGO_ADMIN_LOG"."ACTION_TIME") DESC )[22]
   3 - (#keys=1) "DJANGO_CONTENT_TYPE"."ID"[NUMBER,22], "DJANGO_ADMIN_LOG"."CONTENT_TYPE_ID"[NUMBER,22], 
       "DJANGO_CONTENT_TYPE"."MODEL"[NVARCHAR2,200], "DJANGO_CONTENT_TYPE"."APP_LABEL"[NVARCHAR2,200], 
       "AUTH_USER".ROWID[ROWID,10], "AUTH_USER"."ID"[NUMBER,22], "AUTH_USER"."PASSWORD"[NVARCHAR2,256], 
       "AUTH_USER"."LAST_LOGIN"[TIMESTAMP,11], "AUTH_USER"."IS_SUPERUSER"[NUMBER,22], 
       "AUTH_USER"."USERNAME"[NVARCHAR2,300], "AUTH_USER"."FIRST_NAME"[NVARCHAR2,300], 
       "AUTH_USER"."LAST_NAME"[NVARCHAR2,300], "AUTH_USER"."EMAIL"[NVARCHAR2,508], "AUTH_USER"."IS_STAFF"[NUMBER,22], 
       "AUTH_USER"."IS_ACTIVE"[NUMBER,22], "AUTH_USER"."DATE_JOINED"[TIMESTAMP,11], 
       "DJANGO_ADMIN_LOG".ROWID[ROWID,10], "DJANGO_ADMIN_LOG"."ID"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."ACTION_TIME"[TIMESTAMP,11], "DJANGO_ADMIN_LOG"."OBJECT_ID"[LOB,4000], 
       "DJANGO_ADMIN_LOG"."OBJECT_REPR"[NVARCHAR2,400], "DJANGO_ADMIN_LOG"."ACTION_FLAG"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."CHANGE_MESSAGE"[LOB,4000], "DJANGO_ADMIN_LOG"."USER_ID"[NUMBER,22]
   4 - (rowset=256) "DJANGO_CONTENT_TYPE"."ID"[NUMBER,22], "DJANGO_CONTENT_TYPE"."APP_LABEL"[NVARCHAR2,200], 
       "DJANGO_CONTENT_TYPE"."MODEL"[NVARCHAR2,200]
   5 - (#keys=0) "AUTH_USER".ROWID[ROWID,10], "AUTH_USER"."ID"[NUMBER,22], 
       "AUTH_USER"."PASSWORD"[NVARCHAR2,256], "AUTH_USER"."LAST_LOGIN"[TIMESTAMP,11], 
       "AUTH_USER"."IS_SUPERUSER"[NUMBER,22], "AUTH_USER"."USERNAME"[NVARCHAR2,300], 
       "AUTH_USER"."FIRST_NAME"[NVARCHAR2,300], "AUTH_USER"."LAST_NAME"[NVARCHAR2,300], 
       "AUTH_USER"."EMAIL"[NVARCHAR2,508], "AUTH_USER"."IS_STAFF"[NUMBER,22], "AUTH_USER"."IS_ACTIVE"[NUMBER,22], 
       "AUTH_USER"."DATE_JOINED"[TIMESTAMP,11], "DJANGO_ADMIN_LOG".ROWID[ROWID,10], 
       "DJANGO_ADMIN_LOG"."ID"[NUMBER,22], "DJANGO_ADMIN_LOG"."ACTION_TIME"[TIMESTAMP,11], 
       "DJANGO_ADMIN_LOG"."OBJECT_ID"[LOB,4000], "DJANGO_ADMIN_LOG"."OBJECT_REPR"[NVARCHAR2,400], 
       "DJANGO_ADMIN_LOG"."ACTION_FLAG"[NUMBER,22], "DJANGO_ADMIN_LOG"."CHANGE_MESSAGE"[LOB,4000], 
       "DJANGO_ADMIN_LOG"."CONTENT_TYPE_ID"[NUMBER,22], "DJANGO_ADMIN_LOG"."USER_ID"[NUMBER,22]
   6 - "AUTH_USER".ROWID[ROWID,10], "AUTH_USER"."ID"[NUMBER,22], "AUTH_USER"."PASSWORD"[NVARCHAR2,256], 
       "AUTH_USER"."LAST_LOGIN"[TIMESTAMP,11], "AUTH_USER"."IS_SUPERUSER"[NUMBER,22], 
       "AUTH_USER"."USERNAME"[NVARCHAR2,300], "AUTH_USER"."FIRST_NAME"[NVARCHAR2,300], 
       "AUTH_USER"."LAST_NAME"[NVARCHAR2,300], "AUTH_USER"."EMAIL"[NVARCHAR2,508], "AUTH_USER"."IS_STAFF"[NUMBER,22], 
       "AUTH_USER"."IS_ACTIVE"[NUMBER,22], "AUTH_USER"."DATE_JOINED"[TIMESTAMP,11]
   7 - "AUTH_USER".ROWID[ROWID,10], "AUTH_USER"."ID"[NUMBER,22]
   8 - "DJANGO_ADMIN_LOG".ROWID[ROWID,10], "DJANGO_ADMIN_LOG"."ID"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."ACTION_TIME"[TIMESTAMP,11], "DJANGO_ADMIN_LOG"."OBJECT_ID"[LOB,4000], 
       "DJANGO_ADMIN_LOG"."OBJECT_REPR"[NVARCHAR2,400], "DJANGO_ADMIN_LOG"."ACTION_FLAG"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."CHANGE_MESSAGE"[LOB,4000], "DJANGO_ADMIN_LOG"."CONTENT_TYPE_ID"[NUMBER,22], 
       "DJANGO_ADMIN_LOG"."USER_ID"[NUMBER,22]
   9 - "DJANGO_ADMIN_LOG".ROWID[ROWID,10], "DJANGO_ADMIN_LOG"."USER_ID"[NUMBER,22]
 
Schema Health & Statistics Audit (ALL_TABLES & ALL_INDEXES)
================================================================
Table Statistics:
+-----------------+--------------------------------+------------+------------+-------------+---------------------+
| Owner           | Table Name                     | Num Rows   | Blocks     | Avg Row Len | Last Analyzed       |
+-----------------+--------------------------------+------------+------------+-------------+---------------------+
| DUSER           | AUTH_USER                      | 712        | 22         | 166         | 2026-06-05 22:01:29 |
| DUSER           | DJANGO_ADMIN_LOG               | 840        | 30         | 209         | 2026-05-27 22:00:10 |
| DUSER           | DJANGO_CONTENT_TYPE            | 31         | 8          | 41          | 2026-07-04 14:03:27 |
+-----------------+--------------------------------+------------+------------+-------------+---------------------+

Index Statistics & Status:
+-----------------+--------------------------------+---------------------------+------------+----------+---------------------+
| Owner           | Index Name                     | Table Name                | Uniqueness | Status   | Last Analyzed       |
+-----------------+--------------------------------+---------------------------+------------+----------+---------------------+
| DUSER           | SYS_C0032862                   | AUTH_USER                 | UNIQUE     | VALID    | 2026-06-05 22:01:29 |
| DUSER           | SYS_C0032863                   | AUTH_USER                 | UNIQUE     | VALID    | 2026-06-05 22:01:29 |
| DUSER           | DJANGO_ADM_CONTENT_TY_C4BCE8EB | DJANGO_ADMIN_LOG          | NONUNIQUE  | VALID    | 2026-05-27 22:00:10 |
| DUSER           | DJANGO_ADM_USER_ID_C564EBA6    | DJANGO_ADMIN_LOG          | NONUNIQUE  | VALID    | 2026-05-27 22:00:10 |
| DUSER           | SYS_C0032847                   | DJANGO_ADMIN_LOG          | UNIQUE     | VALID    | 2026-05-27 22:00:10 |
| DUSER           | DJANGO_CO_APP_LABEL_76BD3D3B_U | DJANGO_CONTENT_TYPE       | UNIQUE     | VALID    | 2026-07-04 14:03:27 |
| DUSER           | SYS_C0032820                   | DJANGO_CONTENT_TYPE       | UNIQUE     | VALID    | 2026-07-04 14:03:27 |
+-----------------+--------------------------------+---------------------------+------------+----------+---------------------+

Checklist:

  • I have added the relevant tests for this change.
  • I have added an item to the Pending section of docs/changes.rst.

AI/LLM Usage

  • This PR includes code generated with the help of an AI/LLM

Introduce comprehensive support for Oracle Database to the SQL Explain
panel in django-debug-toolbar. Unlike databases that support single-step
integrated query analysis (such as PostgreSQL's "EXPLAIN ANALYZE"),
Oracle requires a multi-step orchestration workflow to execute an
explain plan and format its output securely without session pollution.

Key Implementations & Changes:

1. Core Oracle Explain Orchestration:
   - Implement `OracleExplainPlanHelper` to encapsulate the multi-step
     explain plan lifecycle.
   - Generate temporary execution nodes under a session-safe, unique
     `STATEMENT_ID` (`dt_<uuid>`) within the session's `PLAN_TABLE`.
   - Query `DBMS_XPLAN.DISPLAY` with `ADVANCED +ADAPTIVE` flags to
     retrieve structured, human-readable execution plans.
   - Clean up temporary rows from `PLAN_TABLE` at the end of execution.
   - Suppress Query Block Registry (QBR) XML output on legacy Oracle
     connections (< 21) to prevent memory exhaustion and infinite
     recursion bugs on terminal client markdown parsers.
   - Include a graceful schema health audit fallback that queries
     `ALL_TABLES` and `ALL_INDEXES` metadata to format and append table
     and index statistics (sizes, status, performance counters) as
     aligned ASCII tables, sanitizing permission/database errors.

2. Query Tracking and Integration:
   - Update `debug_toolbar/panels/sql/forms.py` to hook `SQLSelectForm`
     and delegate select-explain execution to the new Oracle helper
     when connected to an Oracle database.
   - Update `debug_toolbar/panels/sql/tracking.py` to escape and filter
     out toolbar queries using vendor-specific quoting (`quote_name`)
     to prevent the toolbar's internal queries from showing up in and
     polluting SQL panels.

3. Docker & Infrastructure Support:
   - Introduce `docker-compose.yml` to provision local PostgreSQL,
     MariaDB, and Oracle Database Free containers to facilitate
     comprehensive multi-backend integration testing.
   - Update `.github/workflows/test.yml` and `tox.ini` to define and
     run tests against Oracle, and also ensuring coverage databases
     do not collide during parallel test execution.

4. Comprehensive Test Coverage:
   - Introduce `tests/panels/test_sql_oracle.py` with rigorous unit
     tests mocking and verifying the helper's edge cases, legacy vs.
     modern QBR handling, error boundary sanitization, and fallback
     options.
   - Add integration tests verifying correct explain output and
     formatting across both synchronous and asynchronous contexts.

Fixes django-commons#227
@luzfcb
luzfcb force-pushed the initial_oracle_support branch from 3aca578 to 3404c4b Compare August 14, 2026 22:07
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  debug_toolbar/panels
  profiling.py
  debug_toolbar/panels/sql
  forms.py
  oracle_helper.py
  tracking.py
  utils.py
  views.py
Project Total  

This report was generated by python-coverage-comment-action

@tim-schilling

Copy link
Copy Markdown
Member

At first glance, this may be too big of a feature to merge in without knowing how many people would utilize this. It's a lot more code to maintain for a specific vendor. An alternative is to create a OracleSQLPanel as a separate package and list it as a third-party panel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants