From a6bcb346a32cdfe014747a74cc5582a9a4744649 Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Fri, 31 Oct 2025 21:50:27 +0530 Subject: [PATCH 1/7] feat(schemas): add orderby column validation to ChartDataQueryObjectSchema --- superset/charts/schemas.py | 10 +++-- tests/unit_tests/charts/test_schemas.py | 59 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index 2e3dec7fd856..d63a0d6c0479 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -1131,6 +1131,12 @@ class AnnotationLayerSchema(Schema): ) +def validate_orderby_column(value: Any) -> bool: + if value is None or (isinstance(value, str) and len(value) == 0): + raise validate.ValidationError(_("orderby column must be populated")) + return True + + class ChartDataDatasourceSchema(Schema): description = "Chart datasource" id = Union( @@ -1328,9 +1334,7 @@ class Meta: # pylint: disable=too-few-public-methods fields.Tuple( ( fields.Raw( - validate=[ - Length(min=1, error=_("orderby column must be populated")) - ], + validate=validate_orderby_column, allow_none=False, ), fields.Boolean(), diff --git a/tests/unit_tests/charts/test_schemas.py b/tests/unit_tests/charts/test_schemas.py index 5466a0deadd9..9697e23460a2 100644 --- a/tests/unit_tests/charts/test_schemas.py +++ b/tests/unit_tests/charts/test_schemas.py @@ -152,3 +152,62 @@ def test_time_grain_validation_with_config_addons(app_context: None) -> None: } result = schema.load(custom_data) assert result["time_grain"] == "PT10M" + + +def test_chart_data_query_object_schema_orderby_validation( + app_context: None, +) -> None: + """Test that ChartDataQueryObjectSchema validates orderby with various types""" + schema = ChartDataQueryObjectSchema() + + # String column name should pass + result = schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [["column_name", True]], + } + ) + assert result["orderby"] == [("column_name", True)] + + # Integer column ID should pass (fixes deck.gl bug) + result = schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [[123, False]], + } + ) + assert result["orderby"] == [(123, False)] + + # Adhoc metric object should pass + result = schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [[{"label": "my_metric", "sqlExpression": "SUM(col)"}, False]], + } + ) + assert result["orderby"][0][0]["label"] == "my_metric" + + # Empty string should fail + with pytest.raises(ValidationError) as exc_info: + schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [["", True]], + } + ) + assert "orderby" in exc_info.value.messages + + # None should fail + with pytest.raises(ValidationError) as exc_info: + schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [[None, True]], + } + ) + assert "orderby" in exc_info.value.messages From ded18b1c7a053583832a3fde8ceda9df68ff373a Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Fri, 31 Oct 2025 22:49:39 +0530 Subject: [PATCH 2/7] feat(query): enhance orderby and metric handling to support integers and improve validation --- .../src/layers/Scatter/buildQuery.ts | 16 +++- .../src/layers/spatialUtils.ts | 19 +++-- superset/common/query_object.py | 7 +- superset/superset_typing.py | 4 +- superset/utils/core.py | 6 ++ tests/unit_tests/charts/test_schemas.py | 76 +++++++------------ tests/unit_tests/utils/test_core.py | 32 ++++++++ 7 files changed, 99 insertions(+), 61 deletions(-) diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts index 73c1482a0444..915c3392f9a9 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts @@ -33,13 +33,15 @@ import { addJsColumnsToColumns, processMetricsArray, addTooltipColumnsToQuery, + ensureColumnsUnique, } from '../buildQueryUtils'; export interface DeckScatterFormData extends Omit, SqlaFormData { point_radius_fixed?: { - value?: string; + type?: string; + value?: string | number; }; multiplier?: number; point_unit?: string; @@ -75,17 +77,23 @@ export default function buildQuery(formData: DeckScatterFormData) { typeof col === 'string' ? col : col.label || col.sqlExpression || '', ); const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns); + const uniqueColumns = ensureColumnsUnique(withJsColumns); - columns = withJsColumns as QueryFormColumn[]; + columns = uniqueColumns as QueryFormColumn[]; columns = addTooltipColumnsToQuery(columns, tooltip_contents); - const metrics = processMetricsArray([point_radius_fixed?.value]); + const isRadiusMetric = + point_radius_fixed?.type === 'metric' && point_radius_fixed?.value; + const metrics = isRadiusMetric + ? processMetricsArray([String(point_radius_fixed.value)]) + : []; + const filters = addSpatialNullFilters( spatial, ensureIsArray(baseQueryObject.filters || []), ); - const orderby = point_radius_fixed?.value + const orderby = isRadiusMetric ? ([[point_radius_fixed.value, false]] as QueryFormOrderBy[]) : (baseQueryObject.orderby as QueryFormOrderBy[]) || []; diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts index 28625c5872b4..eee52afb34b6 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts @@ -109,11 +109,20 @@ export function addSpatialNullFilters( if (!spatial) return filters; const spatialColumns = getSpatialColumns(spatial); - const nullFilters: QueryObjectFilterClause[] = spatialColumns.map(column => ({ - col: column, - op: 'IS NOT NULL', - val: null, - })); + const uniqueSpatialColumns = [...new Set(spatialColumns)]; + const existingFilterCols = new Set( + filters + .filter(filter => filter.op === 'IS NOT NULL') + .map(filter => filter.col), + ); + + const nullFilters: QueryObjectFilterClause[] = uniqueSpatialColumns + .filter(column => !existingFilterCols.has(column)) + .map(column => ({ + col: column, + op: 'IS NOT NULL', + val: null, + })); return [...filters, ...nullFilters]; } diff --git a/superset/common/query_object.py b/superset/common/query_object.py index 0740ca889da5..f802fe487374 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -189,11 +189,12 @@ def _set_metrics(self, metrics: list[Metric] | None = None) -> None: # 1. 'metric_name' - name of predefined metric # 2. { label: 'label_name' } - legacy format for a predefined metric # 3. { expressionType: 'SIMPLE' | 'SQL', ... } - adhoc metric - def is_str_or_adhoc(metric: Metric) -> bool: - return isinstance(metric, str) or is_adhoc_metric(metric) + # 4. int - metric ID + def is_str_int_or_adhoc(metric: Metric) -> bool: + return isinstance(metric, (str, int)) or is_adhoc_metric(metric) self.metrics = metrics and [ - x if is_str_or_adhoc(x) else x["label"] # type: ignore + x if is_str_int_or_adhoc(x) else x["label"] # type: ignore for x in metrics ] diff --git a/superset/superset_typing.py b/superset/superset_typing.py index 02a4a32f2498..a3f4a35769b2 100644 --- a/superset/superset_typing.py +++ b/superset/superset_typing.py @@ -106,8 +106,8 @@ class ResultSetColumnType(TypedDict): FilterValues = Union[FilterValue, list[FilterValue], tuple[FilterValue]] FormData = dict[str, Any] Granularity = Union[str, dict[str, Union[str, float]]] -Column = Union[AdhocColumn, str] -Metric = Union[AdhocMetric, str] +Column = Union[AdhocColumn, str, int] +Metric = Union[AdhocMetric, str, int] OrderBy = tuple[Union[Metric, Column], bool] QueryObjectDict = dict[str, Any] VizData = Optional[Union[list[Any], dict[Any, Any]]] diff --git a/superset/utils/core.py b/superset/utils/core.py index d4a3806e92b0..37a2d6ea650c 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1231,6 +1231,9 @@ def get_column_name(column: Column, verbose_map: dict[str, Any] | None = None) - verbose_map = verbose_map or {} return verbose_map.get(column, column) + if isinstance(column, int): + return str(column) + raise ValueError("Missing label") @@ -1263,6 +1266,9 @@ def get_metric_name(metric: Metric, verbose_map: dict[str, Any] | None = None) - verbose_map = verbose_map or {} return verbose_map.get(metric, metric) + if isinstance(metric, int): + return str(metric) + raise ValueError(__("Invalid metric object: %(metric)s", metric=str(metric))) diff --git a/tests/unit_tests/charts/test_schemas.py b/tests/unit_tests/charts/test_schemas.py index 9697e23460a2..46865b0d26c4 100644 --- a/tests/unit_tests/charts/test_schemas.py +++ b/tests/unit_tests/charts/test_schemas.py @@ -157,57 +157,39 @@ def test_time_grain_validation_with_config_addons(app_context: None) -> None: def test_chart_data_query_object_schema_orderby_validation( app_context: None, ) -> None: - """Test that ChartDataQueryObjectSchema validates orderby with various types""" + """Test that orderby field handles strings, integers and objects""" schema = ChartDataQueryObjectSchema() - # String column name should pass - result = schema.load( - { + # Valid values should pass + for orderby_value in ["column_name", 123, {"label": "my_metric", "sqlExpression": "SUM(col)"}]: + result = schema.load({ "datasource": {"type": "table", "id": 1}, "metrics": ["count"], - "orderby": [["column_name", True]], - } - ) - assert result["orderby"] == [("column_name", True)] - - # Integer column ID should pass (fixes deck.gl bug) - result = schema.load( - { - "datasource": {"type": "table", "id": 1}, - "metrics": ["count"], - "orderby": [[123, False]], - } - ) - assert result["orderby"] == [(123, False)] - - # Adhoc metric object should pass - result = schema.load( - { - "datasource": {"type": "table", "id": 1}, - "metrics": ["count"], - "orderby": [[{"label": "my_metric", "sqlExpression": "SUM(col)"}, False]], - } - ) - assert result["orderby"][0][0]["label"] == "my_metric" - - # Empty string should fail - with pytest.raises(ValidationError) as exc_info: - schema.load( - { + "orderby": [[orderby_value, False]], + }) + assert result["orderby"][0][1] is False + + # None and empty string should fail + for invalid_value in [None, ""]: + with pytest.raises(ValidationError): + schema.load({ "datasource": {"type": "table", "id": 1}, "metrics": ["count"], - "orderby": [["", True]], - } - ) - assert "orderby" in exc_info.value.messages + "orderby": [[invalid_value, True]], + }) - # None should fail - with pytest.raises(ValidationError) as exc_info: - schema.load( - { - "datasource": {"type": "table", "id": 1}, - "metrics": ["count"], - "orderby": [[None, True]], - } - ) - assert "orderby" in exc_info.value.messages + +def test_chart_data_query_object_schema_metrics_validation( + app_context: None, +) -> None: + """Test that metrics field handles strings, integers and objects""" + schema = ChartDataQueryObjectSchema() + + # Mix of different types should all pass + result = schema.load({ + "datasource": {"type": "table", "id": 1}, + "metrics": ["count", 123, {"expressionType": "SQL", "sqlExpression": "SUM(col)", "label": "sum"}], + }) + assert result["metrics"][0] == "count" + assert result["metrics"][1] == 123 + assert result["metrics"][2]["label"] == "sum" diff --git a/tests/unit_tests/utils/test_core.py b/tests/unit_tests/utils/test_core.py index 37b8ed1877a1..36650e36ef8a 100644 --- a/tests/unit_tests/utils/test_core.py +++ b/tests/unit_tests/utils/test_core.py @@ -1162,3 +1162,35 @@ def test_sanitize_url_blocks_dangerous(): """Test that dangerous URL schemes are blocked.""" assert sanitize_url("javascript:alert('xss')") == "" assert sanitize_url("data:text/html,") == "" + + +def test_get_metric_name_with_integers() -> None: + """Test get_metric_name with integer IDs""" + from superset.utils.core import get_metric_name + + assert get_metric_name("count") == "count" + assert get_metric_name(123) == "123" + assert get_metric_name(456) == "456" + + from superset.superset_typing import AdhocMetric + + adhoc_metric: AdhocMetric = { + "expressionType": "SQL", + "sqlExpression": "SUM(amount)", + "label": "total_amount", + } + assert get_metric_name(adhoc_metric) == "total_amount" + + +def test_get_column_name_with_integers() -> None: + """Test get_column_name with integer IDs""" + from superset.utils.core import get_column_name + + assert get_column_name("age") == "age" + assert get_column_name(123) == "123" + assert get_column_name(789) == "789" + + from superset.superset_typing import AdhocColumn + + column_dict: AdhocColumn = {"label": "customer_name", "sqlExpression": "name"} + assert get_column_name(column_dict) == "customer_name" From c5bb49043e46dcef4ec16ce77863c702f8a78a47 Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Fri, 31 Oct 2025 23:29:59 +0530 Subject: [PATCH 3/7] feat(transform): enhance getMetricLabelFromFormData to support metric types and improve handling of undefined values --- .../legacy-preset-chart-deckgl/src/layers/transformUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts index 6427db900a7f..4dff65a36d31 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts @@ -134,9 +134,10 @@ export function addPropertiesToFeature>( } export function getMetricLabelFromFormData( - metric: string | { value?: string } | undefined, + metric: string | { type?: string; value?: string | number } | undefined, ): string | undefined { if (!metric) return undefined; if (typeof metric === 'string') return getMetricLabel(metric); - return metric.value ? getMetricLabel(metric.value) : undefined; + if (metric.type === 'fix') return undefined; + return metric.value ? getMetricLabel(String(metric.value)) : undefined; } From e3a329dc116b969e7cc5f175f3c66a19330dd891 Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Wed, 5 Nov 2025 00:04:27 +0530 Subject: [PATCH 4/7] fix(tests): remove redundant test case for invalid metric input --- tests/unit_tests/core_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit_tests/core_tests.py b/tests/unit_tests/core_tests.py index acd0501d82fa..cfef7868796b 100644 --- a/tests/unit_tests/core_tests.py +++ b/tests/unit_tests/core_tests.py @@ -111,8 +111,6 @@ def test_get_metric_name_invalid_metric(): with pytest.raises(ValueError): # noqa: PT011 get_metric_name(None) - with pytest.raises(ValueError): # noqa: PT011 - get_metric_name(0) with pytest.raises(ValueError): # noqa: PT011 get_metric_name({}) From 0dcb52e9389c9d947d438fd6968f12c641e9627d Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Fri, 14 Nov 2025 22:58:39 +0530 Subject: [PATCH 5/7] style: apply ruff formatting to test_schemas.py --- tests/unit_tests/charts/test_schemas.py | 44 ++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/unit_tests/charts/test_schemas.py b/tests/unit_tests/charts/test_schemas.py index 46865b0d26c4..a7b53a848a5c 100644 --- a/tests/unit_tests/charts/test_schemas.py +++ b/tests/unit_tests/charts/test_schemas.py @@ -161,22 +161,30 @@ def test_chart_data_query_object_schema_orderby_validation( schema = ChartDataQueryObjectSchema() # Valid values should pass - for orderby_value in ["column_name", 123, {"label": "my_metric", "sqlExpression": "SUM(col)"}]: - result = schema.load({ - "datasource": {"type": "table", "id": 1}, - "metrics": ["count"], - "orderby": [[orderby_value, False]], - }) + for orderby_value in [ + "column_name", + 123, + {"label": "my_metric", "sqlExpression": "SUM(col)"}, + ]: + result = schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [[orderby_value, False]], + } + ) assert result["orderby"][0][1] is False # None and empty string should fail for invalid_value in [None, ""]: with pytest.raises(ValidationError): - schema.load({ - "datasource": {"type": "table", "id": 1}, - "metrics": ["count"], - "orderby": [[invalid_value, True]], - }) + schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": ["count"], + "orderby": [[invalid_value, True]], + } + ) def test_chart_data_query_object_schema_metrics_validation( @@ -186,10 +194,16 @@ def test_chart_data_query_object_schema_metrics_validation( schema = ChartDataQueryObjectSchema() # Mix of different types should all pass - result = schema.load({ - "datasource": {"type": "table", "id": 1}, - "metrics": ["count", 123, {"expressionType": "SQL", "sqlExpression": "SUM(col)", "label": "sum"}], - }) + result = schema.load( + { + "datasource": {"type": "table", "id": 1}, + "metrics": [ + "count", + 123, + {"expressionType": "SQL", "sqlExpression": "SUM(col)", "label": "sum"}, + ], + } + ) assert result["metrics"][0] == "count" assert result["metrics"][1] == 123 assert result["metrics"][2]["label"] == "sum" From 228cb9a9aff1c8d3973f500052e67869ddab226b Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Tue, 18 Nov 2025 18:20:42 +0530 Subject: [PATCH 6/7] refactor: update type aliases to modern TypeAlias syntax while keeping int support --- superset/superset_typing.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/superset/superset_typing.py b/superset/superset_typing.py index a3f4a35769b2..bd1f2c9f11c6 100644 --- a/superset/superset_typing.py +++ b/superset/superset_typing.py @@ -16,7 +16,7 @@ # under the License. from collections.abc import Sequence from datetime import datetime -from typing import Any, Literal, Optional, TYPE_CHECKING, TypedDict, Union +from typing import Any, Literal, Optional, TYPE_CHECKING, TypeAlias, TypedDict, Union from sqlalchemy.sql.type_api import TypeEngine from typing_extensions import NotRequired @@ -100,22 +100,24 @@ class ResultSetColumnType(TypedDict): Optional[int], bool, ] -DbapiDescription = Union[list[DbapiDescriptionRow], tuple[DbapiDescriptionRow, ...]] -DbapiResult = Sequence[Union[list[Any], tuple[Any, ...]]] -FilterValue = Union[bool, datetime, float, int, str] -FilterValues = Union[FilterValue, list[FilterValue], tuple[FilterValue]] -FormData = dict[str, Any] -Granularity = Union[str, dict[str, Union[str, float]]] -Column = Union[AdhocColumn, str, int] -Metric = Union[AdhocMetric, str, int] -OrderBy = tuple[Union[Metric, Column], bool] -QueryObjectDict = dict[str, Any] -VizData = Optional[Union[list[Any], dict[Any, Any]]] -VizPayload = dict[str, Any] +DbapiDescription: TypeAlias = ( + list[DbapiDescriptionRow] | tuple[DbapiDescriptionRow, ...] +) +DbapiResult: TypeAlias = Sequence[list[Any] | tuple[Any, ...]] +FilterValue: TypeAlias = bool | datetime | float | int | str +FilterValues: TypeAlias = FilterValue | list[FilterValue] | tuple[FilterValue] +FormData: TypeAlias = dict[str, Any] +Granularity: TypeAlias = str | dict[str, str | float] +Column: TypeAlias = AdhocColumn | str | int +Metric: TypeAlias = AdhocMetric | str | int +OrderBy: TypeAlias = tuple[Metric | Column, bool] +QueryObjectDict: TypeAlias = dict[str, Any] +VizData: TypeAlias = list[Any] | dict[Any, Any] | None +VizPayload: TypeAlias = dict[str, Any] # Flask response. -Base = Union[bytes, str] -Status = Union[int, str] +Base: TypeAlias = bytes | str +Status: TypeAlias = int | str Headers = dict[str, Any] FlaskResponse = Union[ Response, From 4a4b047edf3e95041149ed9981079db247d5d55d Mon Sep 17 00:00:00 2001 From: Debjit Mandal Date: Tue, 18 Nov 2025 18:30:29 +0530 Subject: [PATCH 7/7] fix: resolve merge conflict in superset_typing.py preserving int support - Updated Column and Metric types to include int (needed for deck.gl) - Added QueryObjectDict, BaseDatasourceData, and QueryData TypedDict classes from master - Modernized all type hints to use TypeAlias and | syntax (Python 3.10+) - Removed deprecated Optional/Union imports in favor of modern syntax --- superset/superset_typing.py | 293 +++++++++++++++++++++++++++++++----- 1 file changed, 256 insertions(+), 37 deletions(-) diff --git a/superset/superset_typing.py b/superset/superset_typing.py index bd1f2c9f11c6..488d6b6b9bb4 100644 --- a/superset/superset_typing.py +++ b/superset/superset_typing.py @@ -14,58 +14,61 @@ # 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 __future__ import annotations + +from collections.abc import Hashable, Sequence from datetime import datetime -from typing import Any, Literal, Optional, TYPE_CHECKING, TypeAlias, TypedDict, Union +from typing import Any, Literal, TYPE_CHECKING, TypeAlias, TypedDict from sqlalchemy.sql.type_api import TypeEngine 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]] +SQLType: TypeAlias = TypeEngine | type[TypeEngine] class LegacyMetric(TypedDict): - label: Optional[str] + label: str | None class AdhocMetricColumn(TypedDict, total=False): - column_name: Optional[str] - description: Optional[str] - expression: Optional[str] + column_name: str | None + description: str | None + expression: str | None filterable: bool groupby: bool id: int is_dttm: bool - python_date_format: Optional[str] + python_date_format: str | None type: str type_generic: "GenericDataType" - verbose_name: Optional[str] + verbose_name: str | None class AdhocMetric(TypedDict, total=False): aggregate: str - column: Optional[AdhocMetricColumn] + column: AdhocMetricColumn | None expressionType: Literal["SIMPLE", "SQL"] - hasCustomLabel: Optional[bool] - label: Optional[str] - sqlExpression: Optional[str] + hasCustomLabel: bool | None + label: str | None + sqlExpression: str | None class AdhocColumn(TypedDict, total=False): - hasCustomLabel: Optional[bool] + hasCustomLabel: bool | None label: str sqlExpression: str - columnType: Optional[Literal["BASE_AXIS", "SERIES"]] - timeGrain: Optional[str] + isColumnReference: bool | None + columnType: Literal["BASE_AXIS", "SERIES"] | None + timeGrain: str | None class SQLAColumnType(TypedDict): name: str - type: Optional[str] + type: str | None is_dttm: bool @@ -76,9 +79,9 @@ class ResultSetColumnType(TypedDict): name: str # legacy naming convention keeping this for backwards compatibility column_name: str - type: Optional[Union[SQLType, str]] - is_dttm: Optional[bool] - type_generic: NotRequired[Optional["GenericDataType"]] + type: SQLType | str | None + is_dttm: bool | None + type_generic: NotRequired["GenericDataType" | None] nullable: NotRequired[Any] default: NotRequired[Any] @@ -90,14 +93,14 @@ class ResultSetColumnType(TypedDict): query_as: NotRequired[Any] -CacheConfig = dict[str, Any] -DbapiDescriptionRow = tuple[ - Union[str, bytes], +CacheConfig: TypeAlias = dict[str, Any] +DbapiDescriptionRow: TypeAlias = tuple[ + str | bytes, str, - Optional[str], - Optional[str], - Optional[int], - Optional[int], + str | None, + str | None, + int | None, + int | None, bool, ] DbapiDescription: TypeAlias = ( @@ -111,21 +114,237 @@ class ResultSetColumnType(TypedDict): Column: TypeAlias = AdhocColumn | str | int Metric: TypeAlias = AdhocMetric | str | int OrderBy: TypeAlias = tuple[Metric | Column, bool] -QueryObjectDict: TypeAlias = 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: datetime | None + granularity: str | None + inner_from_dttm: datetime | None + inner_to_dttm: datetime | None + is_rowcount: bool + is_timeseries: bool + metrics: list[Metric] | None + order_desc: bool + orderby: list[OrderBy] + row_limit: int | None + row_offset: int + series_columns: list[Column] + series_limit: int + series_limit_metric: Metric | None + group_others_when_limit_reached: bool + to_dttm: datetime | None + time_shift: str | None + + # Additional fields used throughout the codebase + time_range: str | None + datasource: Any # BaseDatasource instance + extra_cache_keys: list[Hashable] + rls: list[Any] + changed_on: datetime | None + + # Deprecated fields (still in use) + groupby: list[Column] + timeseries_limit: int + timeseries_limit_metric: Metric | None + + +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, str | None] + description: str | None + database: dict[str, Any] + default_endpoint: str | None + filter_select: bool + filter_select_enabled: bool + name: str + datasource_name: str + table_name: str + type: str + catalog: str | None + schema: str | None + offset: int + cache_timeout: int | None + params: str | None + perm: str | None + edit_url: str + sql: str | None + 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: list[int] | list[dict[str, Any]] # Can be either format + verbose_map: dict[str, str] + select_star: str | None + + # Additional fields from SqlaTable and data_for_slices + column_types: list[Any] + column_names: set[str] | set[Any] + granularity_sqla: list[tuple[Any, Any]] + time_grain_sqla: list[tuple[Any, Any]] + main_dttm_col: str | None + fetch_values_predicate: str | None + template_params: str | None + is_sqllab_view: bool + health_check_message: str | None + extra: str | None + 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: 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] # Flask response. Base: TypeAlias = bytes | str Status: TypeAlias = int | str -Headers = dict[str, Any] -FlaskResponse = Union[ - Response, - Base, - tuple[Base, Status], - tuple[Base, Status, Headers], - tuple[Response, Status], -] +Headers: TypeAlias = dict[str, Any] +FlaskResponse: TypeAlias = ( + Response + | Base + | tuple[Base, Status] + | tuple[Base, Status, Headers] + | tuple[Response, Status] +) class OAuth2ClientConfig(TypedDict):