From 6d8679cb7c4fcc246e2bfcd58845e7aea1c05fac Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 9 Jul 2026 14:03:03 -0400 Subject: [PATCH 1/3] fix: Use configured timestamp column names in RemoteOnlineStore.online_write_batch RemoteOnlineStore.online_write_batch hardcoded "event_timestamp" and "created" as column names when serializing data for the feature server. On the server side, _convert_arrow_fv_to_proto resolves columns by the feature view's configured batch_source.timestamp_field and batch_source.created_timestamp_column. When those differ from the hardcoded defaults (e.g. "created_at"), the server throws a KeyError. Use the feature view's configured column names instead, falling back to the previous defaults when unset. Closes #6595 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Francisco Javier Arceo --- .../feast/infra/online_stores/remote.py | 8 +- .../online_store/test_remote_online_store.py | 143 ++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 05a6b05dbea..9cb4fabc517 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -251,6 +251,9 @@ def online_write_batch( columnar_data: Dict[str, List[Any]] = defaultdict(list) + event_col = table.batch_source.timestamp_field or "event_timestamp" + created_col = table.batch_source.created_timestamp_column or "created" + # Iterate through each row to populate columnar data directly for entity_key_proto, feature_values_proto, event_ts, created_ts in data: # Populate entity key values @@ -267,9 +270,8 @@ def online_write_batch( self._proto_value_to_transport_value(feature_value_proto) ) - # Populate timestamps - columnar_data["event_timestamp"].append(_to_naive_utc(event_ts).isoformat()) - columnar_data["created"].append( + columnar_data[event_col].append(_to_naive_utc(event_ts).isoformat()) + columnar_data[created_col].append( _to_naive_utc(created_ts).isoformat() if created_ts else None ) diff --git a/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py b/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py index 13c1392fe22..e85c4752176 100644 --- a/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py +++ b/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py @@ -1,6 +1,7 @@ import inspect import json from datetime import datetime, timedelta +from typing import Optional from unittest.mock import Mock, patch import pytest @@ -661,3 +662,145 @@ def test_all_base_class_params_forwarded( assert expected_keys.issubset(req_body.keys()), ( f"Missing keys in req_body: {expected_keys - req_body.keys()}" ) + + +def _make_entity_key(join_key: str, value: int) -> EntityKeyProto: + ek = EntityKeyProto() + ek.join_keys.append(join_key) + v = ValueProto() + v.int64_val = value + ek.entity_values.append(v) + return ek + + +def _make_feature_values(feature_name: str, value: float) -> dict: + v = ValueProto() + v.double_val = value + return {feature_name: v} + + +def _make_write_data( + join_key: str, + feature_name: str, + event_ts: datetime, + created_ts: Optional[datetime], +): + return [( + _make_entity_key(join_key, 1001), + _make_feature_values(feature_name, 0.5), + event_ts, + created_ts, + )] + + +class TestRemoteOnlineStoreWriteBatch: + """Tests for RemoteOnlineStore.online_write_batch timestamp column naming.""" + + @pytest.fixture + def remote_store(self): + return RemoteOnlineStore() + + @pytest.fixture + def config(self): + return RepoConfig( + project="test_project", + online_store=RemoteOnlineStoreConfig( + type="remote", path="http://localhost:6566" + ), + registry="dummy_registry", + ) + + @staticmethod + def _make_feature_view( + timestamp_field: str = "event_timestamp", + created_timestamp_column: str = "created", + ) -> FeatureView: + entity = Entity( + name="driver", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + source = FileSource( + path="test.parquet", + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + ) + fv = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=source, + ) + return fv + + @patch("feast.infra.online_stores.remote.post_remote_online_write") + def test_write_batch_uses_default_timestamp_columns( + self, mock_post, remote_store, config + ): + """With default column names, the request body should use + 'event_timestamp' and 'created'.""" + mock_resp = Mock() + mock_resp.status_code = 200 + mock_post.return_value = mock_resp + + fv = self._make_feature_view() + now = datetime(2024, 1, 1, 12, 0, 0) + data = _make_write_data("driver_id", "conv_rate", now, now) + + remote_store.online_write_batch(config, fv, data, progress=None) + + req_body = mock_post.call_args[1]["req_body"] + df = req_body["df"] + assert "event_timestamp" in df + assert "created" in df + + @patch("feast.infra.online_stores.remote.post_remote_online_write") + def test_write_batch_uses_custom_timestamp_columns( + self, mock_post, remote_store, config + ): + """When the feature view's batch source uses custom column names, + the request body must use those names — not hardcoded defaults.""" + mock_resp = Mock() + mock_resp.status_code = 200 + mock_post.return_value = mock_resp + + fv = self._make_feature_view( + timestamp_field="ts", + created_timestamp_column="created_at", + ) + now = datetime(2024, 1, 1, 12, 0, 0) + data = _make_write_data("driver_id", "conv_rate", now, now) + + remote_store.online_write_batch(config, fv, data, progress=None) + + req_body = mock_post.call_args[1]["req_body"] + df = req_body["df"] + assert "ts" in df, "Expected custom timestamp_field 'ts' in request" + assert "created_at" in df, "Expected custom created_timestamp_column 'created_at' in request" + assert "event_timestamp" not in df, "Hardcoded 'event_timestamp' should not appear" + assert "created" not in df, "Hardcoded 'created' should not appear" + + @patch("feast.infra.online_stores.remote.post_remote_online_write") + def test_write_batch_with_no_created_timestamp_column( + self, mock_post, remote_store, config + ): + """When created_timestamp_column is empty, should fall back to 'created'.""" + mock_resp = Mock() + mock_resp.status_code = 200 + mock_post.return_value = mock_resp + + fv = self._make_feature_view( + timestamp_field="my_event_ts", + created_timestamp_column="", + ) + now = datetime(2024, 1, 1, 12, 0, 0) + data = _make_write_data("driver_id", "conv_rate", now, None) + + remote_store.online_write_batch(config, fv, data, progress=None) + + req_body = mock_post.call_args[1]["req_body"] + df = req_body["df"] + assert "my_event_ts" in df + assert "created" in df From 9a104b63b65e216223f15edd5104337a2d8d3145 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 9 Jul 2026 14:11:23 -0400 Subject: [PATCH 2/3] style: Fix ruff formatting in test_remote_online_store.py Co-Authored-By: Claude Opus 4.6 Signed-off-by: Francisco Javier Arceo --- .../online_store/test_remote_online_store.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py b/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py index e85c4752176..0062ea8b4e2 100644 --- a/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py +++ b/sdk/python/tests/unit/infra/online_store/test_remote_online_store.py @@ -685,12 +685,14 @@ def _make_write_data( event_ts: datetime, created_ts: Optional[datetime], ): - return [( - _make_entity_key(join_key, 1001), - _make_feature_values(feature_name, 0.5), - event_ts, - created_ts, - )] + return [ + ( + _make_entity_key(join_key, 1001), + _make_feature_values(feature_name, 0.5), + event_ts, + created_ts, + ) + ] class TestRemoteOnlineStoreWriteBatch: @@ -778,8 +780,12 @@ def test_write_batch_uses_custom_timestamp_columns( req_body = mock_post.call_args[1]["req_body"] df = req_body["df"] assert "ts" in df, "Expected custom timestamp_field 'ts' in request" - assert "created_at" in df, "Expected custom created_timestamp_column 'created_at' in request" - assert "event_timestamp" not in df, "Hardcoded 'event_timestamp' should not appear" + assert "created_at" in df, ( + "Expected custom created_timestamp_column 'created_at' in request" + ) + assert "event_timestamp" not in df, ( + "Hardcoded 'event_timestamp' should not appear" + ) assert "created" not in df, "Hardcoded 'created' should not appear" @patch("feast.infra.online_stores.remote.post_remote_online_write") From 30d0fdc87a7d372bc44f6a50cd37915ca23b1c06 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Thu, 9 Jul 2026 14:33:42 -0400 Subject: [PATCH 3/3] fix: Guard against None batch_source for mypy Co-Authored-By: Claude Opus 4.6 Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/infra/online_stores/remote.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 9cb4fabc517..17538ee7b81 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -251,8 +251,12 @@ def online_write_batch( columnar_data: Dict[str, List[Any]] = defaultdict(list) - event_col = table.batch_source.timestamp_field or "event_timestamp" - created_col = table.batch_source.created_timestamp_column or "created" + if table.batch_source is not None: + event_col = table.batch_source.timestamp_field or "event_timestamp" + created_col = table.batch_source.created_timestamp_column or "created" + else: + event_col = "event_timestamp" + created_col = "created" # Iterate through each row to populate columnar data directly for entity_key_proto, feature_values_proto, event_ts, created_ts in data: