kaya-abdullah opened a new pull request, #45309:
URL: https://github.com/apache/superset/pull/45309
### SUMMARY
Oracle added `FETCH FIRST n ROWS ONLY` in 12c, and sqlglot renders every
Oracle limit that way. So on 10g and 11g every SQL Lab query fails with
"ORA-00933: SQL command not properly ended", because Superset adds a row limit
to it (#35197, #38520).
SQLAlchemy switches its own Oracle limits to `ROWNUM` below 12c, but only
after the engine's first connection, since that is when it learns the server
version. Superset compiles a chart's query before running it, so the first
chart on a new engine, for example right after a restart, fails on 11g with the
same error, and later ones work.
This PR fixes both.
**SQL that Superset limits (SQL Lab, `apply_limit_to_sql`).** On servers
older than 12c the query is wrapped:
SELECT * FROM (<original query>) WHERE ROWNUM <= n
`ROWNUM` is assigned after the inner `ORDER BY`, so top-n queries still
return the top rows. CTEs are moved to the outer statement, like the existing
wrap in `cap_limit_value`. The server version is detected, so 11g works without
any setup:
- If the database `version` extra (Advanced > Other > Version) is set, it is
used and no connection is made. Values like `11g`, `11.2.0.4` and `19c` work,
since only the major number is read.
- Otherwise the version is taken from SQLAlchemy's `server_version_info`,
connecting once if the engine hasn't connected yet, and cached per Oracle
database. If it can't be read, nothing changes and the query keeps `FETCH
FIRST`.
12c and later keep `FETCH FIRST` on purpose. I first tried `ROWNUM` for
every version, as suggested in #35197, but on Oracle 23ai that breaks queries
with duplicate output names, for example `SELECT a.id, b.id FROM a JOIN b ...`:
`FETCH FIRST` accepts them, the `ROWNUM` wrapper fails with ORA-00918. On 11g
the wrapper is the only option, so repeated output names get a `_2`, `_3`, ...
suffix before wrapping, kept within Oracle's 30-byte limit for those releases.
Metabase's Oracle driver handles its wrapped queries the same way.
**SQL that SQLAlchemy compiles (charts).**
`OracleEngineSpec.register_engine_events` starts each new engine's dialect on
`ROWNUM`. That works on every release, and SQLAlchemy's first connection sets
it back to `FETCH FIRST` for 12c and later, so nothing changes there after the
first query. This needs no extra connection.
Changes:
- `BaseEngineSpec.get_limit_method(database)`, mirroring
`get_allows_alias_in_select`, returns `limit_method` by default. The code that
applies a limit (SQL Lab, `SQLExecutor`, `Database.apply_limit_to_sql`) calls
it instead of reading the attribute.
- `OracleEngineSpec.get_limit_method` returns the new
`LimitMethod.WRAP_ROWNUM` below 12c, using the configured or detected version.
- `OracleEngineSpec.register_engine_events` compiles limits with `ROWNUM`
until SQLAlchemy knows the server version.
- `SQLStatement.get_limit_value` also reads a top-level `ROWNUM <= n` (or `<
n`) filter on Oracle, so a user's own `ROWNUM` limit is kept by
`cap_limit_value` and the wrapper isn't stacked on itself.
- Note on Oracle in the engine spec metadata.
The version detection only runs for Oracle databases without a `version`
extra, and opens at most one connection per Oracle database and process, only
if the engine hasn't connected yet. Other engines keep using the `limit_method`
attribute and never connect for this.
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
Superset master (`apache/superset:367b6f5-dev`) against Oracle XE 11.2.0.2,
no `version` extra. Before is the image as is, after is the same image with
this branch's files.
SQL Lab, `SELECT * FROM nums`
Before:
SCREENSHOT_1_BEFORE
After:
SCREENSHOT_1_AFTER
First chart after a restart
Before:
SCREENSHOT_2_BEFORE
After:
SCREENSHOT_2_AFTER
SQL Lab, join returning two `id` columns
Before:
SCREENSHOT_3_BEFORE
After:
SCREENSHOT_3_AFTER
### TESTING INSTRUCTIONS
Unit tests:
pytest tests/unit_tests/db_engine_specs/test_oracle.py
tests/unit_tests/sql/parse_tests.py tests/unit_tests/models/core_test.py -k
"rownum or limit_method or limit_value or apply_limit or deduplicate or
register_engine_events"
In the UI, `apache/superset:367b6f5-dev` with and without this branch's
files, against `gvenzl/oracle-xe:11` (11.2.0.2), no `version` extra,
python-oracledb in thick mode (Instant Client 19, needed for 11g). Both
containers were restarted before the chart step, so it is the first query on a
new engine:
| Step | Before | After |
|---|---|---|
| SQL Lab, `SELECT * FROM nums` (the query from #38520) | ORA-00933 | 20
rows |
| SQL Lab, join returning two `id` columns | ORA-00933 | 50 rows, columns
`ID`, `ID_2` |
| SQL Lab, running the table's generated `SELECT` (`/select_star/`) |
ORA-00933 | 50 rows |
| Table chart, first query after a restart | ORA-00933 | 10 rows |
| Same chart, second query | 10 rows | 10 rows |
The same code paths were also run in-process against 11.2.0.2 and 23ai with
both python-oracledb and cx_Oracle 8.3, the driver from #38520. On 23ai nothing
changes except that a chart compiled before the engine's first connection uses
`ROWNUM` once. SQL Lab shapes checked on both servers: `SELECT 1 FROM dual`,
top-n with `ORDER BY ... DESC`, a CTE, `UNION ALL`, `GROUP BY`, a window
function, `DISTINCT`, `SELECT *`, a query that already has `WHERE ROWNUM <= 3`,
a join returning two `n` columns, a parenthesized query, and a query formatted
with Format SQL first. All return the same rows on both servers.
### ADDITIONAL INFORMATION
- [x] Has associated issue: Fixes #35197, fixes #38520
- [ ] Required feature flags:
- [ ] Changes UI
- [ ] Includes DB Migration (follow approval process in
[SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [ ] Introduces new feature or API
- [ ] Removes existing feature or API
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]