Skip to content

Commit 43674ac

Browse files
fix: Resolve three versioning regressions from review feedback
1. Stop injecting version_tag on FeatureService projections in utils.py, which was causing non-SQLite stores to reject FeatureService reads when versioning was enabled. 2. Persist version_tag in FeatureViewProjection proto (field 10) so it survives registry round-trips. 3. Fix _update_metadata_fields() to reset current_version_number to 0 when unpinning a feature view back to version="latest". Update tests to match new behavior and add test_unpin_from_versioned_to_latest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 280daf6 commit 43674ac

File tree

7 files changed

+125
-98
lines changed

7 files changed

+125
-98
lines changed
Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
1-
syntax = "proto3";
2-
package feast.core;
3-
4-
option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
5-
option java_outer_classname = "FeatureReferenceProto";
6-
option java_package = "feast.proto.core";
7-
8-
import "feast/core/Feature.proto";
9-
import "feast/core/DataSource.proto";
10-
11-
12-
// A projection to be applied on top of a FeatureView.
13-
// Contains the modifications to a FeatureView such as the features subset to use.
14-
message FeatureViewProjection {
15-
// The feature view name
16-
string feature_view_name = 1;
17-
18-
// Alias for feature view name
19-
string feature_view_name_alias = 3;
20-
21-
// The features of the feature view that are a part of the feature reference.
22-
repeated FeatureSpecV2 feature_columns = 2;
23-
24-
// Map for entity join_key overrides of feature data entity join_key to entity data join_key
25-
map<string,string> join_key_map = 4;
26-
27-
string timestamp_field = 5;
28-
string date_partition_column = 6;
29-
string created_timestamp_column = 7;
30-
// Batch/Offline DataSource where this view can retrieve offline feature data.
31-
DataSource batch_source = 8;
32-
// Streaming DataSource from where this view can consume "online" feature data.
33-
DataSource stream_source = 9;
34-
35-
}
1+
syntax = "proto3";
2+
package feast.core;
3+
4+
option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
5+
option java_outer_classname = "FeatureReferenceProto";
6+
option java_package = "feast.proto.core";
7+
8+
import "feast/core/Feature.proto";
9+
import "feast/core/DataSource.proto";
10+
11+
12+
// A projection to be applied on top of a FeatureView.
13+
// Contains the modifications to a FeatureView such as the features subset to use.
14+
message FeatureViewProjection {
15+
// The feature view name
16+
string feature_view_name = 1;
17+
18+
// Alias for feature view name
19+
string feature_view_name_alias = 3;
20+
21+
// The features of the feature view that are a part of the feature reference.
22+
repeated FeatureSpecV2 feature_columns = 2;
23+
24+
// Map for entity join_key overrides of feature data entity join_key to entity data join_key
25+
map<string,string> join_key_map = 4;
26+
27+
string timestamp_field = 5;
28+
string date_partition_column = 6;
29+
string created_timestamp_column = 7;
30+
// Batch/Offline DataSource where this view can retrieve offline feature data.
31+
DataSource batch_source = 8;
32+
// Streaming DataSource from where this view can consume "online" feature data.
33+
DataSource stream_source = 9;
34+
35+
// Optional version tag for version-qualified feature references (e.g., @v2).
36+
int32 version_tag = 10;
37+
38+
}

sdk/python/feast/feature_view_projection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def to_proto(self) -> FeatureViewProjectionProto:
7474
for feature in self.features:
7575
feature_reference_proto.feature_columns.append(feature.to_proto())
7676

77+
if self.version_tag is not None:
78+
feature_reference_proto.version_tag = self.version_tag
79+
7780
return feature_reference_proto
7881

7982
@staticmethod
@@ -97,6 +100,9 @@ def from_proto(proto: FeatureViewProjectionProto) -> "FeatureViewProjection":
97100
for feature_column in proto.feature_columns:
98101
feature_view_projection.features.append(Field.from_proto(feature_column))
99102

