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/05/15 17:14:09 UTC

(superset) branch sl-cache updated (23889a8d270 -> 47c38158445)

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

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


 discard 23889a8d270 Fix lint
     new 8f36148693e Fix lint
     new 47c38158445 Coerce values in filters

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   (23889a8d270)
            \
             N -- N -- N   refs/heads/sl-cache (47c38158445)

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 2 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/semantic_layers/mapper.py                 | 129 ++++++++++++++++++++-
 .../semantic_layers/cache_integration_test.py      |  21 ++++
 tests/unit_tests/semantic_layers/mapper_test.py    |  85 ++++++++++++++
 3 files changed, 230 insertions(+), 5 deletions(-)


(superset) 02/02: Coerce values in filters

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

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

commit 47c3815844551909d1055ce9b301c841d13a510d
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Thu May 14 20:56:43 2026 -0400

    Coerce values in filters
---
 superset/semantic_layers/mapper.py                 | 129 ++++++++++++++++++++-
 .../semantic_layers/cache_integration_test.py      |  21 ++++
 tests/unit_tests/semantic_layers/mapper_test.py    |  85 ++++++++++++++
 3 files changed, 230 insertions(+), 5 deletions(-)

diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 5f52bfe31b9..01b89363d1e 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -24,8 +24,8 @@ single dataframe.
 
 """
 
-from datetime import datetime, timedelta
-from time import time
+from datetime import date, datetime, time, timedelta
+from time import time as current_time
 from typing import Any, Callable, cast, Sequence, TypeGuard
 
 import isodate
@@ -108,7 +108,7 @@ def get_results(query_object: QueryObject) -> QueryResult:
         raise ValueError("QueryObject must have a datasource defined.")
 
     # Track execution time
-    start_time = time()
+    start_time = current_time()
 
     semantic_view = query_object.datasource.implementation
     dispatcher = (
@@ -134,7 +134,7 @@ def get_results(query_object: QueryObject) -> QueryResult:
 
     # If no time offsets, return the main result as-is
     if not query_object.time_offsets or len(queries) <= 1:
-        duration = timedelta(seconds=time() - start_time)
+        duration = timedelta(seconds=current_time() - start_time)
         return map_semantic_result_to_query_result(
             main_result,
             query_object,
@@ -204,7 +204,7 @@ def get_results(query_object: QueryObject) -> QueryResult:
         requests=all_requests,
         results=pa.Table.from_pandas(main_df),
     )
-    duration = timedelta(seconds=time() - start_time)
+    duration = timedelta(seconds=current_time() - start_time)
     return map_semantic_result_to_query_result(
         semantic_result,
         query_object,
@@ -598,6 +598,8 @@ def _convert_query_object_filter(
             ),
         }
 
+    value = _coerce_filter_value(value, dimension)
+
     # Map QueryObject operators to semantic layer operators
     operator_mapping = {
         FilterOperator.EQUALS.value: Operator.EQUALS,
@@ -629,6 +631,123 @@ def _convert_query_object_filter(
     }
 
 
+def _coerce_filter_value(
+    value: FilterValues | frozenset[FilterValues],
+    dimension: Dimension,
+) -> FilterValues | frozenset[FilterValues]:
+    if isinstance(value, frozenset):
+        return frozenset(_coerce_scalar_filter_value(v, dimension) for v in value)
+    return _coerce_scalar_filter_value(value, dimension)
+
+
+def _coerce_scalar_filter_value(value: FilterValues, dimension: Dimension) -> FilterValues:
+    if value is None:
+        return None
+
+    dtype = dimension.type
+
+    if pa.types.is_boolean(dtype):
+        if isinstance(value, bool):
+            return value
+        if isinstance(value, str):
+            parsed = value.strip().lower()
+            if parsed in {"true", "t", "1", "yes", "y", "on"}:
+                return True
+            if parsed in {"false", "f", "0", "no", "n", "off"}:
+                return False
+        raise ValueError(
+            f"Invalid boolean value {value!r} for filter column {dimension.name}"
+        )
+
+    if pa.types.is_integer(dtype):
+        if isinstance(value, bool):
+            raise ValueError(
+                f"Invalid integer value {value!r} for filter column {dimension.name}"
+            )
+        if isinstance(value, int):
+            return value
+        if isinstance(value, str):
+            try:
+                return int(value.strip())
+            except ValueError as ex:
+                raise ValueError(
+                    f"Invalid integer value {value!r} for filter column "
+                    f"{dimension.name}"
+                ) from ex
+        raise ValueError(
+            f"Invalid integer value {value!r} for filter column {dimension.name}"
+        )
+
+    if pa.types.is_floating(dtype) or pa.types.is_decimal(dtype):
+        if isinstance(value, bool):
+            raise ValueError(
+                f"Invalid numeric value {value!r} for filter column {dimension.name}"
+            )
+        if isinstance(value, (int, float)):
+            return float(value)
+        if isinstance(value, str):
+            try:
+                return float(value.strip())
+            except ValueError as ex:
+                raise ValueError(
+                    f"Invalid numeric value {value!r} for filter column "
+                    f"{dimension.name}"
+                ) from ex
+        raise ValueError(
+            f"Invalid numeric value {value!r} for filter column {dimension.name}"
+        )
+
+    if pa.types.is_date(dtype):
+        if isinstance(value, datetime):
+            return value.date()
+        if isinstance(value, date):
+            return value
+        if isinstance(value, str):
+            try:
+                return datetime.fromisoformat(value.strip()).date()
+            except ValueError as ex:
+                raise ValueError(
+                    f"Invalid date value {value!r} for filter column {dimension.name}"
+                ) from ex
+        raise ValueError(
+            f"Invalid date value {value!r} for filter column {dimension.name}"
+        )
+
+    if pa.types.is_timestamp(dtype):
+        if isinstance(value, datetime):
+            return value
+        if isinstance(value, date):
+            return datetime.combine(value, time.min)
+        if isinstance(value, str):
+            normalized = value.strip().replace("Z", "+00:00")
+            try:
+                return datetime.fromisoformat(normalized)
+            except ValueError as ex:
+                raise ValueError(
+                    f"Invalid timestamp value {value!r} for filter column "
+                    f"{dimension.name}"
+                ) from ex
+        raise ValueError(
+            f"Invalid timestamp value {value!r} for filter column {dimension.name}"
+        )
+
+    if pa.types.is_time(dtype):
+        if isinstance(value, time):
+            return value
+        if isinstance(value, str):
+            try:
+                return time.fromisoformat(value.strip())
+            except ValueError as ex:
+                raise ValueError(
+                    f"Invalid time value {value!r} for filter column {dimension.name}"
+                ) from ex
+        raise ValueError(
+            f"Invalid time value {value!r} for filter column {dimension.name}"
+        )
+
+    return value
+
+
 def _get_order_from_query_object(
     query_object: ValidatedQueryObject,
     all_metrics: dict[str, Metric],
diff --git a/tests/unit_tests/semantic_layers/cache_integration_test.py b/tests/unit_tests/semantic_layers/cache_integration_test.py
index 7d056e26386..6656ec5b002 100644
--- a/tests/unit_tests/semantic_layers/cache_integration_test.py
+++ b/tests/unit_tests/semantic_layers/cache_integration_test.py
@@ -141,6 +141,27 @@ def test_narrower_filter_reuses_cache(
     assert sorted(second.df["a"].tolist()) == [3, 5]
 
 
+def test_narrower_filter_reuses_cache_when_values_are_strings(
+    fake_cache: _InMemoryCache,
+    view_implementation: Any,
+    datasource: MagicMock,
+) -> None:
+    # Regression: QueryObject filters may provide numeric values as strings.
+    # When the semantic dimension is numeric, mapper coercion should convert
+    # these values so cache containment (`a >= 1984` subset of `a >= 1982`)
+    # can be evaluated correctly.
+    cached = _result([(1982, 2.0), (1984, 3.0), (1985, 5.0)])
+    view_implementation.get_table = MagicMock(return_value=cached)
+
+    first = get_results(_qo(datasource, ">=", "1982"))
+    assert view_implementation.get_table.call_count == 1
+    assert sorted(first.df["a"].tolist()) == [1982, 1984, 1985]
+
+    second = get_results(_qo(datasource, ">=", "1984"))
+    assert view_implementation.get_table.call_count == 1  # cache hit
+    assert sorted(second.df["a"].tolist()) == [1984, 1985]
+
+
 def test_smaller_limit_reuses_cache(
     fake_cache: _InMemoryCache,
     view_implementation: Any,
diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py
index 3ab64251ae8..b34176bd655 100644
--- a/tests/unit_tests/semantic_layers/mapper_test.py
+++ b/tests/unit_tests/semantic_layers/mapper_test.py
@@ -1204,6 +1204,91 @@ def test_convert_query_object_filter_like() -> None:
     }
 
 
+def test_convert_query_object_filter_coerces_integer_string_value() -> None:
+    """Test scalar filter values are coerced to dimension type."""
+    all_dimensions = {
+        "birthyear": Dimension(
+            "birthyear",
+            "birthyear",
+            pa.int64(),
+            "birthyear",
+            "Birthyear",
+        )
+    }
+
+    filter_: ValidatedQueryObjectFilterClause = {
+        "op": FilterOperator.GREATER_THAN_OR_EQUALS.value,
+        "col": "birthyear",
+        "val": "1982",
+    }
+
+    result = _convert_query_object_filter(filter_, all_dimensions)
+
+    assert result == {
+        Filter(
+            type=PredicateType.WHERE,
+            column=all_dimensions["birthyear"],
+            operator=Operator.GREATER_THAN_OR_EQUAL,
+            value=1982,
+        )
+    }
+
+
+def test_convert_query_object_filter_coerces_in_integer_values() -> None:
+    """Test IN filter list values are coerced element-wise."""
+    all_dimensions = {
+        "order_id__amount": Dimension(
+            "order_id__amount",
+            "order_id__amount",
+            pa.int64(),
+            "order_id__amount",
+            "Order amount",
+        )
+    }
+
+    filter_: ValidatedQueryObjectFilterClause = {
+        "op": FilterOperator.IN.value,
+        "col": "order_id__amount",
+        "val": ["58", "61"],
+    }
+
+    result = _convert_query_object_filter(filter_, all_dimensions)
+
+    assert result == {
+        Filter(
+            type=PredicateType.WHERE,
+            column=all_dimensions["order_id__amount"],
+            operator=Operator.IN,
+            value=frozenset({58, 61}),
+        )
+    }
+
+
+def test_convert_query_object_filter_invalid_integer_value_raises() -> None:
+    """Test invalid integer value raises a clear error."""
+    all_dimensions = {
+        "birthyear": Dimension(
+            "birthyear",
+            "birthyear",
+            pa.int64(),
+            "birthyear",
+            "Birthyear",
+        )
+    }
+
+    filter_: ValidatedQueryObjectFilterClause = {
+        "op": FilterOperator.GREATER_THAN_OR_EQUALS.value,
+        "col": "birthyear",
+        "val": "nineteen-eighty-two",
+    }
+
+    with pytest.raises(
+        ValueError,
+        match="Invalid integer value 'nineteen-eighty-two' for filter column birthyear",
+    ):
+        _convert_query_object_filter(filter_, all_dimensions)
+
+
 def test_get_results_without_time_offsets(
     mock_datasource: MagicMock,
     mocker: MockerFixture,


(superset) 01/02: Fix lint

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

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

commit 8f36148693ee5e01b33e1809fe701d580b2a7028
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Thu May 14 18:38:22 2026 -0400

    Fix lint
---
 superset-frontend/.eslintrc.js                                     | 2 +-
 .../src/components/CachedLabel/TooltipContent.test.tsx             | 4 +++-
 .../src/BigNumber/BigNumberWithTrendline/transformProps.test.ts    | 4 +---
 superset-frontend/src/dashboard/util/getDashboardUrl.test.ts       | 4 +++-
 .../components/ExploreChartPanel/ExploreChartPanel.test.tsx        | 6 +++++-
 .../src/explore/components/controls/CollectionControl/index.tsx    | 4 +++-
 superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx | 4 +---
 superset-frontend/src/middleware/loggerMiddleware.ts               | 7 ++++++-
 8 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js
index 002d3f39252..d7b72a6d93e 100644
--- a/superset-frontend/.eslintrc.js
+++ b/superset-frontend/.eslintrc.js
@@ -80,7 +80,7 @@ const restrictedImportsRules = {
   'no-jest-mock-console': {
     name: 'jest-mock-console',
     message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
-  }
+  },
 };
 
 module.exports = {
diff --git a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
index 2bc44409e44..22a537ed8b7 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
@@ -38,7 +38,9 @@ test('Rendering TooltipContent correctly - with timestep', () => {
 });
 
 test('Rendering TooltipContent correctly - semantic cache', () => {
-  render(<TooltipContent cacheSource="semantic" cachedTimestamp="01-01-2000" />);
+  render(
+    <TooltipContent cacheSource="semantic" cachedTimestamp="01-01-2000" />,
+  );
   expect(screen.getByTestId('tooltip-content')?.textContent).toBe(
     `Loaded from semantic smart cache ${extendedDayjs
       .utc('01-01-2000')
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts
index 6ce84f5ffe4..6a9752d57b9 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts
@@ -288,9 +288,7 @@ describe('BigNumberWithTrendline transformProps', () => {
       height: 300,
       queriesData: [
         {
-          data: [
-            { __timestamp: 1, value: 100 },
-          ] as unknown as BigNumberDatum[],
+          data: [{ __timestamp: 1, value: 100 }] as unknown as BigNumberDatum[],
           colnames: ['__timestamp', 'value'],
           coltypes: ['TEMPORAL', 'NUMERIC'],
         },
diff --git a/superset-frontend/src/dashboard/util/getDashboardUrl.test.ts b/superset-frontend/src/dashboard/util/getDashboardUrl.test.ts
index 530d6a8ed06..8d33bbf1b1d 100644
--- a/superset-frontend/src/dashboard/util/getDashboardUrl.test.ts
+++ b/superset-frontend/src/dashboard/util/getDashboardUrl.test.ts
@@ -122,7 +122,9 @@ describe('getChartIdsFromLayout', () => {
       hash: '',
       standalone: DashboardStandaloneMode.HideNav,
     });
-    expect(url).toBe(`/dashboard/1/?standalone=${DashboardStandaloneMode.HideNav}`);
+    expect(url).toBe(
+      `/dashboard/1/?standalone=${DashboardStandaloneMode.HideNav}`,
+    );
   });
 
   test('should process native filters key', () => {
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
index 276d2f759c8..5e3474555da 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
@@ -173,7 +173,11 @@ describe('ChartContainer', () => {
       chart: {
         chartStatus: 'rendered',
         queriesResponse: [
-          { is_cached: false, semantic_cache_hit: true, queried_dttm: '2026-01-01' },
+          {
+            is_cached: false,
+            semantic_cache_hit: true,
+            queried_dttm: '2026-01-01',
+          },
         ],
       },
     });
diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx b/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
index 2b0e01b0129..1a0393577ad 100644
--- a/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
@@ -188,7 +188,9 @@ function CollectionControl({
   // Two items can collide when keyAccessor returns falsy and the index
   // fallback is used — breaking dnd-kit reordering and React reconciliation.
   // Assign a stable nanoid per item ref when no key is available.
-  const generatedIdsRef = useRef<WeakMap<CollectionItem, string>>(new WeakMap());
+  const generatedIdsRef = useRef<WeakMap<CollectionItem, string>>(
+    new WeakMap(),
+  );
   const itemIds = useMemo(
     () =>
       value.map(item => {
diff --git a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
index 7368f61ceec..fd2c3cb8ac5 100644
--- a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
+++ b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
@@ -264,9 +264,7 @@ const enumNamesEntry = {
         const names = (s as Record<string, unknown>)['x-enumNames'];
         return Array.isArray(names) && (names as unknown[]).length > 0;
       }),
-      schemaMatches(
-        s => (s as Record<string, unknown>)?.type !== 'array',
-      ),
+      schemaMatches(s => (s as Record<string, unknown>)?.type !== 'array'),
     ),
   ),
   renderer: EnumNamesRenderer,
diff --git a/superset-frontend/src/middleware/loggerMiddleware.ts b/superset-frontend/src/middleware/loggerMiddleware.ts
index a41d96a53e6..3182bd18771 100644
--- a/superset-frontend/src/middleware/loggerMiddleware.ts
+++ b/superset-frontend/src/middleware/loggerMiddleware.ts
@@ -33,7 +33,12 @@ import { ensureAppRoot } from '../utils/pathUtils';
 import type { DashboardInfo, DashboardLayoutState } from '../dashboard/types';
 import type { QueryEditor } from '../SqlLab/types';
 
-type LogEventSource = 'dashboard' | 'embedded_dashboard' | 'explore' | 'sqlLab' | 'slice';
+type LogEventSource =
+  | 'dashboard'
+  | 'embedded_dashboard'
+  | 'explore'
+  | 'sqlLab'
+  | 'slice';
 
 interface LogEventData {
   source?: LogEventSource;