This is an automated email from the ASF dual-hosted git repository. maximebeauchemin pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push: new fe9eef9198 feat: removing dup logic in sqla/models.py and models/helpers.py (#34177) fe9eef9198 is described below commit fe9eef9198e09c1ca543c1179f1053d2fc6daf7c Author: Maxime Beauchemin <maximebeauche...@gmail.com> AuthorDate: Tue Jul 15 14:02:57 2025 -0700 feat: removing dup logic in sqla/models.py and models/helpers.py (#34177) --- superset/connectors/sqla/models.py | 167 ++----------------------------------- superset/models/helpers.py | 9 +- 2 files changed, 14 insertions(+), 162 deletions(-) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index e21bb012a4..5bbafda72d 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -20,15 +20,12 @@ from __future__ import annotations import builtins import dataclasses import logging -import re from collections import defaultdict from collections.abc import Hashable from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Any, Callable, cast, Optional, Union -import dateutil.parser -import numpy as np import pandas as pd import sqlalchemy as sa from flask_appbuilder import Model @@ -78,7 +75,6 @@ from superset.connectors.sqla.utils import ( get_physical_table_metadata, get_virtual_table_metadata, ) -from superset.constants import EMPTY_STRING, NULL_STRING from superset.db_engine_specs.base import BaseEngineSpec, TimestampExpression from superset.exceptions import ( ColumnNotFoundException, @@ -108,8 +104,6 @@ from superset.sql.parse import Table from superset.superset_typing import ( AdhocColumn, AdhocMetric, - FilterValue, - FilterValues, Metric, QueryObjectDict, ResultSetColumnType, @@ -506,66 +500,6 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable= return data - @staticmethod - def filter_values_handler( # pylint: disable=too-many-arguments # noqa: C901 - values: FilterValues | None, - operator: str, - target_generic_type: utils.GenericDataType, - target_native_type: str | None = None, - is_list_target: bool = False, - db_engine_spec: builtins.type[BaseEngineSpec] | None = None, - db_extra: dict[str, Any] | None = None, - ) -> FilterValues | None: - if values is None: - return None - - def handle_single_value(value: FilterValue | None) -> FilterValue | None: - if operator == utils.FilterOperator.TEMPORAL_RANGE: - return value - if ( - isinstance(value, (float, int)) - and target_generic_type == utils.GenericDataType.TEMPORAL - and target_native_type is not None - and db_engine_spec is not None - ): - value = db_engine_spec.convert_dttm( - target_type=target_native_type, - dttm=datetime.utcfromtimestamp(value / 1000), - db_extra=db_extra, - ) - value = literal_column(value) - if isinstance(value, str): - value = value.strip("\t\n") - - if ( - target_generic_type == utils.GenericDataType.NUMERIC - and operator - not in { - utils.FilterOperator.ILIKE, - utils.FilterOperator.LIKE, - } - ): - # For backwards compatibility and edge cases - # where a column data type might have changed - return utils.cast_to_num(value) - if value == NULL_STRING: - return None - if value == EMPTY_STRING: - return "" - if target_generic_type == utils.GenericDataType.BOOLEAN: - return utils.cast_to_boolean(value) - return value - - if isinstance(values, (list, tuple)): - values = [handle_single_value(v) for v in values] # type: ignore - else: - values = handle_single_value(values) - if is_list_target and not isinstance(values, (tuple, list)): - values = [values] # type: ignore - elif not is_list_target and isinstance(values, (tuple, list)): - values = values[0] if values else None - return values - def external_metadata(self) -> list[ResultSetColumnType]: """Returns column information from the external system""" raise NotImplementedError() @@ -1227,29 +1161,10 @@ class SqlaTable( def db_extra(self) -> dict[str, Any]: return self.database.get_extra() - @staticmethod - def _apply_cte(sql: str, cte: str | None) -> str: - """ - Append a CTE before the SELECT statement if defined - - :param sql: SELECT statement - :param cte: CTE statement - :return: - """ - if cte: - sql = f"{cte}\n{sql}" - return sql - @property def db_engine_spec(self) -> __builtins__.type[BaseEngineSpec]: return self.database.db_engine_spec - @property - def changed_by_name(self) -> str: - if not self.changed_by: - return "" - return str(self.changed_by) - @property def connection(self) -> str: return str(self.database) @@ -1452,11 +1367,6 @@ class SqlaTable( def get_template_processor(self, **kwargs: Any) -> BaseTemplateProcessor: return get_template_processor(table=self, database=self.database, **kwargs) - def get_query_str(self, query_obj: QueryObjectDict) -> str: - query_str_ext = self.get_query_str_extended(query_obj) - all_queries = query_str_ext.prequeries + [query_str_ext.sql] - return ";\n\n".join(all_queries) + ";" - def get_sqla_table(self) -> TableClause: tbl = table(self.table_name) if self.schema: @@ -1588,38 +1498,6 @@ class SqlaTable( ) return self.make_sqla_column_compatible(sqla_column, label) - def make_orderby_compatible( - self, select_exprs: list[ColumnElement], orderby_exprs: list[ColumnElement] - ) -> None: - """ - If needed, make sure aliases for selected columns are not used in - `ORDER BY`. - - In some databases (e.g. Presto), `ORDER BY` clause is not able to - automatically pick the source column if a `SELECT` clause alias is named - the same as a source column. In this case, we update the SELECT alias to - another name to avoid the conflict. - """ - if self.db_engine_spec.allows_alias_to_source_column: - return - - def is_alias_used_in_orderby(col: ColumnElement) -> bool: - if not isinstance(col, Label): - return False - regexp = re.compile(f"\\(.*\\b{re.escape(col.name)}\\b.*\\)", re.IGNORECASE) - return any(regexp.search(str(x)) for x in orderby_exprs) - - # Iterate through selected columns, if column alias appears in orderby - # use another `alias`. The final output columns will still use the - # original names, because they are updated by `labels_expected` after - # querying. - for col in select_exprs: - if is_alias_used_in_orderby(col): - col.name = f"{col.name}__" - - def text(self, clause: str) -> TextClause: - return self.db_engine_spec.get_text_clause(clause) - def _get_series_orderby( self, series_limit_metric: Metric, @@ -1643,43 +1521,6 @@ class SqlaTable( ) return ob - def _normalize_prequery_result_type( - self, - row: pd.Series, - dimension: str, - columns_by_name: dict[str, TableColumn], - ) -> str | int | float | bool | Text: - """ - Convert a prequery result type to its equivalent Python type. - - Some databases like Druid will return timestamps as strings, but do not perform - automatic casting when comparing these strings to a timestamp. For cases like - this we convert the value via the appropriate SQL transform. - - :param row: A prequery record - :param dimension: The dimension name - :param columns_by_name: The mapping of columns by name - :return: equivalent primitive python type - """ - - value = row[dimension] - - if isinstance(value, np.generic): - value = value.item() - - column_ = columns_by_name.get(dimension) - db_extra: dict[str, Any] = self.database.get_extra() - - if column_ and column_.type and column_.is_temporal and isinstance(value, str): - sql = self.db_engine_spec.convert_dttm( - column_.type, dateutil.parser.parse(value), db_extra=db_extra - ) - - if sql: - value = self.text(sql) - - return value - def _get_top_groups( self, df: pd.DataFrame, @@ -2053,6 +1894,14 @@ class SqlaTable( session = inspect(self).session # pylint: disable=disallowed-name self.database = session.query(Database).filter_by(id=self.database_id).one() + def get_query_str(self, query_obj: QueryObjectDict) -> str: + """Returns a query as a string using ExploreMixin implementation""" + return ExploreMixin.get_query_str(self, query_obj) + + def text(self, clause: str) -> TextClause: + """Returns a text clause using ExploreMixin implementation""" + return ExploreMixin.text(self, clause) + sa.event.listen(SqlaTable, "before_update", SqlaTable.before_update) sa.event.listen(SqlaTable, "after_insert", SqlaTable.after_insert) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 813b6c7341..930b84aa34 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -919,10 +919,13 @@ class ExploreMixin: # pylint: disable=too-many-public-methods if isinstance(value, np.generic): value = value.item() - column_ = columns_by_name[dimension] + column_ = columns_by_name.get(dimension) db_extra: dict[str, Any] = self.database.get_extra() - if isinstance(column_, dict): + if column_ is None: + # Column not found, return value as-is + pass + elif isinstance(column_, dict): if ( column_.get("type") and column_.get("is_temporal") @@ -941,7 +944,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods ) if sql: - value = self.text(sql) + value = self.db_engine_spec.get_text_clause(sql) return value def make_orderby_compatible(