This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch explorable in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3fb58b996ac506185d776efb5dc116dec991cabc Author: Beto Dealmeida <[email protected]> AuthorDate: Mon Oct 20 12:40:29 2025 -0400 Improve response to include queries --- superset/semantic_layers/snowflake_.py | 79 +++++++++++++++++++++++++++++----- superset/semantic_layers/types.py | 27 ++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/superset/semantic_layers/snowflake_.py b/superset/semantic_layers/snowflake_.py index d8b8a7a2d8..e45a6a6ca8 100644 --- a/superset/semantic_layers/snowflake_.py +++ b/superset/semantic_layers/snowflake_.py @@ -23,7 +23,7 @@ import itertools import re from collections import defaultdict from textwrap import dedent -from typing import Any, Literal, Union +from typing import Any, Literal, Sequence, Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -58,11 +58,38 @@ from superset.semantic_layers.types import ( Operator, OrderDirection, PredicateType, + SemanticRequest, + SemanticResult, STRING, TIME, Type, ) +REQUEST_TYPE = "snowflake" + + +def substitute_parameters(query: str, parameters: Sequence[Any] | None) -> str: + """Substitute parametereters for logging only - NOT for execution""" + if not parameters: + return query + + result = query + for parameter in parameters: + if parameter is None: + replacement = "NULL" + elif isinstance(parameter, (int, float)): + replacement = str(parameter) + elif isinstance(parameter, bool): + replacement = str(parameter).upper() + else: + # String - escape single quotes + quoted = str(parameter).replace("'", "''") + replacement = f"'{quoted}'" + + result = result.replace("?", replacement, 1) + + return result + class UserPasswordAuth(BaseModel): """ @@ -376,7 +403,7 @@ class SnowflakeSemanticLayer: def get_explorables( self, runtime_configuration: BaseModel, - ) -> set[SnowflakeExplorable]: + ) -> set[SnowflakeSemanticView]: """ Get a list of available explorables (databases/schemas). """ @@ -395,12 +422,12 @@ class SnowflakeSemanticLayer: """ ) return { - SnowflakeExplorable(configuration, row[0]) + SnowflakeSemanticView(configuration, row[0]) for row in cursor.execute(query) } -class SnowflakeExplorable: +class SnowflakeSemanticView: def __init__(self, configuration: SnowflakeConfiguration, name: str): self.configuration = configuration self.name = name @@ -558,7 +585,7 @@ class SnowflakeExplorable: self, dimension: Dimension, filters: set[Filter | NativeFilter] | None = None, - ) -> set[Any]: + ) -> SemanticResult: """ Return distinct values for a dimension. """ @@ -581,8 +608,17 @@ class SnowflakeExplorable: ) connection_parameters = get_connection_parameters(self.configuration) with connect(**connection_parameters) as connection: - cursor = connection.cursor() - return {row[0] for row in cursor.execute(query, parameters)} + df = connection.cursor().execute(query, parameters).fetch_pandas_all() + + return SemanticResult( + requests=[ + SemanticRequest( + REQUEST_TYPE, + substitute_parameters(query, parameters), + ) + ], + results=df, + ) def _build_native_filter(self, filter_: Filter) -> str: """ @@ -617,7 +653,7 @@ class SnowflakeExplorable: offset: int | None = None, *, group_limit: GroupLimit | None = None, - ) -> DataFrame: + ) -> SemanticResult: """ Execute a query and return the results as a Pandas DataFrame. """ @@ -635,7 +671,17 @@ class SnowflakeExplorable: ) connection_parameters = get_connection_parameters(self.configuration) with connect(**connection_parameters) as connection: - return connection.cursor().execute(query, parameters).fetch_pandas_all() + df = connection.cursor().execute(query, parameters).fetch_pandas_all() + + return SemanticResult( + requests=[ + SemanticRequest( + REQUEST_TYPE, + substitute_parameters(query, parameters), + ) + ], + results=df, + ) def get_row_count( self, @@ -666,7 +712,17 @@ class SnowflakeExplorable: query = f"SELECT COUNT(*) FROM ({query}) AS subquery" connection_parameters = get_connection_parameters(self.configuration) with connect(**connection_parameters) as connection: - return connection.cursor().execute(query, parameters).fechone()[0] + df = connection.cursor().execute(query, parameters).fechone()[0] + + return SemanticResult( + requests=[ + SemanticRequest( + REQUEST_TYPE, + substitute_parameters(query, parameters), + ) + ], + results=df, + ) def _get_query( self, @@ -1080,6 +1136,9 @@ if __name__ == "__main__": Filter(PredicateType.WHERE, dimension, Operator.NOT_EQUALS, "Books"), } print(explorable.get_values(dimension, filters)) + import sys + + sys.exit() filters = { Filter( PredicateType.WHERE, diff --git a/superset/semantic_layers/types.py b/superset/semantic_layers/types.py index c43d46b691..7e9194ca48 100644 --- a/superset/semantic_layers/types.py +++ b/superset/semantic_layers/types.py @@ -20,6 +20,8 @@ from dataclasses import dataclass from datetime import date, datetime, time, timedelta from functools import total_ordering +from pandas import DataFrame + __all__ = [ "BINARY", "BOOLEAN", @@ -216,3 +218,28 @@ class GroupLimit: metric: Metric | None direction: OrderDirection = OrderDirection.DESC group_others: bool = False + + +@dataclass(frozen=True) +class SemanticRequest: + """ + Represents a request made to obtain semantic results. + + This could be a SQL query, an HTTP request, etc. + """ + + type: str + definition: str + + +@dataclass(frozen=True) +class SemanticResult: + """ + Represents the results of a semantic query. + + This includes any requests (SQL queries, HTTP requests) that were performed in order + to obtain the results, in order to help troubleshooting. + """ + + requests: list[SemanticRequest] + results: DataFrame
