This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch semantic-layer-explore-integration in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3e0473606a3aeaf7f4bbb84679a79bdd2f0cac0d Author: Beto Dealmeida <[email protected]> AuthorDate: Fri Feb 6 16:28:08 2026 -0500 feat: Explore integration --- .../superset-ui-core/src/query/DatasourceKey.ts | 17 +++++-- .../superset-ui-core/src/query/types/Datasource.ts | 3 +- .../superset-ui-core/src/query/types/Query.ts | 2 +- .../src/components/Chart/DrillBy/DrillByModal.tsx | 8 +++- superset-frontend/src/dashboard/types.ts | 2 +- .../src/explore/actions/saveModalActions.ts | 5 +- .../src/explore/exploreUtils/formData.ts | 8 ++-- superset/commands/explore/get.py | 19 ++++++-- superset/commands/explore/parameters.py | 11 +++-- superset/daos/datasource.py | 8 ++-- superset/explore/api.py | 2 +- ...6_33d7e0e21daa_add_semantic_layers_and_views.py | 4 +- superset/static/service-worker.js | 28 +---------- superset/views/core.py | 35 ++++++++------ superset/views/utils.py | 55 ++++++++++++++-------- 15 files changed, 117 insertions(+), 90 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts b/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts index 38a38e10b13..170f09331b9 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts @@ -19,16 +19,25 @@ import { DatasourceType } from './types/Datasource'; +const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = { + table: DatasourceType.Table, + query: DatasourceType.Query, + dataset: DatasourceType.Dataset, + sl_table: DatasourceType.SlTable, + saved_query: DatasourceType.SavedQuery, + semantic_view: DatasourceType.SemanticView, +}; + export default class DatasourceKey { - readonly id: number; + readonly id: number | string; readonly type: DatasourceType; constructor(key: string) { const [idStr, typeStr] = key.split('__'); - this.id = parseInt(idStr, 10); - this.type = DatasourceType.Table; // default to SqlaTable model - this.type = typeStr === 'query' ? DatasourceType.Query : this.type; + const isNumeric = /^\d+$/.test(idStr); + this.id = isNumeric ? parseInt(idStr, 10) : idStr; + this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table; } public toString() { diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts index 47902cf07ae..8fbf63aa4b3 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts @@ -26,6 +26,7 @@ export enum DatasourceType { Dataset = 'dataset', SlTable = 'sl_table', SavedQuery = 'saved_query', + SemanticView = 'semantic_view', } export interface Currency { @@ -37,7 +38,7 @@ export interface Currency { * Datasource metadata. */ export interface Datasource { - id: number; + id: number | string; name: string; type: DatasourceType; columns: Column[]; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts index 14d4e2273b2..c1ecc99fae5 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts @@ -159,7 +159,7 @@ export interface QueryObject export interface QueryContext { datasource: { - id: number; + id: number | string; type: DatasourceType; }; /** Force refresh of all queries */ diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx index 6245945de45..978277931e2 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx @@ -90,11 +90,15 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => { findPermission('can_explore', 'Superset', state.user?.roles), ); - const [datasource_id, datasource_type] = formData.datasource.split('__'); + const [datasourceIdStr, datasource_type] = formData.datasource.split('__'); + const isNumeric = /^\d+$/.test(datasourceIdStr); + const datasource_id = isNumeric + ? parseInt(datasourceIdStr, 10) + : datasourceIdStr; useEffect(() => { // short circuit if the user is embedded as explore is not available if (isEmbedded()) return; - postFormData(Number(datasource_id), datasource_type, formData, 0) + postFormData(datasource_id, datasource_type, formData, 0) .then(key => { setUrl( `/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`, diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index b2bde968368..162031c695c 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -272,7 +272,7 @@ export type Slice = { changed_on: number; changed_on_humanized: string; modified: string; - datasource_id: number; + datasource_id: number | string; datasource_type: DatasourceType; datasource_url: string; datasource_name: string; diff --git a/superset-frontend/src/explore/actions/saveModalActions.ts b/superset-frontend/src/explore/actions/saveModalActions.ts index 978c5cb09ba..ed1d13fa93b 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.ts +++ b/superset-frontend/src/explore/actions/saveModalActions.ts @@ -144,12 +144,13 @@ export const getSlicePayload = async ( ...adhocFilters, dashboards, }; - let datasourceId = 0; + let datasourceId: number | string = 0; let datasourceType: DatasourceType = DatasourceType.Table; if (formData.datasource) { const [id, typeString] = formData.datasource.split('__'); - datasourceId = parseInt(id, 10); + const isNumeric = /^\d+$/.test(id); + datasourceId = isNumeric ? parseInt(id, 10) : id; const formattedTypeString = typeString.charAt(0).toUpperCase() + typeString.slice(1); diff --git a/superset-frontend/src/explore/exploreUtils/formData.ts b/superset-frontend/src/explore/exploreUtils/formData.ts index 9a83d8fd8da..994b7f0b4c5 100644 --- a/superset-frontend/src/explore/exploreUtils/formData.ts +++ b/superset-frontend/src/explore/exploreUtils/formData.ts @@ -20,7 +20,7 @@ import { SupersetClient, JsonObject, JsonResponse } from '@superset-ui/core'; import { sanitizeFormData } from 'src/utils/sanitizeFormData'; type Payload = { - datasource_id: number; + datasource_id: number | string; datasource_type: string; form_data: string; chart_id?: number; @@ -36,7 +36,7 @@ const assembleEndpoint = (key?: string, tabId?: string) => { }; const assemblePayload = ( - datasourceId: number, + datasourceId: number | string, datasourceType: string, formData: JsonObject, chartId?: number, @@ -53,7 +53,7 @@ const assemblePayload = ( }; export const postFormData = ( - datasourceId: number, + datasourceId: number | string, datasourceType: string, formData: JsonObject, chartId?: number, @@ -70,7 +70,7 @@ export const postFormData = ( }).then((r: JsonResponse) => r.json.key); export const putFormData = ( - datasourceId: number, + datasourceId: number | string, datasourceType: string, key: string, formData: JsonObject, diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py index 78142eb5ec1..caa309f2f39 100644 --- a/superset/commands/explore/get.py +++ b/superset/commands/explore/get.py @@ -18,6 +18,7 @@ import contextlib import logging from abc import ABC from typing import Any, cast, Optional +from uuid import UUID from flask import request from flask_babel import lazy_gettext as _ @@ -100,9 +101,12 @@ class GetExploreCommand(BaseCommand, ABC): use_slice_data=True, initial_form_data=initial_form_data, ) + ds_id: int | UUID | None = None try: - self._datasource_id, self._datasource_type = get_datasource_info( - self._datasource_id, self._datasource_type, form_data + ds_id, self._datasource_type = get_datasource_info( + self._datasource_id, + self._datasource_type, + form_data, ) except SupersetException: self._datasource_id = None @@ -111,10 +115,11 @@ class GetExploreCommand(BaseCommand, ABC): datasource: Optional[BaseDatasource] = None - if self._datasource_id is not None: + if ds_id is not None: with contextlib.suppress(DatasourceNotFound): datasource = DatasourceDAO.get_datasource( - cast(str, self._datasource_type), self._datasource_id + cast(str, self._datasource_type), + ds_id, ) datasource_name = _("[Missing Dataset]") @@ -124,7 +129,11 @@ class GetExploreCommand(BaseCommand, ABC): security_manager.raise_for_access(datasource=datasource) viz_type = form_data.get("viz_type") - if not viz_type and datasource and datasource.default_endpoint: + if ( + not viz_type + and datasource + and getattr(datasource, "default_endpoint", None) + ): raise WrongEndpointError(redirect=datasource.default_endpoint) form_data["datasource"] = ( diff --git a/superset/commands/explore/parameters.py b/superset/commands/explore/parameters.py index 1aa5418d626..529225cb1a9 100644 --- a/superset/commands/explore/parameters.py +++ b/superset/commands/explore/parameters.py @@ -14,14 +14,17 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + +from __future__ import annotations + from dataclasses import dataclass from typing import Optional @dataclass class CommandParameters: - permalink_key: Optional[str] - form_data_key: Optional[str] - datasource_id: Optional[int] - datasource_type: Optional[str] + permalink_key: str | None + form_data_key: str | None + datasource_id: int | str | None + datasource_type: str | None slice_id: Optional[int] diff --git a/superset/daos/datasource.py b/superset/daos/datasource.py index 308785f625e..0321a082cb9 100644 --- a/superset/daos/datasource.py +++ b/superset/daos/datasource.py @@ -16,8 +16,8 @@ # under the License. import logging -import uuid from typing import Union +from uuid import UUID from superset import db from superset.connectors.sqla.models import SqlaTable @@ -28,6 +28,7 @@ from superset.daos.exceptions import ( DatasourceValueIsIncorrect, ) from superset.models.sql_lab import Query, SavedQuery +from superset.semantic_layers.models import SemanticView from superset.utils.core import DatasourceType logger = logging.getLogger(__name__) @@ -40,13 +41,14 @@ class DatasourceDAO(BaseDAO[Datasource]): DatasourceType.TABLE: SqlaTable, DatasourceType.QUERY: Query, DatasourceType.SAVEDQUERY: SavedQuery, + DatasourceType.SEMANTIC_VIEW: SemanticView, } @classmethod def get_datasource( cls, datasource_type: Union[DatasourceType, str], - database_id_or_uuid: int | str, + database_id_or_uuid: int | str | UUID, ) -> Datasource: if datasource_type not in cls.sources: raise DatasourceTypeNotSupportedError() @@ -57,7 +59,7 @@ class DatasourceDAO(BaseDAO[Datasource]): filter = model.id == int(database_id_or_uuid) else: try: - uuid.UUID(str(database_id_or_uuid)) # uuid validation + UUID(str(database_id_or_uuid)) # uuid validation filter = model.uuid == database_id_or_uuid except ValueError as err: logger.warning( diff --git a/superset/explore/api.py b/superset/explore/api.py index e16b083feb8..a9b66d468c9 100644 --- a/superset/explore/api.py +++ b/superset/explore/api.py @@ -109,7 +109,7 @@ class ExploreRestApi(BaseSupersetApi): params = CommandParameters( permalink_key=request.args.get("permalink_key", type=str), form_data_key=request.args.get("form_data_key", type=str), - datasource_id=request.args.get("datasource_id", type=int), + datasource_id=request.args.get("datasource_id"), datasource_type=request.args.get("datasource_type", type=str), slice_id=request.args.get("slice_id", type=int), ) diff --git a/superset/migrations/versions/2025-11-04_11-26_33d7e0e21daa_add_semantic_layers_and_views.py b/superset/migrations/versions/2025-11-04_11-26_33d7e0e21daa_add_semantic_layers_and_views.py index 1e3b42c5dc3..cd022dfdd62 100644 --- a/superset/migrations/versions/2025-11-04_11-26_33d7e0e21daa_add_semantic_layers_and_views.py +++ b/superset/migrations/versions/2025-11-04_11-26_33d7e0e21daa_add_semantic_layers_and_views.py @@ -17,7 +17,7 @@ """add_semantic_layers_and_views Revision ID: 33d7e0e21daa -Revises: f5b5f88d8526 +Revises: 9787190b3d89 Create Date: 2025-11-04 11:26:00.000000 """ @@ -37,7 +37,7 @@ from superset.migrations.shared.utils import ( # revision identifiers, used by Alembic. revision = "33d7e0e21daa" -down_revision = "f5b5f88d8526" +down_revision = "9787190b3d89" def upgrade(): diff --git a/superset/static/service-worker.js b/superset/static/service-worker.js index 43cb14a4894..394fa207693 100644 --- a/superset/static/service-worker.js +++ b/superset/static/service-worker.js @@ -1,27 +1 @@ -/** - * 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. - */ - -// Minimal service worker for PWA file handling support -self.addEventListener('install', event => { - event.waitUntil(self.skipWaiting()); -}); - -self.addEventListener('activate', event => { - event.waitUntil(self.clients.claim()); -}); +(()=>{"use strict";let e;var r,t,n,o,a,i,f,u,l,s,d,p,c,v,h,g,y={55725(){self.addEventListener("install",e=>{e.waitUntil(self.skipWaiting())}),self.addEventListener("activate",e=>{e.waitUntil(self.clients.claim())})}},b={};function m(e){var r=b[e];if(void 0!==r)return r.exports;var t=b[e]={id:e,loaded:!1,exports:{}};return y[e].call(t.exports,t,t.exports,m),t.loaded=!0,t.exports}m.m=y,m.c=b,r=[],m.O=(e,t,n,o)=>{if(t){o=o||0;for(var a=r.length;a>0&&r[a-1][2]>o;a--)r[a]=r[a-1];r[a]=[t,n,o]; [...] diff --git a/superset/views/core.py b/superset/views/core.py index 690c00bbefd..97811dee933 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -24,6 +24,7 @@ import re from datetime import datetime from typing import Any, Callable, cast from urllib import parse +from uuid import UUID from flask import ( abort, @@ -169,9 +170,9 @@ class Superset(BaseSupersetView): if viz_obj.has_error(payload): return json_error_response(payload=payload, status=400) response = { - "data": payload["df"].to_dict("records") - if payload["df"] is not None - else [], + "data": ( + payload["df"].to_dict("records") if payload["df"] is not None else [] + ), "colnames": payload.get("colnames"), "coltypes": payload.get("coltypes"), "rowcount": payload.get("rowcount"), @@ -268,7 +269,9 @@ class Superset(BaseSupersetView): @check_resource_permissions(check_datasource_perms) @deprecated(eol_version="5.0.0") def explore_json( - self, datasource_type: str | None = None, datasource_id: int | None = None + self, + datasource_type: str | None = None, + datasource_id: int | str | None = None, ) -> FlaskResponse: """Serves all request that GET or POST form_data @@ -302,8 +305,10 @@ class Superset(BaseSupersetView): form_data = get_form_data()[0] try: - datasource_id, datasource_type = get_datasource_info( - datasource_id, datasource_type, form_data + ds_id, datasource_type = get_datasource_info( + datasource_id, + datasource_type, + form_data, ) force = request.args.get("force") == "true" @@ -316,7 +321,7 @@ class Superset(BaseSupersetView): with contextlib.suppress(CacheLoadError): viz_obj = get_viz( datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force_cached=True, force=force, @@ -343,7 +348,7 @@ class Superset(BaseSupersetView): viz_obj = get_viz( datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force=force, ) @@ -407,7 +412,7 @@ class Superset(BaseSupersetView): def explore( # noqa: C901 self, datasource_type: str | None = None, - datasource_id: int | None = None, + datasource_id: int | str | None = None, key: str | None = None, ) -> FlaskResponse: if request.method == "GET": @@ -451,21 +456,23 @@ class Superset(BaseSupersetView): query_context = request.form.get("query_context") + ds_id: int | UUID | None = None try: - datasource_id, datasource_type = get_datasource_info( - datasource_id, datasource_type, form_data + ds_id, datasource_type = get_datasource_info( + datasource_id, + datasource_type, + form_data, ) except SupersetException: - datasource_id = None # fallback unknown datasource to table type datasource_type = SqlaTable.type datasource: BaseDatasource | None = None - if datasource_id is not None: + if ds_id is not None: with contextlib.suppress(DatasetNotFoundError): datasource = DatasourceDAO.get_datasource( DatasourceType("table"), - datasource_id, + ds_id, ) datasource_name = datasource.name if datasource else _("[Missing Dataset]") diff --git a/superset/views/utils.py b/superset/views/utils.py index 1ec9e2a54ac..2c9555d8eae 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -14,12 +14,16 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + +from __future__ import annotations + import contextlib import logging from collections import defaultdict from functools import wraps -from typing import Any, Callable, DefaultDict, Optional, Union +from typing import Any, Callable, DefaultDict from urllib import parse +from uuid import UUID import msgpack import pyarrow as pa @@ -163,7 +167,7 @@ def get_permissions( def get_viz( form_data: FormData, datasource_type: str, - datasource_id: int, + datasource_id: int | UUID, force: bool = False, force_cached: bool = False, ) -> BaseViz: @@ -186,10 +190,10 @@ def loads_request_json(request_json_data: str) -> dict[Any, Any]: def get_form_data( - slice_id: Optional[int] = None, + slice_id: int | None = None, use_slice_data: bool = False, - initial_form_data: Optional[dict[str, Any]] = None, -) -> tuple[dict[str, Any], Optional[Slice]]: + initial_form_data: dict[str, Any] | None = None, +) -> tuple[dict[str, Any], Slice | None]: form_data: dict[str, Any] = initial_form_data or {} if has_request_context(): @@ -272,8 +276,10 @@ def add_sqllab_custom_filters(form_data: dict[Any, Any]) -> Any: def get_datasource_info( - datasource_id: Optional[int], datasource_type: Optional[str], form_data: FormData -) -> tuple[int, Optional[str]]: + datasource_id: int | str | None, + datasource_type: str | None, + form_data: FormData, +) -> tuple[int | UUID, str | None]: """ Compatibility layer for handling of datasource info @@ -300,12 +306,16 @@ def get_datasource_info( _("The dataset associated with this chart no longer exists") ) - datasource_id = int(datasource_id) - return datasource_id, datasource_type + # Convert datasource_id to appropriate type + if isinstance(datasource_id, int): + return datasource_id, datasource_type + if datasource_id.isdigit(): + return int(datasource_id), datasource_type + return UUID(datasource_id), datasource_type def apply_display_max_row_limit( - sql_results: dict[str, Any], rows: Optional[int] = None + sql_results: dict[str, Any], rows: int | None = None ) -> dict[str, Any]: """ Given a `sql_results` nested structure, applies a limit to the number of rows @@ -482,8 +492,8 @@ def check_explore_cache_perms(_self: Any, cache_key: str) -> None: def check_datasource_perms( _self: Any, - datasource_type: Optional[str] = None, - datasource_id: Optional[int] = None, + datasource_type: str | None = None, + datasource_id: int | str | None = None, **kwargs: Any, ) -> None: """ @@ -500,8 +510,10 @@ def check_datasource_perms( form_data = kwargs["form_data"] if "form_data" in kwargs else get_form_data()[0] try: - datasource_id, datasource_type = get_datasource_info( - datasource_id, datasource_type, form_data + ds_id, datasource_type = get_datasource_info( + datasource_id, + datasource_type, + form_data, ) except SupersetException as ex: raise SupersetSecurityException( @@ -524,7 +536,7 @@ def check_datasource_perms( try: viz_obj = get_viz( datasource_type=datasource_type, - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force=False, ) @@ -541,7 +553,9 @@ def check_datasource_perms( def _deserialize_results_payload( - payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False + payload: bytes | str, + query: Query, + use_msgpack: bool | None = False, ) -> dict[str, Any]: logger.debug("Deserializing from msgpack: %r", use_msgpack) if use_msgpack: @@ -579,9 +593,12 @@ def _deserialize_results_payload( def get_cta_schema_name( - database: Database, user: ab_models.User, schema: str, sql: str -) -> Optional[str]: - func: Optional[Callable[[Database, ab_models.User, str, str], str]] = app.config[ + database: Database, + user: ab_models.User, + schema: str, + sql: str, +) -> str | None: + func: Callable[[Database, ab_models.User, str, str], str] | None = app.config[ "SQLLAB_CTAS_SCHEMA_NAME_FUNC" ] if not func:
