Working on exporable
diff --git a/superset/explorables/base.py b/superset/explorables/base.py
index c13b5dd..00939c8 100644
--- a/superset/explorables/base.py
+++ b/superset/explorables/base.py
@@ -27,6 +27,7 @@
from datetime import datetime
from typing import Any, Protocol, runtime_checkable
+from superset.common.query_object import QueryObject
from superset.models.helpers import QueryResult
from superset.superset_typing import QueryObjectDict
@@ -50,25 +51,15 @@
# Core Query Interface
# =========================================================================
- def query(self, query_obj: QueryObjectDict) -> QueryResult:
+ def get_query_result(self, query_object: QueryObject) -> QueryResult:
"""
Execute a query and return results.
This is the primary method for data retrieval. It takes a query
- object dictionary describing what data to fetch (columns, metrics,
- filters, time range, etc.) and returns a QueryResult containing
- a pandas DataFrame with the results.
+ object describing what data to fetch (columns, metrics, filters, time range,
+ etc.) and returns a QueryResult containing a pandas DataFrame with the results.
- :param query_obj: Dictionary describing the query with keys like:
- - columns: list of column names to select
- - metrics: list of metrics to compute
- - filter: list of filter clauses
- - from_dttm/to_dttm: time range
- - granularity: time column for grouping
- - groupby: columns to group by
- - orderby: ordering specification
- - row_limit/row_offset: pagination
- - extras: additional parameters
+ :param query_obj: QueryObject describing the query
:return: QueryResult containing:
- df: pandas DataFrame with query results
@@ -77,7 +68,6 @@
- status: QueryStatus (SUCCESS/FAILED)
- error_message: error details if query failed
"""
- ...
def get_query_str(self, query_obj: QueryObjectDict) -> str:
"""
@@ -90,7 +80,6 @@
:param query_obj: Dictionary describing the query
:return: String representation of the query (SQL, GraphQL, etc.)
"""
- ...
# =========================================================================
# Identity & Metadata
@@ -109,7 +98,6 @@
:return: Unique identifier string
"""
- ...
@property
def type(self) -> str:
@@ -121,7 +109,6 @@
:return: Type identifier string
"""
- ...
@property
def columns(self) -> list[Any]:
@@ -137,7 +124,6 @@
:return: List of column metadata objects
"""
- ...
@property
def column_names(self) -> list[str]:
@@ -149,7 +135,36 @@
:return: List of column name strings
"""
- ...
+
+ @property
+ def data(self) -> dict[str, Any]:
+ """
+ Full metadata representation sent to the frontend.
+
+ This property returns a dictionary containing all the metadata
+ needed by the Explore UI, including columns, metrics, and
+ other configuration.
+
+ Required keys in the returned dictionary:
+ - id: unique identifier (int or str)
+ - uid: unique string identifier
+ - name: display name
+ - type: explorable type ('table', 'query', 'semantic_view', etc.)
+ - columns: list of column metadata dicts (with column_name, type, etc.)
+ - metrics: list of metric metadata dicts (with metric_name, expression, etc.)
+ - database: database metadata dict (with id, backend, etc.)
+
+ Optional keys:
+ - description: human-readable description
+ - schema: schema name (if applicable)
+ - catalog: catalog name (if applicable)
+ - cache_timeout: default cache timeout
+ - offset: timezone offset
+ - owners: list of owner IDs
+ - verbose_map: dict mapping column/metric names to display names
+
+ :return: Dictionary with complete explorable metadata
+ """
# =========================================================================
# Caching
@@ -165,7 +180,6 @@
:return: Cache timeout in seconds, or None for system default
"""
- ...
@property
def changed_on(self) -> datetime | None:
@@ -177,7 +191,6 @@
:return: Datetime of last modification, or None
"""
- ...
def get_extra_cache_keys(self, query_obj: QueryObjectDict) -> list[Hashable]:
"""
@@ -191,7 +204,6 @@
:param query_obj: The query being executed
:return: List of additional hashable values for cache key
"""
- ...
# =========================================================================
# Security
@@ -208,7 +220,6 @@
:return: Permission identifier string
"""
- ...
@property
def schema_perm(self) -> str | None:
@@ -220,7 +231,6 @@
:return: Schema permission string, or None
"""
- ...
# =========================================================================
# Time/Date Handling
@@ -236,4 +246,3 @@
:return: Timezone offset in seconds (0 for UTC)
"""
- ...
diff --git a/superset/semantic_layers/mapper.py b/superset/semantic_layers/mapper.py
index 68c6c65..a9ee68f 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -24,14 +24,17 @@
"""
-from datetime import datetime
+from datetime import datetime, timedelta
+from time import time
from typing import Any, cast, Sequence, TypeGuard
import numpy as np
+from superset.common.db_query_status import QueryStatus
from superset.common.query_object import QueryObject
from superset.common.utils.time_range_utils import get_since_until_from_query_object
from superset.connectors.sqla.models import BaseDatasource
+from superset.models.helpers import QueryResult
from superset.semantic_layers.types import (
AdhocExpression,
AdhocFilter,
@@ -86,16 +89,19 @@
series_limit_metric: str | None
-def get_results(query_object: QueryObject) -> SemanticResult:
+def get_results(query_object: QueryObject) -> QueryResult:
"""
Run 1+ queries based on `QueryObject` and return the results.
:param query_object: The QueryObject containing query specifications
- :return: SemanticResult with combined DataFrame and all requests
+ :return: QueryResult compatible with Superset's query interface
"""
if not validate_query_object(query_object):
raise ValueError("QueryObject must have a datasource defined.")
+ # Track execution time
+ start_time = time()
+
semantic_view = query_object.datasource.implementation
dispatcher = (
semantic_view.get_row_count
@@ -126,10 +132,16 @@
# If no time offsets, return the main result as-is
if not query_object.time_offsets or len(queries) <= 1:
- return SemanticResult(
+ semantic_result = SemanticResult(
requests=all_requests,
results=main_df,
)
+ duration = timedelta(seconds=time() - start_time)
+ return map_semantic_result_to_query_result(
+ semantic_result,
+ query_object,
+ duration,
+ )
# Get metric names from the main query
# These are the columns that will be renamed with offset suffixes
@@ -197,7 +209,58 @@
if duplicate_cols:
main_df = main_df.drop(columns=duplicate_cols)
- return SemanticResult(requests=all_requests, results=main_df)
+ # Convert final result to QueryResult
+ semantic_result = SemanticResult(requests=all_requests, results=main_df)
+ duration = timedelta(seconds=time() - start_time)
+ return map_semantic_result_to_query_result(
+ semantic_result,
+ query_object,
+ duration,
+ )
+
+
+def map_semantic_result_to_query_result(
+ semantic_result: SemanticResult,
+ query_object: ValidatedQueryObject,
+ duration: timedelta,
+) -> QueryResult:
+ """
+ Convert a SemanticResult to a QueryResult.
+
+ :param semantic_result: Result from the semantic layer
+ :param query_object: Original QueryObject (for passthrough attributes)
+ :param duration: Time taken to execute the query
+ :return: QueryResult compatible with Superset's query interface
+ """
+ # Get the query string from requests (typically one or more SQL queries)
+ query_str = ""
+ if semantic_result.requests:
+ # Join all requests for display (could be multiple for time comparisons)
+ query_str = "\n\n".join(
+ f"-- {req.type}\n{req.definition}" for req in semantic_result.requests
+ )
+
+ return QueryResult(
+ # Core data
+ df=semantic_result.results,
+ query=query_str,
+ duration=duration,
+ # Template filters - not applicable to semantic layers
+ # (semantic layers don't use Jinja templates)
+ applied_template_filters=None,
+ # Filter columns - not applicable to semantic layers
+ # (semantic layers handle filter validation internally)
+ applied_filter_columns=None,
+ rejected_filter_columns=None,
+ # Status - always success if we got here
+ # (errors would raise exceptions before reaching this point)
+ status=QueryStatus.SUCCESS,
+ error_message=None,
+ errors=None,
+ # Time range - pass through from original query_object
+ from_dttm=query_object.from_dttm,
+ to_dttm=query_object.to_dttm,
+ )
def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
diff --git a/superset/semantic_layers/models.py b/superset/semantic_layers/models.py
index b146007..e9ab970 100644
--- a/superset/semantic_layers/models.py
+++ b/superset/semantic_layers/models.py
@@ -28,11 +28,14 @@
from sqlalchemy.orm import relationship
from sqlalchemy_utils import UUIDType
-from superset.models.helpers import AuditMixinNullable
+from superset.common.query_object import QueryObject
+from superset.models.helpers import AuditMixinNullable, QueryResult
+from superset.semantic_layers.mapper import get_results
from superset.semantic_layers.types import (
SemanticLayerImplementation,
SemanticViewImplementation,
)
+from superset.superset_typing import QueryObjectDict
from superset.utils import core as utils
@@ -69,7 +72,9 @@
return self.name or str(self.uuid)
@property
- def implementation(self) -> SemanticLayerImplementation[Any]:
+ def implementation(
+ self,
+ ) -> SemanticLayerImplementation[Any, SemanticViewImplementation]:
"""
Return semantic layer implementation.
"""
@@ -138,3 +143,17 @@
# =========================================================================
# Explorable protocol implementation
# =========================================================================
+
+ def get_query_result(self, query_object: QueryObject) -> QueryResult:
+ return get_results(query_object)
+
+ def get_query_str(self, query_obj: QueryObjectDict) -> str:
+ return "Not implemented for semantic layers"
+
+ @property
+ def uid(self) -> str:
+ return self.implementation.uid()
+
+ @property
+ def type(self) -> str:
+ return "semantic_view"
diff --git a/superset/semantic_layers/snowflake/semantic_layer.py b/superset/semantic_layers/snowflake/semantic_layer.py
index 9b3591d..8f99f67 100644
--- a/superset/semantic_layers/snowflake/semantic_layer.py
+++ b/superset/semantic_layers/snowflake/semantic_layer.py
@@ -18,7 +18,7 @@
from __future__ import annotations
from textwrap import dedent
-from typing import Any, Literal
+from typing import Any, Literal, TYPE_CHECKING
from pydantic import create_model, Field
from snowflake.connector import connect
@@ -28,11 +28,15 @@
from superset.semantic_layers.snowflake.utils import get_connection_parameters
from superset.semantic_layers.types import (
SemanticLayerImplementation,
- SemanticViewImplementation,
)
+if TYPE_CHECKING:
+ from superset.semantic_layers.snowflake.semantic_view import SnowflakeSemanticView
-class SnowflakeSemanticLayer(SemanticLayerImplementation[SnowflakeConfiguration]):
+
+class SnowflakeSemanticLayer(
+ SemanticLayerImplementation[SnowflakeConfiguration, SnowflakeSemanticView]
+):
id = "snowflake"
name = "Snowflake Semantic Layer"
description = "Connect to semantic views stored in Snowflake."
@@ -204,7 +208,7 @@
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
- ) -> set[SemanticViewImplementation]:
+ ) -> set[SnowflakeSemanticView]:
"""
Get the semantic views available in the semantic layer.
"""
diff --git a/superset/semantic_layers/types.py b/superset/semantic_layers/types.py
index 54b555a..8233c7b 100644
--- a/superset/semantic_layers/types.py
+++ b/superset/semantic_layers/types.py
@@ -289,11 +289,12 @@
GROUP_OTHERS = "GROUP_OTHERS"
-LayerConfigT = TypeVar("LayerConfigT", bound=BaseModel)
+ConfigT = TypeVar("ConfigT", bound=BaseModel, contravariant=True)
+SemanticViewT = TypeVar("SemanticViewT", bound="SemanticViewImplementation")
@runtime_checkable
-class SemanticLayerImplementation(Protocol[LayerConfigT]):
+class SemanticLayerImplementation(Protocol[ConfigT, SemanticViewT]):
"""
A protocol for semantic layers.
"""
@@ -302,7 +303,7 @@
def from_configuration(
cls,
configuration: dict[str, Any],
- ) -> SemanticLayerImplementation[LayerConfigT]:
+ ) -> SemanticLayerImplementation[ConfigT, SemanticViewT]:
"""
Create a semantic layer from its configuration.
"""
@@ -310,7 +311,7 @@
@classmethod
def get_configuration_schema(
cls,
- configuration: LayerConfigT | None = None,
+ configuration: ConfigT | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the configuration needed to add the semantic layer.
@@ -334,7 +335,7 @@
@classmethod
def get_runtime_schema(
cls,
- configuration: LayerConfigT,
+ configuration: ConfigT,
runtime_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
@@ -361,7 +362,7 @@
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
- ) -> set[SemanticViewImplementation]:
+ ) -> set[SemanticViewT]:
"""
Get the semantic views available in the semantic layer.
@@ -373,7 +374,7 @@
self,
name: str,
additional_configuration: dict[str, Any],
- ) -> SemanticViewImplementation:
+ ) -> SemanticViewT:
"""
Get a specific semantic view by its name and additional configuration.
"""