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 7e40403287a64af3b1d8ccaeb596e9b1a0233ea9 Author: Beto Dealmeida <[email protected]> AuthorDate: Tue Oct 14 17:09:02 2025 -0400 Adding get_dataframe --- superset/semantic_layers/snowflake_.py | 130 +++++++++++++++++++++++---------- superset/semantic_layers/types.py | 4 +- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/superset/semantic_layers/snowflake_.py b/superset/semantic_layers/snowflake_.py index be1dab9414..40b8cf52b6 100644 --- a/superset/semantic_layers/snowflake_.py +++ b/superset/semantic_layers/snowflake_.py @@ -24,6 +24,7 @@ from typing import Any, Literal, Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization +from pandas import DataFrame from pydantic import ( BaseModel, ConfigDict, @@ -36,6 +37,7 @@ from snowflake.connector import connect, DictCursor from snowflake.connector.connection import SnowflakeConnection from snowflake.sqlalchemy.snowdialect import SnowflakeDialect +from superset.common.query_object import QueryObject from superset.semantic_layers.types import ( BINARY, BOOLEAN, @@ -44,11 +46,13 @@ from superset.semantic_layers.types import ( DECIMAL, Dimension, Filter, + FilterValues, INTEGER, Metric, NativeFilter, NUMBER, OBJECT, + Operator, STRING, TIME, Type, @@ -437,44 +441,6 @@ class SnowflakeExplorable: return metrics - def get_values( - self, - dimension: Dimension, - filters: set[Filter | NativeFilter] | None = None, - ) -> set[Any]: - """ - Return distinct values for a dimension. - """ - native_filters = { - ( - filter_ - if isinstance(filter_, NativeFilter) - else self._build_native_filter(filter_) - ) - for filter_ in (filters or set()) - } - parenthesized = {f"({filter_.definition})" for filter_ in native_filters} - predicates = f"WHERE {' AND '.join(parenthesized)}" if parenthesized else "" - - query = f""" - SELECT {self._quote(dimension.name)} - FROM - SEMANTIC_VIEW( - {self.uid()} - DIMENSIONS {self._quote(dimension.id)} - ) - {predicates} - """ # noqa: S608 - connection_parameters = get_connection_parameters(self.configuration) - with connect(**connection_parameters) as connection: - cursor = connection.cursor(DictCursor) - return {row[0] for row in cursor.execute(query)} - - def _build_native_filter_(self, filter_: Filter) -> NativeFilter: - """ - Convert a Filter to a NativeFilter. - """ - def _get_type(self, snowflake_type: str | None) -> type[Type]: """ Return the semantic type corresponding to a Snowflake type. @@ -502,6 +468,74 @@ class SnowflakeExplorable: return STRING + def get_values( + self, + dimension: Dimension, + filters: set[Filter | NativeFilter] | None = None, + ) -> set[Any]: + """ + Return distinct values for a dimension. + """ + # convert filters predicate with associated parameters; native filters are + # already strings, so we keep them as-is + unary_operators = {Operator.IS_NULL, Operator.IS_NOT_NULL} + predicates: list[str] = [] + parameters: list[FilterValues] = [] + for filter_ in filters or set(): + if isinstance(filter, NativeFilter): + predicates.append(f"({filter_.definition})") + else: + predicates.append(f"({self._build_predicate(filter_)})") + if filter_.operator not in unary_operators: + parameters.extend( + [filter_.value] + if not isinstance(filter_.value, frozenset) + else filter_.value + ) + + query = f""" + SELECT {self._quote(dimension.name)} + FROM + SEMANTIC_VIEW( + {self.uid()} + DIMENSIONS {dimension.id} + ) + {"WHERE " + " AND ".join(predicates) if predicates else ""} + """ # noqa: S608 + 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, tuple(parameters))} + + def _build_predicate(self, filter_: Filter) -> str: + """ + Convert a Filter to a NativeFilter. + """ + column = filter_.column + operator = filter_.operator + value = filter_.value + + column_name = self._quote(column.name) + + # Handle IS NULL and IS NOT NULL operators (no value needed) + if operator in {Operator.IS_NULL, Operator.IS_NOT_NULL}: + return f"{column_name} {operator.value}" + + # Handle IN and NOT IN operators (set values) + if operator in {Operator.IN, Operator.NOT_IN}: + if not isinstance(value, frozenset): + value = {value} + formatted_values = ", ".join("?" for _ in value) + return f"{column_name} {operator.value} ({formatted_values})" + + return f"{column_name} {operator.value} ?" + + def get_dataframe(self, query_object: QueryObject) -> DataFrame: + """ + Execute a query and return the results as a Pandas DataFrame. + """ + pass + __repr__ = uid @@ -546,3 +580,23 @@ if __name__ == "__main__": print("=======") for metric in explorable.get_metrics(): print(metric) + print("VALUES") + print("======") + dimension = Dimension( + id="ITEM.CATEGORY", + name="CATEGORY", + type=STRING, + description=None, + definition="I_CATEGORY", + grain=None, + ) + print(explorable.get_values(dimension)) + filters = { + Filter(dimension, Operator.IS_NOT_NULL, None), + Filter(dimension, Operator.NOT_EQUALS, "Books"), + } + print(explorable.get_values(dimension, filters)) + filters = { + Filter(dimension, Operator.IN, frozenset({"Children", "Electronics"})), + } + print(explorable.get_values(dimension, filters)) diff --git a/superset/semantic_layers/types.py b/superset/semantic_layers/types.py index eb4ef5e341..0e76703920 100644 --- a/superset/semantic_layers/types.py +++ b/superset/semantic_layers/types.py @@ -178,14 +178,14 @@ class Operator(enum.Enum): IS_NOT_NULL = "IS NOT NULL" -FilterTypes = str | int | float | bool | datetime | date | time | timedelta | None +FilterValues = str | int | float | bool | datetime | date | time | timedelta | None @dataclass(frozen=True) class Filter: column: Dimension | Metric operator: Operator - value: FilterTypes | set[FilterTypes] + value: FilterValues | set[FilterValues] @dataclass(frozen=True)
