FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix: Read naive datetimes as UTC in UnixTimestamp values by MohammadHijjawi97 · Pull Request #6937 · feast-dev/feast · GitHub

Repository navigation

fix: Read naive datetimes as UTC in UnixTimestamp values - #6937

Merged
jyejare merged 3 commits into
feast-dev:masterfrom
MohammadHijjawi97:fix/naive-unix-timestamp-utc
Oct 7, 2026
Merged

jyejare merged 3 commits into
feast-dev:masterfrom
MohammadHijjawi97:fix/naive-unix-timestamp-utc

Conversation

MohammadHijjawi97 commented Oct 5, 2026 •
edited
Loading

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

_python_datetime_to_int_timestamp in feast/type_map.py converts a naive datetime.datetime with datetime.timestamp(), which reads a naive value in the machine's local timezone. Everything else treats a naive value as UTC: a naive pd.Timestamp or np.datetime64 in the same function, a naive ZonedTimestamp, and utils.make_tzaware ("We assume tz-naive datetimes are UTC").

So on any host that is not on UTC, UnixTimestamp values (scalar, Array and Set) built from Python datetimes are shifted by the host's UTC offset. The same wall-clock time also lands at a different instant depending on whether it arrives as a datetime or a pd.Timestamp.

Repro: a local sqlite store, a RequestSource with a UnixTimestamp field, and a python-mode ODFV that returns it unchanged. Called with entity_rows=[{"driver_id": 1, "request_ts": datetime(2024, 7, 1, 12)}] on a host in Europe/London:

before: sent 2024-07-01 12:00:00 -> returned 2024-07-01 11:00:00+00:00
after:  sent 2024-07-01 12:00:00 -> returned 2024-07-01 12:00:00+00:00

The fix attaches UTC to a naive datetime before taking its timestamp, the same way the ZONED_TIMESTAMP path already does. Aware datetimes are unchanged.

Which issue(s) this PR fixes:

No existing issue; found while testing timestamp round-trips.

Checks

  • I've made sure the tests are passing.
  • My commits are signed off (git commit -s)
  • My PR title follows conventional commits format

Testing Strategy

  • Unit tests
  • Integration tests
  • Manual tests
  • Testing is not required for this change

Misc

test_naive_datetime_unix_timestamp_is_utc covers the scalar, list and set types. A fixture switches the local timezone to the fixed offset UTC+8 via TZ + time.tzset() (and skips where tzset is unavailable), so the test fails without the fix even on UTC CI runners. It also covers a tzinfo whose utcoffset() returns None. ruff check/format and mypy on type_map.py are clean.

Converting a naive datetime.datetime to a UnixTimestamp value called
datetime.timestamp(), which reads a naive value in the machine's local
timezone. A naive pd.Timestamp or np.datetime64 is read as UTC, as is a
naive ZonedTimestamp, and Feast treats naive datetimes as UTC elsewhere
(make_tzaware). So the same wall-clock value was stored at a different
instant depending on its Python type and on the host timezone, e.g. a
naive request timestamp passed to get_online_features came back shifted
by the server's UTC offset.

Attach UTC to naive datetimes before taking the timestamp. This covers
UnixTimestamp scalars, lists and sets.

Signed-off-by: Mohammad Hijjawi <mohammad.hijjawi1997@gmail.com>
MohammadHijjawi97 requested a review from a team as a code owner October 5, 2026 03:04

jyejare left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The change fixes the common case where naive datetimes were interpreted in the host’s local timezone instead of UTC, and the regression test covers scalar, list, and set values. One edge case remains: Python also considers datetimes with a non-None tzinfo but a None utcoffset() to be naive, and those still use the local timezone. The test setup could also be more explicit on platforms where it cannot set the process timezone.

Comment thread sdk/python/feast/type_map.py Outdated
Comment on lines +728 to +730
# A naive datetime is UTC, as everywhere else in Feast. Without this,
# datetime.timestamp() would read it in the machine's local timezone.
if value.tzinfo is None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

A datetime is naive not only when tzinfo is None, but also when tzinfo is set and utcoffset() returns None. For that valid case, this condition leaves the value unchanged, so timestamp() still interprets it in the machine’s local timezone. Consider checking value.utcoffset() is None instead.

Suggested:

Suggested change
# A naive datetime is UTC, as everywhere else in Feast. Without this,
# datetime.timestamp() would read it in the machine's local timezone.
if value.tzinfo is None:
if value.utcoffset() is None:
value = value.replace(tzinfo=timezone.utc)
int_timestamps.append(int(value.timestamp()))

