Skip to content

Commit 68f5b95

Browse files
fix(test): support out-of-nanosecond-range timestamps in unit test comparisons (#6009)
Signed-off-by: mokashang <shangmengjiajiajia@gmail.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com>
1 parent 8d0b4de commit 68f5b95

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

sqlmesh/core/test/definition.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,9 @@ def assert_equal(
263263
for col, value in object_sentinel_values.items():
264264
try:
265265
# can't use `isinstance()` here - https://stackoverflow.com/a/68743663/1707525
266-
if type(value) is datetime.date:
267-
expected[col] = pd.to_datetime(expected[col]).dt.date
268-
elif type(value) is datetime.time:
269-
expected[col] = pd.to_datetime(expected[col]).dt.time
270-
elif type(value) is datetime.datetime:
271-
expected[col] = pd.to_datetime(expected[col]).dt.to_pydatetime()
266+
value_type = type(value)
267+
if value_type in (datetime.date, datetime.time, datetime.datetime):
268+
expected[col] = _parse_expected_datetime_column(expected[col], value_type)
272269
except Exception as e:
273270
from sqlmesh.core.console import get_console
274271

@@ -1014,6 +1011,34 @@ def _raise_error(msg: str, path: Path | None = None) -> None:
10141011
raise TestError(f"Failed to run test:\n{msg}")
10151012

10161013

1014+
def _parse_expected_datetime_column(series: pd.Series, target_type: type) -> pd.Series:
1015+
"""Convert a series of expected values to python ``date``/``time``/``datetime``.
1016+
1017+
Falls back to microsecond resolution when pandas' default nanosecond
1018+
parsing overflows. SQL ``TIMESTAMP`` columns can carry values outside
1019+
pandas' default ``datetime64[ns]`` range (1677-09-21..2262-04-11), so
1020+
unit tests may compare against values like ``0001-01-01`` which are
1021+
valid in the database but overflow the default resolution.
1022+
"""
1023+
import pandas as pd
1024+
from pandas.errors import OutOfBoundsDatetime
1025+
1026+
try:
1027+
parsed = pd.to_datetime(series)
1028+
except OutOfBoundsDatetime:
1029+
parsed = series.astype("datetime64[us]")
1030+
1031+
if target_type is datetime.date:
1032+
return parsed.dt.date
1033+
if target_type is datetime.time:
1034+
return parsed.dt.time
1035+
# `Series.dt.to_pydatetime()` returns an `ndarray` in pandas 2.x. Wrap it in a
1036+
# Series with ``dtype=object`` so pandas does not coerce the values back to
1037+
# ``pd.Timestamp`` (which would reintroduce the nanosecond overflow this
1038+
# function exists to avoid).
1039+
return pd.Series(parsed.dt.to_pydatetime(), index=parsed.index, dtype="object")
1040+
1041+
10171042
def _normalize_df_value(value: t.Any) -> t.Any:
10181043
"""Normalize data in a pandas dataframe so ruamel and sqlglot can deal with it."""
10191044
import numpy as np

tests/core/test_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,6 +2931,48 @@ def test_timestamp_normalization() -> None:
29312931
)
29322932

29332933

2934+
def test_out_of_bounds_nanosecond_timestamp_comparison(mocker: MockerFixture) -> None:
2935+
# https://github.com/TobikoData/sqlmesh/issues/5929
2936+
# Engines like Redshift may return a TIMESTAMP column as an object-dtype
2937+
# series of python `datetime.datetime` instances. Values outside pandas'
2938+
# default `datetime64[ns]` range (1677-09-21..2262-04-11) - which SQL
2939+
# `TIMESTAMP` fully supports - previously raised `OutOfBoundsDatetime`
2940+
# while parsing the expected values, producing a "Failed to convert
2941+
# expected value into `datetime`" warning and a false mismatch on values
2942+
# whose repr survives str-coercion (the values below happen to compare
2943+
# equal via `str()`, so the mismatch was silent).
2944+
test = _create_test(
2945+
body=load_yaml(
2946+
"""
2947+
test_foo:
2948+
model: sushi.foo
2949+
outputs:
2950+
query:
2951+
- ts_col: "0001-01-01 00:00:00"
2952+
- ts_col: "9999-12-31 23:59:59"
2953+
"""
2954+
),
2955+
test_name="test_foo",
2956+
model=_create_model("SELECT ts_col FROM raw"),
2957+
context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))),
2958+
)
2959+
actual = pd.DataFrame(
2960+
{
2961+
"ts_col": pd.Series(
2962+
[datetime.datetime(1, 1, 1), datetime.datetime(9999, 12, 31, 23, 59, 59)],
2963+
dtype=object,
2964+
)
2965+
}
2966+
)
2967+
# Use T separator so a string-only comparison (broken path) would mismatch
2968+
# against str(datetime.datetime(1, 1, 1)) == "0001-01-01 00:00:00".
2969+
expected = pd.DataFrame({"ts_col": ["0001-01-01T00:00:00", "9999-12-31T23:59:59"]})
2970+
log_warning = mocker.spy(get_console(), "log_warning")
2971+
test.assert_equal(expected=expected, actual=actual, sort=False)
2972+
for call_args in log_warning.call_args_list:
2973+
assert "Failed to convert expected value" not in call_args.args[0]
2974+
2975+
29342976
@use_terminal_console
29352977
def test_disable_test_logging_if_no_tests_found(mocker: MockerFixture, tmp_path: Path) -> None:
29362978
init_example_project(tmp_path, engine_type="duckdb")

0 commit comments

Comments
 (0)