103+
if proto.version_tag > 0:
104+
feature_view_projection.version_tag = proto.version_tag
105+
100106
return feature_view_projection
101107

102108
@staticmethod

sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class FeatureViewProjection(google.protobuf.message.Message):
4949
CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int
5050
BATCH_SOURCE_FIELD_NUMBER: builtins.int
5151
STREAM_SOURCE_FIELD_NUMBER: builtins.int
52+
VERSION_TAG_FIELD_NUMBER: builtins.int
5253
feature_view_name: builtins.str
5354
"""The feature view name"""
5455
feature_view_name_alias: builtins.str
@@ -68,6 +69,8 @@ class FeatureViewProjection(google.protobuf.message.Message):
6869
@property
6970
def stream_source(self) -> feast.core.DataSource_pb2.DataSource:
7071
"""Streaming DataSource from where this view can consume "online" feature data."""
72+
version_tag: builtins.int
73+
"""Optional version tag for version-qualified feature references (e.g., @v2)."""
7174
def __init__(
7275
self,
7376
*,
@@ -80,8 +83,9 @@ class FeatureViewProjection(google.protobuf.message.Message):
8083
created_timestamp_column: builtins.str = ...,
8184
batch_source: feast.core.DataSource_pb2.DataSource | None = ...,
8285
stream_source: feast.core.DataSource_pb2.DataSource | None = ...,
86+
version_tag: builtins.int = ...,
8387
) -> None: ...
8488
def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "stream_source", b"stream_source"]) -> builtins.bool: ...
85-
def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "created_timestamp_column", b"created_timestamp_column", "date_partition_column", b"date_partition_column", "feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map", "stream_source", b"stream_source", "timestamp_field", b"timestamp_field"]) -> None: ...
89+
def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "created_timestamp_column", b"created_timestamp_column", "date_partition_column", b"date_partition_column", "feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map", "stream_source", b"stream_source", "timestamp_field", b"timestamp_field", "version_tag", b"version_tag"]) -> None: ...
8690

8791
global___FeatureViewProjection = FeatureViewProjection

