From 77f60f42e6e8f968213e2c6c43f247edeef6a705 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 2 Dec 2025 12:13:49 -0500 Subject: [PATCH] More cleanup --- superset/commands/explore/get.py | 4 +-- superset/models/sql_lab.py | 31 ++++++++++++++++-- superset/superset_typing.py | 54 ++++++-------------------------- superset/utils/core.py | 9 +++--- superset/views/core.py | 3 +- superset/views/utils.py | 5 +-- 6 files changed, 46 insertions(+), 60 deletions(-) diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py index 70de735d7b0..d9712741e8b 100644 --- a/superset/commands/explore/get.py +++ b/superset/commands/explore/get.py @@ -37,7 +37,7 @@ from superset.exceptions import SupersetException from superset.explore.exceptions import WrongEndpointError from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError from superset.extensions import security_manager -from superset.superset_typing import BaseDatasourceData, QueryData +from superset.superset_typing import BaseDatasourceData from superset.utils import core as utils, json from superset.views.utils import ( get_datasource_info, @@ -136,7 +136,7 @@ class GetExploreCommand(BaseCommand, ABC): utils.merge_extra_filters(form_data) utils.merge_request_params(form_data, request.args) - datasource_data: BaseDatasourceData | QueryData = { + datasource_data: BaseDatasourceData = { "type": self._datasource_type or "unknown", "name": datasource_name, "columns": [], diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index 3c3acb2570c..35e3f126ad6 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -51,6 +51,7 @@ from superset_core.api.models import Query as CoreQuery, SavedQuery as CoreSaved from superset import security_manager from superset.exceptions import SupersetParseError, SupersetSecurityException from superset.jinja_context import BaseTemplateProcessor, get_template_processor +from superset.explorables.base import TimeGrainDict from superset.models.helpers import ( AuditMixinNullable, ExploreMixin, @@ -63,7 +64,7 @@ from superset.sql.parse import ( Table, ) from superset.sqllab.limiting_factor import LimitingFactor -from superset.superset_typing import QueryData, QueryObjectDict +from superset.superset_typing import BaseDatasourceData, QueryObjectDict from superset.utils import json from superset.utils.core import ( get_column_name, @@ -239,7 +240,7 @@ class Query( return None @property - def data(self) -> QueryData: + def data(self) -> BaseDatasourceData: """Returns query data for the frontend""" order_by_choices = [] for col in self.columns: @@ -335,6 +336,32 @@ class Query( def get_extra_cache_keys(self, query_obj: QueryObjectDict) -> list[Hashable]: return [] + def get_time_grains(self) -> list[TimeGrainDict]: + """ + Get available time granularities from the database. + + Delegates to the database's time grain definitions. + """ + return [ + { + "name": grain.name, + "function": grain.function, + "duration": grain.duration, + } + for grain in (self.database.grains() or []) + ] + + def has_drill_by_columns(self, column_names: list[str]) -> bool: + """ + Check if the specified columns support drill-by operations. + + For Query objects, all columns are considered drillable since they + come from ad-hoc SQL queries without predefined metadata. + """ + if not column_names: + return False + return set(column_names).issubset(set(self.column_names)) + @property def tracking_url(self) -> Optional[str]: """ diff --git a/superset/superset_typing.py b/superset/superset_typing.py index d182e5eb490..e2bacda72ec 100644 --- a/superset/superset_typing.py +++ b/superset/superset_typing.py @@ -197,13 +197,17 @@ class QueryObjectDict(TypedDict, total=False): class BaseDatasourceData(TypedDict, total=False): """ - TypedDict for datasource data returned to the frontend. + TypedDict for explorable data returned to the frontend. - This represents the structure of the dictionary returned from BaseDatasource.data - property. It provides datasource information to the frontend for visualization - and querying. + This represents the structure of the dictionary returned from the `data` property + of any Explorable (BaseDatasource, Query, etc.). It provides datasource/query + information to the frontend for visualization and querying. - Core fields from BaseDatasource.data: + All fields are optional (total=False) since different explorable types provide + different subsets of these fields. Query objects provide a minimal subset while + SqlaTable provides the full set. + + Core fields: id: Unique identifier for the datasource uid: Unique identifier including type (e.g., "1__table") column_formats: D3 format strings for columns @@ -291,46 +295,6 @@ class BaseDatasourceData(TypedDict, total=False): normalize_columns: bool -class QueryData(TypedDict, total=False): - """ - TypedDict for SQL Lab query data returned to the frontend. - - This represents the structure of the dictionary returned from Query.data property - in SQL Lab. It provides query information to the frontend for execution and display. - - Fields: - time_grain_sqla: Available time grains for this database - filter_select: Whether filter select is enabled - name: Query tab name - columns: List of column definitions - metrics: List of metrics (always empty for queries) - id: Query ID - type: Object type (always "query") - sql: SQL query text - owners: List of owner information - database: Database connection details - order_by_choices: Available ordering options - catalog: Catalog name if applicable - schema: Schema name if applicable - verbose_map: Mapping of column names to verbose names (empty for queries) - """ - - time_grain_sqla: list[tuple[Any, Any]] - filter_select: bool - name: str | None - columns: list[dict[str, Any]] - metrics: list[Any] - id: int - type: str - sql: str | None - owners: list[dict[str, Any]] - database: dict[str, Any] - order_by_choices: list[tuple[str, str]] - catalog: str | None - schema: str | None - verbose_map: dict[str, str] - - VizData: TypeAlias = list[Any] | dict[Any, Any] | None VizPayload: TypeAlias = dict[str, Any] diff --git a/superset/utils/core.py b/superset/utils/core.py index 05c7f51cf34..02756bf1607 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -114,10 +114,9 @@ from superset.utils.hashing import md5_sha_from_dict, md5_sha_from_str from superset.utils.pandas import detect_datetime_format if TYPE_CHECKING: - from superset.connectors.sqla.models import BaseDatasource, TableColumn + from superset.connectors.sqla.models import TableColumn from superset.explorables.base import Explorable from superset.models.core import Database - from superset.models.sql_lab import Query logging.getLogger("MARKDOWN").setLevel(logging.INFO) logger = logging.getLogger(__name__) @@ -1658,7 +1657,7 @@ def map_sql_type_to_inferred_type(sql_type: Optional[str]) -> str: def get_metric_type_from_column( - column: Any, datasource: BaseDatasource | Explorable | Query + column: Any, datasource: "Explorable" ) -> str: """ Determine the metric type from a given column in a datasource. @@ -1701,7 +1700,7 @@ def get_metric_type_from_column( def extract_dataframe_dtypes( df: pd.DataFrame, - datasource: BaseDatasource | Explorable | Query | None = None, + datasource: "Explorable | None" = None, ) -> list[GenericDataType]: """Serialize pandas/numpy dtypes to generic types""" @@ -1772,7 +1771,7 @@ def is_test() -> bool: def get_time_filter_status( - datasource: BaseDatasource | Explorable, + datasource: "Explorable", applied_time_extras: dict[str, str], ) -> tuple[list[dict[str, str]], list[dict[str, str]]]: temporal_columns: set[Any] = { diff --git a/superset/views/core.py b/superset/views/core.py index f888e889b75..de33f26dc8d 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -80,7 +80,6 @@ from superset.models.user_attributes import UserAttribute from superset.superset_typing import ( BaseDatasourceData, FlaskResponse, - QueryData, ) from superset.tasks.utils import get_current_user from superset.utils import core as utils, json @@ -538,7 +537,7 @@ class Superset(BaseSupersetView): "metrics": [], "database": {"id": 0, "backend": ""}, } - datasource_data: BaseDatasourceData | QueryData + datasource_data: BaseDatasourceData try: datasource_data = datasource.data if datasource else dummy_datasource_data except (SupersetException, SQLAlchemyError): diff --git a/superset/views/utils.py b/superset/views/utils.py index d30e8c1c2b7..22da8a3aad8 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -49,7 +49,6 @@ from superset.superset_typing import ( BaseDatasourceData, FlaskResponse, FormData, - QueryData, ) from superset.utils import json from superset.utils.core import DatasourceType @@ -92,12 +91,10 @@ def redirect_to_login(next_target: str | None = None) -> FlaskResponse: def sanitize_datasource_data( - datasource_data: BaseDatasourceData | QueryData, + datasource_data: BaseDatasourceData, ) -> dict[str, Any]: """ Sanitize datasource data by removing sensitive database parameters. - - Accepts TypedDict types (BaseDatasourceData, QueryData). """ if datasource_data: datasource_database = datasource_data.get("database")