mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
171 lines
6.5 KiB
Python
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
|