Comment thread sdk/python/tests/unit/test_type_map.py Outdated
Comment on lines +57 to +64
@pytest.fixture
def non_utc_local_timezone(monkeypatch):
"""Run the test with a local timezone that is not UTC, where tzset exists."""
if hasattr(time, "tzset"):
monkeypatch.setenv("TZ", "America/Los_Angeles")
time.tzset()
yield
if hasattr(time, "tzset"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

On platforms without time.tzset, this fixture does not change the process timezone, but the test still runs. If that platform happens to use UTC, the test passes without exercising the regression. Consider skipping the test when tzset is unavailable, or otherwise ensuring the test verifies that the local timezone actually changed.

Suggested:

Suggested change
@pytest.fixture
def non_utc_local_timezone(monkeypatch):
"""Run the test with a local timezone that is not UTC, where tzset exists."""
if hasattr(time, "tzset"):
monkeypatch.setenv("TZ", "America/Los_Angeles")
time.tzset()
yield
if hasattr(time, "tzset"):
if not hasattr(time, "tzset"):
pytest.skip("Changing the process timezone requires time.tzset")
monkeypatch.setenv("TZ", "America/Los_Angeles")
time.tzset()
yield
monkeypatch.undo()
time.tzset()

Comment thread sdk/python/tests/unit/test_type_map.py Outdated
Comment on lines +59 to +60
"""Run the test with a local timezone that is not UTC, where tzset exists."""
if hasattr(time, "tzset"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

America/Los_Angeles depends on timezone database availability. In minimal environments without that data, setting TZ may not produce a non-UTC timezone, weakening the regression test. A POSIX-style fixed offset such as UTC+8, or an explicit assertion that the resulting local offset is nonzero, would make the test’s setup more reliable.

Suggested:

Suggested change
"""Run the test with a local timezone that is not UTC, where tzset exists."""
if hasattr(time, "tzset"):
monkeypatch.setenv("TZ", "UTC+8")
time.tzset()
assert datetime.now().astimezone().utcoffset() != timezone.utc.utcoffset(None)

Comment on lines +75 to +88
],
)
def test_naive_datetime_unix_timestamp_is_utc(non_utc_local_timezone, value_type):
"""A naive datetime is read as UTC, not in the local timezone."""
naive = datetime(2024, 7, 1, 12, 0, 0)
value = naive if value_type == ValueType.UNIX_TIMESTAMP else [naive]

proto = python_values_to_proto_values([value], value_type)[0]
converted = feast_value_type_to_python_type(proto)

expected = naive.replace(tzinfo=timezone.utc)
if value_type == ValueType.UNIX_TIMESTAMP:
assert converted == expected
else:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The production condition misses datetimes whose tzinfo is non-None but whose utcoffset() is None. Adding a small custom tzinfo test case would lock in Python’s full definition of a naive datetime and prevent this edge case from regressing.

A datetime is naive when utcoffset() returns None, which includes a
tzinfo whose utcoffset() is None, not only tzinfo=None. Check
utcoffset() so those values are also read as UTC instead of raising.

Make the regression test stricter: skip it where time.tzset is not
available, use the fixed POSIX offset UTC+8 so no timezone database is
needed, assert the local offset really changed, and cover a custom
tzinfo whose utcoffset() is None.

Signed-off-by: Mohammad Hijjawi <mohammad.hijjawi1997@gmail.com>

codecov-commenter commented Oct 7, 2026 •
edited
Loading

Copy link
Copy Markdown

⚠️ Please install the to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.32%. Comparing base (b9c65f1) to head (4a821e8).
⚠️ Report is 4 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

@@            Coverage Diff             @@
##           master    #6937      +/-   ##
==========================================
+ Coverage   49.06%   49.32%   +0.25%     
==========================================
  Files         435      443       +8     
  Lines       54528    55080     +552     
  Branches     7954     8016      +62     
==========================================
+ Hits        26752    27166     +414     
- Misses      25898    26030     +132     
- Partials     1878     1884       +6     
Flag Coverage Δ
go-feature-server 30.58% <ø> (ø)
python-unit 50.71% <100.00%> (+0.26%) ⬆️
Files with missing lines Coverage Δ
sdk/python/feast/type_map.py 64.11% <100.00%> (+0.57%) ⬆️

... and 12 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6259f5c...4a821e8. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor Author

Thanks @jyejare, all four applied in 8983022:

  1. type_map.py: the naive check is now value.utcoffset() is None, so a tzinfo whose utcoffset() returns None is treated as naive and read as UTC. (With the old check, timestamp() actually raised "can't subtract offset-naive and offset-aware datetimes" for these values.)
  2. The fixture now calls pytest.skip when time.tzset isn't available, so the test can't pass without exercising the regression.
  3. It uses the POSIX fixed offset UTC+8 instead of America/Los_Angeles, and asserts the local UTC offset is nonzero after tzset().
  4. The test is also parametrized with a custom tzinfo whose utcoffset() returns None, for the scalar, list and set types.

ruff and mypy are clean on the touched files.

jyejare enabled auto-merge (rebase) October 7, 2026 11:31
jyejare merged commit 687bee8 into feast-dev:master Oct 7, 2026
19 of 23 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL