You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/07/08 00:08:48 UTC

(superset) branch master updated: feat(cache): warm up native filter option queries (#41531)

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

rusackas 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 98fc8576131 feat(cache): warm up native filter option queries (#41531)
98fc8576131 is described below

commit 98fc8576131cbbcabe9e4222898d2d13e13c14a6
Author: abdullah reveha <15...@users.noreply.github.com>
AuthorDate: Wed Jul 8 03:08:40 2026 +0300

    feat(cache): warm up native filter option queries (#41531)
    
    Co-authored-by: Abdullah Sahin <yo...@example.comclear>
---
 superset/config.py                                 |  29 ++
 superset/tasks/cache.py                            | 208 +++++++++--
 superset/tasks/native_filter_cache.py              | 245 +++++++++++++
 tests/unit_tests/tasks/test_cache.py               | 288 ++++++++++++++-
 tests/unit_tests/tasks/test_native_filter_cache.py | 405 +++++++++++++++++++++
 5 files changed, 1132 insertions(+), 43 deletions(-)

diff --git a/superset/config.py b/superset/config.py
index 910322d7542..8df4bb3d4e9 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1216,6 +1216,35 @@ THUMBNAIL_COMPUTING_CACHE_TTL = int(timedelta(seconds=360).total_seconds())
 # Celery task. Intentionally defaults to None so operators pick a dedicated
 # least-privilege user rather than inadvertently running warmup as "admin".
 SUPERSET_CACHE_WARMUP_USER: str | None = None
+# To warm up native filter option queries, use the `native_filter_options` strategy.
+# This strategy pre-populates the cache for Value-type native filter dropdowns by
+# executing the same chart-data queries the UI sends when a user opens a filter.
+#
+# Cache entries are warmed under SUPERSET_CACHE_WARMUP_USER. Without the shared
+# cache key optimization (not included in this PR), entries are scoped to the
+# warm-up user's normal cache partition. Users with different role sets may still
+# experience cache misses on first load.
+#
+# Example Celery beat configuration:
+#
+#     beat_schedule = {
+#         "cache-warmup-native-filters": {
+#             "task": "cache-warmup",
+#             "schedule": crontab(minute=0, hour=3),  # daily at 03:00
+#             "kwargs": {
+#                 "strategy_name": "native_filter_options",
+#                 "dashboard_ids": [1, 2, 3],
+#             },
+#         },
+#     }
+#
+# Requirements:
+# - SUPERSET_CACHE_WARMUP_USER must be set to a user with access to the
+#   dashboards and datasets referenced by the native filters.
+# - DATA_CACHE_CONFIG must be configured (Redis recommended).
+# - Celery beat must be running.
+# - Cascade/dependent filters and search-term variants are not warmed in this
+#   version; only base option queries are supported.
 
 # Time before selenium times out after trying to locate an element on the page and wait
 # for that element to load for a screenshot.
diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py
index a8aaaf38928..c994e735dc3 100644
--- a/superset/tasks/cache.py
+++ b/superset/tasks/cache.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import logging
+from dataclasses import dataclass
 from typing import Any, Optional, Union
 
 from celery.utils.log import get_task_logger
@@ -25,21 +26,37 @@ from sqlalchemy import and_, func
 from sqlalchemy.orm import selectinload
 
 from superset import db, security_manager
+from superset.common.query_context import QueryContext
+from superset.daos.dashboard import DashboardDAO
 from superset.extensions import celery_app
 from superset.models.core import Log
 from superset.models.dashboard import Dashboard
 from superset.tags.models import Tag, TaggedObject
+from superset.tasks.native_filter_cache import (
+    build_native_filter_option_form_data,
+    build_native_filter_option_query_context,
+    get_eligible_native_filters,
+)
 from superset.utils.date_parser import parse_human_datetime
 from superset.utils.webdriver import WebDriverSelenium
 
-logger = get_task_logger(__name__)
+logger: logging.Logger = get_task_logger(__name__)
 logger.setLevel(logging.INFO)
 
 
+@dataclass(frozen=True)
+class CacheWarmupTask:
+    """A chart data query to execute for cache warm-up."""
+
+    query_context: QueryContext
+    dashboard_id: int
+    native_filter_id: str | None = None
+
+
 def get_dash_url(dashboard: Dashboard) -> str:
     """Return external URL for warming up a given dashboard cache."""
     with current_app.test_request_context():
-        baseurl = (
+        baseurl: str = (
             # when running this as an async task, drop the request context with
             # app.test_request_context()
             current_app.config.get("WEBDRIVER_BASEURL")
@@ -54,8 +71,8 @@ class Strategy:  # pylint: disable=too-few-public-methods
     """
     A cache warm up strategy.
 
-    Each strategy defines a `get_urls` method that returns a list of dashboard URLs to
-    warm up using WebDriver.
+    WebDriver strategies define a `get_urls` method that returns a list of
+    dashboard URLs to warm up. Query task strategies define `get_tasks`.
 
     Strategies can be configured in `superset/config.py`:
 
@@ -73,12 +90,18 @@ class Strategy:  # pylint: disable=too-few-public-methods
 
     """
 
+    name: str = ""
+    uses_webdriver: bool = True
+
     def __init__(self) -> None:
         pass
 
     def get_urls(self) -> list[str]:
         raise NotImplementedError("Subclasses must implement get_urls!")
 
+    def get_tasks(self) -> list[CacheWarmupTask]:
+        return []
+
 
 class DummyStrategy(Strategy):  # pylint: disable=too-few-public-methods
     """
@@ -97,11 +120,11 @@ class DummyStrategy(Strategy):  # pylint: disable=too-few-public-methods
 
     """
 
-    name = "dummy"
+    name: str = "dummy"
 
     def get_urls(self) -> list[str]:
         # Use selectinload to avoid N+1 queries when checking dashboard.slices
-        dashboards = (
+        dashboards: list[Dashboard] = (
             db.session.query(Dashboard)
             .options(selectinload(Dashboard.slices))
             .filter(Dashboard.published.is_(True))
@@ -129,15 +152,15 @@ class TopNDashboardsStrategy(Strategy):  # pylint: disable=too-few-public-method
 
     """
 
-    name = "top_n_dashboards"
+    name: str = "top_n_dashboards"
 
     def __init__(self, top_n: int = 5, since: str = "7 days ago") -> None:
         super().__init__()
-        self.top_n = top_n
-        self.since = parse_human_datetime(since) if since else None
+        self.top_n: int = top_n
+        self.since: Any = parse_human_datetime(since) if since else None
 
     def get_urls(self) -> list[str]:
-        records = (
+        records: list[Any] = (
             db.session.query(Log.dashboard_id, func.count(Log.dashboard_id))
             .filter(and_(Log.dashboard_id.isnot(None), Log.dttm >= self.since))
             .group_by(Log.dashboard_id)
@@ -145,8 +168,8 @@ class TopNDashboardsStrategy(Strategy):  # pylint: disable=too-few-public-method
             .limit(self.top_n)
             .all()
         )
-        dash_ids = [record.dashboard_id for record in records]
-        dashboards = (
+        dash_ids: list[int] = [record.dashboard_id for record in records]
+        dashboards: list[Dashboard] = (
             db.session.query(Dashboard).filter(Dashboard.id.in_(dash_ids)).all()
         )
 
@@ -169,19 +192,19 @@ class DashboardTagsStrategy(Strategy):  # pylint: disable=too-few-public-methods
         }
     """
 
-    name = "dashboard_tags"
+    name: str = "dashboard_tags"
 
     def __init__(self, tags: Optional[list[str]] = None) -> None:
         super().__init__()
-        self.tags = tags or []
+        self.tags: list[str] = tags or []
 
     def get_urls(self) -> list[str]:
-        urls = []
-        tags = db.session.query(Tag).filter(Tag.name.in_(self.tags)).all()
-        tag_ids = [tag.id for tag in tags]
+        urls: list[str] = []
+        tags: list[Tag] = db.session.query(Tag).filter(Tag.name.in_(self.tags)).all()
+        tag_ids: list[int] = [tag.id for tag in tags]
 
         # add dashboards that are tagged
-        tagged_objects = (
+        tagged_objects: list[TaggedObject] = (
             db.session.query(TaggedObject)
             .filter(
                 and_(
@@ -191,8 +214,10 @@ class DashboardTagsStrategy(Strategy):  # pylint: disable=too-few-public-methods
             )
             .all()
         )
-        dash_ids = [tagged_object.object_id for tagged_object in tagged_objects]
-        tagged_dashboards = db.session.query(Dashboard).filter(
+        dash_ids: list[int] = [
+            tagged_object.object_id for tagged_object in tagged_objects
+        ]
+        tagged_dashboards: list[Dashboard] = db.session.query(Dashboard).filter(
             Dashboard.id.in_(dash_ids)
         )
         for dashboard in tagged_dashboards:
@@ -201,7 +226,105 @@ class DashboardTagsStrategy(Strategy):  # pylint: disable=too-few-public-methods
         return urls
 
 
-strategies = [DummyStrategy, TopNDashboardsStrategy, DashboardTagsStrategy]
+class NativeFilterOptionsStrategy(Strategy):  # pylint: disable=too-few-public-methods
+    """
+    Build chart data query tasks for native filter option cache warm-up.
+    """
+
+    name: str = "native_filter_options"
+    uses_webdriver: bool = False
+
+    def __init__(self, dashboard_ids: list[int]) -> None:
+        super().__init__()
+        self.dashboard_ids: list[int] = dashboard_ids
+
+    def get_urls(self) -> list[str]:
+        return []
+
+    def get_tasks(self) -> list[CacheWarmupTask]:
+        tasks: list[CacheWarmupTask] = []
+
+        for dashboard_id in self.dashboard_ids:
+            skipped: int = 0
+            built: int = 0
+
+            try:
+                dashboard: Dashboard | None = DashboardDAO.find_by_id(dashboard_id)
+                if dashboard is None:
+                    logger.warning(
+                        "Dashboard %s not found; skipping native filter option "
+                        "cache warm-up",
+                        dashboard_id,
+                    )
+                    continue
+
+                filter_configs: list[dict[str, Any]] = get_eligible_native_filters(
+                    dashboard
+                )
+
+                for filter_config in filter_configs:
+                    try:
+                        form_data: dict[str, Any] | None = (
+                            build_native_filter_option_form_data(
+                                dashboard,
+                                filter_config,
+                            )
+                        )
+                        if form_data is None:
+                            skipped += 1
+                            continue
+
+                        query_context: QueryContext | None = (
+                            build_native_filter_option_query_context(form_data)
+                        )
+                        if query_context is None:
+                            skipped += 1
+                            continue
+
+                        tasks.append(
+                            CacheWarmupTask(
+                                query_context=query_context,
+                                dashboard_id=dashboard.id,
+                                native_filter_id=filter_config.get("id"),
+                            )
+                        )
+                        built += 1
+                    except Exception:  # noqa: BLE001
+                        skipped += 1
+                        logger.exception(
+                            "Error building native filter option cache warm-up "
+                            "task for dashboard %s filter %s",
+                            dashboard_id,
+                            filter_config.get("id"),
+                        )
+
+                logger.info(
+                    "Dashboard %s native filter option cache warm-up: %s filters "
+                    "found, %s tasks built, %s skipped",
+                    dashboard_id,
+                    len(filter_configs),
+                    built,
+                    skipped,
+                )
+            except Exception:  # noqa: BLE001
+                logger.exception(
+                    "Error building native filter option cache warm-up tasks for "
+                    "dashboard %s",
+                    dashboard_id,
+                )
+
+        return tasks
+
+
+strategies: list[type[Strategy]] = [
+    DummyStrategy,
+    TopNDashboardsStrategy,
+    DashboardTagsStrategy,
+    NativeFilterOptionsStrategy,
+]
+strategy_registry: dict[str, type[Strategy]] = {
+    strategy.name: strategy for strategy in strategies
+}
 
 
 @celery_app.task(name="cache-warmup")
@@ -215,18 +338,15 @@ def cache_warmup(
 
     """
     logger.info("Loading strategy")
-    class_ = None
-    for class_ in strategies:
-        if class_.name == strategy_name:  # type: ignore
-            break
-    else:
-        message = "No strategy %s found!" % strategy_name
+    class_: type[Strategy] | None = strategy_registry.get(strategy_name)
+    if class_ is None:
+        message: str = "No strategy %s found!" % strategy_name
         logger.error(message, exc_info=True)
         return message
 
     logger.info("Loading %s", class_.__name__)
     try:
-        strategy = class_(*args, **kwargs)
+        strategy: Strategy = class_(*args, **kwargs)
         logger.info("Success!")
     except TypeError:
         message = "Error loading strategy!"
@@ -235,7 +355,7 @@ def cache_warmup(
 
     results: dict[str, list[str]] = {"success": [], "errors": []}
 
-    warmup_username = current_app.config.get("SUPERSET_CACHE_WARMUP_USER")
+    warmup_username: str | None = current_app.config.get("SUPERSET_CACHE_WARMUP_USER")
     if not warmup_username:
         message = (
             "SUPERSET_CACHE_WARMUP_USER is not configured. Set it to a dedicated "
@@ -244,7 +364,7 @@ def cache_warmup(
         logger.error(message)
         return message
 
-    user = security_manager.find_user(username=warmup_username)
+    user: Any = security_manager.find_user(username=warmup_username)
     if not user:
         message = (
             f"Cache warmup user '{warmup_username}' not found. Please configure "
@@ -253,7 +373,33 @@ def cache_warmup(
         logger.error(message)
         return message
 
-    wd = WebDriverSelenium(current_app.config["WEBDRIVER_TYPE"], user=user)
+    if not strategy.uses_webdriver:
+        # pylint: disable=import-outside-toplevel
+        from superset.commands.chart.data.get_data_command import ChartDataCommand
+        from superset.utils.core import override_user
+
+        with override_user(user, force=False):
+            tasks: list[CacheWarmupTask] = strategy.get_tasks()
+            for task in tasks:
+                task_name: str = (
+                    f"dashboard:{task.dashboard_id}:"
+                    f"native_filter:{task.native_filter_id}"
+                )
+                try:
+                    logger.info("Warming up cache for %s", task_name)
+                    command: Any = ChartDataCommand(task.query_context)
+                    command.validate()
+                    command.run(cache=True)
+                    results["success"].append(task_name)
+                except Exception:  # noqa: BLE001
+                    logger.exception("Error warming up cache for %s", task_name)
+                    results["errors"].append(task_name)
+
+        return results
+
+    wd: WebDriverSelenium = WebDriverSelenium(
+        current_app.config["WEBDRIVER_TYPE"], user=user
+    )
 
     try:
         for url in strategy.get_urls():
diff --git a/superset/tasks/native_filter_cache.py b/superset/tasks/native_filter_cache.py
new file mode 100644
index 00000000000..54fe530a062
--- /dev/null
+++ b/superset/tasks/native_filter_cache.py
@@ -0,0 +1,245 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import logging
+from typing import Any, Final
+
+from marshmallow import ValidationError
+
+from superset.charts.schemas import ChartDataQueryContextSchema
+from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
+from superset.common.query_context import QueryContext
+from superset.models.dashboard import Dashboard
+from superset.utils import json
+from superset.utils.core import DatasourceType, split_adhoc_filters_into_base_filters
+
+logger: logging.Logger = logging.getLogger(__name__)
+
+NATIVE_FILTER_DEFAULT_ROW_LIMIT: Final[int] = 1000
+NO_FILTER_TIME_RANGE: Final[str] = "No filter"
+
+
+def _resolve_datasource_engine(datasource_id: int) -> str:
+    """Return the datasource engine name, or ``"base"`` if it cannot be resolved."""
+    try:
+        # pylint: disable=import-outside-toplevel
+        from superset.daos.datasource import DatasourceDAO
+
+        datasource: Any = DatasourceDAO.get_datasource(
+            datasource_type=DatasourceType.TABLE,
+            database_id_or_uuid=datasource_id,
+        )
+        return datasource.database.db_engine_spec.engine
+    except Exception:  # noqa: BLE001
+        logger.debug("Could not resolve engine for datasource %s", datasource_id)
+        return "base"
+
+
+def _get_filter_target(filter_config: dict[str, Any]) -> tuple[int | str, str] | None:
+    for target in filter_config.get("targets") or []:
+        if not isinstance(target, dict):
+            continue
+
+        dataset_id: Any = target.get("datasetId")
+        column: Any = target.get("column") or {}
+        column_name: str | None = (
+            column.get("name") if isinstance(column, dict) else None
+        )
+
+        if dataset_id is not None and column_name:
+            return dataset_id, column_name
+
+    return None
+
+
+def get_eligible_native_filters(dashboard: Dashboard) -> list[dict[str, Any]]:
+    """Return native filter value filters eligible for option cache warm-up."""
+    if not dashboard.json_metadata:
+        logger.warning(
+            "Dashboard %s has no json_metadata; skipping native filter warm-up",
+            dashboard.id,
+        )
+        return []
+
+    try:
+        metadata: dict[str, Any] = json.loads(dashboard.json_metadata)
+    except (TypeError, ValueError):
+        logger.warning(
+            "Dashboard %s has malformed json_metadata; skipping native filter warm-up",
+            dashboard.id,
+        )
+        return []
+
+    native_filter_config: Any = metadata.get("native_filter_configuration")
+    if not isinstance(native_filter_config, list):
+        logger.warning(
+            "Dashboard %s has no native_filter_configuration; skipping native "
+            "filter warm-up",
+            dashboard.id,
+        )
+        return []
+
+    logger.debug(
+        "Dashboard %s native_filter_configuration count: %s",
+        dashboard.id,
+        len(native_filter_config),
+    )
+
+    eligible_filters: list[dict[str, Any]] = []
+    for filter_config in native_filter_config:
+        if not isinstance(filter_config, dict):
+            continue
+        if filter_config.get("type") == "DIVIDER":
+            continue
+        if filter_config.get("filterType") != "filter_select":
+            continue
+        if _get_filter_target(filter_config) is None:
+            continue
+
+        eligible_filters.append(filter_config)
+
+    logger.debug(
+        "Dashboard %s eligible filters: %s",
+        dashboard.id,
+        [filter_config.get("id") for filter_config in eligible_filters],
+    )
+
+    return eligible_filters
+
+
+def build_native_filter_option_form_data(
+    dashboard: Dashboard,
+    filter_config: dict[str, Any],
+) -> dict[str, Any] | None:
+    """Build form data for a native filter option query."""
+    target: tuple[int | str, str] | None = _get_filter_target(filter_config)
+    if target is None:
+        logger.warning(
+            "Native filter %s on dashboard %s has no valid target; skipping",
+            filter_config.get("id"),
+            dashboard.id,
+        )
+        return None
+
+    dataset_id: int | str
+    column_name: str
+    dataset_id, column_name = target
+    control_values: dict[str, Any] = filter_config.get("controlValues") or {}
+
+    return {
+        "datasource": f"{dataset_id}__table",
+        "viz_type": "filter_select",
+        "type": "NATIVE_FILTER",
+        "native_filter_id": filter_config["id"],
+        "dashboardId": dashboard.id,
+        "groupby": [column_name],
+        "adhoc_filters": filter_config.get("adhoc_filters", []),
+        "extra_filters": [],
+        "extra_form_data": {},
+        "metrics": ["count"],
+        "row_limit": NATIVE_FILTER_DEFAULT_ROW_LIMIT,
+        "time_range": filter_config.get("time_range") or NO_FILTER_TIME_RANGE,
+        "granularity_sqla": filter_config.get("granularity_sqla"),
+        "showSearch": True,
+        "sortAscending": control_values.get("sortAscending", True),
+        "sortMetric": filter_config.get("sortMetric", None),
+    }
+
+
+def build_native_filter_option_query_context(
+    form_data: dict[str, Any],
+) -> QueryContext | None:
+    """Build a query context for a native filter option query."""
+    datasource: Any = form_data.get("datasource")
+    groupby: Any = form_data.get("groupby") or []
+    column_name: str | None = (
+        groupby[0]
+        if isinstance(groupby, list) and groupby and isinstance(groupby[0], str)
+        else None
+    )
+
+    if not datasource or not column_name:
+        logger.warning(
+            "Invalid native filter option form_data; datasource and groupby are "
+            "required"
+        )
+        return None
+
+    try:
+        datasource_id: int = int(str(datasource).split("__", 1)[0])
+    except (TypeError, ValueError):
+        logger.warning(
+            "Invalid native filter option datasource %r; skipping query context",
+            datasource,
+        )
+        return None
+
+    sort_metric: str | None = form_data.get("sortMetric")
+    sort_ascending: bool = form_data.get("sortAscending", True)
+    metrics: list[str] = [sort_metric] if sort_metric else []
+    orderby: list[list[str | bool]] = [
+        [sort_metric or column_name, bool(sort_ascending)]
+    ]
+    adhoc_filters: list[Any] = form_data.get("adhoc_filters") or []
+    has_sql_filter: bool = any(
+        isinstance(filter_, dict) and filter_.get("expressionType") == "SQL"
+        for filter_ in adhoc_filters
+    )
+    engine: str = (
+        _resolve_datasource_engine(datasource_id) if has_sql_filter else "base"
+    )
+    split_adhoc_filters_into_base_filters(form_data, engine)
+
+    payload: dict[str, Any] = {
+        "datasource": {
+            "id": datasource_id,
+            "type": DatasourceType.TABLE,
+        },
+        "force": False,
+        "queries": [
+            {
+                "groupby": [column_name],
+                "filters": form_data.get("filters", []),
+                "extras": {},
+                "metrics": metrics,
+                "orderby": orderby,
+                "row_limit": form_data.get(
+                    "row_limit",
+                    NATIVE_FILTER_DEFAULT_ROW_LIMIT,
+                ),
+                "time_range": form_data.get("time_range", NO_FILTER_TIME_RANGE),
+            }
+        ],
+        "form_data": form_data,
+        "result_format": ChartDataResultFormat.JSON,
+        "result_type": ChartDataResultType.FULL,
+    }
+
+    try:
+        logger.debug(
+            "Built query context payload for datasource=%s groupby=%s",
+            datasource_id,
+            column_name,
+        )
+        return ChartDataQueryContextSchema().load(payload)
+    except (KeyError, ValidationError, ValueError) as ex:
+        logger.warning(
+            "Unable to build native filter option query context: %s",
+            ex,
+        )
+        return None
diff --git a/tests/unit_tests/tasks/test_cache.py b/tests/unit_tests/tasks/test_cache.py
index 53871d4fd4d..aebaa22725a 100644
--- a/tests/unit_tests/tasks/test_cache.py
+++ b/tests/unit_tests/tasks/test_cache.py
@@ -23,7 +23,7 @@ def _fake_app(config: Optional[dict[str, Any]] = None) -> mock.MagicMock:
     base: dict[str, Any] = {"WEBDRIVER_TYPE": "chrome"}
     if config:
         base.update(config)
-    app = mock.MagicMock()
+    app: mock.MagicMock = mock.MagicMock()
     app.config = base
     return app
 
@@ -33,7 +33,7 @@ def test_cache_warmup_unknown_strategy(app_context: None) -> None:
     from superset.tasks.cache import cache_warmup
 
     with mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd:
-        result = cache_warmup("does_not_exist")
+        result: dict[str, list[str]] | str = cache_warmup("does_not_exist")
 
     assert result == "No strategy does_not_exist found!"
     mock_wd.assert_not_called()
@@ -50,7 +50,7 @@ def test_cache_warmup_missing_config(app_context: None) -> None:
         ),
         mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
     ):
-        result = cache_warmup("dummy")
+        result: dict[str, list[str]] | str = cache_warmup("dummy")
 
     assert isinstance(result, str)
     assert "SUPERSET_CACHE_WARMUP_USER is not configured" in result
@@ -70,7 +70,7 @@ def test_cache_warmup_user_not_found(app_context: None) -> None:
         mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
     ):
         mock_sm.find_user = mock.MagicMock(return_value=None)
-        result = cache_warmup("dummy")
+        result: dict[str, list[str]] | str = cache_warmup("dummy")
 
     assert isinstance(result, str)
     assert "not found" in result
@@ -82,8 +82,8 @@ def test_cache_warmup_happy_path(app_context: None) -> None:
     """Each URL is screenshotted as the configured user and the driver is destroyed."""
     from superset.tasks.cache import cache_warmup
 
-    urls = ["http://localhost/dash/1", "http://localhost/dash/2"]
-    user = mock.MagicMock()
+    urls: list[str] = ["http://localhost/dash/1", "http://localhost/dash/2"]
+    user: mock.MagicMock = mock.MagicMock()
 
     with (
         mock.patch(
@@ -95,8 +95,8 @@ def test_cache_warmup_happy_path(app_context: None) -> None:
         mock.patch("superset.tasks.cache.DummyStrategy.get_urls", return_value=urls),
     ):
         mock_sm.find_user = mock.MagicMock(return_value=user)
-        driver = mock_wd.return_value
-        result = cache_warmup("dummy")
+        driver: mock.MagicMock = mock_wd.return_value
+        result: dict[str, list[str]] | str = cache_warmup("dummy")
 
     assert result == {"success": urls, "errors": []}
     mock_wd.assert_called_once_with("chrome", user=user)
@@ -108,8 +108,8 @@ def test_cache_warmup_collects_errors_and_destroys(app_context: None) -> None:
     """A failing URL is recorded as an error and cleanup still runs in finally."""
     from superset.tasks.cache import cache_warmup
 
-    urls = ["http://localhost/dash/ok", "http://localhost/dash/boom"]
-    user = mock.MagicMock()
+    urls: list[str] = ["http://localhost/dash/ok", "http://localhost/dash/boom"]
+    user: mock.MagicMock = mock.MagicMock()
 
     def side_effect(url: str, _element: str) -> None:
         if url.endswith("boom"):
@@ -125,12 +125,276 @@ def test_cache_warmup_collects_errors_and_destroys(app_context: None) -> None:
         mock.patch("superset.tasks.cache.DummyStrategy.get_urls", return_value=urls),
     ):
         mock_sm.find_user = mock.MagicMock(return_value=user)
-        driver = mock_wd.return_value
+        driver: mock.MagicMock = mock_wd.return_value
         driver.get_screenshot.side_effect = side_effect
-        result = cache_warmup("dummy")
+        result: dict[str, list[str]] | str = cache_warmup("dummy")
 
     assert result == {
         "success": ["http://localhost/dash/ok"],
         "errors": ["http://localhost/dash/boom"],
     }
     driver.destroy.assert_called_once_with()
+
+
+def test_native_filter_options_strategy_returns_tasks_for_eligible_filters() -> None:
+    """Eligible native filters are converted into cache warm-up tasks."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 10
+    filter_configs: list[dict[str, Any]] = [{"id": "filter-1"}, {"id": "filter-2"}]
+    form_data: list[dict[str, Any]] = [
+        {"groupby": ["country"]},
+        {"groupby": ["state"]},
+    ]
+    query_contexts: list[mock.MagicMock] = [mock.MagicMock(), mock.MagicMock()]
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=dashboard,
+        ),
+        mock.patch(
+            "superset.tasks.cache.get_eligible_native_filters",
+            return_value=filter_configs,
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_form_data",
+            side_effect=form_data,
+        ) as mock_build_form_data,
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            side_effect=query_contexts,
+        ) as mock_build_query_context,
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert len(tasks) == 2
+    assert [task.query_context for task in tasks] == query_contexts
+    assert [task.dashboard_id for task in tasks] == [10, 10]
+    assert [task.native_filter_id for task in tasks] == ["filter-1", "filter-2"]
+    assert mock_build_form_data.call_count == 2
+    assert mock_build_query_context.call_count == 2
+
+
+def test_native_filter_options_strategy_skips_missing_dashboard() -> None:
+    """Missing dashboards are skipped without raising."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=None,
+        ),
+        mock.patch("superset.tasks.cache.get_eligible_native_filters") as mock_filters,
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert tasks == []
+    mock_filters.assert_not_called()
+
+
+def test_native_filter_options_strategy_skips_when_form_data_is_none() -> None:
+    """Filters without form data are skipped without raising."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 10
+    filter_configs: list[dict[str, Any]] = [{"id": "filter-1"}, {"id": "filter-2"}]
+    query_context: mock.MagicMock = mock.MagicMock()
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=dashboard,
+        ),
+        mock.patch(
+            "superset.tasks.cache.get_eligible_native_filters",
+            return_value=filter_configs,
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_form_data",
+            side_effect=[None, {"groupby": ["country"]}],
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            return_value=query_context,
+        ) as mock_build_query_context,
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert len(tasks) == 1
+    assert tasks[0].query_context == query_context
+    mock_build_query_context.assert_called_once_with({"groupby": ["country"]})
+
+
+def test_native_filter_options_strategy_skips_when_query_context_is_none() -> None:
+    """Filters without a query context are skipped without raising."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 10
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=dashboard,
+        ),
+        mock.patch(
+            "superset.tasks.cache.get_eligible_native_filters",
+            return_value=[{"id": "filter-1"}],
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_form_data",
+            return_value={"groupby": ["country"]},
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            return_value=None,
+        ),
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert tasks == []
+
+
+def test_native_filter_options_strategy_uses_real_filter_helpers() -> None:
+    """Eligible filter config is handled by real helper functions."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+    from superset.utils import json
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 10
+    dashboard.json_metadata = json.dumps(
+        {
+            "native_filter_configuration": [
+                "bad-entry",
+                {
+                    "id": "filter-1",
+                    "filterType": "filter_select",
+                    "type": "NATIVE_FILTER",
+                    "targets": [{"datasetId": 7, "column": {"name": "country"}}],
+                    "adhoc_filters": [
+                        {
+                            "expressionType": "SIMPLE",
+                            "subject": "region",
+                            "operator": "==",
+                            "comparator": "EMEA",
+                        }
+                    ],
+                },
+                {
+                    "id": "range-filter",
+                    "filterType": "filter_range",
+                    "type": "NATIVE_FILTER",
+                    "targets": [{"datasetId": 7, "column": {"name": "value"}}],
+                },
+            ]
+        }
+    )
+    query_context: mock.MagicMock = mock.MagicMock()
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=dashboard,
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            return_value=query_context,
+        ) as mock_build_query_context,
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert len(tasks) == 1
+    assert tasks[0].dashboard_id == 10
+    assert tasks[0].native_filter_id == "filter-1"
+    assert tasks[0].query_context == query_context
+    mock_build_query_context.assert_called_once()
+    form_data: dict[str, Any] = mock_build_query_context.call_args.args[0]
+    assert form_data["datasource"] == "7__table"
+    assert form_data["groupby"] == ["country"]
+    assert form_data["adhoc_filters"] == [
+        {
+            "expressionType": "SIMPLE",
+            "subject": "region",
+            "operator": "==",
+            "comparator": "EMEA",
+        }
+    ]
+
+
+def test_native_filter_options_strategy_skips_filter_helper_exceptions() -> None:
+    """A failing filter config is skipped while remaining filters are processed."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 10
+    filter_configs: list[dict[str, Any]] = [{"missing_id": True}, {"id": "filter-2"}]
+    query_context: mock.MagicMock = mock.MagicMock()
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            return_value=dashboard,
+        ),
+        mock.patch(
+            "superset.tasks.cache.get_eligible_native_filters",
+            return_value=filter_configs,
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_form_data",
+            side_effect=[RuntimeError("boom"), {"groupby": ["country"]}],
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            return_value=query_context,
+        ),
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
+
+    assert len(tasks) == 1
+    assert tasks[0].native_filter_id == "filter-2"
+    assert tasks[0].query_context == query_context
+
+
+def test_native_filter_options_strategy_skips_dashboard_lookup_exceptions() -> None:
+    """A dashboard-level failure is caught and does not stop other dashboard ids."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy
+
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = 11
+    query_context: mock.MagicMock = mock.MagicMock()
+
+    with (
+        mock.patch(
+            "superset.tasks.cache.DashboardDAO.find_by_id",
+            side_effect=[RuntimeError("boom"), dashboard],
+        ),
+        mock.patch(
+            "superset.tasks.cache.get_eligible_native_filters",
+            return_value=[{"id": "filter-1"}],
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_form_data",
+            return_value={"groupby": ["country"]},
+        ),
+        mock.patch(
+            "superset.tasks.cache.build_native_filter_option_query_context",
+            return_value=query_context,
+        ),
+    ):
+        tasks: list[Any] = NativeFilterOptionsStrategy(
+            dashboard_ids=[10, 11]
+        ).get_tasks()
+
+    assert len(tasks) == 1
+    assert tasks[0].dashboard_id == 11
+    assert tasks[0].native_filter_id == "filter-1"
+
+
+def test_native_filter_options_strategy_registered() -> None:
+    """The native filter options strategy is registered by name."""
+    from superset.tasks.cache import NativeFilterOptionsStrategy, strategy_registry
+
+    assert strategy_registry["native_filter_options"] is NativeFilterOptionsStrategy
diff --git a/tests/unit_tests/tasks/test_native_filter_cache.py b/tests/unit_tests/tasks/test_native_filter_cache.py
new file mode 100644
index 00000000000..a3ae29ca51d
--- /dev/null
+++ b/tests/unit_tests/tasks/test_native_filter_cache.py
@@ -0,0 +1,405 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import Any
+from unittest import mock
+
+from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
+from superset.common.query_context import QueryContext
+from superset.tasks.native_filter_cache import (
+    build_native_filter_option_form_data,
+    build_native_filter_option_query_context,
+    get_eligible_native_filters,
+)
+from superset.utils import json
+
+
+def _dashboard(json_metadata: str | None, dashboard_id: int = 1) -> mock.MagicMock:
+    dashboard: mock.MagicMock = mock.MagicMock()
+    dashboard.id = dashboard_id
+    dashboard.json_metadata = json_metadata
+    return dashboard
+
+
+def _filter_config(
+    filter_id: str = "filter-1",
+    filter_type: str = "filter_select",
+    entry_type: str = "NATIVE_FILTER",
+    targets: list[dict[str, Any]] | None = None,
+) -> dict[str, Any]:
+    return {
+        "id": filter_id,
+        "filterType": filter_type,
+        "type": entry_type,
+        "targets": targets
+        if targets is not None
+        else [{"datasetId": 10, "column": {"name": "country"}}],
+    }
+
+
+def test_get_eligible_native_filters_returns_only_filter_select() -> None:
+    filter_select: dict[str, Any] = _filter_config(filter_id="select")
+    metadata: dict[str, Any] = {
+        "native_filter_configuration": [
+            filter_select,
+            _filter_config(filter_id="range", filter_type="filter_range"),
+            _filter_config(filter_id="divider", entry_type="DIVIDER"),
+        ]
+    }
+
+    result: list[dict[str, Any]] = get_eligible_native_filters(
+        _dashboard(json.dumps(metadata))
+    )
+
+    assert result == [filter_select]
+
+
+def test_get_eligible_native_filters_skips_non_dict_entries() -> None:
+    valid_filter: dict[str, Any] = _filter_config(filter_id="valid")
+    metadata: dict[str, Any] = {
+        "native_filter_configuration": [
+            "not-a-filter",
+            valid_filter,
+            None,
+            ["also", "not", "a", "filter"],
+        ]
+    }
+
+    result: list[dict[str, Any]] = get_eligible_native_filters(
+        _dashboard(json.dumps(metadata))
+    )
+
+    assert result == [valid_filter]
+
+
+def test_get_eligible_native_filters_skips_missing_target() -> None:
+    metadata: dict[str, Any] = {
+        "native_filter_configuration": [
+            _filter_config(targets=[]),
+        ]
+    }
+
+    result: list[dict[str, Any]] = get_eligible_native_filters(
+        _dashboard(json.dumps(metadata))
+    )
+
+    assert result == []
+
+
+def test_get_eligible_native_filters_skips_missing_column() -> None:
+    metadata: dict[str, Any] = {
+        "native_filter_configuration": [
+            _filter_config(targets=[{"datasetId": 10, "column": {}}]),
+        ]
+    }
+
+    result: list[dict[str, Any]] = get_eligible_native_filters(
+        _dashboard(json.dumps(metadata))
+    )
+
+    assert result == []
+
+
+def test_get_eligible_native_filters_malformed_json_metadata() -> None:
+    result: list[dict[str, Any]] = get_eligible_native_filters(_dashboard("{"))
+
+    assert result == []
+
+
+def test_get_eligible_native_filters_missing_native_filter_configuration() -> None:
+    result: list[dict[str, Any]] = get_eligible_native_filters(
+        _dashboard(json.dumps({"label": "dash"}))
+    )
+
+    assert result == []
+
+
+def test_build_native_filter_option_form_data_correct_shape() -> None:
+    dashboard: mock.MagicMock = _dashboard(json_metadata=None, dashboard_id=42)
+    filter_config: dict[str, Any] = {
+        **_filter_config(filter_id="native-filter-1"),
+        "adhoc_filters": [{"expressionType": "SIMPLE"}],
+        "controlValues": {"sortAscending": False},
+        "sortMetric": "sum__value",
+    }
+
+    result: dict[str, Any] | None = build_native_filter_option_form_data(
+        dashboard, filter_config
+    )
+
+    assert result == {
+        "datasource": "10__table",
+        "viz_type": "filter_select",
+        "type": "NATIVE_FILTER",
+        "native_filter_id": "native-filter-1",
+        "dashboardId": 42,
+        "groupby": ["country"],
+        "adhoc_filters": [{"expressionType": "SIMPLE"}],
+        "extra_filters": [],
+        "extra_form_data": {},
+        "metrics": ["count"],
+        "row_limit": 1000,
+        "time_range": "No filter",
+        "granularity_sqla": None,
+        "showSearch": True,
+        "sortAscending": False,
+        "sortMetric": "sum__value",
+    }
+
+
+def test_build_native_filter_option_form_data_uses_filter_level_prefilters() -> None:
+    filter_config: dict[str, Any] = {
+        **_filter_config(),
+        "time_range": "Last week",
+        "granularity_sqla": "ds",
+    }
+
+    result: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        filter_config,
+    )
+
+    assert result is not None
+    assert result["time_range"] == "Last week"
+    assert result["granularity_sqla"] == "ds"
+
+
+def test_build_native_filter_option_form_data_falls_back_to_default_time_range() -> (
+    None
+):
+    result: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        _filter_config(),
+    )
+
+    assert result is not None
+    assert result["time_range"] == "No filter"
+
+
+def test_build_native_filter_option_form_data_missing_dataset_id() -> None:
+    result: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        _filter_config(targets=[{"column": {"name": "country"}}]),
+    )
+
+    assert result is None
+
+
+def test_build_native_filter_option_form_data_missing_column_name() -> None:
+    result: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        _filter_config(targets=[{"datasetId": 10, "column": {}}]),
+    )
+
+    assert result is None
+
+
+def test_build_native_filter_option_query_context_returns_query_context() -> None:
+    form_data: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        _filter_config(),
+    )
+    datasource: mock.MagicMock = mock.MagicMock()
+    datasource.cache_timeout = None
+    datasource.type = "table"
+    datasource.data = {"id": 10}
+    datasource.uid = "10__table"
+    datasource.column_names = ["country"]
+
+    expected: QueryContext = QueryContext(
+        datasource=datasource,
+        queries=[],
+        slice_=None,
+        form_data=form_data,
+        result_type=ChartDataResultType.FULL,
+        result_format=ChartDataResultFormat.JSON,
+        force=False,
+        custom_cache_timeout=None,
+        cache_values={},
+    )
+
+    with mock.patch(
+        "superset.tasks.native_filter_cache.ChartDataQueryContextSchema"
+    ) as schema_class:
+        schema_class.return_value.load.return_value = expected
+        result: QueryContext | None = build_native_filter_option_query_context(
+            form_data or {}
+        )
+
+    assert isinstance(result, QueryContext)
+    assert result == expected
+    schema_class.return_value.load.assert_called_once()
+
+
+def test_build_native_filter_option_query_context_groupby_in_payload() -> None:
+    form_data: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        {
+            **_filter_config(),
+            "adhoc_filters": [
+                {
+                    "expressionType": "SIMPLE",
+                    "clause": "WHERE",
+                    "subject": "region",
+                    "operator": "==",
+                    "comparator": "EMEA",
+                }
+            ],
+        },
+    )
+    expected: mock.MagicMock = mock.MagicMock(spec=QueryContext)
+
+    with (
+        mock.patch(
+            "superset.tasks.native_filter_cache.ChartDataQueryContextSchema"
+        ) as schema_class,
+        mock.patch(
+            "superset.tasks.native_filter_cache._resolve_datasource_engine",
+            return_value="postgresql",
+        ) as mock_resolve_engine,
+    ):
+        schema_class.return_value.load.return_value = expected
+        result: QueryContext | None = build_native_filter_option_query_context(
+            form_data or {}
+        )
+
+    mock_resolve_engine.assert_not_called()
+    assert result == expected
+    payload: dict[str, Any] = schema_class.return_value.load.call_args.args[0]
+    query: dict[str, Any] = payload["queries"][0]
+    assert query["groupby"] == ["country"]
+    assert "columns" not in query
+    assert query["filters"] == [{"col": "region", "op": "==", "val": "EMEA"}]
+    assert "adhoc_filters" not in query
+
+
+def test_build_native_filter_option_query_context_with_adhoc_filters() -> None:
+    adhoc_filters: list[dict[str, Any]] = [
+        {
+            "expressionType": "SIMPLE",
+            "clause": "WHERE",
+            "subject": "region",
+            "operator": "IN",
+            "comparator": ["EMEA", "APAC"],
+        }
+    ]
+    form_data: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        {
+            **_filter_config(),
+            "adhoc_filters": adhoc_filters,
+        },
+    )
+    expected: mock.MagicMock = mock.MagicMock(spec=QueryContext)
+
+    with (
+        mock.patch(
+            "superset.tasks.native_filter_cache.ChartDataQueryContextSchema"
+        ) as schema_class,
+        mock.patch(
+            "superset.tasks.native_filter_cache._resolve_datasource_engine",
+            return_value="postgresql",
+        ) as mock_resolve_engine,
+    ):
+        schema_class.return_value.load.return_value = expected
+        result: QueryContext | None = build_native_filter_option_query_context(
+            form_data or {}
+        )
+
+    mock_resolve_engine.assert_not_called()
+    assert result == expected
+    payload: dict[str, Any] = schema_class.return_value.load.call_args.args[0]
+    query: dict[str, Any] = payload["queries"][0]
+    assert query["filters"] == [{"col": "region", "op": "IN", "val": ["EMEA", "APAC"]}]
+    assert "adhoc_filters" not in query
+
+
+def test_resolve_datasource_engine_only_called_for_sql_filters() -> None:
+    simple_form_data: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        {
+            **_filter_config(),
+            "adhoc_filters": [
+                {
+                    "expressionType": "SIMPLE",
+                    "clause": "WHERE",
+                    "subject": "region",
+                    "operator": "==",
+                    "comparator": "EMEA",
+                }
+            ],
+        },
+    )
+    sql_form_data: dict[str, Any] | None = build_native_filter_option_form_data(
+        _dashboard(json_metadata=None),
+        {
+            **_filter_config(),
+            "adhoc_filters": [
+                {
+                    "expressionType": "SQL",
+                    "clause": "WHERE",
+                    "sqlExpression": "region = 'EMEA'",
+                }
+            ],
+        },
+    )
+    expected: mock.MagicMock = mock.MagicMock(spec=QueryContext)
+
+    with (
+        mock.patch(
+            "superset.tasks.native_filter_cache.ChartDataQueryContextSchema"
+        ) as schema_class,
+        mock.patch(
+            "superset.tasks.native_filter_cache._resolve_datasource_engine",
+            return_value="postgresql",
+        ) as mock_resolve_engine,
+    ):
+        schema_class.return_value.load.return_value = expected
+
+        build_native_filter_option_query_context(simple_form_data or {})
+        mock_resolve_engine.assert_not_called()
+
+        build_native_filter_option_query_context(sql_form_data or {})
+        mock_resolve_engine.assert_called_once_with(10)
+
+
+def test_build_native_filter_option_query_context_missing_groupby() -> None:
+    result: QueryContext | None = build_native_filter_option_query_context(
+        {"datasource": "10__table"}
+    )
+
+    assert result is None
+
+
+def test_build_native_filter_option_query_context_malformed_groupby() -> None:
+    malformed_groupby_values: list[Any] = [
+        "country",
+        [{"label": "country"}],
+        [None],
+        [],
+    ]
+
+    for groupby in malformed_groupby_values:
+        result: QueryContext | None = build_native_filter_option_query_context(
+            {
+                "datasource": "10__table",
+                "groupby": groupby,
+            }
+        )
+
+        assert result is None