This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch semantic-layer-implementation
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/semantic-layer-implementation
by this push:
new b9ab0ced77 Fix order
b9ab0ced77 is described below
commit b9ab0ced773dff245858c60e61bfb7a2daa5ae33
Author: Beto Dealmeida <[email protected]>
AuthorDate: Mon Jan 26 18:47:44 2026 -0500
Fix order
---
.../superset-ui-core/src/query/DatasourceKey.ts | 7 +--
superset/semantic_layers/mapper.py | 41 +++++++++++++---
.../semantic_layers/snowflake/semantic_view.py | 56 ++++++++++++++++++++--
3 files changed, 91 insertions(+), 13 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 07b296c6d0..5eb22613b4 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/DatasourceKey.ts
@@ -35,9 +35,10 @@ export default class DatasourceKey {
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
- // Try to parse as integer, fall back to string (UUID) if NaN
- const parsedId = parseInt(idStr, 10);
- this.id = Number.isNaN(parsedId) ? idStr : parsedId;
+ // Only parse as integer if the entire string is numeric
+ // (parseInt would incorrectly parse "85d3139f..." as 85)
+ const isNumeric = /^\d+$/.test(idStr);
+ this.id = isNumeric ? parseInt(idStr, 10) : idStr;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
}
diff --git a/superset/semantic_layers/mapper.py
b/superset/semantic_layers/mapper.py
index f6bfbeb0fb..2bc485fbae 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -270,6 +270,27 @@ def map_semantic_result_to_query_result(
)
+def _normalize_column(column: str | dict, dimension_names: set[str]) -> str:
+ """
+ Normalize a column to its dimension name.
+
+ Columns can be either:
+ - A string (dimension name directly)
+ - A dict with isColumnReference=True and sqlExpression containing the
dimension name
+ """
+ if isinstance(column, str):
+ return column
+
+ if isinstance(column, dict):
+ # Handle column references (e.g., from time-series charts)
+ if column.get("isColumnReference") and column.get("sqlExpression"):
+ sql_expr = column["sqlExpression"]
+ if sql_expr in dimension_names:
+ return sql_expr
+
+ raise ValueError("Adhoc dimensions are not supported in Semantic Views.")
+
+
def map_query_object(query_object: ValidatedQueryObject) ->
list[SemanticQuery]:
"""
Convert a `QueryObject` into a list of `SemanticQuery`.
@@ -277,8 +298,6 @@ def map_query_object(query_object: ValidatedQueryObject) ->
list[SemanticQuery]:
This function maps the `QueryObject` into query objects that focus less on
visualization and more on semantics.
"""
- print("BETO")
- print(query_object)
semantic_view = query_object.datasource.implementation
all_metrics = {metric.name: metric for metric in semantic_view.metrics}
@@ -286,6 +305,12 @@ def map_query_object(query_object: ValidatedQueryObject)
-> list[SemanticQuery]:
dimension.name: dimension for dimension in semantic_view.dimensions
}
+ # Normalize columns (may be dicts with isColumnReference=True for
time-series)
+ dimension_names = set(all_dimensions.keys())
+ normalized_columns = {
+ _normalize_column(column, dimension_names) for column in
query_object.columns
+ }
+
metrics = [all_metrics[metric] for metric in (query_object.metrics or [])]
grain = (
@@ -296,7 +321,7 @@ def map_query_object(query_object: ValidatedQueryObject) ->
list[SemanticQuery]:
dimensions = [
dimension
for dimension in semantic_view.dimensions
- if dimension.name in query_object.columns
+ if dimension.name in normalized_columns
and (
# if a grain is specified, only include the time dimension if its
grain
# matches the requested grain
@@ -794,12 +819,14 @@ def _validate_dimensions(query_object:
ValidatedQueryObject) -> None:
Make sure all dimensions are defined in the semantic view.
"""
semantic_view = query_object.datasource.implementation
+ dimension_names = {dimension.name for dimension in
semantic_view.dimensions}
- if any(not isinstance(column, str) for column in query_object.columns):
- raise ValueError("Adhoc dimensions are not supported in Semantic
Views.")
+ # Normalize all columns to dimension names
+ normalized_columns = [
+ _normalize_column(column, dimension_names) for column in
query_object.columns
+ ]
- dimension_names = {dimension.name for dimension in
semantic_view.dimensions}
- if not set(query_object.columns) <= dimension_names:
+ if not set(normalized_columns) <= dimension_names:
raise ValueError("All dimensions must be defined in the Semantic
View.")
diff --git a/superset/semantic_layers/snowflake/semantic_view.py
b/superset/semantic_layers/snowflake/semantic_view.py
index 4c7ae770e0..9f34e6f202 100644
--- a/superset/semantic_layers/snowflake/semantic_view.py
+++ b/superset/semantic_layers/snowflake/semantic_view.py
@@ -51,6 +51,7 @@ from superset.semantic_layers.types import (
NUMBER,
OBJECT,
Operator,
+ OrderDirection,
OrderTuple,
PredicateType,
SemanticRequest,
@@ -479,6 +480,49 @@ class SnowflakeSemanticView(SemanticViewImplementation):
for element, direction in order
)
+ def _get_temporal_dimension(
+ self,
+ dimensions: list[Dimension],
+ ) -> Dimension | None:
+ """
+ Find the first temporal dimension in the list.
+
+ Returns the first dimension with a temporal type (DATE, DATETIME,
TIME),
+ or None if no temporal dimension is found.
+ """
+ temporal_types = {DATE, DATETIME, TIME}
+ for dimension in dimensions:
+ if dimension.type in temporal_types:
+ return dimension
+ return None
+
+ def _get_default_order(
+ self,
+ dimensions: list[Dimension],
+ order: list[OrderTuple] | None,
+ ) -> list[OrderTuple] | None:
+ """
+ Get the order to use, prepending temporal sort if needed.
+
+ If there's a temporal dimension in the query and it's not already
+ in the order, prepends an ascending sort by that dimension.
+ This ensures time-series data is always sorted chronologically first.
+ """
+ temporal_dimension = self._get_temporal_dimension(dimensions)
+ if not temporal_dimension:
+ return order
+
+ # Check if temporal dimension is already in the order
+ if order:
+ for element, _ in order:
+ if isinstance(element, Dimension) and element.id ==
temporal_dimension.id:
+ return order
+ # Prepend temporal dimension to existing order
+ return [(temporal_dimension, OrderDirection.ASC)] + list(order)
+
+ # No order specified, use temporal dimension
+ return [(temporal_dimension, OrderDirection.ASC)]
+
def _build_simple_query(
self,
metrics: list[Metric],
@@ -495,7 +539,9 @@ class SnowflakeSemanticView(SemanticViewImplementation):
self._alias_element(dimension) for dimension in dimensions
)
metric_arguments = ", ".join(self._alias_element(metric) for metric in
metrics)
- order_clause = self._build_order_clause(order)
+ # Use default temporal ordering if no explicit order is provided
+ effective_order = self._get_default_order(dimensions, order)
+ order_clause = self._build_order_clause(effective_order)
return dedent(
f"""
@@ -740,7 +786,9 @@ class SnowflakeSemanticView(SemanticViewImplementation):
select_clause = ",\n ".join(select_columns)
# Build ORDER BY clause (need to reference the aliased columns)
- order_clause = self._build_order_clause(order)
+ # Use default temporal ordering if no explicit order is provided
+ effective_order = self._get_default_order(dimensions, order)
+ order_clause = self._build_order_clause(effective_order)
query = dedent(
f"""
@@ -794,7 +842,9 @@ class SnowflakeSemanticView(SemanticViewImplementation):
self._alias_element(dimension) for dimension in dimensions
)
metric_arguments = ", ".join(self._alias_element(metric) for metric in
metrics)
- order_clause = self._build_order_clause(order)
+ # Use default temporal ordering if no explicit order is provided
+ effective_order = self._get_default_order(dimensions, order)
+ order_clause = self._build_order_clause(effective_order)
top_groups_cte, cte_params = self._build_top_groups_cte(
group_limit,