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

rebenitez1802 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 204ccc7ce7f fix(datasets): apply Schema filter on the combined 
datasource list (#43829)
204ccc7ce7f is described below

commit 204ccc7ce7fc0f050b634e1b7187955587812ea9
Author: Rafael Benitez <[email protected]>
AuthorDate: Tue Sep 8 14:24:27 2026 -0300

    fix(datasets): apply Schema filter on the combined datasource list (#43829)
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 superset/commands/datasource/list.py              | 112 ++++++++++-
 superset/daos/datasource.py                       |   4 +
 tests/integration_tests/datasource/api_tests.py   |  62 ++++++
 tests/unit_tests/commands/datasource/list_test.py | 224 ++++++++++++++++++++++
 tests/unit_tests/dao/datasource_test.py           |  31 +++
 5 files changed, 423 insertions(+), 10 deletions(-)

diff --git a/superset/commands/datasource/list.py 
b/superset/commands/datasource/list.py
index 20b3002c2a5..1f58001a3b3 100644
--- a/superset/commands/datasource/list.py
+++ b/superset/commands/datasource/list.py
@@ -70,14 +70,22 @@ class GetCombinedDatasourceListCommand(BaseCommand):
             type_filter,
             database_id,
             semantic_layer_uuid,
+            schema_filter,
         ) = self._parse_filters(filters)
 
         source_type = self._resolve_connection_source_type(
             source_type,
             database_id,
             semantic_layer_uuid,
+            schema_filter,
         )
-        source_type = self._resolve_source_type(source_type, sql_filter, 
type_filter)
+        # A connection filter can already resolve to "empty" (e.g. a 
semantic-layer
+        # connection combined with a dataset-only schema filter); don't let the
+        # content-filter resolution override that terminal decision.
+        if source_type != "empty":
+            source_type = self._resolve_source_type(
+                source_type, sql_filter, type_filter, schema_filter
+            )
 
         if source_type == "empty":
             return {"count": 0, "result": []}
@@ -88,6 +96,7 @@ class GetCombinedDatasourceListCommand(BaseCommand):
             sql_filter,
             database_id,
             semantic_layer_uuid,
+            schema_filter,
         )
         total_count, rows = DatasourceDAO.paginate_combined_query(
             combined, order_column, order_direction, page, page_size
@@ -102,6 +111,7 @@ class GetCombinedDatasourceListCommand(BaseCommand):
         source_type: str,
         database_id: int | None,
         semantic_layer_uuid: str | None,
+        schema_filter: str | None = None,
     ) -> str:
         # A connection filter implicitly narrows the source type: selecting a
         # database ID means "show only datasets", and selecting a semantic 
layer
@@ -111,6 +121,14 @@ class GetCombinedDatasourceListCommand(BaseCommand):
             if database_id is not None:
                 return "database"
             elif semantic_layer_uuid is not None:
+                # A semantic-layer connection selects only that layer's
+                # (schema-less) views, so a dataset-only schema filter matches
+                # nothing: the honest result is empty. Unlike an explicit
+                # Source="Semantic layer" selection (handled in
+                # _resolve_source_type), the user never picked a source type
+                # here, so the "explicit selection wins" rule does not apply.
+                if schema_filter is not None:
+                    return "empty"
                 return "semantic_layer"
 
         return source_type
@@ -122,8 +140,11 @@ class GetCombinedDatasourceListCommand(BaseCommand):
         sql_filter: bool | None,
         database_id: int | None,
         semantic_layer_uuid: str | None,
+        schema_filter: str | None = None,
     ) -> Any:
-        ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter, 
database_id)
+        ds_q = DatasourceDAO.build_dataset_query(
+            name_filter, sql_filter, database_id, schema_filter
+        )
         sv_q = DatasourceDAO.build_semantic_view_query(name_filter, 
semantic_layer_uuid)
 
         if source_type == "database":
@@ -184,12 +205,40 @@ class GetCombinedDatasourceListCommand(BaseCommand):
         source_type: str,
         sql_filter: bool | None,
         type_filter: str | None,
