Files
superset2/superset/commands/datasource/list.py
Beto Dealmeida 0d09ecaae1 Small fixes
2026-05-05 09:06:10 -04:00

171 lines
6.5 KiB
Python

# 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.
"""Command for the combined dataset + semantic view list endpoint."""
from __future__ import annotations
import logging
from typing import Any, cast
from sqlalchemy import union_all
from superset.commands.base import BaseCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.datasource import DatasourceDAO
from superset.datasource.schemas import DatasetListSchema, SemanticViewListSchema
from superset.semantic_layers.models import SemanticView
logger = logging.getLogger(__name__)
_dataset_schema = DatasetListSchema()
_semantic_view_schema = SemanticViewListSchema()
class GetCombinedDatasourceListCommand(BaseCommand):
"""
Fetch and serialize a paginated, combined list of datasets and semantic views.
Callers are responsible for checking access permissions before constructing
this command and for passing the appropriate ``can_read_*`` flags.
"""
def __init__(
self,
args: dict[str, Any],
can_read_datasets: bool,
can_read_semantic_views: bool,
) -> None:
self._args = args
self._can_read_datasets = can_read_datasets
self._can_read_semantic_views = can_read_semantic_views
def run(self) -> dict[str, Any]:
self.validate()
page = self._args.get("page", 0)
page_size = self._args.get("page_size", 25)
order_column = self._args.get("order_column", "changed_on")
order_direction = self._args.get("order_direction", "desc")
filters = self._args.get("filters", [])
source_type, name_filter, sql_filter, type_filter = self._parse_filters(filters)
source_type = self._resolve_source_type(source_type, sql_filter, type_filter)
if source_type == "empty":
return {"count": 0, "result": []}
ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter)
sv_q = DatasourceDAO.build_semantic_view_query(name_filter)
if source_type == "database":
combined = ds_q.subquery()
elif source_type == "semantic_layer":
combined = sv_q.subquery()
else:
combined = union_all(ds_q, sv_q).subquery()
total_count, rows = DatasourceDAO.paginate_combined_query(
combined, order_column, order_direction, page, page_size
)
datasets_map = DatasourceDAO.fetch_datasets_by_ids(
[r.item_id for r in rows if r.source_type == "database"]
)
sv_map = DatasourceDAO.fetch_semantic_views_by_ids(
[r.item_id for r in rows if r.source_type == "semantic_layer"]
)
result: list[dict[str, Any]] = []
for row in rows:
if row.source_type == "database":
ds_obj = cast(SqlaTable | None, datasets_map.get(row.item_id))
if ds_obj:
result.append(_dataset_schema.dump(ds_obj))
else:
sv_obj = cast(SemanticView | None, sv_map.get(row.item_id))
if sv_obj:
result.append(_semantic_view_schema.dump(sv_obj))
return {"count": total_count, "result": result}
def validate(self) -> None:
pass # access checks are performed by the caller (API layer)
def _resolve_source_type(
self,
source_type: str,
sql_filter: bool | None,
type_filter: str | 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).
"""
if not self._can_read_semantic_views:
# If the user explicitly asked for semantic views but cannot read them,
# return "empty" so the caller yields zero results rather than silently
# falling back to the full dataset list.
if source_type == "semantic_layer" or type_filter == "semantic_view":
return "empty"
return "database"
if not self._can_read_datasets:
return "semantic_layer"
# sql_filter (physical/virtual toggle) only applies to datasets
if sql_filter is not None:
return "database"
# Explicit semantic-view type filter
if type_filter == "semantic_view":
return "semantic_layer"
return source_type
@staticmethod
def _parse_filters(
filters: list[dict[str, Any]],
) -> tuple[str, str | None, bool | None, str | None]:
"""
Translate raw rison filter dicts into typed query parameters.
Returns:
source_type: "all" | "database" | "semantic_layer"
name_filter: substring to match against name/table_name
sql_filter: True → physical only, False → virtual only, None → both
type_filter: "semantic_view" when the caller wants only semantic views
"""
source_type = "all"
name_filter: str | None = None
sql_filter: bool | None = None
type_filter: str | None = None
for f in filters:
col = f.get("col")
opr = f.get("opr")
value = f.get("value")
if col == "source_type":
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
return source_type, name_filter, sql_filter, type_filter