diff --git a/.changes/unreleased/Fixes-20230525-091955.yaml b/.changes/unreleased/Fixes-20230525-091955.yaml new file mode 100644 index 00000000000..a2e4daf39f9 --- /dev/null +++ b/.changes/unreleased/Fixes-20230525-091955.yaml @@ -0,0 +1,6 @@ +kind: Fixes +body: Unified to UTC +time: 2023-05-25T09:19:55.865281+09:00 +custom: + Author: d-kaneshiro + Issue: "7664" diff --git a/tests/functional/simple_snapshot/test_hard_delete_snapshot.py b/tests/functional/simple_snapshot/test_hard_delete_snapshot.py index edb7324f884..4b4b9e281a6 100644 --- a/tests/functional/simple_snapshot/test_hard_delete_snapshot.py +++ b/tests/functional/simple_snapshot/test_hard_delete_snapshot.py @@ -1,12 +1,9 @@ import os -from datetime import datetime -from datetime import timedelta -from datetime import timezone -import time - +from datetime import datetime, timedelta import pytz import pytest from dbt.tests.util import run_dbt, check_relations_equal +from dbt.tests.adapter.utils.test_current_timestamp import is_aware from tests.functional.simple_snapshot.fixtures import ( models__schema_yml, models__ref_snapshot_sql, @@ -18,13 +15,30 @@ # These tests uses the same seed data, containing 20 records of which we hard delete the last 10. # These deleted records set the dbt_valid_to to time the snapshot was ran. -# Using replace on a timestamp won't account for hour differences unless given the local timezone. -# We can force python as utc but not postgres fields which need to be handled as local timestamps. -def currenttz(): - if time.daylight: - return timezone(timedelta(seconds=-time.altzone), time.tzname[1]) + +def convert_to_aware(d: datetime) -> datetime: + # There are two types of datetime objects in Python: naive and aware + # Assume any dbt snapshot timestamp that is naive is meant to represent UTC + if d is None: + return d + elif is_aware(d): + return d + else: + return d.replace(tzinfo=pytz.UTC) + + +def is_close_datetime( + dt1: datetime, dt2: datetime, atol: timedelta = timedelta(microseconds=1) +) -> bool: + # Similar to pytest.approx, math.isclose, and numpy.isclose + # Use an absolute tolerance to compare datetimes that may not be perfectly equal. + # Two None values will compare as equal. + if dt1 is None and dt2 is None: + return True + elif dt1 is not None and dt2 is not None: + return (dt1 > (dt2 - atol)) and (dt1 < (dt2 + atol)) else: - return timezone(timedelta(seconds=-time.timezone), time.tzname[0]) + return False def datetime_snapshot(): @@ -94,10 +108,16 @@ def test_snapshot_hard_delete(project): for result in snapshotted[10:]: # result is a tuple, the dbt_valid_to column is the latest assert isinstance(result[-1], datetime) - assert result[-1].replace(tzinfo=currenttz()) >= invalidated_snapshot_datetime + dbt_valid_to = convert_to_aware(result[-1]) + + # Plenty of wiggle room if clocks aren't perfectly sync'd, etc + assert is_close_datetime( + dbt_valid_to, invalidated_snapshot_datetime, timedelta(minutes=1) + ), f"SQL timestamp {dbt_valid_to.isoformat()} is not close enough to Python UTC {invalidated_snapshot_datetime.isoformat()}" # revive records # Timestamp must have microseconds for tests below to be meaningful + # Assume `updated_at` is TIMESTAMP WITHOUT TIME ZONE that implicitly represents UTC revival_timestamp = datetime.now(pytz.UTC).strftime("%Y-%m-%d %H:%M:%S.%f") project.run_sql( """ @@ -133,7 +153,12 @@ def test_snapshot_hard_delete(project): for result in invalidated_records: # result is a tuple, the dbt_valid_to column is the latest assert isinstance(result[1], datetime) - assert result[1].replace(tzinfo=currenttz()) >= invalidated_snapshot_datetime + dbt_valid_to = convert_to_aware(result[1]) + + # Plenty of wiggle room if clocks aren't perfectly sync'd, etc + assert is_close_datetime( + dbt_valid_to, invalidated_snapshot_datetime, timedelta(minutes=1) + ), f"SQL timestamp {dbt_valid_to.isoformat()} is not close enough to Python UTC {invalidated_snapshot_datetime.isoformat()}" # records which were revived (id = 10, 11) # dbt_valid_to is null @@ -158,5 +183,8 @@ def test_snapshot_hard_delete(project): # dbt_valid_from is the same as the 'updated_at' added in the revived_rows # dbt_valid_to is null assert isinstance(result[1], datetime) - assert result[1].replace(tzinfo=pytz.UTC) <= revived_snapshot_datetime - assert result[2] is None + dbt_valid_from = convert_to_aware(result[1]) + dbt_valid_to = result[2] + + assert dbt_valid_from <= revived_snapshot_datetime + assert dbt_valid_to is None