+        schema_filter: str | None = None,
     ) -> str:
         """Narrow source_type based on access flags, sql filter, and type 
filter.
 
         Returns one of: "database", "semantic_layer", "all", or "empty".
         "empty" signals that the caller should short-circuit and return no 
results
         (used when the user explicitly requests semantic views but lacks 
access).
+
+        Resolution follows a single precedence order (highest to lowest). This
+        is what makes a dataset-only filter (schema/sql) combined with a
+        semantic-view result behave consistently across entry points, with one
+        deliberate exception noted below:
+
+        1. Access — a principal never sees a source type it cannot read; a
+           dataset-only filter applied by a user without dataset access yields
+           "empty" (nothing to match).
+        2. Explicit ``Source`` selection — an explicit ``source_type`` of
+           "database"/"semantic_layer" is authoritative and suppresses
+           otherwise-contradictory cross-type filters (a leftover Schema chip
+           becomes a no-op rather than a contradiction). This is the one place 
a
+           dataset-only filter is intentionally dropped instead of yielding
+           "empty".
+        3. Implicit narrowing and content filters — honest AND: a filter that
+           cannot match the resulting rows returns "empty" rather than being
+           silently dropped. This covers Type="Semantic View" + schema and the
+           semantic-layer-*connection* + schema route (see
+           ``_resolve_connection_source_type``).
+
+        Consequence: "views + schema=X" resolves to "empty" via the Type filter
+        and via a semantic-layer connection, but an explicit 
``Source``="Semantic
+        layer" selection shows all views with the schema ignored (rule 2). A
+        views-only user hits rule 1 first, so the same explicit selection 
yields
+        "empty" for them — access restrictions outrank the explicit-selection
+        escape hatch. All intended.
         """
         if not self._can_read_semantic_views:
             # If the user explicitly asked for semantic views but cannot read 
them,
@@ -199,24 +248,61 @@ class GetCombinedDatasourceListCommand(BaseCommand):
                 return "empty"
             return "database"
         if not self._can_read_datasets:
+            # schema and sql_filter are both dataset-only, so a 
semantic-views-only
+            # user matches nothing under AND semantics; return "empty" rather 
than
+            # showing views with the filter dropped (mirrors the
+            # not-can_read_semantic_views branch above and the
+            # schema/Type="Semantic View" case below).
+            if schema_filter is not None or sql_filter is not None:
+                return "empty"
             return "semantic_layer"
         # An explicit source_type selection ("database" or "semantic_layer") 
always
         # wins. This prevents e.g. Type="Semantic View" from overriding an 
explicit
         # Source="Database" filter and showing inconsistent results.
         if source_type in ("database", "semantic_layer"):
             return source_type
-        # sql_filter (physical/virtual toggle) only applies to datasets
-        if sql_filter is not None:
+        # sql_filter (physical/virtual toggle) and schema both only apply to
+        # datasets (semantic views have no schema), so either narrows to 
datasets.
+        if sql_filter is not None or schema_filter is not None:
+            # A schema filter combined with an explicit Type="Semantic View" is
+            # contradictory: no semantic view has a schema, so under AND 
semantics
+            # the honest result is zero rows rather than silently dropping 
either
+            # filter. This pair is reachable because the Schema control is not 
part
+            # of the frontend cascade. (Via the UI, sql_filter and type_filter 
come
+            # from one control and cannot collide; a direct API payload could 
set
+            # both, in which case sql_filter wins — see 
_apply_sql_null_filter.)
+            if schema_filter is not None and type_filter == "semantic_view":
+                return "empty"
             return "database"
         # Explicit semantic-view type filter (only reached when 
source_type="all")
         if type_filter == "semantic_view":
             return "semantic_layer"
         return source_type
 
+    @staticmethod
+    def _apply_sql_null_filter(
+        value: Any,
+        type_filter: str | None,
+        sql_filter: bool | None,
+    ) -> tuple[str | None, bool | None]:
+        """Interpret a ``sql``/``dataset_is_null_or_empty`` filter value.
+
+        ``"semantic_view"`` selects semantic views; a boolean toggles the
+        physical/virtual dataset split. Unrecognized values leave both inputs
+        unchanged, so the caller can pass its current values straight through.
+        """
+        if value == "semantic_view":
+            return "semantic_view", sql_filter
+        if isinstance(value, bool):
+            return type_filter, value
+        return type_filter, sql_filter
+
     @staticmethod
     def _parse_filters(
         filters: list[dict[str, Any]],
-    ) -> tuple[str, str | None, bool | None, str | None, int | None, str | 
None]:
+    ) -> tuple[
+        str, str | None, bool | None, str | None, int | None, str | None, str 
| None
+    ]:
         """
         Translate raw rison filter dicts into typed query parameters.
 
@@ -228,6 +314,7 @@ class GetCombinedDatasourceListCommand(BaseCommand):
                                 semantic views
             database_id:        filter datasets to a specific database ID
             semantic_layer_uuid: filter semantic views to a specific semantic 
layer UUID
+            schema_filter:      filter datasets to a specific schema name
         """
         source_type = "all"
         name_filter: str | None = None
