You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2025/11/06 18:24:42 UTC

(superset) branch annotate-important-types updated (b41de17784 -> 5dfc39ace3)

This is an automated email from the ASF dual-hosted git repository.

beto pushed a change to branch annotate-important-types
in repository https://gitbox.apache.org/repos/asf/superset.git


 discard b41de17784 chore: annotate important types
     new 5dfc39ace3 chore: annotate important types

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (b41de17784)
            \
             N -- N -- N   refs/heads/annotate-important-types (5dfc39ace3)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 superset/commands/explore/get.py             |  2 +-
 superset/common/query_actions.py             |  7 ++-
 superset/common/query_context_processor.py   | 22 +++++---
 superset/connectors/sqla/models.py           | 81 ++++++++++++++++++----------
 superset/jinja_context.py                    |  5 +-
 superset/models/helpers.py                   | 36 +++++++++++--
 superset/models/sql_lab.py                   | 11 ++--
 superset/superset_typing.py                  | 21 ++------
 superset/views/core.py                       | 10 ++--
 superset/views/datasource/views.py           |  8 +--
 superset/viz.py                              |  4 +-
 tests/integration_tests/sqla_models_tests.py | 14 +++--
 12 files changed, 144 insertions(+), 77 deletions(-)


(superset) 01/01: chore: annotate important types

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch annotate-important-types
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 5dfc39ace3870d51252af216f63c1cc648f83308
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Thu Nov 6 12:17:03 2025 -0500

    chore: annotate important types
---
 superset/commands/explore/get.py             |   2 +-
 superset/common/query_actions.py             |   7 +-
 superset/common/query_context_processor.py   |  22 ++-
 superset/connectors/sqla/models.py           |  58 +++++--
 superset/jinja_context.py                    |   5 +-
 superset/models/helpers.py                   |  36 ++++-
 superset/models/sql_lab.py                   |   6 +-
 superset/superset_typing.py                  | 222 ++++++++++++++++++++++++++-
 superset/views/core.py                       |  10 +-
 superset/views/datasource/views.py           |   8 +-
 superset/viz.py                              |   4 +-
 tests/integration_tests/sqla_models_tests.py |  14 +-
 12 files changed, 352 insertions(+), 42 deletions(-)

diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py
index df71a931b2..18d63281b4 100644
--- a/superset/commands/explore/get.py
+++ b/superset/commands/explore/get.py
@@ -145,7 +145,7 @@ class GetExploreCommand(BaseCommand, ABC):
         }
         try:
             if datasource:
-                datasource_data = datasource.data
+                datasource_data = cast(dict[str, Any], datasource.data)
         except SupersetException as ex:
             message = ex.message
         except SQLAlchemyError:
diff --git a/superset/common/query_actions.py b/superset/common/query_actions.py
index a18e40a66b..ff7f9c170b 100644
--- a/superset/common/query_actions.py
+++ b/superset/common/query_actions.py
@@ -17,7 +17,7 @@
 from __future__ import annotations
 
 import copy
-from typing import Any, Callable, TYPE_CHECKING
+from typing import Any, Callable, cast, TYPE_CHECKING
 
 from flask_babel import _
 
@@ -25,6 +25,7 @@ from superset.common.chart_data import ChartDataResultType
 from superset.common.db_query_status import QueryStatus
 from superset.connectors.sqla.models import BaseDatasource
 from superset.exceptions import QueryObjectValidationError
