rebenitez1802 commented on code in PR #45675:
URL: https://github.com/apache/superset/pull/45675#discussion_r4402211565
##########
superset/models/helpers.py:
##########
@@ -6098,7 +6100,7 @@ def get_sqla_query( # pylint:
disable=too-many-arguments,too-many-locals,too-ma
def _create_join_condition(col_name: str, expr: Any) ->
Any:
# Get the corresponding column from the subquery
subq_col_name = db_engine_spec.make_label_compatible(
- col_name + "__"
+ col_name + "__", self.database
)
# Reference the column from the already-created
aliased subquery
subq_col = subq_alias.c[subq_col_name]
Review Comment:
🔴 High — "Group remaining as Others" crashes when Oracle hashes a series
label
`make_sqla_column_compatible` sets `sqla_col.key = label_expected`, so
`subq_alias.c` is keyed by `col_name + "__"`, not by the hashed alias. On
Oracle before 12.2, a series limit with "Others" on a series column of 29 or
more bytes now raises `KeyError: '0f979e1f19fd1f30ca919a1be3168a'` (a 500)
instead of running. A 12.2+ database whose Version is set to `12c` builds this
query on master and fails on this branch. Looking the column up by its expected
label fixes it, and it also fixes the same crash that master already has for
long Postgres columns:
```suggestion
# The subquery keys its columns by the expected
label, which
# differs from the SQL alias when the engine
shortened it
subq_col = subq_alias.c[col_name + "__"]
```
A test that builds the query with `group_others_when_limit_reached=True`,
`series_limit`, and a 29-byte series column on version `11.2.0.4` fails with
the KeyError on this branch and passes with the change.
##########
superset/db_engine_specs/oracle.py:
##########
@@ -86,24 +90,57 @@ def get_limit_method(cls, database: Database) ->
LimitMethod:
return cls.limit_method
+ @classmethod
+ def get_max_column_name_length(cls, database: Database) -> int | None:
+ """
+ Cap labels at 30 bytes on Oracle releases before 12.2.
+
+ Those releases reject longer labels with "ORA-00972: identifier is too
+ long". A version given only as "12c" is treated as 12.1, since 30 bytes
+ is accepted by every release.
+ """
+ version = cls.get_server_version(database)
+ if version is not None and version < LONG_IDENTIFIERS_VERSION:
+ return ORACLE_LEGACY_MAX_IDENTIFIER_LENGTH
+
+ return cls.max_column_name_length
+
+ @staticmethod
+ def _label_length(label: str) -> int:
+ """
+ Oracle limits identifiers in bytes, so count the label's UTF-8 bytes.
+
+ A label of non-ASCII characters can be too long with fewer characters
+ than the limit.
+ """
+ return len(label.encode())
+
@classmethod
def get_server_major_version(cls, database: Database) -> int | None:
"""
Return the major version of the Oracle server, or `None` if unknown.
+ """
+ version = cls.get_server_version(database)
+ return version[0] if version is not None else None
+
+ @classmethod
+ def get_server_version(cls, database: Database) -> tuple[int, ...] | None:
+ """
+ Return the version of the Oracle server, or `None` if unknown.
A version specified in the DB extra wins. Oracle names its releases
"11g", "12c" or "23ai", or by a dotted number like "11.2.0.4", so only
- the leading major number is read. Otherwise the version is read from
the
- server once and cached per database. SQLAlchemy already knows it if the
- engine has connected before; if not, this costs one connection per
+ the leading dotted numbers are read. Otherwise the version is read from
+ the server once and cached per database. SQLAlchemy already knows it if
+ the engine has connected before; if not, this costs one connection per
process.
"""
version = database.get_extra().get("version")
- if match := re.match(r"\s*(\d+)", str(version or "")):
- return int(match.group(1))
+ if match := re.match(r"\s*(\d+(?:\.\d+)*)", str(version or "")):
+ return tuple(int(part) for part in match.group(1).split("."))
key = f"{database.id}:{database.sqlalchemy_uri}"
- if key not in _server_major_versions:
+ if key not in _server_versions:
Review Comment:
🔴 High — A failed version lookup is retried once per label
`make_label_compatible` now reaches this lookup for every label, and a
failed lookup isn't remembered. With no Version set and the database down or
the password wrong, building one chart query tries to connect about 20 times;
master tries 0 times for "View query" and once to run the chart. With
python-oracledb's default 20-second connect timeout, one chart can hang for
about 400 seconds and hold a web worker, and a rotated password produces about
20 failed logins per chart. This is different from the earlier thread: the
retries happen many times inside one query, not once on the next query.
Remembering the failure for the current request or task keeps "try again
next time" and limits it to one attempt per request:
```python
from flask import g, has_app_context
...
key = f"{database.id}:{database.sqlalchemy_uri}"
if key not in _server_versions:
# Every label of a chart query asks for the version, so a failed
# lookup is retried on the next request rather than once per
label.
failed: set[str] = (
g.setdefault("oracle_version_lookup_failures", set())
if has_app_context()
else set()
)
if key in failed:
return None
try:
... # unchanged
except Exception: # pylint: disable=broad-except
logger.warning(
"Unable to read the Oracle server version", exc_info=True
)
failed.add(key)
return None
if not version_info:
failed.add(key)
return None
```
With this change, `test_get_limit_method_unknown_version` needs its second
call inside a new `app.app_context()`, and a test that builds 20 labels against
an unreachable database can assert a single `get_sqla_engine` call.
--
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]