sdk/python/feast/utils.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,17 +1237,6 @@ def _get_features(
12371237

12381238
# Build feature reference list
12391239
for projection in feature_service_from_registry.feature_view_projections:
1240-
if getattr(registry, "enable_online_versioning", False):
1241-
try:
1242-
fv = registry.get_any_feature_view(
1243-
projection.name, project, allow_cache
1244-
)
1245-
ver = getattr(fv, "current_version_number", None)
1246-
if ver is not None and ver > 0:
1247-
projection = copy.copy(projection)
1248-
projection.version_tag = ver
1249-
except Exception:
1250-
pass
12511240
_feature_refs.extend(
12521241
[f"{projection.name_to_use()}:{f.name}" for f in projection.features]
12531242
)
@@ -1333,28 +1322,6 @@ def _get_feature_views_to_use(
13331322
fv.projection.version_tag = version_num
13341323
else:
13351324
fv = registry.get_any_feature_view(name, project, allow_cache)
1336-
# Gate: feature services must not resolve to versioned FVs when online versioning is off
1337-
cur_ver: Optional[int] = getattr(fv, "current_version_number", None)
1338-
if (
1339-
isinstance(features, FeatureService)
1340-
and cur_ver is not None
1341-
and cur_ver > 0
1342-
and not getattr(registry, "enable_online_versioning", False)
1343-
):
1344-
raise ValueError(
1345-
f"Feature service references feature view '{name}' which is at version "
1346-
f"v{cur_ver}, but online versioning is disabled. "
1347-
f"Set 'enable_online_feature_view_versioning: true' under 'registry' "
1348-
f"in feature_store.yaml."
1349-
)
1350-
# For FeatureService refs: resolve the active version when online versioning is on
1351-
if (
1352-
isinstance(features, FeatureService)
1353-
and cur_ver is not None
1354-
and cur_ver > 0
1355-
and getattr(registry, "enable_online_versioning", False)
1356-
):
1357-
version_num = cur_ver
13581325

13591326
if isinstance(fv, OnDemandFeatureView):
13601327
od_fvs_to_use.append(

sdk/python/tests/integration/registration/test_versioning.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -994,10 +994,10 @@ def _make_store(self, tmpdir, enable_versioning=False):
994994
)
995995
)
996996

997-
def test_feature_service_apply_fails_with_versioned_fv_when_flag_off(
997+
def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_off(
998998
self, versioned_fv_and_entity
999999
):
1000-
"""Apply a feature service referencing a versioned FV with flag off -> ValueError."""
1000+
"""Apply a feature service referencing a versioned FV with flag off -> succeeds."""
10011001
entity, fv_v0, fv_v1 = versioned_fv_and_entity
10021002

10031003
with tempfile.TemporaryDirectory() as tmpdir:
@@ -1012,9 +1012,7 @@ def test_feature_service_apply_fails_with_versioned_fv_when_flag_off(
10121012
name="driver_service",
10131013
features=[fv_v1],
10141014
)
1015-
1016-
with pytest.raises(ValueError, match="version v1"):
1017-
store.apply([fs])
1015+
store.apply([fs]) # Should not raise
10181016

10191017
def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_on(
10201018
self, versioned_fv_and_entity
@@ -1036,10 +1034,10 @@ def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_on(
10361034
)
10371035
store.apply([fs]) # Should not raise
10381036

1039-
def test_feature_service_retrieval_fails_with_versioned_fv_when_flag_off(
1037+
def test_feature_service_retrieval_succeeds_with_versioned_fv_when_flag_off(
10401038
self, versioned_fv_and_entity
10411039
):
1042-
"""get_online_features with a feature service referencing a versioned FV, flag off -> ValueError."""
1040+
"""get_online_features with a feature service referencing a versioned FV, flag off -> succeeds."""
10431041
entity, fv_v0, fv_v1 = versioned_fv_and_entity
10441042
from feast.utils import _get_feature_views_to_use
10451043

@@ -1060,14 +1058,15 @@ def test_feature_service_retrieval_fails_with_versioned_fv_when_flag_off(
10601058
"driver_service", "test_project"
10611059
)
10621060

1063-
with pytest.raises(ValueError, match="online versioning is disabled"):
1064-
_get_feature_views_to_use(
1065-
registry=store_off.registry,
1066-
project="test_project",
1067-
features=registered_fs,
1068-
allow_cache=False,
1069-
hide_dummy_entity=False,
1070-
)
1061+
fvs, _ = _get_feature_views_to_use(
1062+
registry=store_off.registry,
1063+
project="test_project",
1064+
features=registered_fs,
1065+
allow_cache=False,
1066+
hide_dummy_entity=False,
1067+
)
1068+
1069+
assert len(fvs) == 1
10711070

10721071
def test_feature_service_with_unversioned_fv_succeeds(
10731072
self, unversioned_fv_and_entity
@@ -1088,7 +1087,8 @@ def test_feature_service_with_unversioned_fv_succeeds(
10881087
def test_feature_service_serves_versioned_fv_when_flag_on(
10891088
self, versioned_fv_and_entity
10901089
):
1091-
"""With online versioning on, FeatureService projections carry the correct version_tag."""
1090+
"""With online versioning on, FeatureService projections do not carry version_tag;
1091+
the FV in the registry carries current_version_number."""
10921092
from feast.utils import _get_feature_views_to_use
10931093

10941094
entity, fv_v0, fv_v1 = versioned_fv_and_entity
@@ -1121,13 +1121,19 @@ def test_feature_service_serves_versioned_fv_when_flag_on(
11211121
)
11221122

11231123
assert len(fvs) == 1
1124-
assert fvs[0].projection.version_tag == 1
1125-
assert fvs[0].projection.name_to_use() == "driver_stats@v1"
1124+
assert fvs[0].projection.version_tag is None
1125+
assert fvs[0].projection.name_to_use() == "driver_stats"
1126+
1127+
# Verify the FV in the registry has the correct version
1128+
fv_from_registry = store.registry.get_feature_view(
1129+
"driver_stats", "test_project"
1130+
)
1131+
assert fv_from_registry.current_version_number == 1
11261132

1127-
def test_feature_service_feature_refs_include_version_when_flag_on(
1133+
def test_feature_service_feature_refs_are_plain_when_flag_on(
11281134
self, versioned_fv_and_entity
11291135
):
1130-
"""With online versioning on, _get_features() produces version-qualified refs."""
1136+
"""With online versioning on, _get_features() produces plain (non-versioned) refs for FeatureService."""
11311137
from feast.utils import _get_features
11321138

11331139
entity, fv_v0, fv_v1 = versioned_fv_and_entity
@@ -1158,9 +1164,44 @@ def test_feature_service_feature_refs_include_version_when_flag_on(
11581164
allow_cache=False,
11591165
)
11601166

1161-
# All refs should be version-qualified
1167+
# Refs should be plain (no version qualifier)
11621168
for ref in refs:
1163-
assert "@v1:" in ref, f"Expected version-qualified ref, got: {ref}"
1169+
assert "@v" not in ref, f"Expected plain ref, got: {ref}"
11641170

11651171
# Check specific ref format
1166-
assert "driver_stats@v1:trips_today" in refs
1172+
assert "driver_stats:trips_today" in refs
1173+
1174+
def test_unpin_from_versioned_to_latest(self, versioned_fv_and_entity):
1175+
"""Pin a FV to v1, then apply with version='latest' (no schema change) -> unpinned."""
1176+
entity, fv_v0, fv_v1 = versioned_fv_and_entity
1177+
1178+
with tempfile.TemporaryDirectory() as tmpdir:
1179+
store = self._make_store(tmpdir, enable_versioning=True)
1180+
1181+
# Apply v0 then v1 to create version history (v1 has schema change)
1182+
store.apply([entity, fv_v0])
1183+
store.apply([entity, fv_v1])
1184+
1185+
# Verify it's pinned to v1
1186+
reloaded = store.registry.get_feature_view("driver_stats", "test_project")
1187+
assert reloaded.current_version_number == 1
1188+
1189+
# Now re-apply the same schema with version="latest" to unpin
1190+
fv_latest = FeatureView(
1191+
name="driver_stats",
1192+
entities=[entity],
1193+
ttl=timedelta(days=1),
1194+
schema=[
1195+
Field(name="driver_id", dtype=Int64),
1196+
Field(name="trips_today", dtype=Int64),
1197+
Field(name="avg_rating", dtype=Float32),
1198+
],
1199+
version="latest",
1200+
description="v1",
1201+
)
1202+
store.apply([entity, fv_latest])
1203+
1204+
# Reload and verify unpinned
1205+
reloaded = store.registry.get_feature_view("driver_stats", "test_project")
1206+
assert reloaded.current_version_number is None
1207+
assert reloaded.version == "latest"

sdk/python/tests/unit/test_feature_view_versioning.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,10 @@ def test_v1_with_suffix(self):
440440
ttl=timedelta(days=1),
441441
)
442442
fv.current_version_number = 1
443-
assert _table_id("my_project", fv) == "my_project_test_fv_v1"
443+
assert (
444+
_table_id("my_project", fv, enable_versioning=True)
445+
== "my_project_test_fv_v1"
446+
)
444447

445448
def test_v5_with_suffix(self):
446449
from datetime import timedelta
@@ -456,7 +459,10 @@ def test_v5_with_suffix(self):
456459
ttl=timedelta(days=1),
457460
)
458461
fv.current_version_number = 5
459-
assert _table_id("my_project", fv) == "my_project_test_fv_v5"
462+
assert (
463+
_table_id("my_project", fv, enable_versioning=True)
464+
== "my_project_test_fv_v5"
465+
)
460466

461467

462468
class TestValidateFeatureRefsVersioned:

0 commit comments

Comments
 (0)