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