From ef960073f851cb1f701cf49e2f0b3dcd8765bb0c Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Mon, 18 May 2026 14:26:13 -0400 Subject: [PATCH] Add more tests --- .../unit_tests/semantic_layers/mapper_test.py | 161 +++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/semantic_layers/mapper_test.py b/tests/unit_tests/semantic_layers/mapper_test.py index 33f3e11b03c..ad121de740a 100644 --- a/tests/unit_tests/semantic_layers/mapper_test.py +++ b/tests/unit_tests/semantic_layers/mapper_test.py @@ -15,7 +15,8 @@ # specific language governing permissions and limitations # under the License. -from datetime import datetime +from datetime import date, datetime, time +from typing import Any from unittest.mock import MagicMock import pandas as pd @@ -40,6 +41,7 @@ from superset_core.semantic_layers.types import ( from superset_core.semantic_layers.view import SemanticViewFeature from superset.semantic_layers.mapper import ( + _coerce_scalar_filter_value, _convert_query_object_filter, _convert_time_grain, _get_filters_from_extras, @@ -2826,3 +2828,160 @@ def test_get_group_limit_filters_no_granularity( # Should return None - no granularity means no time filters added assert result is None + + +# --------------------------------------------------------------------------- +# _coerce_scalar_filter_value: per-dtype branches +# --------------------------------------------------------------------------- + + +def _dim(dtype: pa.DataType, name: str = "d") -> Dimension: + return Dimension(name, name, dtype, name, name.capitalize()) + + +def test_coerce_none_returns_none() -> None: + assert _coerce_scalar_filter_value(None, _dim(pa.int64())) is None + + +def test_coerce_unsupported_dtype_passes_through() -> None: + # utf8 (and any dtype not branched in the function) returns the value as-is. + assert _coerce_scalar_filter_value("abc", _dim(pa.utf8())) == "abc" + + +@pytest.mark.parametrize( + "raw,expected", + [ + (True, True), + (False, False), + ("true", True), + ("T", True), + (" 1 ", True), + ("yes", True), + ("Y", True), + ("on", True), + ("false", False), + ("F", False), + ("0", False), + ("no", False), + ("N", False), + ("off", False), + ], +) +def test_coerce_boolean(raw: Any, expected: bool) -> None: + assert _coerce_scalar_filter_value(raw, _dim(pa.bool_())) is expected + + +@pytest.mark.parametrize("raw", ["maybe", 1, 0.5]) +def test_coerce_boolean_invalid_raises(raw: Any) -> None: + with pytest.raises(ValueError, match="Invalid boolean value"): + _coerce_scalar_filter_value(raw, _dim(pa.bool_())) + + +def test_coerce_integer_passthrough() -> None: + assert _coerce_scalar_filter_value(42, _dim(pa.int64())) == 42 + + +def test_coerce_integer_rejects_bool() -> None: + # bool is a subclass of int; we explicitly reject it. + with pytest.raises(ValueError, match="Invalid integer value"): + _coerce_scalar_filter_value(True, _dim(pa.int64())) + + +def test_coerce_integer_rejects_other_types() -> None: + with pytest.raises(ValueError, match="Invalid integer value"): + _coerce_scalar_filter_value(1.5, _dim(pa.int64())) + + +@pytest.mark.parametrize( + "dtype", + [pa.float64(), pa.decimal128(10, 2)], +) +def test_coerce_floating_or_decimal(dtype: pa.DataType) -> None: + assert _coerce_scalar_filter_value(1, _dim(dtype)) == 1.0 + assert _coerce_scalar_filter_value(1.5, _dim(dtype)) == 1.5 + assert _coerce_scalar_filter_value(" 2.5 ", _dim(dtype)) == 2.5 + + +def test_coerce_floating_rejects_bool() -> None: + with pytest.raises(ValueError, match="Invalid numeric value"): + _coerce_scalar_filter_value(True, _dim(pa.float64())) + + +def test_coerce_floating_invalid_string_raises() -> None: + with pytest.raises(ValueError, match="Invalid numeric value"): + _coerce_scalar_filter_value("not-a-number", _dim(pa.float64())) + + +def test_coerce_floating_rejects_other_types() -> None: + with pytest.raises(ValueError, match="Invalid numeric value"): + _coerce_scalar_filter_value([1.0], _dim(pa.float64())) + + +def test_coerce_date_from_datetime() -> None: + out = _coerce_scalar_filter_value(datetime(2025, 1, 2, 12, 0), _dim(pa.date32())) + assert out == date(2025, 1, 2) + + +def test_coerce_date_passthrough() -> None: + out = _coerce_scalar_filter_value(date(2025, 1, 2), _dim(pa.date32())) + assert out == date(2025, 1, 2) + + +def test_coerce_date_from_iso_string() -> None: + out = _coerce_scalar_filter_value(" 2025-01-02 ", _dim(pa.date32())) + assert out == date(2025, 1, 2) + + +def test_coerce_date_invalid_string_raises() -> None: + with pytest.raises(ValueError, match="Invalid date value"): + _coerce_scalar_filter_value("not-a-date", _dim(pa.date32())) + + +def test_coerce_date_rejects_other_types() -> None: + with pytest.raises(ValueError, match="Invalid date value"): + _coerce_scalar_filter_value(20250102, _dim(pa.date32())) + + +def test_coerce_timestamp_from_datetime_passthrough() -> None: + dt = datetime(2025, 1, 2, 3, 4, 5) + assert _coerce_scalar_filter_value(dt, _dim(pa.timestamp("us"))) is dt + + +def test_coerce_timestamp_from_date() -> None: + out = _coerce_scalar_filter_value(date(2025, 1, 2), _dim(pa.timestamp("us"))) + assert out == datetime(2025, 1, 2, 0, 0) + + +def test_coerce_timestamp_from_iso_string_with_z() -> None: + out = _coerce_scalar_filter_value("2025-01-02T03:04:05Z", _dim(pa.timestamp("us"))) + assert out == datetime.fromisoformat("2025-01-02T03:04:05+00:00") + + +def test_coerce_timestamp_invalid_string_raises() -> None: + with pytest.raises(ValueError, match="Invalid timestamp value"): + _coerce_scalar_filter_value("not-a-ts", _dim(pa.timestamp("us"))) + + +def test_coerce_timestamp_rejects_other_types() -> None: + with pytest.raises(ValueError, match="Invalid timestamp value"): + _coerce_scalar_filter_value(1234567890, _dim(pa.timestamp("us"))) + + +def test_coerce_time_passthrough() -> None: + out = _coerce_scalar_filter_value(time(3, 4, 5), _dim(pa.time64("us"))) + assert out == time(3, 4, 5) + + +def test_coerce_time_from_iso_string() -> None: + out = _coerce_scalar_filter_value(" 03:04:05 ", _dim(pa.time64("us"))) + assert out == time(3, 4, 5) + + +def test_coerce_time_invalid_string_raises() -> None: + with pytest.raises(ValueError, match="Invalid time value"): + _coerce_scalar_filter_value("not-a-time", _dim(pa.time64("us"))) + + +def test_coerce_time_rejects_other_types() -> None: + with pytest.raises(ValueError, match="Invalid time value"): + _coerce_scalar_filter_value(123, _dim(pa.time64("us")))