You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ri...@apache.org on 2026/09/18 19:36:00 UTC
(superset) branch master updated: fix(semantic-layer): mapper honors the SemanticView ABC contract (#43886)
This is an automated email from the ASF dual-hosted git repository.
richardfogaca 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 43f85576d99 fix(semantic-layer): mapper honors the SemanticView ABC contract (#43886)
43f85576d99 is described below
commit 43f85576d99380bb7f7e4180041850a45b26c6e4
Author: Mike Bridge <mi...@preset.io>
AuthorDate: Fri Sep 18 13:35:53 2026 -0600
fix(semantic-layer): mapper honors the SemanticView ABC contract (#43886)
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
superset/semantic_layers/mapper.py | 31 ++--
tests/unit_tests/semantic_layers/mapper_test.py | 213 +++++++++++++++++-------
2 files changed, 167 insertions(+), 77 deletions(-)
diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 32a6ea2c450..af253ccc679 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -346,11 +346,13 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
visualization and more on semantics.
"""
semantic_view = query_object.datasource.implementation
+ # The SemanticView ABC contract is the get_metrics()/get_dimensions()
+ # methods; never read undeclared attributes off the provider view.
+ view_metrics = semantic_view.get_metrics()
+ view_dimensions = semantic_view.get_dimensions()
- all_metrics = {metric.name: metric for metric in semantic_view.metrics}
- all_dimensions = {
- dimension.name: dimension for dimension in semantic_view.dimensions
- }
+ all_metrics = {metric.name: metric for metric in view_metrics}
+ all_dimensions = {dimension.name: dimension for dimension in view_dimensions}
# Normalize columns (may be dicts with isColumnReference=True for time-series)
dimension_names = set(all_dimensions.keys())
@@ -373,7 +375,7 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
seen_non_axis: dict[str, Dimension] = {}
axis_variants: list[Dimension] = []
axis_match: Dimension | None = None
- for dimension in semantic_view.dimensions:
+ for dimension in view_dimensions:
if dimension.name not in normalized_columns:
continue
if dimension.name == time_axis_column:
@@ -1123,7 +1125,7 @@ def _validate_metrics(query_object: ValidatedQueryObject) -> None:
if any(not isinstance(metric, str) for metric in (query_object.metrics or [])):
raise ValueError("Adhoc metrics are not supported in Semantic Views.")
- metric_names = {metric.name for metric in semantic_view.metrics}
+ metric_names = {metric.name for metric in semantic_view.get_metrics()}
if not set(query_object.metrics or []) <= metric_names:
raise ValueError("All metrics must be defined in the Semantic View.")
@@ -1133,7 +1135,7 @@ def _validate_dimensions(query_object: ValidatedQueryObject) -> None:
Make sure all dimensions are defined in the semantic view.
"""
semantic_view = query_object.datasource.implementation
- dimension_names = {dimension.name for dimension in semantic_view.dimensions}
+ dimension_names = {dimension.name for dimension in semantic_view.get_dimensions()}
# Normalize all columns to dimension names
normalized_columns = [
@@ -1162,9 +1164,8 @@ def _validate_granularity(query_object: ValidatedQueryObject) -> None:
Make sure time column and time grain are valid.
"""
semantic_view = query_object.datasource.implementation
- all_dimensions = {
- dimension.name: dimension for dimension in semantic_view.dimensions
- }
+ view_dimensions = semantic_view.get_dimensions()
+ all_dimensions = {dimension.name: dimension for dimension in view_dimensions}
dimension_names = set(all_dimensions.keys())
if (legacy_time_column := query_object.granularity) and (
@@ -1183,7 +1184,7 @@ def _validate_granularity(query_object: ValidatedQueryObject) -> None:
supported_time_grains = {
dimension.grain
- for dimension in semantic_view.dimensions
+ for dimension in view_dimensions
if dimension.name == time_column and dimension.grain
}
if _convert_time_grain(time_grain) not in supported_time_grains:
@@ -1212,7 +1213,7 @@ def _validate_group_limit(query_object: ValidatedQueryObject) -> None:
if any(not isinstance(col, str) for col in query_object.series_columns):
raise ValueError("Adhoc dimensions are not supported in series columns.")
- metric_names = {metric.name for metric in semantic_view.metrics}
+ metric_names = {metric.name for metric in semantic_view.get_metrics()}
if query_object.series_limit_metric and (
not isinstance(query_object.series_limit_metric, str)
or query_object.series_limit_metric not in metric_names
@@ -1221,7 +1222,7 @@ def _validate_group_limit(query_object: ValidatedQueryObject) -> None:
"The series limit metric must be defined in the Semantic View."
)
- dimension_names = {dimension.name for dimension in semantic_view.dimensions}
+ dimension_names = {dimension.name for dimension in semantic_view.get_dimensions()}
if not set(query_object.series_columns) <= dimension_names:
raise ValueError("All series columns must be defined in the Semantic View.")
@@ -1251,7 +1252,7 @@ def _validate_orderby(query_object: ValidatedQueryObject) -> None:
)
elements = {orderby[0] for orderby in query_object.orderby}
- metric_names = {metric.name for metric in semantic_view.metrics}
- dimension_names = {dimension.name for dimension in semantic_view.dimensions}
+ metric_names = {metric.name for metric in semantic_view.get_metrics()}
+ dimension_names = {dimension.name for dimension in semantic_view.get_dimensions()}
if not elements <= metric_names | dimension_names:
raise ValueError("All order by elements must be defined in the Semantic View.")
diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py
index 47afe4434d3..b7ae9371b16 100644
--- a/tests/unit_tests/semantic_layers/mapper_test.py
+++ b/tests/unit_tests/semantic_layers/mapper_test.py
@@ -40,7 +40,7 @@ from superset_core.semantic_layers.types import (
SemanticRequest,
SemanticResult,
)
-from superset_core.semantic_layers.view import SemanticViewFeature
+from superset_core.semantic_layers.view import SemanticView, SemanticViewFeature
from superset.semantic_layers.mapper import (
_coerce_scalar_filter_value,
@@ -75,7 +75,12 @@ Feature = SemanticViewFeature
class MockSemanticView:
"""
- Mock implementation of SemanticView protocol.
+ Mock implementation of the SemanticView ABC surface.
+
+ Deliberately exposes NO public ``metrics``/``dimensions`` attributes —
+ the ABC declares only ``get_metrics()``/``get_dimensions()`` — so every
+ test in this file exercises the method contract; mapper code that
+ duck-types the undeclared attributes fails here (apache/superset#43886).
"""
def __init__(
@@ -84,18 +89,70 @@ class MockSemanticView:
metrics: set[Metric],
features: frozenset[SemanticViewFeature],
):
- self.dimensions = dimensions
- self.metrics = metrics
+ self._dimensions = dimensions
+ self._metrics = metrics
self.features = features
def uid(self) -> str:
return "mock_semantic_view"
def get_dimensions(self) -> set[Dimension]:
- return self.dimensions
+ return self._dimensions
+
+ def get_metrics(self) -> set[Metric]:
+ return self._metrics
+
+
+class AbcOnlyView(SemanticView):
+ """Strictly ABC-compliant view: implements ONLY the abstract surface.
+
+ Regression fixture for apache/superset#43886 — no ``metrics`` or
+ ``dimensions`` attributes exist on it, so mapper code that duck-types
+ those names (instead of
+ calling the declared ``get_metrics()``/``get_dimensions()``) raises
+ AttributeError here. A provider built exactly to the documented ABC
+ used to 500 on its first chart query.
+ """
+
+ features: frozenset[SemanticViewFeature] = frozenset()
+ name = "abc_only"
+
+ def __init__(self, dimensions: set[Dimension], metrics: set[Metric]) -> None:
+ # Name-mangled on purpose (unlike MockSemanticView's _-prefixed
+ # storage): guarantees no duck-typable metrics/dimensions — or even
+ # _metrics/_dimensions — attribute exists on this fixture.
+ self.__dimensions = dimensions
+ self.__metrics = metrics
+
+ def uid(self) -> str:
+ return "abc_only"
+
+ def get_dimensions(self) -> set[Dimension]:
+ return self.__dimensions
def get_metrics(self) -> set[Metric]:
- return self.metrics
+ return self.__metrics
+
+ def get_values(
+ self, dimension: Dimension, filters: set[Filter] | None = None
+ ) -> SemanticResult:
+ raise NotImplementedError
+
+ def get_table(self, query: SemanticQuery) -> SemanticResult:
+ raise NotImplementedError
+
+ def get_row_count(self, query: SemanticQuery) -> SemanticResult:
+ raise NotImplementedError
+
+ def get_compatible_metrics(
+ self, selected_metrics: set[Metric], selected_dimensions: set[Dimension]
+ ) -> set[Metric]:
+ raise NotImplementedError
+
+ def get_compatible_dimensions(
+ self, selected_metrics: set[Metric], selected_dimensions: set[Dimension]
+ ) -> set[Dimension]:
+ raise NotImplementedError
@pytest.fixture
@@ -289,7 +346,7 @@ def test_get_time_filter_no_granularity(mock_datasource: MagicMock) -> None:
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_time_filter(query_object, None, all_dimensions)
@@ -314,7 +371,7 @@ def test_get_time_filter_with_granularity(mock_datasource: MagicMock) -> None:
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_time_filter(query_object, None, all_dimensions)
@@ -356,7 +413,7 @@ def test_convert_query_object_filter_in(mock_datasource: MagicMock) -> None:
Test conversion of IN filter.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
filter_: ValidatedQueryObjectFilterClause = {
"op": FilterOperator.IN.value,
@@ -386,7 +443,7 @@ def test_convert_query_object_filter_ilike(
reject the operator for every provider.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
for op, expected in (
(FilterOperator.ILIKE.value, Operator.ILIKE),
@@ -411,7 +468,7 @@ def test_convert_query_object_filter_is_null(mock_datasource: MagicMock) -> None
Test conversion of IS_NULL filter.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
filter_: ValidatedQueryObjectFilterClause = {
"op": FilterOperator.IS_NULL.value,
@@ -445,7 +502,7 @@ def test_get_filters_from_query_object_basic(mock_datasource: MagicMock) -> None
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_filters_from_query_object(query_object, None, all_dimensions)
@@ -481,7 +538,7 @@ def test_get_filters_from_query_object_with_extras(mock_datasource: MagicMock) -
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_filters_from_query_object(query_object, None, all_dimensions)
@@ -527,7 +584,7 @@ def test_get_filters_from_query_object_with_fetch_values(
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_filters_from_query_object(query_object, None, all_dimensions)
@@ -559,10 +616,10 @@ def test_get_order_from_query_object_metric(mock_datasource: MagicMock) -> None:
Test order extraction with metric.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -582,10 +639,10 @@ def test_get_order_from_query_object_dimension(mock_datasource: MagicMock) -> No
Test order extraction with dimension.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -605,10 +662,10 @@ def test_get_order_from_query_object_adhoc(mock_datasource: MagicMock) -> None:
Test order extraction with adhoc expression.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -636,10 +693,10 @@ def test_get_group_limit_from_query_object_none(mock_datasource: MagicMock) -> N
Test that None is returned with no columns.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -662,10 +719,10 @@ def test_get_group_limit_from_query_object_basic(mock_datasource: MagicMock) ->
Test basic group limit creation.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -701,10 +758,10 @@ def test_get_group_limit_from_query_object_with_group_others(
Test group limit with group_others enabled.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -732,7 +789,7 @@ def test_get_group_limit_filters_no_inner_bounds(mock_datasource: MagicMock) ->
Test that None is returned when no inner bounds.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -755,7 +812,7 @@ def test_get_group_limit_filters_same_bounds(mock_datasource: MagicMock) -> None
Test that None is returned when inner bounds equal outer bounds.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
from_dttm = datetime(2025, 10, 15)
@@ -782,7 +839,7 @@ def test_get_group_limit_filters_different_bounds(mock_datasource: MagicMock) ->
Test filter creation when inner bounds differ.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -819,7 +876,7 @@ def test_get_group_limit_filters_with_extras(mock_datasource: MagicMock) -> None
Test that extras filters are included in group limit filters.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -1416,7 +1473,7 @@ def test_convert_query_object_filter_unknown_operator(
Test filter with unknown operator raises ValueError.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
filter_: ValidatedQueryObjectFilterClause = {
@@ -1515,9 +1572,11 @@ def test_validate_query_object_group_limit_not_supported_error(
"total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales"
)
- mock_datasource.implementation.dimensions = {time_dim, category_dim}
- mock_datasource.implementation.metrics = {sales_metric}
- mock_datasource.implementation.features = frozenset() # No GROUP_LIMIT feature
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={time_dim, category_dim},
+ metrics={sales_metric},
+ features=frozenset(), # No GROUP_LIMIT feature
+ )
query_object = ValidatedQueryObject(
datasource=mock_datasource,
@@ -2351,7 +2410,7 @@ def test_get_filters_from_query_object_with_filter_clauses(
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_filters_from_query_object(query_object, None, all_dimensions)
@@ -2378,7 +2437,7 @@ def test_get_time_filter_unknown_granularity(mock_datasource: MagicMock) -> None
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_time_filter(query_object, None, all_dimensions)
@@ -2400,7 +2459,7 @@ def test_get_time_filter_missing_bounds(mock_datasource: MagicMock) -> None:
)
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
result = _get_time_filter(query_object, None, all_dimensions)
@@ -2643,10 +2702,10 @@ def test_get_order_adhoc_with_none_sql_expression(mock_datasource: MagicMock) ->
Test order extraction skips adhoc expression with None sqlExpression.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -2669,10 +2728,10 @@ def test_get_order_unknown_element(mock_datasource: MagicMock) -> None:
Test order extraction skips unknown elements.
"""
all_metrics = {
- metric.name: metric for metric in mock_datasource.implementation.metrics
+ metric.name: metric for metric in mock_datasource.implementation.get_metrics()
}
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -2697,7 +2756,7 @@ def test_get_group_limit_filters_with_granularity_no_time_dimension(
Test group limit filters when granularity doesn't match any dimension.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -2726,7 +2785,7 @@ def test_get_group_limit_filters_with_fetch_values_predicate(
mock_datasource.fetch_values_predicate = "tenant_id = 123"
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -2762,7 +2821,7 @@ def test_get_group_limit_filters_with_filter_clauses(
Test group limit filters include converted filter clauses.
"""
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
query_object = ValidatedQueryObject(
@@ -2824,8 +2883,11 @@ def test_validate_metrics_adhoc_error(
"total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales"
)
- mock_datasource.implementation.dimensions = {category_dim}
- mock_datasource.implementation.metrics = {sales_metric}
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={category_dim},
+ metrics={sales_metric},
+ features=frozenset(),
+ )
# Manually create a query object with an adhoc metric
query_object = mocker.Mock()
@@ -2949,11 +3011,11 @@ def test_validate_query_object_group_others_not_supported_error(
"total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales"
)
- mock_datasource.implementation.dimensions = {time_dim, category_dim}
- mock_datasource.implementation.metrics = {sales_metric}
- # Has GROUP_LIMIT but not GROUP_OTHERS
- mock_datasource.implementation.features = frozenset(
- {SemanticViewFeature.GROUP_LIMIT}
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={time_dim, category_dim},
+ metrics={sales_metric},
+ # Has GROUP_LIMIT but not GROUP_OTHERS
+ features=frozenset({SemanticViewFeature.GROUP_LIMIT}),
)
query_object = ValidatedQueryObject(
@@ -2983,11 +3045,11 @@ def test_validate_query_object_adhoc_orderby_not_supported_error(
"total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales"
)
- mock_datasource.implementation.dimensions = {category_dim}
- mock_datasource.implementation.metrics = {sales_metric}
- mock_datasource.implementation.features = (
- frozenset()
- ) # No ADHOC_EXPRESSIONS_IN_ORDERBY
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={category_dim},
+ metrics={sales_metric},
+ features=frozenset(), # No ADHOC_EXPRESSIONS_IN_ORDERBY
+ )
query_object = ValidatedQueryObject(
datasource=mock_datasource,
@@ -3204,7 +3266,9 @@ def test_validate_granularity_valid(mocker: MockerFixture) -> None:
"order_date", "order_date", pa.utf8(), "order_date", "Date", Grains.DAY
)
- mock_datasource.implementation.dimensions = {time_dim}
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={time_dim}, metrics=set(), features=frozenset()
+ )
query_object = mocker.Mock()
query_object.datasource = mock_datasource
@@ -3226,10 +3290,12 @@ def test_validate_group_limit_valid(mocker: MockerFixture) -> None:
"total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales"
)
- mock_datasource.implementation.dimensions = {category_dim}
- mock_datasource.implementation.metrics = {sales_metric}
- mock_datasource.implementation.features = frozenset(
- {SemanticViewFeature.GROUP_LIMIT, SemanticViewFeature.GROUP_OTHERS}
+ mock_datasource.implementation = MockSemanticView(
+ dimensions={category_dim},
+ metrics={sales_metric},
+ features=frozenset(
+ {SemanticViewFeature.GROUP_LIMIT, SemanticViewFeature.GROUP_OTHERS}
+ ),
)
query_object = mocker.Mock()
@@ -4048,6 +4114,29 @@ def test_mapper_accepts_grain_column_built_by_tabular_query(
assert base_axis["columnType"] == "BASE_AXIS"
all_dimensions = {
- dim.name: dim for dim in mock_datasource.implementation.dimensions
+ dim.name: dim for dim in mock_datasource.implementation.get_dimensions()
}
assert _normalize_column(base_axis, set(all_dimensions)) == "order_date"
+
+
+def test_abc_only_provider_validates_and_maps(mocker: MockerFixture) -> None:
+ """apache/superset#43886 regression: a provider implementing ONLY the SemanticView
+ ABC — the abstract methods, no undeclared attributes — must pass
+ validation and mapping. The mapper used to read bare
+ ``.metrics``/``.dimensions`` and crash with AttributeError (an HTTP 500
+ on the first chart query) for exactly such a provider."""
+ datasource = mocker.Mock()
+ category = Dimension("category", "category", pa.utf8(), "category", "Category")
+ sales = Metric("total_sales", "total_sales", pa.float64(), "SUM(amount)", "Sales")
+ datasource.implementation = AbcOnlyView(dimensions={category}, metrics={sales})
+ query_object = ValidatedQueryObject(
+ datasource=datasource,
+ metrics=["total_sales"],
+ columns=["category"],
+ )
+
+ assert validate_query_object(query_object)
+ queries = map_query_object(query_object)
+
+ assert {metric.name for metric in queries[0].metrics} == {"total_sales"}
+ assert {dim.name for dim in queries[0].dimensions} == {"category"}