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 2026/07/01 00:16:24 UTC

(superset) branch fix-sl-postprocess created (now 60a89cce52c)

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

betodealmeida pushed a change to branch fix-sl-postprocess
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 60a89cce52c fix(semantic layers): apply post-processing

This branch includes the following new commits:

     new 60a89cce52c fix(semantic layers): apply post-processing

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.



(superset) 01/01: fix(semantic layers): apply post-processing

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

betodealmeida pushed a commit to branch fix-sl-postprocess
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 60a89cce52c9f28e5b60a82ebab44afd316f10c2
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Tue Jun 30 20:16:05 2026 -0400

    fix(semantic layers): apply post-processing
---
 superset/semantic_layers/models.py              |  12 ++-
 tests/unit_tests/semantic_layers/models_test.py | 107 ++++++++++++++++++++++++
 2 files changed, 118 insertions(+), 1 deletion(-)

diff --git a/superset/semantic_layers/models.py b/superset/semantic_layers/models.py
index 34cdfbc4792..59434d2d185 100644
--- a/superset/semantic_layers/models.py
+++ b/superset/semantic_layers/models.py
@@ -43,6 +43,10 @@ from superset_core.semantic_layers.view import (
 )
 
 from superset.common.query_object import QueryObject
+from superset.exceptions import (
+    InvalidPostProcessingError,
+    QueryObjectValidationError,
+)
 from superset.explorables.base import TimeGrainDict
 from superset.extensions import encrypted_field_factory
 from superset.models.helpers import AuditMixinNullable, QueryResult
@@ -281,7 +285,13 @@ class SemanticView(AuditMixinNullable, Model):
     # =========================================================================
 
     def get_query_result(self, query_object: QueryObject) -> QueryResult:
-        return get_results(query_object)
+        result = get_results(query_object)
+        if query_object.post_processing and not result.df.empty:
+            try:
+                result.df = query_object.exec_post_processing(result.df)
+            except InvalidPostProcessingError as ex:
+                raise QueryObjectValidationError(ex.message) from ex
+        return result
 
     def get_query_str(self, query_obj: QueryObjectDict) -> str:
         return "Not implemented for semantic layers"
diff --git a/tests/unit_tests/semantic_layers/models_test.py b/tests/unit_tests/semantic_layers/models_test.py
index dd8c1e67dd1..42bb290d60b 100644
--- a/tests/unit_tests/semantic_layers/models_test.py
+++ b/tests/unit_tests/semantic_layers/models_test.py
@@ -662,6 +662,7 @@ def test_semantic_view_get_query_result(
     view = SemanticView()
 
     mock_query_object = MagicMock()
+    mock_query_object.post_processing = []
     mock_result = MagicMock()
 
     with patch(
@@ -671,9 +672,115 @@ def test_semantic_view_get_query_result(
         result = view.get_query_result(mock_query_object)
 
         mock_get_results.assert_called_once_with(mock_query_object)
+        mock_query_object.exec_post_processing.assert_not_called()
         assert result == mock_result
 
 
+def test_semantic_view_get_query_result_runs_post_processing(
+    mock_implementation: MagicMock,
+) -> None:
+    """
+    ``get_query_result`` must run ``query_object.exec_post_processing`` so that
+    features like ``percent_metrics`` (contribution) are applied to the semantic
+    layer's DataFrame — matching the dataset flow in
+    ``superset/models/helpers.py``.
+    """
+    import pandas as pd
+
+    view = SemanticView()
+
+    input_df = pd.DataFrame({"Orders Count": [40000.0]})
+    processed_df = pd.DataFrame({"Orders Count": [40000.0], "%Orders Count": [1.0]})
+
+    mock_query_object = MagicMock()
+    mock_query_object.post_processing = [
+        {
+            "operation": "contribution",
+            "options": {
+                "columns": ["Orders Count"],
+                "rename_columns": ["%Orders Count"],
+            },
+        }
+    ]
+    mock_query_object.exec_post_processing.return_value = processed_df
+
+    mock_result = MagicMock()
+    mock_result.df = input_df
+
+    with patch(
+        "superset.semantic_layers.models.get_results",
+        return_value=mock_result,
+    ):
+        result = view.get_query_result(mock_query_object)
+
+    mock_query_object.exec_post_processing.assert_called_once_with(input_df)
+    assert result is mock_result
+    assert list(result.df.columns) == ["Orders Count", "%Orders Count"]
+
+
+def test_semantic_view_get_query_result_wraps_post_processing_errors(
+    mock_implementation: MagicMock,
+) -> None:
+    """
+    ``InvalidPostProcessingError`` raised from post-processing must be re-raised
+    as ``QueryObjectValidationError`` so the API surfaces a clean 400 rather
+    than a 500.
+    """
+    import pandas as pd
+
+    from superset.exceptions import (
+        InvalidPostProcessingError,
+        QueryObjectValidationError,
+    )
+
+    view = SemanticView()
+
+    mock_query_object = MagicMock()
+    mock_query_object.post_processing = [{"operation": "bogus"}]
+    mock_query_object.exec_post_processing.side_effect = InvalidPostProcessingError(
+        "boom"
+    )
+
+    mock_result = MagicMock()
+    mock_result.df = pd.DataFrame({"count": [1]})
+
+    with (
+        patch(
+            "superset.semantic_layers.models.get_results",
+            return_value=mock_result,
+        ),
+        pytest.raises(QueryObjectValidationError, match="boom"),
+    ):
+        view.get_query_result(mock_query_object)
+
+
+def test_semantic_view_get_query_result_skips_post_processing_on_empty_df(
+    mock_implementation: MagicMock,
+) -> None:
+    """
+    Match the dataset flow's guard: skip post-processing when the DataFrame is
+    empty. Contribution and other ops assume at least one row.
+    """
+    import pandas as pd
+
+    view = SemanticView()
+
+    mock_query_object = MagicMock()
+    mock_query_object.post_processing = [{"operation": "contribution"}]
+
+    mock_result = MagicMock()
+    mock_result.df = pd.DataFrame()
+
+    with patch(
+        "superset.semantic_layers.models.get_results",
+        return_value=mock_result,
+    ):
+        result = view.get_query_result(mock_query_object)
+
+    mock_query_object.exec_post_processing.assert_not_called()
+    assert result is mock_result
+
+
 def test_semantic_view_data_for_slices(
     mock_implementation: MagicMock,
     mock_dimensions: list[Dimension],