diff --git a/superset-core/src/superset_core/semantic_layers/semantic_layer.py b/superset-core/src/superset_core/semantic_layers/layer.py similarity index 98% rename from superset-core/src/superset_core/semantic_layers/semantic_layer.py rename to superset-core/src/superset_core/semantic_layers/layer.py index 1fc421cf359..32d924334ae 100644 --- a/superset-core/src/superset_core/semantic_layers/semantic_layer.py +++ b/superset-core/src/superset_core/semantic_layers/layer.py @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod from typing import Any, Generic, TypeVar from pydantic import BaseModel -from superset_core.semantic_layers.semantic_view import SemanticView +from superset_core.semantic_layers.view import SemanticView ConfigT = TypeVar("ConfigT", bound=BaseModel) SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView") diff --git a/superset-core/src/superset_core/semantic_layers/types.py b/superset-core/src/superset_core/semantic_layers/types.py index d62de2c6cb8..a6165872438 100644 --- a/superset-core/src/superset_core/semantic_layers/types.py +++ b/superset-core/src/superset_core/semantic_layers/types.py @@ -25,29 +25,6 @@ from typing import Type as TypeOf import pyarrow as pa -__all__ = [ - "BINARY", - "BOOLEAN", - "DATE", - "DATETIME", - "DECIMAL", - "Day", - "Dimension", - "Hour", - "INTEGER", - "INTERVAL", - "Minute", - "Month", - "NUMBER", - "OBJECT", - "Quarter", - "Second", - "STRING", - "TIME", - "Week", - "Year", -] - class Type: """ @@ -216,7 +193,7 @@ class Metric: name: str type: TypeOf[Type] - definition: str | None + definition: str | None = None description: str | None = None diff --git a/superset-core/src/superset_core/semantic_layers/semantic_view.py b/superset-core/src/superset_core/semantic_layers/view.py similarity index 84% rename from superset-core/src/superset_core/semantic_layers/semantic_view.py rename to superset-core/src/superset_core/semantic_layers/view.py index 7f7c97fcc12..360f64cdb1c 100644 --- a/superset-core/src/superset_core/semantic_layers/semantic_view.py +++ b/superset-core/src/superset_core/semantic_layers/view.py @@ -77,7 +77,7 @@ class SemanticView(ABC): """ @abstractmethod - def get_dataframe( + def get_table( self, metrics: list[Metric], dimensions: list[Dimension], @@ -107,3 +107,23 @@ class SemanticView(ABC): """ Execute a query and return the number of rows the result would have. """ + + @abstractmethod + def get_compatible_metrics( + self, + selected_metrics: set[Metric], + selected_dimensions: set[Dimension], + ) -> set[Metric]: + """ + Return metrics compatible with the selected dimensions. + """ + + @abstractmethod + def get_compatible_dimensions( + self, + selected_metrics: set[Metric], + selected_dimensions: set[Dimension], + ) -> set[Dimension]: + """ + Return dimensions compatible with the selected metrics. + """ diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py index f137b8fa572..d3220154cc0 100644 --- a/superset/semantic_layers/mapper.py +++ b/superset/semantic_layers/mapper.py @@ -30,7 +30,6 @@ from typing import Any, cast, Sequence, TypeGuard import numpy as np import pyarrow as pa -from superset_core.semantic_layers.semantic_view import SemanticViewFeature from superset_core.semantic_layers.types import ( AdhocExpression, Day, @@ -54,6 +53,7 @@ from superset_core.semantic_layers.types import ( Week, Year, ) +from superset_core.semantic_layers.view import SemanticViewFeature from superset.common.db_query_status import QueryStatus from superset.common.query_object import QueryObject @@ -348,8 +348,6 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]: time_offset, all_dimensions, ) - print(">>", filters) - queries.append( SemanticQuery( metrics=metrics, @@ -603,8 +601,8 @@ def _convert_query_object_filter( operator = operator_mapping.get(operator_str) if not operator: - # Unknown operator - create adhoc filter - return None + # Unknown operator - raise error to prevent unauthorized access + raise ValueError(f"Unsupported filter operator: {operator_str}") return { Filter( diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py index ab6586d0f20..37711ee1686 100644 --- a/tests/unit_tests/semantic_layers/mapper_test.py +++ b/tests/unit_tests/semantic_layers/mapper_test.py @@ -22,7 +22,6 @@ import pandas as pd import pyarrow as pa import pytest from pytest_mock import MockerFixture -from superset_core.semantic_layers.semantic_view import SemanticViewFeature from superset_core.semantic_layers.types import ( AdhocExpression, Day, @@ -48,6 +47,7 @@ from superset_core.semantic_layers.types import ( Week, Year, ) +from superset_core.semantic_layers.view import SemanticViewFeature from superset.semantic_layers.mapper import ( _convert_query_object_filter, @@ -59,12 +59,18 @@ from superset.semantic_layers.mapper import ( _get_order_from_query_object, _get_time_bounds, _get_time_filter, + _normalize_column, + _validate_filters, + _validate_granularity, + _validate_group_limit, + _validate_metrics, get_results, map_query_object, validate_query_object, ValidatedQueryObject, ValidatedQueryObjectFilterClause, ) +from superset.superset_typing import AdhocColumn from superset.utils.core import FilterOperator # Alias for convenience @@ -1717,9 +1723,6 @@ def test_normalize_column_adhoc_not_in_dimensions() -> None: """ Test _normalize_column raises error for AdhocColumn with sqlExpression not in dims. """ - from superset.semantic_layers.mapper import _normalize_column - from superset.superset_typing import AdhocColumn - dimension_names = {"category", "region"} adhoc_column: AdhocColumn = { "isColumnReference": True, @@ -1734,9 +1737,6 @@ def test_normalize_column_adhoc_missing_sql_expression() -> None: """ Test _normalize_column raises error for AdhocColumn without sqlExpression. """ - from superset.semantic_layers.mapper import _normalize_column - from superset.superset_typing import AdhocColumn - dimension_names = {"category", "region"} adhoc_column: AdhocColumn = { "isColumnReference": True, @@ -1750,9 +1750,6 @@ def test_normalize_column_adhoc_valid(mock_datasource: MagicMock) -> None: """ Test _normalize_column with valid AdhocColumn reference. """ - from superset.semantic_layers.mapper import _normalize_column - from superset.superset_typing import AdhocColumn - dimension_names = {"category", "region"} adhoc_column: AdhocColumn = { "isColumnReference": True, @@ -2115,8 +2112,6 @@ def test_validate_metrics_adhoc_error( """ Test validation error for adhoc metrics. """ - from superset.semantic_layers.mapper import _validate_metrics - mock_datasource = mocker.Mock() category_dim = Dimension("category", "category", STRING, "category", "Category") sales_metric = Metric("total_sales", "total_sales", NUMBER, "SUM(amount)", "Sales") @@ -2139,7 +2134,6 @@ def test_validate_filters_adhoc_column_error( """ Test validation error for adhoc column in filter. """ - from superset.semantic_layers.mapper import _validate_filters query_object = mocker.Mock() query_object.filter = [ @@ -2160,7 +2154,6 @@ def test_validate_filters_missing_operator_error( """ Test validation error for filter without operator. """ - from superset.semantic_layers.mapper import _validate_filters query_object = mocker.Mock() query_object.filter = [ @@ -2481,7 +2474,6 @@ def test_validate_filters_empty(mocker: MockerFixture) -> None: """ Test _validate_filters with empty filter list (the loop doesn't run). """ - from superset.semantic_layers.mapper import _validate_filters query_object = mocker.Mock() query_object.filter = [] # Empty filter list @@ -2494,7 +2486,6 @@ def test_validate_granularity_valid(mocker: MockerFixture) -> None: """ Test _validate_granularity with valid granularity and time grain. """ - from superset.semantic_layers.mapper import _validate_granularity mock_datasource = mocker.Mock() time_dim = Dimension("order_date", "order_date", STRING, "order_date", "Date", Day) @@ -2514,7 +2505,6 @@ def test_validate_group_limit_valid(mocker: MockerFixture) -> None: """ Test _validate_group_limit with valid group limit settings. """ - from superset.semantic_layers.mapper import _validate_group_limit mock_datasource = mocker.Mock() category_dim = Dimension("category", "category", STRING, "category", "Category") @@ -2647,7 +2637,6 @@ def test_validate_filters_with_valid_filters(mocker: MockerFixture) -> None: Test _validate_filters with valid filters that pass validation. This covers the branch where the loop completes without raising. """ - from superset.semantic_layers.mapper import _validate_filters query_object = mocker.Mock() query_object.filter = [