+from superset.superset_typing import QueryObjectDict
 from superset.utils.core import (
     extract_column_dtype,
     extract_dataframe_dtypes,
@@ -84,7 +85,9 @@ def _get_query(
     datasource = _get_datasource(query_context, query_obj)
     result = {"language": datasource.query_language}
     try:
-        result["query"] = datasource.get_query_str(query_obj.to_dict())
+        result["query"] = datasource.get_query_str(
+            cast(QueryObjectDict, query_obj.to_dict())
+        )
     except QueryObjectValidationError as err:
         result["error"] = err.message
     return result
diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index 94f54097e2..fd2e42af65 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -49,7 +49,7 @@ from superset.exceptions import (
 from superset.extensions import cache_manager, feature_flag_manager, security_manager
 from superset.models.helpers import QueryResult
 from superset.models.sql_lab import Query
-from superset.superset_typing import AdhocColumn, AdhocMetric
+from superset.superset_typing import AdhocColumn, AdhocMetric, QueryObjectDict
 from superset.utils import csv, excel
 from superset.utils.cache import generate_cache_key, set_and_log_cache
 from superset.utils.core import (
@@ -250,7 +250,9 @@ class QueryContextProcessor:
         Returns a QueryObject cache key for objects in self.queries
         """
         datasource = self._qc_datasource
-        extra_cache_keys = datasource.get_extra_cache_keys(query_obj.to_dict())
+        extra_cache_keys = datasource.get_extra_cache_keys(
+            cast(QueryObjectDict, query_obj.to_dict())
+        )
 
         cache_key = (
             query_obj.cache_key(
@@ -275,9 +277,13 @@ class QueryContextProcessor:
         query = ""
         if isinstance(query_context.datasource, Query):
             # todo(hugh): add logic to manage all sip68 models here
-            result = query_context.datasource.exc_query(query_object.to_dict())
+            result = query_context.datasource.exc_query(
+                cast(QueryObjectDict, query_object.to_dict())
+            )
         else:
-            result = query_context.datasource.query(query_object.to_dict())
+            result = query_context.datasource.query(
+                cast(QueryObjectDict, query_object.to_dict())
+            )
             query = result.query + ";\n\n"
 
         df = result.df
@@ -684,9 +690,13 @@ class QueryContextProcessor:
                 query_object_clone_dct["row_offset"] = 0
 
             if isinstance(self._qc_datasource, Query):
-                result = self._qc_datasource.exc_query(query_object_clone_dct)
+                result = self._qc_datasource.exc_query(
+                    cast(QueryObjectDict, query_object_clone_dct)
+                )
             else:
-                result = self._qc_datasource.query(query_object_clone_dct)
+                result = self._qc_datasource.query(
+                    cast(QueryObjectDict, query_object_clone_dct)
+                )
 
             queries.append(result.query)
             cache_keys.append(None)
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 0ef9572c50..00e9f2ade3 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -106,6 +106,7 @@ from superset.sql.parse import Table
 from superset.superset_typing import (
     AdhocColumn,
     AdhocMetric,
+    BaseDatasourceData,
     Metric,
     QueryObjectDict,
     ResultSetColumnType,
@@ -362,7 +363,7 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin):  # pylint: disable=
         return verb_map
 
     @property
-    def data(self) -> dict[str, Any]:
+    def data(self) -> BaseDatasourceData:
         """Data representation of the datasource sent to the frontend"""
         return {
             # simple fields
@@ -470,14 +471,15 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin):  # pylint: disable=
             or metric["verbose_name"] in metric_names
         ]
 
-        filtered_columns: list[Column] = []
+        filtered_columns: list[dict[str, Any]] = []
         column_types: set[utils.GenericDataType] = set()
-        for column_ in data["columns"]:
-            generic_type = column_.get("type_generic")
+        for column_ in cast(list[dict[str, Any]], data["columns"]):  # type: ignore[assignment]
+            generic_type = cast(dict[str, Any], column_).get("type_generic")
             if generic_type is not None:
                 column_types.add(generic_type)
-            if column_["column_name"] in column_names:
-                filtered_columns.append(column_)
+            column_dict = cast(dict[str, Any], column_)
+            if column_dict["column_name"] in column_names:
+                filtered_columns.append(column_dict)
 
         data["column_types"] = list(column_types)
         del data["description"]
@@ -499,7 +501,7 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin):  # pylint: disable=
         data["verbose_map"] = verbose_map
         data["column_names"] = set(all_columns.values()) | set(self.column_names)
 
-        return data
+        return cast(dict[str, Any], data)
 
     def external_metadata(self) -> list[ResultSetColumnType]:
         """Returns column information from the external system"""
@@ -509,7 +511,8 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin):  # pylint: disable=
         """Returns a query as a string
 
         This is used to be displayed to the user so that they can
-        understand what is taking place behind the scene"""
+        understand what is taking place behind the scene
+        """
         raise NotImplementedError()
 
     def query(self, query_obj: QueryObjectDict) -> QueryResult:
@@ -613,7 +616,8 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin):  # pylint: disable=
         """If a datasource needs to provide additional keys for calculation of
         cache keys, those can be provided via this method
 
-        :param query_obj: The dict representation of a query object
+        :param query_obj: The dict representation of a query object (QueryObjectDict
+            structure expected)
         :return: list of keys
         """
         return []
@@ -1351,7 +1355,7 @@ class SqlaTable(
         return [(g.duration, g.name) for g in self.database.grains() or []]
 
     @property
-    def data(self) -> dict[str, Any]:
+    def data(self) -> BaseDatasourceData:
         data_ = super().data
         if self.type == "table":
             data_["granularity_sqla"] = self.granularity_sqla
@@ -1860,7 +1864,7 @@ class SqlaTable(
         template code unnecessarily, as it may contain expensive calls, e.g. to extract
         the latest partition of a database.
 
-        :param query_obj: query object to analyze
+        :param query_obj: query object to analyze (QueryObjectDict structure expected)
         :return: True if there are call(s) to an `ExtraCache` method, False otherwise
         """
         templatable_statements: list[str] = []
@@ -1912,7 +1916,37 @@ class SqlaTable(
         """
         extra_cache_keys = super().get_extra_cache_keys(query_obj)
         if self.has_extra_cache_key_calls(query_obj):
-            sqla_query = self.get_sqla_query(**query_obj)
+            # Filter out keys that aren't parameters to get_sqla_query
+            sqla_query_keys = {
+                "apply_fetch_values_predicate",
+                "columns",
+                "extras",
+                "filter",
+                "from_dttm",
+                "granularity",
+                "groupby",
+                "inner_from_dttm",
+                "inner_to_dttm",
+                "is_rowcount",
+                "is_timeseries",
+                "metrics",
+                "orderby",
+                "order_desc",
+                "to_dttm",
+                "series_columns",
+                "series_limit",
+                "series_limit_metric",
+                "group_others_when_limit_reached",
+                "row_limit",
+                "row_offset",
+                "timeseries_limit",
+                "timeseries_limit_metric",
+                "time_shift",
+            }
+            filtered_query_obj = {
+                k: v for k, v in query_obj.items() if k in sqla_query_keys
+            }
+            sqla_query = self.get_sqla_query(**cast(Any, filtered_query_obj))
             extra_cache_keys += sqla_query.extra_cache_keys
         return list(set(extra_cache_keys))
 
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 1634af8392..98ca996160 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -46,6 +46,7 @@ from superset.exceptions import (
 )
 from superset.extensions import feature_flag_manager
 from superset.sql.parse import Table
+from superset.superset_typing import QueryObjectDict
 from superset.utils import json
 from superset.utils.core import (
     AdhocFilterClause,
@@ -1015,7 +1016,9 @@ def dataset_macro(
         "from_dttm": from_dttm,
         "to_dttm": to_dttm,
     }
-    sqla_query = dataset.get_query_str_extended(query_obj, mutate=False)
+    sqla_query = dataset.get_query_str_extended(
+        cast(QueryObjectDict, query_obj), mutate=False
+    )
     sql = sqla_query.sql
     return f"(\n{sql}\n) AS dataset_{dataset_id}"
 
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 87f354a6f3..a551715466 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -824,7 +824,7 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
     def columns(self) -> list[Any]:
         raise NotImplementedError()
 
-    def get_extra_cache_keys(self, query_obj: dict[str, Any]) -> list[Hashable]:
+    def get_extra_cache_keys(self, query_obj: QueryObjectDict) -> list[Hashable]:
         raise NotImplementedError()
 
     def get_template_processor(self, **kwargs: Any) -> BaseTemplateProcessor:
@@ -974,7 +974,37 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
         query_obj: QueryObjectDict,
         mutate: bool = True,
     ) -> QueryStringExtended:
-        sqlaq = self.get_sqla_query(**query_obj)
+        # Filter out keys that aren't parameters to get_sqla_query
+        sqla_query_keys = {
+            "apply_fetch_values_predicate",
+            "columns",
+            "extras",
+            "filter",
+            "from_dttm",
+            "granularity",
+            "groupby",
+            "inner_from_dttm",
+            "inner_to_dttm",
+            "is_rowcount",
+            "is_timeseries",
+            "metrics",
+            "orderby",
+            "order_desc",
+            "to_dttm",
+            "series_columns",
+            "series_limit",
+            "series_limit_metric",
+            "group_others_when_limit_reached",
+            "row_limit",
+            "row_offset",
+            "timeseries_limit",
+            "timeseries_limit_metric",
+            "time_shift",
+        }
+        filtered_query_obj = {
+            k: v for k, v in query_obj.items() if k in sqla_query_keys
+        }
+        sqlaq = self.get_sqla_query(**cast(Any, filtered_query_obj))
         sql = self.database.compile_sqla_query(
             sqlaq.sqla_query,
             catalog=self.catalog,
@@ -2426,7 +2456,7 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
                     "order_desc": True,
                 }
 
-                result = self.query(prequery_obj)
+                result = self.query(cast(QueryObjectDict, prequery_obj))
                 prequeries.append(result.query)
                 dimensions = [
                     c
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index 53224f2472..5686424c53 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -62,6 +62,7 @@ from superset.sql.parse import (
     Table,
 )
 from superset.sqllab.limiting_factor import LimitingFactor
+from superset.superset_typing import QueryData, QueryObjectDict
 from superset.utils import json
 from superset.utils.core import (
     get_column_name,
@@ -237,7 +238,8 @@ class Query(
         return None
 
     @property
-    def data(self) -> dict[str, Any]:
+    def data(self) -> QueryData:
+        """Returns query data for the frontend"""
         order_by_choices = []
         for col in self.columns:
             column_name = str(col.column_name or "")
@@ -329,7 +331,7 @@ class Query(
     def default_endpoint(self) -> str:
         return ""
 
-    def get_extra_cache_keys(self, query_obj: dict[str, Any]) -> list[Hashable]:
+    def get_extra_cache_keys(self, query_obj: QueryObjectDict) -> list[Hashable]:
         return []
 
     @property
diff --git a/superset/superset_typing.py b/superset/superset_typing.py
index 02a4a32f24..dea9bd4e2d 100644
--- a/superset/superset_typing.py
+++ b/superset/superset_typing.py
@@ -14,7 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from collections.abc import Sequence
+from collections.abc import Hashable, Sequence
 from datetime import datetime
 from typing import Any, Literal, Optional, TYPE_CHECKING, TypedDict, Union
 
@@ -23,7 +23,7 @@ from typing_extensions import NotRequired
 from werkzeug.wrappers import Response
 
 if TYPE_CHECKING:
-    from superset.utils.core import GenericDataType
+    from superset.utils.core import GenericDataType, QueryObjectFilterClause
 
 SQLType = Union[TypeEngine, type[TypeEngine]]
 
@@ -109,7 +109,223 @@ Granularity = Union[str, dict[str, Union[str, float]]]
 Column = Union[AdhocColumn, str]
 Metric = Union[AdhocMetric, str]
 OrderBy = tuple[Union[Metric, Column], bool]
-QueryObjectDict = dict[str, Any]
+
+
+class QueryObjectDict(TypedDict, total=False):
+    """
+    TypedDict representation of query objects used throughout Superset.
+
+    This represents the dictionary output from QueryObject.to_dict() and is used
+    in datasource query methods throughout Superset.
+
+    Core fields from QueryObject.to_dict():
+        apply_fetch_values_predicate: Whether to apply fetch values predicate
+        columns: List of columns to include
+        extras: Additional options and parameters
+        filter: List of filter clauses
+        from_dttm: Start datetime for time range
+        granularity: Time grain/granularity
+        inner_from_dttm: Inner start datetime for nested queries
+        inner_to_dttm: Inner end datetime for nested queries
+        is_rowcount: Whether this is a row count query
+        is_timeseries: Whether this is a timeseries query
+        metrics: List of metrics to compute
+        order_desc: Whether to order descending
+        orderby: List of order by clauses
+        row_limit: Maximum number of rows
+        row_offset: Number of rows to skip
+        series_columns: Columns to use for series
+        series_limit: Maximum number of series
+        series_limit_metric: Metric to use for series limiting
+        group_others_when_limit_reached: Whether to group remaining items as "Others"
+        to_dttm: End datetime for time range
+        time_shift: Time shift specification
+
+    Additional fields used throughout the codebase:
+        time_range: Human-readable time range string
+        datasource: BaseDatasource instance
+        extra_cache_keys: Additional keys for caching
+        rls: Row level security filters
+        changed_on: Last modified timestamp
+
+    Deprecated fields (still in use):
+        groupby: Columns to group by (use columns instead)
+        timeseries_limit: Series limit (use series_limit instead)
+        timeseries_limit_metric: Series limit metric (use series_limit_metric instead)
+    """
+
+    # Core fields from QueryObject.to_dict()
+    apply_fetch_values_predicate: bool
+    columns: list[Column]
+    extras: dict[str, Any]
+    filter: list["QueryObjectFilterClause"]
+    from_dttm: Optional[datetime]
+    granularity: Optional[str]
+    inner_from_dttm: Optional[datetime]
+    inner_to_dttm: Optional[datetime]
+    is_rowcount: bool
+    is_timeseries: bool
+    metrics: Optional[list[Metric]]
+    order_desc: bool
+    orderby: list[OrderBy]
+    row_limit: Optional[int]
+    row_offset: int
+    series_columns: list[Column]
+    series_limit: int
+    series_limit_metric: Optional[Metric]
+    group_others_when_limit_reached: bool
+    to_dttm: Optional[datetime]
+    time_shift: Optional[str]
+
+    # Additional fields used throughout the codebase
+    time_range: Optional[str]
+    datasource: Any  # BaseDatasource instance
+    extra_cache_keys: list[Hashable]
+    rls: list[Any]
+    changed_on: Optional[datetime]
+
+    # Deprecated fields (still in use)
+    groupby: list[Column]
+    timeseries_limit: int
+    timeseries_limit_metric: Optional[Metric]
+
+
+class BaseDatasourceData(TypedDict, total=False):
+    """
+    TypedDict for datasource 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.
+
+    Core fields from BaseDatasource.data:
+        id: Unique identifier for the datasource
+        uid: Unique identifier including type (e.g., "1__table")
+        column_formats: D3 format strings for columns
+        description: Human-readable description
+        database: Database connection information
+        default_endpoint: Default URL endpoint for this datasource
+        filter_select: Whether filter select is enabled (deprecated)
+        filter_select_enabled: Whether filter select is enabled
+        name: Display name of the datasource
+        datasource_name: Name of the underlying table/query
+        table_name: Table name (same as datasource_name)
+        type: Datasource type (e.g., "table", "query")
+        catalog: Catalog name if applicable
+        schema: Schema name if applicable
+        offset: Default row offset
+        cache_timeout: Cache timeout in seconds
+        params: Additional parameters as JSON string
+        perm: Permission string
+        edit_url: URL to edit this datasource
+        sql: SQL query for virtual datasets
+        columns: List of column definitions
+        metrics: List of metric definitions
+        folders: Folder structure (JSON field)
+        order_by_choices: Available ordering options
+        owners: List of owner IDs or owner details
+        verbose_map: Mapping of column/metric names to verbose names
+        select_star: SELECT * query for this datasource
+
+    Additional fields from SqlaTable and data_for_slices:
+        column_types: List of column data types
+        column_names: Set of column names
+        granularity_sqla: Available time granularities
+        time_grain_sqla: Available time grains
+        main_dttm_col: Main datetime column
+        fetch_values_predicate: Predicate for fetching filter values
+        template_params: Template parameters for Jinja
+        is_sqllab_view: Whether this is a SQL Lab view
+        health_check_message: Health check status message
+        extra: Extra configuration as JSON string
+        always_filter_main_dttm: Whether to always filter on main datetime
+        normalize_columns: Whether to normalize column names
+    """
+
+    # Core fields from BaseDatasource.data
+    id: int
+    uid: str
+    column_formats: dict[str, Optional[str]]
+    description: Optional[str]
+    database: dict[str, Any]
+    default_endpoint: Optional[str]
+    filter_select: bool
+    filter_select_enabled: bool
+    name: str
+    datasource_name: str
+    table_name: str
+    type: str
+    catalog: Optional[str]
+    schema: Optional[str]
+    offset: int
+    cache_timeout: Optional[int]
+    params: Optional[str]
+    perm: Optional[str]
+    edit_url: str
+    sql: Optional[str]
+    columns: list[dict[str, Any]]
+    metrics: list[dict[str, Any]]
+    folders: Any  # JSON field, can be list or dict
+    order_by_choices: list[tuple[str, str]]
+    owners: Union[list[int], list[dict[str, Any]]]  # Can be either format
+    verbose_map: dict[str, str]
+    select_star: Optional[str]
+
+    # Additional fields from SqlaTable and data_for_slices
+    column_types: list[Any]
+    column_names: Union[set[str], set[Any]]
+    granularity_sqla: list[tuple[Any, Any]]
+    time_grain_sqla: list[tuple[Any, Any]]
+    main_dttm_col: Optional[str]
+    fetch_values_predicate: Optional[str]
+    template_params: Optional[str]
+    is_sqllab_view: bool
+    health_check_message: Optional[str]
+    extra: Optional[str]
+    always_filter_main_dttm: bool
+    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: Optional[str]
+    columns: list[dict[str, Any]]
+    metrics: list[Any]
+    id: int
+    type: str
+    sql: Optional[str]
+    owners: list[dict[str, Any]]
+    database: dict[str, Any]
+    order_by_choices: list[tuple[str, str]]
+    catalog: Optional[str]
+    schema: Optional[str]
+    verbose_map: dict[str, str]
+
+
 VizData = Optional[Union[list[Any], dict[Any, Any]]]
 VizPayload = dict[str, Any]
 
diff --git a/superset/views/core.py b/superset/views/core.py
index 3c67d21ffa..30de91a4d3 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -543,11 +543,13 @@ class Superset(BaseSupersetView):
         if datasource:
             datasource_data["owners"] = datasource.owners_data
             if isinstance(datasource, Query):
-                datasource_data["columns"] = datasource.columns
+                datasource_data["columns"] = [col.data for col in datasource.columns]
 
         bootstrap_data = {
             "can_add": slice_add_perm,
-            "datasource": sanitize_datasource_data(datasource_data),
+            "datasource": sanitize_datasource_data(
+                cast(dict[str, Any], datasource_data)
+            ),
             "form_data": form_data,
             "datasource_id": datasource_id,
             "datasource_type": datasource_type,
@@ -866,7 +868,9 @@ class Superset(BaseSupersetView):
             return json_error_response(DATASOURCE_MISSING_ERR)
 
         datasource.raise_for_access()
-        return json_success(json.dumps(sanitize_datasource_data(datasource.data)))
+        return json_success(
+            json.dumps(sanitize_datasource_data(cast(dict[str, Any], datasource.data)))
+        )
 
     @event_logger.log_this
     @has_access
diff --git a/superset/views/datasource/views.py b/superset/views/datasource/views.py
index d0a4b6851c..015591a9f1 100644
--- a/superset/views/datasource/views.py
+++ b/superset/views/datasource/views.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 from collections import Counter
-from typing import Any
+from typing import Any, cast
 
 from flask import redirect, request
 from flask_appbuilder import expose, permission_name
@@ -120,7 +120,7 @@ class Datasource(BaseSupersetView):
         data = orm_datasource.data
         db.session.commit()  # pylint: disable=consider-using-transaction
 
-        return self.json_response(sanitize_datasource_data(data))
+        return self.json_response(sanitize_datasource_data(cast(dict[str, Any], data)))
 
     @expose("/get/<datasource_type>/<datasource_id>/")
     @has_access_api
@@ -131,7 +131,9 @@ class Datasource(BaseSupersetView):
         datasource = DatasourceDAO.get_datasource(
             DatasourceType(datasource_type), datasource_id
         )
-        return self.json_response(sanitize_datasource_data(datasource.data))
+        return self.json_response(
+            sanitize_datasource_data(cast(dict[str, Any], datasource.data))
+        )
 
     @expose("/external_metadata/<datasource_type>/<datasource_id>/")
     @has_access_api
diff --git a/superset/viz.py b/superset/viz.py
index b5a4d5217b..c9d4ec3cab 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -458,7 +458,9 @@ class BaseViz:  # pylint: disable=too-many-public-methods
         different time shifts will differ only in the `from_dttm`, `to_dttm`,
         `inner_from_dttm`, and `inner_to_dttm` values which are stripped.
         """
-        cache_dict = copy.copy(query_obj)
+        # Cast to dict[str, Any] to allow mutable operations (update, del)
+        # since TypedDict doesn't support these operations in the same way
+        cache_dict: dict[str, Any] = copy.copy(cast(dict[str, Any], query_obj))
         cache_dict.update(extra)
 
         for k in ["from_dttm", "to_dttm", "inner_from_dttm", "inner_to_dttm"]:
diff --git a/tests/integration_tests/sqla_models_tests.py b/tests/integration_tests/sqla_models_tests.py
index 2e9c4c00e5..6511fe545d 100644
--- a/tests/integration_tests/sqla_models_tests.py
+++ b/tests/integration_tests/sqla_models_tests.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 
 import re
 from datetime import datetime
-from typing import Any, Literal, NamedTuple, Optional, Union
+from typing import Any, cast, Literal, NamedTuple, Optional, Union
 from re import Pattern
 from unittest.mock import Mock, patch
 import pytest
@@ -35,6 +35,7 @@ from sqlalchemy.sql.elements import TextClause
 from superset import db
 from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric
 from superset.constants import EMPTY_STRING, NULL_STRING
+from superset.superset_typing import QueryObjectDict
 from superset.db_engine_specs.bigquery import BigQueryEngineSpec
 from superset.db_engine_specs.druid import DruidEngineSpec
 from superset.exceptions import (
@@ -975,8 +976,11 @@ def test_extra_cache_keys_in_adhoc_metrics_and_columns(
 
     query_obj = {**base_query_obj, **items}
 
-    extra_cache_keys = table.get_extra_cache_keys(query_obj)
-    assert table.has_extra_cache_key_calls(query_obj) == has_extra_cache_keys
+    extra_cache_keys = table.get_extra_cache_keys(cast(QueryObjectDict, query_obj))
+    assert (
+        table.has_extra_cache_key_calls(cast(QueryObjectDict, query_obj))
+        == has_extra_cache_keys
+    )
     assert extra_cache_keys == expected_cache_keys
 
 
@@ -1017,8 +1021,8 @@ def test_extra_cache_keys_in_dataset_metrics_and_columns(
         "filter": [],
     }
 
-    extra_cache_keys = table.get_extra_cache_keys(query_obj)
-    assert table.has_extra_cache_key_calls(query_obj) is True
+    extra_cache_keys = table.get_extra_cache_keys(cast(QueryObjectDict, query_obj))
+    assert table.has_extra_cache_key_calls(cast(QueryObjectDict, query_obj)) is True
     assert set(extra_cache_keys) == {"abc", None}