Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ import {
addJsColumnsToColumns,
processMetricsArray,
addTooltipColumnsToQuery,
ensureColumnsUnique,
} from '../buildQueryUtils';

export interface DeckScatterFormData
extends Omit<SpatialFormData, 'color_picker'>,
SqlaFormData {
point_radius_fixed?: {
value?: string;
type?: string;
value?: string | number;
};
multiplier?: number;
point_unit?: string;
Expand Down Expand Up @@ -75,17 +77,23 @@ export default function buildQuery(formData: DeckScatterFormData) {
typeof col === 'string' ? col : col.label || col.sqlExpression || '',
);
const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
const uniqueColumns = ensureColumnsUnique(withJsColumns);

columns = withJsColumns as QueryFormColumn[];
columns = uniqueColumns as QueryFormColumn[];
columns = addTooltipColumnsToQuery(columns, tooltip_contents);

const metrics = processMetricsArray([point_radius_fixed?.value]);
const isRadiusMetric =
point_radius_fixed?.type === 'metric' && point_radius_fixed?.value;
const metrics = isRadiusMetric
? processMetricsArray([String(point_radius_fixed.value)])
: [];

const filters = addSpatialNullFilters(
spatial,
ensureIsArray(baseQueryObject.filters || []),
);

const orderby = point_radius_fixed?.value
const orderby = isRadiusMetric
? ([[point_radius_fixed.value, false]] as QueryFormOrderBy[])
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,20 @@ export function addSpatialNullFilters(
if (!spatial) return filters;

const spatialColumns = getSpatialColumns(spatial);
const nullFilters: QueryObjectFilterClause[] = spatialColumns.map(column => ({
col: column,
op: 'IS NOT NULL',
val: null,
}));
const uniqueSpatialColumns = [...new Set(spatialColumns)];
const existingFilterCols = new Set(
filters
.filter(filter => filter.op === 'IS NOT NULL')
.map(filter => filter.col),
);
Comment on lines +113 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient filter lookup with O(n) complexity category Performance

Tell me more
What is the issue?

The code creates a new Set on every function call by filtering and mapping the entire filters array, which has O(n) time complexity where n is the number of filters.

Why this matters

This approach becomes inefficient when the filters array is large or when addSpatialNullFilters is called frequently, as it processes the entire array each time instead of using a more efficient lookup mechanism.

Suggested change ∙ Feature Preview

Consider using a Map or optimized lookup structure if this function is called frequently with large filter arrays, or cache the existingFilterCols Set if the filters don't change between calls:

const existingFilterCols = filters.reduce((acc, filter) => {
  if (filter.op === 'IS NOT NULL') {
    acc.add(filter.col);
  }
  return acc;
}, new Set<string>());
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.


const nullFilters: QueryObjectFilterClause[] = uniqueSpatialColumns
.filter(column => !existingFilterCols.has(column))
.map(column => ({
col: column,
op: 'IS NOT NULL',
val: null,
}));

return [...filters, ...nullFilters];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ export function addPropertiesToFeature<T extends Record<string, unknown>>(
}

export function getMetricLabelFromFormData(
metric: string | { value?: string } | undefined,
metric: string | { type?: string; value?: string | number } | undefined,
): string | undefined {
if (!metric) return undefined;
if (typeof metric === 'string') return getMetricLabel(metric);
return metric.value ? getMetricLabel(metric.value) : undefined;
if (metric.type === 'fix') return undefined;
return metric.value ? getMetricLabel(String(metric.value)) : undefined;
}
10 changes: 7 additions & 3 deletions superset/charts/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,12 @@ class AnnotationLayerSchema(Schema):
)


def validate_orderby_column(value: Any) -> bool:
if value is None or (isinstance(value, str) and len(value) == 0):
raise validate.ValidationError(_("orderby column must be populated"))
return True
Comment on lines +1134 to +1137
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insufficient Type Validation in Order By Column category Design

Tell me more
What is the issue?

The validate_orderby_column function is too permissive in its type checking. It only validates for None and empty strings, but accepts any other type without validation.

Why this matters

This could lead to runtime errors when the validation passes but the value is of an incompatible type (like a number or boolean) that can't be used as an orderby column.

Suggested change ∙ Feature Preview
def validate_orderby_column(value: Any) -> bool:
    if not isinstance(value, str):
        raise validate.ValidationError(_('orderby column must be a string'))
    if len(value) == 0:
        raise validate.ValidationError(_('orderby column must be populated'))
    return True
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.



class ChartDataDatasourceSchema(Schema):
description = "Chart datasource"
id = Union(
Expand Down Expand Up @@ -1328,9 +1334,7 @@ class Meta: # pylint: disable=too-few-public-methods
fields.Tuple(
(
fields.Raw(
validate=[
Length(min=1, error=_("orderby column must be populated"))
],
validate=validate_orderby_column,
allow_none=False,
),
fields.Boolean(),
Expand Down
7 changes: 4 additions & 3 deletions superset/common/query_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,12 @@ def _set_metrics(self, metrics: list[Metric] | None = None) -> None:
# 1. 'metric_name' - name of predefined metric
# 2. { label: 'label_name' } - legacy format for a predefined metric
# 3. { expressionType: 'SIMPLE' | 'SQL', ... } - adhoc metric
def is_str_or_adhoc(metric: Metric) -> bool:
return isinstance(metric, str) or is_adhoc_metric(metric)
# 4. int - metric ID
def is_str_int_or_adhoc(metric: Metric) -> bool:
return isinstance(metric, (str, int)) or is_adhoc_metric(metric)

self.metrics = metrics and [
x if is_str_or_adhoc(x) else x["label"] # type: ignore
x if is_str_int_or_adhoc(x) else x["label"] # type: ignore
for x in metrics
]

Expand Down
4 changes: 2 additions & 2 deletions superset/superset_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ class ResultSetColumnType(TypedDict):
FilterValues = Union[FilterValue, list[FilterValue], tuple[FilterValue]]
FormData = dict[str, Any]
Granularity = Union[str, dict[str, Union[str, float]]]
Column = Union[AdhocColumn, str]
Metric = Union[AdhocMetric, str]
Column = Union[AdhocColumn, str, int]
Metric = Union[AdhocMetric, str, int]
OrderBy = tuple[Union[Metric, Column], bool]
QueryObjectDict = dict[str, Any]
VizData = Optional[Union[list[Any], dict[Any, Any]]]
Expand Down
6 changes: 6 additions & 0 deletions superset/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,9 @@ def get_column_name(column: Column, verbose_map: dict[str, Any] | None = None) -
verbose_map = verbose_map or {}
return verbose_map.get(column, column)

if isinstance(column, int):
return str(column)
Comment on lines +1234 to +1235
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer column ID converted to string without name resolution category Functionality

Tell me more
What is the issue?

The function converts integer column IDs directly to strings without any validation or lookup, which may not provide meaningful column names for users.

Why this matters

This approach bypasses the intended column resolution logic and may result in displaying raw numeric IDs instead of human-readable column names, potentially confusing users who expect descriptive labels.

Suggested change ∙ Feature Preview

Consider implementing a lookup mechanism to resolve integer IDs to actual column names, or add documentation explaining when raw integer strings are acceptable. For example:

if isinstance(column, int):
    # TODO: Implement ID-to-name lookup when datasource context is available
    # For now, return string representation as fallback
    return str(column)
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.


raise ValueError("Missing label")


Expand Down Expand Up @@ -1263,6 +1266,9 @@ def get_metric_name(metric: Metric, verbose_map: dict[str, Any] | None = None) -
verbose_map = verbose_map or {}
return verbose_map.get(metric, metric)

if isinstance(metric, int):
return str(metric)

raise ValueError(__("Invalid metric object: %(metric)s", metric=str(metric)))


Expand Down
41 changes: 41 additions & 0 deletions tests/unit_tests/charts/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,44 @@ def test_time_grain_validation_with_config_addons(app_context: None) -> None:
}
result = schema.load(custom_data)
assert result["time_grain"] == "PT10M"


def test_chart_data_query_object_schema_orderby_validation(
app_context: None,
) -> None:
"""Test that orderby field handles strings, integers and objects"""
schema = ChartDataQueryObjectSchema()

# Valid values should pass
for orderby_value in ["column_name", 123, {"label": "my_metric", "sqlExpression": "SUM(col)"}]:
result = schema.load({
"datasource": {"type": "table", "id": 1},
"metrics": ["count"],
"orderby": [[orderby_value, False]],
})
assert result["orderby"][0][1] is False

# None and empty string should fail
for invalid_value in [None, ""]:
with pytest.raises(ValidationError):
schema.load({
"datasource": {"type": "table", "id": 1},
"metrics": ["count"],
"orderby": [[invalid_value, True]],
})


def test_chart_data_query_object_schema_metrics_validation(
app_context: None,
) -> None:
"""Test that metrics field handles strings, integers and objects"""
schema = ChartDataQueryObjectSchema()

# Mix of different types should all pass
result = schema.load({
"datasource": {"type": "table", "id": 1},
"metrics": ["count", 123, {"expressionType": "SQL", "sqlExpression": "SUM(col)", "label": "sum"}],
})
assert result["metrics"][0] == "count"
assert result["metrics"][1] == 123
assert result["metrics"][2]["label"] == "sum"
32 changes: 32 additions & 0 deletions tests/unit_tests/utils/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,3 +1162,35 @@ def test_sanitize_url_blocks_dangerous():
"""Test that dangerous URL schemes are blocked."""
assert sanitize_url("javascript:alert('xss')") == ""
assert sanitize_url("data:text/html,<script>alert(1)</script>") == ""


def test_get_metric_name_with_integers() -> None:
"""Test get_metric_name with integer IDs"""
from superset.utils.core import get_metric_name

assert get_metric_name("count") == "count"
assert get_metric_name(123) == "123"
assert get_metric_name(456) == "456"

from superset.superset_typing import AdhocMetric

adhoc_metric: AdhocMetric = {
"expressionType": "SQL",
"sqlExpression": "SUM(amount)",
"label": "total_amount",
}
assert get_metric_name(adhoc_metric) == "total_amount"


def test_get_column_name_with_integers() -> None:
"""Test get_column_name with integer IDs"""
from superset.utils.core import get_column_name

assert get_column_name("age") == "age"
assert get_column_name(123) == "123"
assert get_column_name(789) == "789"

from superset.superset_typing import AdhocColumn

column_dict: AdhocColumn = {"label": "customer_name", "sqlExpression": "name"}
assert get_column_name(column_dict) == "customer_name"