mirror of
https://github.com/apache/superset.git
synced 2026-06-12 02:59:27 +00:00
939 lines
30 KiB
Python
939 lines
30 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from superset.mcp_service.chart.chart_helpers import (
|
|
_deck_gl_null_filters,
|
|
_is_metric_ref,
|
|
_resolve_deck_gl_metrics,
|
|
apply_form_data_filters_to_query,
|
|
build_query_dicts_from_form_data,
|
|
extract_form_data_key_from_url,
|
|
find_chart_by_identifier,
|
|
get_cached_form_data,
|
|
merge_extra_form_data_filters_into_query,
|
|
merge_form_data_filters_into_query,
|
|
prepare_form_data_for_query,
|
|
resolve_deck_gl_columns,
|
|
)
|
|
|
|
|
|
def test_extract_form_data_key_from_url_with_key():
|
|
url = "http://localhost:8088/explore/?form_data_key=abc123&slice_id=1"
|
|
assert extract_form_data_key_from_url(url) == "abc123"
|
|
|
|
|
|
def test_extract_form_data_key_from_url_no_key():
|
|
url = "http://localhost:8088/explore/?slice_id=1"
|
|
assert extract_form_data_key_from_url(url) is None
|
|
|
|
|
|
def test_extract_form_data_key_from_url_none():
|
|
assert extract_form_data_key_from_url(None) is None
|
|
|
|
|
|
def test_extract_form_data_key_from_url_empty():
|
|
assert extract_form_data_key_from_url("") is None
|
|
|
|
|
|
def test_extract_form_data_key_from_url_multiple_params():
|
|
url = "http://localhost:8088/explore/?slice_id=5&form_data_key=xyz789&other=val"
|
|
assert extract_form_data_key_from_url(url) == "xyz789"
|
|
|
|
|
|
@patch("superset.daos.chart.ChartDAO.find_by_id")
|
|
def test_find_chart_by_identifier_int(mock_find):
|
|
mock_chart = MagicMock()
|
|
mock_chart.id = 42
|
|
mock_find.return_value = mock_chart
|
|
|
|
result = find_chart_by_identifier(42)
|
|
mock_find.assert_called_once_with(42)
|
|
assert result == mock_chart
|
|
|
|
|
|
@patch("superset.daos.chart.ChartDAO.find_by_id")
|
|
def test_find_chart_by_identifier_str_digit(mock_find):
|
|
mock_chart = MagicMock()
|
|
mock_find.return_value = mock_chart
|
|
|
|
result = find_chart_by_identifier("123")
|
|
mock_find.assert_called_once_with(123)
|
|
assert result == mock_chart
|
|
|
|
|
|
@patch("superset.daos.chart.ChartDAO.find_by_id")
|
|
def test_find_chart_by_identifier_uuid(mock_find):
|
|
mock_chart = MagicMock()
|
|
mock_find.return_value = mock_chart
|
|
|
|
uuid_str = "a1b2c3d4-5678-90ab-cdef-1234567890ab"
|
|
result = find_chart_by_identifier(uuid_str)
|
|
mock_find.assert_called_once_with(uuid_str, id_column="uuid")
|
|
assert result == mock_chart
|
|
|
|
|
|
@patch("superset.daos.chart.ChartDAO.find_by_id")
|
|
def test_find_chart_by_identifier_not_found(mock_find):
|
|
mock_find.return_value = None
|
|
result = find_chart_by_identifier(999)
|
|
assert result is None
|
|
|
|
|
|
@patch(
|
|
"superset.commands.explore.form_data.get.GetFormDataCommand.run",
|
|
return_value='{"viz_type": "table"}',
|
|
)
|
|
@patch("superset.commands.explore.form_data.get.GetFormDataCommand.__init__")
|
|
def test_get_cached_form_data_success(mock_init, mock_run):
|
|
mock_init.return_value = None
|
|
result = get_cached_form_data("test_key")
|
|
assert result == '{"viz_type": "table"}'
|
|
|
|
|
|
@patch(
|
|
"superset.commands.explore.form_data.get.GetFormDataCommand.run",
|
|
side_effect=KeyError("not found"),
|
|
)
|
|
@patch("superset.commands.explore.form_data.get.GetFormDataCommand.__init__")
|
|
def test_get_cached_form_data_key_error(mock_init, mock_run):
|
|
mock_init.return_value = None
|
|
result = get_cached_form_data("bad_key")
|
|
assert result is None
|
|
|
|
|
|
def test_prepare_form_data_for_query_preserves_existing_filters_with_adhoc(
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"filters": [{"col": "gender", "op": "==", "val": "boy"}],
|
|
"adhoc_filters": [
|
|
{
|
|
"clause": "WHERE",
|
|
"expressionType": "SIMPLE",
|
|
"subject": "gender",
|
|
"operator": "==",
|
|
"comparator": "girl",
|
|
}
|
|
],
|
|
}
|
|
query = {}
|
|
|
|
prepare_form_data_for_query(form_data, 1, "table")
|
|
apply_form_data_filters_to_query(query, form_data)
|
|
|
|
assert query["filters"] == [
|
|
{"col": "gender", "op": "==", "val": "boy"},
|
|
{"col": "gender", "op": "==", "val": "girl"},
|
|
]
|
|
|
|
|
|
def test_prepare_form_data_for_query_merges_cached_and_request_extra_form_data(
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"adhoc_filters": [],
|
|
"extra_form_data": {
|
|
"adhoc_filters": [
|
|
{
|
|
"clause": "WHERE",
|
|
"expressionType": "SIMPLE",
|
|
"subject": "country",
|
|
"operator": "==",
|
|
"comparator": "US",
|
|
}
|
|
],
|
|
"time_range": "Last year",
|
|
},
|
|
}
|
|
query = {}
|
|
|
|
prepare_form_data_for_query(
|
|
form_data,
|
|
1,
|
|
"table",
|
|
{
|
|
"adhoc_filters": [
|
|
{
|
|
"clause": "WHERE",
|
|
"expressionType": "SIMPLE",
|
|
"subject": "gender",
|
|
"operator": "==",
|
|
"comparator": "boy",
|
|
}
|
|
],
|
|
"time_range": "No filter",
|
|
},
|
|
)
|
|
apply_form_data_filters_to_query(query, form_data)
|
|
|
|
assert query["filters"] == [
|
|
{"col": "country", "op": "==", "val": "US"},
|
|
{"col": "gender", "op": "==", "val": "boy"},
|
|
]
|
|
assert query["time_range"] == "No filter"
|
|
|
|
|
|
def test_build_query_dicts_from_form_data_uses_raw_all_columns(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "handlebars",
|
|
"query_mode": "raw",
|
|
"all_columns": ["state", "city"],
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries == [
|
|
{
|
|
"columns": ["state", "city"],
|
|
"metrics": [],
|
|
"filters": [],
|
|
}
|
|
]
|
|
|
|
|
|
def test_merge_form_data_filters_into_query_applies_regular_overrides():
|
|
query = {
|
|
"filters": [{"col": "country", "op": "==", "val": "US"}],
|
|
"time_range": "Last year",
|
|
"granularity": "created_at",
|
|
"time_grain": "P1Y",
|
|
"time_grain_sqla": "P1Y",
|
|
"where": "region = 'NA'",
|
|
"having": "SUM(num) > 10",
|
|
}
|
|
|
|
merge_form_data_filters_into_query(
|
|
query,
|
|
{
|
|
"filters": [{"col": "gender", "op": "==", "val": "boy"}],
|
|
"time_range": "No filter",
|
|
"granularity": "updated_at",
|
|
"time_grain": "P1D",
|
|
"time_grain_sqla": "P1D",
|
|
"where": "name IS NOT NULL",
|
|
"having": "COUNT(*) > 1",
|
|
},
|
|
)
|
|
|
|
assert query["filters"] == [
|
|
{"col": "country", "op": "==", "val": "US"},
|
|
{"col": "gender", "op": "==", "val": "boy"},
|
|
]
|
|
assert query["time_range"] == "No filter"
|
|
assert query["granularity"] == "updated_at"
|
|
assert query["time_grain"] == "P1D"
|
|
assert query["time_grain_sqla"] == "P1D"
|
|
assert query["where"] == "(region = 'NA') AND (name IS NOT NULL)"
|
|
assert query["having"] == "(SUM(num) > 10) AND (COUNT(*) > 1)"
|
|
|
|
|
|
def test_merge_extra_form_data_filters_into_query_adds_only_extra_predicates(
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
query = {
|
|
"filters": [{"col": "country", "op": "==", "val": "US"}],
|
|
"time_range": "Last year",
|
|
"granularity": "created_at",
|
|
"time_grain_sqla": "P1Y",
|
|
}
|
|
|
|
merge_extra_form_data_filters_into_query(
|
|
query,
|
|
{
|
|
"filters": [{"col": "gender", "op": "==", "val": "boy"}],
|
|
"granularity_sqla": "updated_at",
|
|
"time_range": "No filter",
|
|
"time_grain_sqla": "P1D",
|
|
},
|
|
1,
|
|
"table",
|
|
)
|
|
|
|
assert query["filters"] == [
|
|
{"col": "country", "op": "==", "val": "US"},
|
|
{"col": "gender", "op": "==", "val": "boy"},
|
|
]
|
|
assert query["time_range"] == "No filter"
|
|
assert query["granularity"] == "updated_at"
|
|
assert query["time_grain_sqla"] == "P1D"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_deck_gl_columns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolve_deck_gl_columns_latlong():
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "longitude", "latCol": "latitude"},
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["longitude", "latitude"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_delimited():
|
|
form_data = {
|
|
"spatial": {"type": "delimited", "lonlatCol": "coords"},
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["coords"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_geohash():
|
|
form_data = {
|
|
"spatial": {"type": "geohash", "geohashCol": "geo"},
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["geo"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_arc_start_end():
|
|
form_data = {
|
|
"start_spatial": {
|
|
"type": "latlong",
|
|
"lonCol": "start_lon",
|
|
"latCol": "start_lat",
|
|
},
|
|
"end_spatial": {"type": "latlong", "lonCol": "end_lon", "latCol": "end_lat"},
|
|
}
|
|
cols = resolve_deck_gl_columns(form_data)
|
|
assert cols == ["start_lon", "start_lat", "end_lon", "end_lat"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_path_line_column():
|
|
form_data = {
|
|
"line_column": "path_wkt",
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["path_wkt"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_geojson():
|
|
form_data = {
|
|
"geojson": "geom_col",
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["geom_col"]
|
|
|
|
|
|
def test_resolve_deck_gl_columns_with_dimension_and_js_columns():
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"dimension": "category",
|
|
"js_columns": ["name", "value"],
|
|
}
|
|
cols = resolve_deck_gl_columns(form_data)
|
|
assert "lon" in cols
|
|
assert "lat" in cols
|
|
assert "category" in cols
|
|
assert "name" in cols
|
|
assert "value" in cols
|
|
|
|
|
|
def test_resolve_deck_gl_columns_deduplicates():
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"dimension": "lon", # same as lonCol — should not duplicate
|
|
}
|
|
cols = resolve_deck_gl_columns(form_data)
|
|
assert cols.count("lon") == 1
|
|
|
|
|
|
def test_resolve_deck_gl_columns_empty():
|
|
assert resolve_deck_gl_columns({}) == []
|
|
|
|
|
|
def test_resolve_deck_gl_columns_ignores_non_string_js_columns():
|
|
form_data = {
|
|
"js_columns": [42, None, "valid_col"],
|
|
}
|
|
assert resolve_deck_gl_columns(form_data) == ["valid_col"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_query_dicts_from_form_data — Deck.gl branch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_latlong(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert len(queries) == 1
|
|
assert queries[0]["columns"] == ["lon", "lat"]
|
|
assert queries[0]["metrics"] == []
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_with_size_metric(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
metric = {
|
|
"expressionType": "SIMPLE",
|
|
"column": {"column_name": "sales"},
|
|
"aggregate": "SUM",
|
|
}
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"size": metric,
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert len(queries) == 1
|
|
assert queries[0]["columns"] == ["lon", "lat"]
|
|
assert queries[0]["metrics"] == [metric]
|
|
|
|
|
|
def test_build_query_dicts_deck_arc(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_arc",
|
|
"start_spatial": {
|
|
"type": "latlong",
|
|
"lonCol": "origin_lon",
|
|
"latCol": "origin_lat",
|
|
},
|
|
"end_spatial": {"type": "latlong", "lonCol": "dest_lon", "latCol": "dest_lat"},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert len(queries) == 1
|
|
assert queries[0]["columns"] == ["origin_lon", "origin_lat", "dest_lon", "dest_lat"]
|
|
assert queries[0]["metrics"] == []
|
|
|
|
|
|
def test_build_query_dicts_deck_geojson(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_geojson",
|
|
"geojson": "geometry",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert len(queries) == 1
|
|
assert queries[0]["columns"] == ["geometry"]
|
|
assert queries[0]["metrics"] == []
|
|
|
|
|
|
def test_build_query_dicts_deck_hex_geohash(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_hex",
|
|
"spatial": {"type": "geohash", "geohashCol": "geohash"},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert len(queries) == 1
|
|
assert queries[0]["columns"] == ["geohash"]
|
|
|
|
|
|
def test_build_query_dicts_deck_path_with_row_limit(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_path",
|
|
"line_column": "path_col",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table", row_limit=50)
|
|
|
|
assert queries[0]["columns"] == ["path_col"]
|
|
assert queries[0]["row_limit"] == 50
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_deck_gl_columns — display-only fields excluded
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolve_deck_gl_columns_ignores_tooltip_contents():
|
|
# tooltip_contents are display-only; BaseDeckGLViz.query_obj() does not
|
|
# include them in columns/groupby, so the fallback should not either.
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"tooltip_contents": ["name", "category"],
|
|
}
|
|
cols = resolve_deck_gl_columns(form_data)
|
|
assert "name" not in cols
|
|
assert "category" not in cols
|
|
|
|
|
|
def test_resolve_deck_gl_columns_ignores_cross_filter_column():
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"cross_filter_column": "region",
|
|
}
|
|
cols = resolve_deck_gl_columns(form_data)
|
|
assert "region" not in cols
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_metric_ref
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_is_metric_ref_dict():
|
|
assert _is_metric_ref({"expressionType": "SIMPLE"}) is True
|
|
|
|
|
|
def test_is_metric_ref_string_key():
|
|
assert _is_metric_ref("count") is True
|
|
assert _is_metric_ref("sum__sales") is True
|
|
|
|
|
|
def test_is_metric_ref_numeric_string_excluded():
|
|
assert _is_metric_ref("100") is False
|
|
assert _is_metric_ref("3.14") is False
|
|
assert _is_metric_ref("0") is False
|
|
|
|
|
|
def test_is_metric_ref_integer_excluded():
|
|
assert _is_metric_ref(100) is False
|
|
|
|
|
|
def test_is_metric_ref_none_and_empty():
|
|
assert _is_metric_ref(None) is False
|
|
assert _is_metric_ref("") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _resolve_deck_gl_metrics (Fix 2)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_no_metrics():
|
|
assert _resolve_deck_gl_metrics({}) == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_size_field():
|
|
metric = {"expressionType": "SIMPLE", "aggregate": "COUNT", "column": None}
|
|
result = _resolve_deck_gl_metrics({"size": metric})
|
|
assert result == [metric]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_metric_field():
|
|
metric = {"expressionType": "SIMPLE", "aggregate": "SUM"}
|
|
result = _resolve_deck_gl_metrics({"metric": metric})
|
|
assert result == [metric]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_point_radius_fixed_metric():
|
|
prf_metric = {"expressionType": "SIMPLE", "aggregate": "AVG"}
|
|
prf = {"type": "metric", "value": prf_metric}
|
|
result = _resolve_deck_gl_metrics({"point_radius_fixed": prf})
|
|
assert result == [prf_metric]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_point_radius_fixed_not_metric():
|
|
prf = {"type": "fix", "value": 100}
|
|
result = _resolve_deck_gl_metrics({"point_radius_fixed": prf})
|
|
assert result == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_polygon_both_metric_and_prf():
|
|
base_metric = {"expressionType": "SIMPLE", "aggregate": "SUM"}
|
|
elevation_metric = {"expressionType": "SIMPLE", "aggregate": "AVG"}
|
|
prf = {"type": "metric", "value": elevation_metric}
|
|
result = _resolve_deck_gl_metrics(
|
|
{"metric": base_metric, "point_radius_fixed": prf}
|
|
)
|
|
assert result == [base_metric, elevation_metric]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_geojson_returns_empty():
|
|
# deck_geojson.query_obj() forces metrics=[] regardless of form_data
|
|
metric = {"expressionType": "SIMPLE", "aggregate": "SUM"}
|
|
result = _resolve_deck_gl_metrics(
|
|
{"size": metric, "metric": metric}, "deck_geojson"
|
|
)
|
|
assert result == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_scalar_size_excluded():
|
|
# Numeric string size values (fixed display settings) must not be metrics
|
|
result = _resolve_deck_gl_metrics({"size": "100"}, "deck_hex")
|
|
assert result == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_integer_size_excluded():
|
|
result = _resolve_deck_gl_metrics({"size": 100}, "deck_path")
|
|
assert result == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_string_metric_included():
|
|
# Non-numeric string metrics (saved metric keys) must be preserved
|
|
result = _resolve_deck_gl_metrics({"size": "count"}, "deck_hex")
|
|
assert result == ["count"]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_string_metric_field():
|
|
result = _resolve_deck_gl_metrics({"metric": "sum__sales"}, "deck_arc")
|
|
assert result == ["sum__sales"]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_string_point_radius_fixed():
|
|
# Legacy deck_scatter: point_radius_fixed as a bare metric key string
|
|
result = _resolve_deck_gl_metrics({"point_radius_fixed": "count"}, "deck_scatter")
|
|
assert result == ["count"]
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_numeric_point_radius_fixed_excluded():
|
|
# Numeric string point_radius_fixed is a fixed pixel radius, not a metric
|
|
result = _resolve_deck_gl_metrics({"point_radius_fixed": "100"}, "deck_scatter")
|
|
assert result == []
|
|
|
|
|
|
def test_resolve_deck_gl_metrics_non_string_point_radius_fixed_excluded():
|
|
# Non-string point_radius_fixed values (int, None, list) are excluded by
|
|
# the isinstance(prf, str) guard in the elif branch
|
|
assert _resolve_deck_gl_metrics({"point_radius_fixed": 100}, "deck_scatter") == []
|
|
assert _resolve_deck_gl_metrics({"point_radius_fixed": None}, "deck_scatter") == []
|
|
assert (
|
|
_resolve_deck_gl_metrics({"point_radius_fixed": ["bad"]}, "deck_scatter") == []
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _deck_gl_null_filters (Fix 3)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_deck_gl_null_filters_latlong():
|
|
form_data = {
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
}
|
|
result = _deck_gl_null_filters(form_data)
|
|
assert result == [
|
|
{"col": "lon", "op": "IS NOT NULL", "val": ""},
|
|
{"col": "lat", "op": "IS NOT NULL", "val": ""},
|
|
]
|
|
|
|
|
|
def test_deck_gl_null_filters_arc_start_end():
|
|
form_data = {
|
|
"start_spatial": {"type": "latlong", "lonCol": "s_lon", "latCol": "s_lat"},
|
|
"end_spatial": {"type": "latlong", "lonCol": "e_lon", "latCol": "e_lat"},
|
|
}
|
|
result = _deck_gl_null_filters(form_data)
|
|
assert result == [
|
|
{"col": "s_lon", "op": "IS NOT NULL", "val": ""},
|
|
{"col": "s_lat", "op": "IS NOT NULL", "val": ""},
|
|
{"col": "e_lon", "op": "IS NOT NULL", "val": ""},
|
|
{"col": "e_lat", "op": "IS NOT NULL", "val": ""},
|
|
]
|
|
|
|
|
|
def test_deck_gl_null_filters_line_column():
|
|
form_data = {"line_column": "path_col"}
|
|
result = _deck_gl_null_filters(form_data)
|
|
assert result == [{"col": "path_col", "op": "IS NOT NULL", "val": ""}]
|
|
|
|
|
|
def test_deck_gl_null_filters_empty():
|
|
assert _deck_gl_null_filters({}) == []
|
|
|
|
|
|
def test_deck_gl_null_filters_geojson_column():
|
|
# geojson column gets an IS NOT NULL filter just like spatial columns
|
|
form_data = {"geojson": "geometry"}
|
|
assert _deck_gl_null_filters(form_data) == [
|
|
{"col": "geometry", "op": "IS NOT NULL", "val": ""}
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_query_dicts_from_form_data — null filters behavior (Fix 3)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_adds_null_filters_by_default(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert {"col": "lon", "op": "IS NOT NULL", "val": ""} in queries[0]["filters"]
|
|
assert {"col": "lat", "op": "IS NOT NULL", "val": ""} in queries[0]["filters"]
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_filter_nulls_false(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"filter_nulls": False,
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
null_filters = [
|
|
f for f in queries[0].get("filters", []) if f.get("op") == "IS NOT NULL"
|
|
]
|
|
assert null_filters == []
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_point_radius_fixed_metric(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
radius_metric = {
|
|
"expressionType": "SIMPLE",
|
|
"aggregate": "AVG",
|
|
"column": {"column_name": "radius"},
|
|
}
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"point_radius_fixed": {"type": "metric", "value": radius_metric},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["metrics"] == [radius_metric]
|
|
|
|
|
|
def test_build_query_dicts_deck_geojson_scalar_size_produces_no_metrics(monkeypatch):
|
|
# Regression: deck_geojson fixture has size='100' (scalar, not a metric).
|
|
# The fallback must produce metrics=[] to match DeckGeoJson.query_obj().
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_geojson",
|
|
"geojson": "geometry",
|
|
"size": "100",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["metrics"] == []
|
|
|
|
|
|
def test_build_query_dicts_deck_path_scalar_size_produces_no_metrics(monkeypatch):
|
|
# deck_path fixture also has size='100' — scalar must not become a metric.
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_path",
|
|
"line_column": "path_col",
|
|
"size": "100",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["metrics"] == []
|
|
|
|
|
|
def test_build_query_dicts_deck_geojson_adds_geojson_null_filter(monkeypatch):
|
|
# deck_geojson should add IS NOT NULL on the geojson column when filter_nulls
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_geojson",
|
|
"geojson": "geometry_col",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert {"col": "geometry_col", "op": "IS NOT NULL", "val": ""} in queries[0][
|
|
"filters"
|
|
]
|
|
|
|
|
|
def test_build_query_dicts_deck_hex_string_metric(monkeypatch):
|
|
# Non-numeric string size (saved metric key) must be included as a metric
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_hex",
|
|
"spatial": {"type": "geohash", "geohashCol": "geo"},
|
|
"size": "count",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["metrics"] == ["count"]
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_string_point_radius_fixed(monkeypatch):
|
|
# Legacy deck_scatter with point_radius_fixed as a bare metric key string
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"point_radius_fixed": "count",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["metrics"] == ["count"]
|
|
|
|
|
|
def test_build_query_dicts_deck_hex_orderby_when_metrics_present(monkeypatch):
|
|
# Mirrors BaseDeckGLViz.query_obj(): orderby set from first metric (desc by default)
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
metric = {"expressionType": "SIMPLE", "aggregate": "COUNT", "column": None}
|
|
form_data = {
|
|
"viz_type": "deck_hex",
|
|
"spatial": {"type": "geohash", "geohashCol": "geo"},
|
|
"size": metric,
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["orderby"] == [(metric, False)]
|
|
|
|
|
|
def test_build_query_dicts_deck_scatter_no_orderby_without_metrics(monkeypatch):
|
|
# No metrics → no orderby (pure spatial column query)
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_scatter",
|
|
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert "orderby" not in queries[0]
|
|
|
|
|
|
def test_build_query_dicts_deck_arc_time_grain(monkeypatch):
|
|
# deck_arc with time_grain_sqla → is_timeseries, granularity, extras set
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_arc",
|
|
"spatial": {"type": "latlong", "lonCol": "start_lon", "latCol": "start_lat"},
|
|
"end_spatial": {
|
|
"type": "latlong",
|
|
"lonCol": "end_lon",
|
|
"latCol": "end_lat",
|
|
},
|
|
"granularity_sqla": "ts",
|
|
"time_grain_sqla": "P1D",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert queries[0]["is_timeseries"] is True
|
|
assert queries[0]["granularity"] == "ts"
|
|
assert queries[0].get("extras", {}).get("time_grain_sqla") == "P1D"
|
|
|
|
|
|
def test_build_query_dicts_deck_geojson_ignores_time_grain(monkeypatch):
|
|
# deck_geojson is not in _DECK_TIMESERIES_VIZ_TYPES; time grain fields not added
|
|
monkeypatch.setattr(
|
|
"superset.mcp_service.chart.chart_helpers.resolve_datasource_engine",
|
|
lambda datasource_id, datasource_type: "base",
|
|
)
|
|
form_data = {
|
|
"viz_type": "deck_geojson",
|
|
"geojson": "geometry",
|
|
"granularity_sqla": "ts",
|
|
"time_grain_sqla": "P1D",
|
|
"adhoc_filters": [],
|
|
}
|
|
|
|
queries = build_query_dicts_from_form_data(form_data, 1, "table")
|
|
|
|
assert "is_timeseries" not in queries[0]
|
|
assert queries[0].get("extras", {}).get("time_grain_sqla") is None
|