Skip to content

Commit 1e7134f

Browse files
falloficarussntkathole
authored andcommitted
fix: Serialize UnixTimestamp proto values as raw int64 in remote online store transport
Signed-off-by: Abhishek Shinde <[email protected]>
1 parent 43ae993 commit 1e7134f

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

‎sdk/python/feast/infra/online_stores/remote.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ def _proto_value_to_transport_value(proto_value: ValueProto) -> Any:
151151
if val_attr in ("uuid_set_val", "time_uuid_set_val"):
152152
return list(getattr(proto_value, val_attr).val)
153153

154+
# UnixTimestamp values are stored as int64 (epoch seconds) in proto.
155+
# Return them directly to avoid feast_value_type_to_python_type
156+
# converting to datetime objects which are not JSON-serializable.
157+
if val_attr == "unix_timestamp_val":
158+
return getattr(proto_value, val_attr)
159+
if val_attr in ("unix_timestamp_list_val", "unix_timestamp_set_val"):
160+
return list(getattr(proto_value, val_attr).val)
161+
154162
return feast_value_type_to_python_type(proto_value)
155163

156164
_STATUS_MAP = {

‎sdk/python/tests/unit/infra/online_store/test_remote_online_store.py‎

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from feast.online_response import OnlineResponse
1313
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
1414
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
15-
from feast.types import Float32, Int64, String
15+
from feast.types import Float32, Int64, String, UnixTimestamp
1616
from feast.value_type import ValueType
1717

1818

@@ -661,3 +661,121 @@ def test_all_base_class_params_forwarded(
661661
assert expected_keys.issubset(req_body.keys()), (
662662
f"Missing keys in req_body: {expected_keys - req_body.keys()}"
663663
)
664+
665+
666+
class TestProtoValueToTransportValue:
667+
"""Tests for RemoteOnlineStore._proto_value_to_transport_value."""
668+
669+
def test_unix_timestamp_val_returns_raw_int(self):
670+
"""UnixTimestamp scalar should return the raw int64 epoch seconds."""
671+
proto_val = ValueProto(unix_timestamp_val=1700000000)
672+
result = RemoteOnlineStore._proto_value_to_transport_value(proto_val)
673+
assert result == 1700000000
674+
assert isinstance(result, int)
675+
676+
def test_unix_timestamp_list_val_returns_int_list(self):
677+
"""UnixTimestamp list should return a list of int64 epoch seconds."""
678+
proto_val = ValueProto()
679+
proto_val.unix_timestamp_list_val.val.extend([1700000000, 1700000001])
680+
result = RemoteOnlineStore._proto_value_to_transport_value(proto_val)
681+
assert result == [1700000000, 1700000001]
682+
assert all(isinstance(v, int) for v in result)
683+
684+
def test_unix_timestamp_set_val_returns_flat_list(self):
685+
"""UnixTimestamp set should return a list (not a set) of int64
686+
epoch seconds, so the value is JSON-serializable."""
687+
proto_val = ValueProto()
688+
proto_val.unix_timestamp_set_val.val.extend([1700000000, 1700000001])
689+
result = RemoteOnlineStore._proto_value_to_transport_value(proto_val)
690+
assert result == [1700000000, 1700000001]
691+
assert isinstance(result, list)
692+
693+
def test_unix_timestamp_val_null_sentinel_returns_raw_int(self):
694+
"""The NULL_TIMESTAMP sentinel should still be passed through as
695+
a raw int64 — it is up to the server to interpret it."""
696+
proto_val = ValueProto(unix_timestamp_val=-9223372036854775808)
697+
result = RemoteOnlineStore._proto_value_to_transport_value(proto_val)
698+
assert result == -9223372036854775808
699+
assert isinstance(result, int)
700+
701+
def test_empty_proto_returns_none(self):
702+
"""A ValueProto with no val set should return None."""
703+
proto_val = ValueProto()
704+
result = RemoteOnlineStore._proto_value_to_transport_value(proto_val)
705+
assert result is None
706+
707+
708+
class TestRemoteOnlineStoreWriteBatch:
709+
"""Tests for RemoteOnlineStore.online_write_batch."""
710+
711+
@pytest.fixture
712+
def remote_store(self):
713+
return RemoteOnlineStore()
714+
715+
@pytest.fixture
716+
def config(self):
717+
return RepoConfig(
718+
project="test_project",
719+
online_store=RemoteOnlineStoreConfig(
720+
type="remote", path="http://localhost:6566"
721+
),
722+
registry="dummy_registry",
723+
)
724+
725+
@pytest.fixture
726+
def feature_view(self):
727+
entity = Entity(
728+
name="user_id", description="User ID", value_type=ValueType.INT64
729+
)
730+
source = FileSource(path="test.parquet", timestamp_field="event_timestamp")
731+
return FeatureView(
732+
name="test_feature_view",
733+
entities=[entity],
734+
ttl=timedelta(days=1),
735+
schema=[
736+
Field(name="user_id", dtype=Int64),
737+
Field(name="feature1", dtype=String),
738+
Field(name="feature2", dtype=UnixTimestamp),
739+
],
740+
source=source,
741+
)
742+
743+
@patch("feast.infra.online_stores.remote.post_remote_online_write")
744+
def test_unix_timestamp_value_serialized_as_int(
745+
self, mock_post, remote_store, config, feature_view
746+
):
747+
"""online_write_batch should send int64 epoch seconds in the
748+
DataFrame for UnixTimestamp features."""
749+
mock_response = Mock()
750+
mock_response.status_code = 200
751+
mock_post.return_value = mock_response
752+
753+
entity_key = EntityKeyProto(
754+
join_keys=["user_id"],
755+
entity_values=[ValueProto(int64_val=42)],
756+
)
757+
feature_values = {
758+
"feature1": ValueProto(string_val="hello"),
759+
"feature2": ValueProto(unix_timestamp_val=1700000000),
760+
}
761+
event_ts = datetime(2023, 11, 15, 0, 0, 0)
762+
created_ts = datetime(2023, 11, 14, 0, 0, 0)
763+
data = [(entity_key, feature_values, event_ts, created_ts)]
764+
765+
remote_store.online_write_batch(
766+
config=config, table=feature_view, data=data, progress=None
767+
)
768+
769+
mock_post.assert_called_once()
770+
req_body = mock_post.call_args[1]["req_body"]
771+
df = req_body["df"]
772+
773+
# UnixTimestamp feature value must be a raw int, not a datetime
774+
assert df["feature2"] == [1700000000]
775+
assert isinstance(df["feature2"][0], int)
776+
777+
# Other feature types should remain unchanged
778+
assert df["feature1"] == ["hello"]
779+
780+
# Event timestamps should be ISO strings as before
781+
assert df["event_timestamp"] == ["2023-11-15T00:00:00"]

0 commit comments

Comments
 (0)