@@ -235,6 +322,7 @@ class GetCombinedDatasourceListCommand(BaseCommand):
         type_filter: str | None = None
         database_id: int | None = None
         semantic_layer_uuid: str | None = None
+        schema_filter: str | None = None
 
         for f in filters:
             col = f.get("col")
@@ -245,11 +333,12 @@ class GetCombinedDatasourceListCommand(BaseCommand):
                 source_type = value or "all"
             elif col == "table_name" and f.get("opr") == "ct":
                 name_filter = value
-            elif col == "sql":
-                if opr == "dataset_is_null_or_empty" and value == 
"semantic_view":
-                    type_filter = "semantic_view"
-                elif opr == "dataset_is_null_or_empty" and isinstance(value, 
bool):
-                    sql_filter = value
+            elif col == "sql" and opr == "dataset_is_null_or_empty":
+                type_filter, sql_filter = (
+                    GetCombinedDatasourceListCommand._apply_sql_null_filter(
+                        value, type_filter, sql_filter
+                    )
+                )
             elif col == "database" and value is not None:
                 try:
                     database_id = int(value)
@@ -257,6 +346,8 @@ class GetCombinedDatasourceListCommand(BaseCommand):
                     pass
             elif col == "semantic_layer_uuid" and value is not None:
                 semantic_layer_uuid = str(value)
+            elif col == "schema" and opr == "eq" and value is not None:
+                schema_filter = str(value)
 
         return (
             source_type,
@@ -265,4 +356,5 @@ class GetCombinedDatasourceListCommand(BaseCommand):
             type_filter,
             database_id,
             semantic_layer_uuid,
+            schema_filter,
         )
diff --git a/superset/daos/datasource.py b/superset/daos/datasource.py
index f96aa83c7ec..93c52c88e25 100644
--- a/superset/daos/datasource.py
+++ b/superset/daos/datasource.py
@@ -96,6 +96,7 @@ class DatasourceDAO(BaseDAO[Datasource]):
         name_filter: str | None,
         sql_filter: bool | None,
         database_id: int | None = None,
+        schema_filter: str | None = None,
     ) -> Select:
         """Build a SELECT for datasets, applying access and content filters."""
         ds_table = SqlaTable.__table__
@@ -141,6 +142,9 @@ class DatasourceDAO(BaseDAO[Datasource]):
         if database_id is not None:
             ds_q = ds_q.where(SqlaTable.database_id == database_id)
 
+        if schema_filter is not None:
+            ds_q = ds_q.where(SqlaTable.schema == schema_filter)
+
         return ds_q
 
     @staticmethod
diff --git a/tests/integration_tests/datasource/api_tests.py 
b/tests/integration_tests/datasource/api_tests.py
index 454f91ce57e..33217ffdad9 100644
--- a/tests/integration_tests/datasource/api_tests.py
+++ b/tests/integration_tests/datasource/api_tests.py
@@ -29,6 +29,7 @@ from superset import db, security_manager
 from superset.connectors.sqla.models import SqlaTable
 from superset.daos.exceptions import DatasourceTypeNotSupportedError
 from superset.extensions import cache_manager
+from superset.models.core import Database
 from superset.semantic_layers.models import SemanticLayer, SemanticView
 from superset.utils import json
 from tests.integration_tests.base_tests import SupersetTestCase
@@ -110,6 +111,34 @@ def _gamma_granted(*pvms: tuple[str, str]) -> 
Iterator[None]:
         db.session.commit()
 
 
+@contextmanager
+def _datasets_with_schemas() -> Iterator[str]:
+    """Persist two datasets under distinct, unique schemas; clean up on exit.
+
+    Yields a unique suffix so the caller can build the exact table and schema
+    names (``main_ds_<suffix>`` in schema ``main_<suffix>`` and
+    ``other_ds_<suffix>`` in schema ``other_<suffix>``). The unique schema name
+    guarantees the filter value cannot collide with pre-existing datasets.
+    """
+    suffix = uuid.uuid4().hex
+    database = Database(database_name=f"schema_db_{suffix}", 
sqlalchemy_uri="sqlite://")
+    main_ds = SqlaTable(
+        table_name=f"main_ds_{suffix}", schema=f"main_{suffix}", 
database=database
+    )
+    other_ds = SqlaTable(
+        table_name=f"other_ds_{suffix}", schema=f"other_{suffix}", 
database=database
+    )
+    db.session.add_all([database, main_ds, other_ds])
+    db.session.commit()
+    try:
+        yield suffix
+    finally:
+        db.session.rollback()
+        for obj in (main_ds, other_ds, database):
+            db.session.delete(obj)
+        db.session.commit()
+
+
 class TestDatasourceApi(SupersetTestCase):
     def setUp(self):
         # Clear the column-values cache before every test so that
@@ -628,3 +657,36 @@ class TestDatasourceApi(SupersetTestCase):
                 status, names = self._list_semantic_views_as_gamma()
             assert status == 200
             assert names == set()
+
+    @with_feature_flags(SEMANTIC_LAYERS=True)
+    def test_combined_list_filters_by_schema(self):
+        """The schema filter narrows the combined list to matching datasets.
+
+        The per-run schema name is unique, so exactly one dataset can match:
+        the result set must equal ``{main_ds_<suffix>}``. That single exact-set
+        assertion proves both halves independently of pagination and of 
whatever
+        other datasets exist in the test database:
+
+        * ``other_ds`` (a different schema) is excluded by the WHERE clause —
+          were it dropped, the result would include the full dataset list.
+        * the semantic view (which has no schema) is excluded by the 
source-type
+          narrowing a schema filter triggers — with SEMANTIC_LAYERS on it would
+          otherwise appear in the combined ``all`` list, so its absence is
+          load-bearing, not incidental.
+        """
+        self.login(ADMIN_USERNAME)
+        with (
+            _datasets_with_schemas() as suffix,
+            _semantic_views("schema_filter"),
+        ):
+            rv = self.client.get(
+                "api/v1/datasource/?q="
+                f"(filters:!((col:schema,opr:eq,value:main_{suffix})),"
+                
"order_column:table_name,order_direction:asc,page:0,page_size:100)"
+            )
+
+            assert rv.status_code == 200
+            payload = json.loads(rv.data.decode("utf-8"))
+            names = {item["table_name"] for item in payload["result"]}
+
+            assert names == {f"main_ds_{suffix}"}
diff --git a/tests/unit_tests/commands/datasource/list_test.py 
b/tests/unit_tests/commands/datasource/list_test.py
index a487e0c5137..a237162882c 100644
--- a/tests/unit_tests/commands/datasource/list_test.py
+++ b/tests/unit_tests/commands/datasource/list_test.py
@@ -34,6 +34,7 @@ def 
test_parse_filters_semantic_view_requires_dataset_operator() -> None:
         type_filter,
         database_id,
         semantic_layer_uuid,
+        schema_filter,
     ) = GetCombinedDatasourceListCommand._parse_filters(
         [{"col": "sql", "opr": "eq", "value": "semantic_view"}]
     )
@@ -44,6 +45,7 @@ def 
test_parse_filters_semantic_view_requires_dataset_operator() -> None:
     assert type_filter is None
     assert database_id is None
     assert semantic_layer_uuid is None
+    assert schema_filter is None
 
 
 def test_parse_filters_semantic_view_with_dataset_operator() -> None:
@@ -54,6 +56,7 @@ def test_parse_filters_semantic_view_with_dataset_operator() 
-> None:
         type_filter,
         database_id,
         semantic_layer_uuid,
+        schema_filter,
     ) = GetCombinedDatasourceListCommand._parse_filters(
         [
             {
@@ -70,6 +73,7 @@ def test_parse_filters_semantic_view_with_dataset_operator() 
-> None:
     assert type_filter == "semantic_view"
     assert database_id is None
     assert semantic_layer_uuid is None
+    assert schema_filter is None
 
 
 def test_parse_filters_sql_bool_requires_dataset_operator() -> None:
@@ -80,6 +84,7 @@ def test_parse_filters_sql_bool_requires_dataset_operator() 
-> None:
         type_filter,
         database_id,
         semantic_layer_uuid,
+        schema_filter,
     ) = GetCombinedDatasourceListCommand._parse_filters(
         [{"col": "sql", "opr": "eq", "value": True}]
     )
@@ -90,6 +95,53 @@ def test_parse_filters_sql_bool_requires_dataset_operator() 
-> None:
     assert type_filter is None
     assert database_id is None
     assert semantic_layer_uuid is None
+    assert schema_filter is None
+
+
+def test_parse_filters_extracts_schema() -> None:
+    (
+        source_type,
+        name_filter,
+        sql_filter,
+        type_filter,
+        database_id,
+        semantic_layer_uuid,
+        schema_filter,
+    ) = GetCombinedDatasourceListCommand._parse_filters(
+        [{"col": "schema", "opr": "eq", "value": "main"}]
+    )
+
+    assert schema_filter == "main"
+    assert source_type == "all"
+    assert name_filter is None
+    assert sql_filter is None
+    assert type_filter is None
+    assert database_id is None
+    assert semantic_layer_uuid is None
+
+
+def test_parse_filters_ignores_schema_with_wrong_operator() -> None:
+    (*_, schema_filter) = GetCombinedDatasourceListCommand._parse_filters(
+        [{"col": "schema", "opr": "ct", "value": "main"}]
+    )
+
+    assert schema_filter is None
+
+
+def test_parse_filters_schema_boundary_values() -> None:
+    # An empty string is a real value: it becomes ``schema == ''`` (matching
+    # empty-schema rows, not NULL), mirroring the canonical /api/v1/dataset/
+    # FilterEqual behavior.
+    (*_, empty) = GetCombinedDatasourceListCommand._parse_filters(
+        [{"col": "schema", "opr": "eq", "value": ""}]
+    )
+    assert empty == ""
+
+    # A null value is ignored so no schema filter is applied.
+    (*_, missing) = GetCombinedDatasourceListCommand._parse_filters(
+        [{"col": "schema", "opr": "eq", "value": None}]
+    )
+    assert missing is None
 
 
 def test_resolve_source_type_semantic_view_filter_forces_semantic_layer() -> 
None:
@@ -124,6 +176,178 @@ def test_resolve_source_type_sql_filter_forces_database() 
-> None:
     assert source_type == "database"
 
 
+def test_resolve_source_type_schema_filter_forces_database() -> None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=True,
+        can_read_semantic_views=True,
+    )
+
+    source_type = command._resolve_source_type(
+        source_type="all",
+        sql_filter=None,
+        type_filter=None,
+        schema_filter="main",
+    )
+
+    assert source_type == "database"
+
+
+def test_resolve_source_type_explicit_semantic_layer_wins_over_schema() -> 
None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=True,
+        can_read_semantic_views=True,
+    )
+
+    source_type = command._resolve_source_type(
+        source_type="semantic_layer",
+        sql_filter=None,
+        type_filter=None,
+        schema_filter="main",
+    )
+
+    assert source_type == "semantic_layer"
+
+
+def test_resolve_source_type_semantic_view_type_plus_schema_is_empty() -> None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=True,
+        can_read_semantic_views=True,
+    )
+
+    # Type="Semantic View" AND a schema is contradictory (views have no 
schema);
+    # the honest AND result is zero rows, not a silently-dropped filter.
+    source_type = command._resolve_source_type(
+        source_type="all",
+        sql_filter=None,
+        type_filter="semantic_view",
+        schema_filter="main",
+    )
+
+    assert source_type == "empty"
+
+
+def test_resolve_source_type_views_only_user_with_schema_is_empty() -> None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=False,
+        can_read_semantic_views=True,
+    )
+
+    # A user who can only read semantic views matches nothing when a
+    # (dataset-only) schema filter is applied, so the honest result is empty.
+    source_type = command._resolve_source_type(
+        source_type="all",
+        sql_filter=None,
+        type_filter=None,
+        schema_filter="main",
+    )
+
+    assert source_type == "empty"
+
+
+def 
test_resolve_source_type_views_only_user_without_schema_is_semantic_layer() -> 
None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=False,
+        can_read_semantic_views=True,
+    )
+
+    # Without a schema filter the views-only user still sees semantic views.
+    source_type = command._resolve_source_type(
+        source_type="all",
+        sql_filter=None,
+        type_filter=None,
+    )
+
+    assert source_type == "semantic_layer"
+
+
+def test_resolve_source_type_views_only_user_with_sql_filter_is_empty() -> 
None:
+    command = GetCombinedDatasourceListCommand(
+        args={},
+        can_read_datasets=False,
+        can_read_semantic_views=True,
+    )
+
+    # sql_filter (physical/virtual) is dataset-only, so a views-only user 
matches
+    # nothing rather than seeing the full view list with the filter dropped.
+    source_type = command._resolve_source_type(
+        source_type="all",
+        sql_filter=True,
+        type_filter=None,
+    )
+
+    assert source_type == "empty"
+
+
+def test_resolve_connection_semantic_layer_uuid_with_schema_is_empty() -> None:
+    # Picking a semantic-layer connection (not an explicit source type) 
narrows to
+    # that layer's schema-less views; a dataset-only schema filter matches 
nothing.
+    source_type = 
GetCombinedDatasourceListCommand._resolve_connection_source_type(
+        source_type="all",
+        database_id=None,
+        semantic_layer_uuid="uuid-123",
+        schema_filter="main",
+    )
+
+    assert source_type == "empty"
+
+
+def test_resolve_connection_semantic_layer_uuid_without_schema() -> None:
+    # Without a schema filter the connection still narrows to that layer's 
views.
+    source_type = 
GetCombinedDatasourceListCommand._resolve_connection_source_type(
+        source_type="all",
+        database_id=None,
+        semantic_layer_uuid="uuid-123",
+        schema_filter=None,
+    )
+
+    assert source_type == "semantic_layer"
+
+
+def test_resolve_connection_explicit_semantic_layer_ignores_schema() -> None:
+    # The "empty" narrowing only applies to the implicit (source_type="all") 
route;
+    # an explicit source_type is left alone even with uuid + schema both set.
+    source_type = 
GetCombinedDatasourceListCommand._resolve_connection_source_type(
+        source_type="semantic_layer",
+        database_id=None,
+        semantic_layer_uuid="uuid-123",
+        schema_filter="main",
+    )
+
+    assert source_type == "semantic_layer"
+
+
+def test_run_semantic_layer_connection_with_schema_returns_empty() -> None:
+    """A semantic-layer connection + schema filter short-circuits to empty.
+
+    Pins the run() guard that stops the content-filter stage from overriding a
+    connection-stage "empty": without it, the query would fall through to a
+    dataset source instead of returning zero rows.
+    """
+    command = GetCombinedDatasourceListCommand(
+        args={
+            "filters": [
+                {"col": "semantic_layer_uuid", "opr": "eq", "value": 
"uuid-123"},
+                {"col": "schema", "opr": "eq", "value": "main"},
+            ]
+        },
+        can_read_datasets=True,
+        can_read_semantic_views=True,
+    )
+
+    with patch(
+        
"superset.commands.datasource.list.DatasourceDAO.paginate_combined_query"
+    ) as paginate_mock:
+        result = command.run()
+
+    assert result == {"count": 0, "result": []}
+    paginate_mock.assert_not_called()
+
+
 @pytest.mark.parametrize(
     "order_column",
     ["unknown", "database.database_name", "id"],
diff --git a/tests/unit_tests/dao/datasource_test.py 
b/tests/unit_tests/dao/datasource_test.py
index 113201464f9..75bd72974dc 100644
--- a/tests/unit_tests/dao/datasource_test.py
+++ b/tests/unit_tests/dao/datasource_test.py
@@ -57,3 +57,34 @@ def test_build_dataset_query_excludes_soft_deleted(session: 
Session) -> None:
 
     assert "live_t" in names
     assert "hidden_t" not in names
+
+
+def test_build_dataset_query_filters_by_schema(session: Session) -> None:
+    """``build_dataset_query(schema_filter=...)`` restricts rows to that 
schema.
+
+    The existing fixtures above are all ``schema="main"``, so this test adds a
+    contrasting ``schema="public"`` row to prove non-matching schemas are 
excluded.
+    """
+    from superset import db, security_manager
+    from superset.connectors.sqla.models import SqlaTable
+    from superset.daos.datasource import DatasourceDAO
+    from superset.models.core import Database
+
+    SqlaTable.metadata.create_all(session.get_bind())
+
+    database = Database(database_name="schema_db", sqlalchemy_uri="sqlite://")
+    main_t = SqlaTable(table_name="main_t", schema="main", database=database)
+    public_t = SqlaTable(table_name="public_t", schema="public", 
database=database)
+    db.session.add_all([database, main_t, public_t])
+    db.session.flush()
+
+    with patch.object(
+        security_manager, "can_access_all_datasources", return_value=True
+    ):
+        query = DatasourceDAO.build_dataset_query(
+            name_filter=None, sql_filter=None, schema_filter="main"
+        )
+        names = {row.table_name for row in session.execute(query)}
+
+    assert "main_t" in names
+    assert "public_t" not in names

Reply via email to