Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a90cb7f
feat: Add version tracking to FeatureView, StreamFeatureView, and OnD…
franciscojavierarceo Mar 12, 2026
f28942b
fix: Address PR review feedback from Devin
franciscojavierarceo Mar 12, 2026
171785e
docs: Add feature view versioning documentation
franciscojavierarceo Mar 13, 2026
f035e96
fix: Address second round of PR review feedback from Devin
franciscojavierarceo Mar 13, 2026
0c12655
fix: Clean up version history on delete and use write_engine consiste…
franciscojavierarceo Mar 13, 2026
d32ed52
docs: Clarify versioning auto-increment behavior and pin/revert flow
franciscojavierarceo Mar 13, 2026
f9e896f
fix: Add pin conflict detection to both file and SQL registries
franciscojavierarceo Mar 13, 2026
2069b22
fix: Address Devin review feedback on versioning
franciscojavierarceo Mar 13, 2026
83393aa
docs: Document concurrent multi-version serving limitations
franciscojavierarceo Mar 13, 2026
94afe6e
feat: Implement version-qualified feature references (@v<N>)
franciscojavierarceo Mar 14, 2026
76d1afc
fix: Resolve mypy type errors in proto_registry_utils.py
franciscojavierarceo Mar 15, 2026
2541e41
feat: Add version metadata to clean @v2 syntax from feature names
franciscojavierarceo Mar 16, 2026
bceb052
fix: Update provider implementations with version metadata parameter
franciscojavierarceo Mar 16, 2026
14b2da0
fix: Add version metadata parameter to all online store implementations
franciscojavierarceo Mar 16, 2026
fd776fc
fix: Resolve mypy type errors in versioning code
franciscojavierarceo Mar 17, 2026
e9c4c68
fix: Address Devin review feedback on versioning
franciscojavierarceo Mar 17, 2026
903bda5
fix: Address additional Devin review feedback
franciscojavierarceo Mar 17, 2026
af47911
Merge branch 'master' into featureview-versioning
franciscojavierarceo Mar 17, 2026
dd31cdb
feat: Make feature view versioning opt-in via registry config
franciscojavierarceo Mar 17, 2026
8809805
fix: Address Devin review feedback on versioning issues
franciscojavierarceo Mar 17, 2026
d23c4bb
fix: Preserve version tag in response column names for multi-version …
franciscojavierarceo Mar 17, 2026
c5d4b49
feat: Handle version race conditions gracefully with retry and forwar…
franciscojavierarceo Mar 18, 2026
2a3e544
feat: Gate feature services that reference versioned feature views
franciscojavierarceo Mar 18, 2026
66c280b
fix: Resolve mypy errors and rename config field for clarity
franciscojavierarceo Mar 18, 2026
cfc038b
feat: Enable feature service serving for versioned feature views
franciscojavierarceo Mar 18, 2026
c9aea43
docs: Update RFC for feature service support and rename CLI command
franciscojavierarceo Mar 18, 2026
221e0ed
feat(ui): Add version display and Versions tab to feature view pages
franciscojavierarceo Mar 19, 2026
3efccbf
style(ui): Fix prettier formatting in feature view components
franciscojavierarceo Mar 19, 2026
6878fb0
updated utcnow
franciscojavierarceo Mar 20, 2026
280daf6
feat: Add version-aware materialization support
franciscojavierarceo Mar 20, 2026
43674ac
fix: Resolve three versioning regressions from review feedback
franciscojavierarceo Mar 20, 2026
01e4e77
feat: Add --no-promote flag to feast apply and fix versioned ref parsing
franciscojavierarceo Mar 23, 2026
1876060
docs: Consolidate versioning docs into alpha reference page
franciscojavierarceo Mar 23, 2026
760c003
docs: Add no_promote to apply_diff_to_registry docstring
franciscojavierarceo Mar 24, 2026
bc986ef
fix: Reject reserved chars in FV names and make version parser resilient
franciscojavierarceo Mar 24, 2026
b2d6c09
Merge branch 'master' into featureview-versioning
franciscojavierarceo Mar 24, 2026
3a73c87
fix: Add ensure_valid() call in Snowflake registry apply_feature_view
franciscojavierarceo Mar 24, 2026
1468bc5
Merge branch 'master' into featureview-versioning
franciscojavierarceo Mar 25, 2026
3c1ddbe
fix: Make version_tag optional in proto and use HasField() for correc…
franciscojavierarceo Mar 25, 2026
8c1259f
fix: Address versioning review feedback (Snowflake, Go server, SQL re…
franciscojavierarceo Mar 26, 2026
ac0348d
Merge branch 'master' into featureview-versioning
franciscojavierarceo Mar 26, 2026
7dfc447
fix: Handle @latest in Go feature server and pre-compile version regex
franciscojavierarceo Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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>
  • Loading branch information
franciscojavierarceo and claude committed Mar 20, 2026
commit 43674acbab005dbae1698b7427170aca836cd027
73 changes: 38 additions & 35 deletions protos/feast/core/FeatureViewProjection.proto
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
syntax = "proto3";
package feast.core;

option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
option java_outer_classname = "FeatureReferenceProto";
option java_package = "feast.proto.core";

import "feast/core/Feature.proto";
import "feast/core/DataSource.proto";


// A projection to be applied on top of a FeatureView.
// Contains the modifications to a FeatureView such as the features subset to use.
message FeatureViewProjection {
// The feature view name
string feature_view_name = 1;

// Alias for feature view name
string feature_view_name_alias = 3;

// The features of the feature view that are a part of the feature reference.
repeated FeatureSpecV2 feature_columns = 2;

// Map for entity join_key overrides of feature data entity join_key to entity data join_key
map<string,string> join_key_map = 4;

string timestamp_field = 5;
string date_partition_column = 6;
string created_timestamp_column = 7;
// Batch/Offline DataSource where this view can retrieve offline feature data.
DataSource batch_source = 8;
// Streaming DataSource from where this view can consume "online" feature data.
DataSource stream_source = 9;

}
syntax = "proto3";
package feast.core;

option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
option java_outer_classname = "FeatureReferenceProto";
option java_package = "feast.proto.core";

import "feast/core/Feature.proto";
import "feast/core/DataSource.proto";


// A projection to be applied on top of a FeatureView.
// Contains the modifications to a FeatureView such as the features subset to use.
message FeatureViewProjection {
// The feature view name
string feature_view_name = 1;

// Alias for feature view name
string feature_view_name_alias = 3;

// The features of the feature view that are a part of the feature reference.
repeated FeatureSpecV2 feature_columns = 2;

// Map for entity join_key overrides of feature data entity join_key to entity data join_key
map<string,string> join_key_map = 4;

string timestamp_field = 5;
string date_partition_column = 6;
string created_timestamp_column = 7;
// Batch/Offline DataSource where this view can retrieve offline feature data.
DataSource batch_source = 8;
// Streaming DataSource from where this view can consume "online" feature data.
DataSource stream_source = 9;

// Optional version tag for version-qualified feature references (e.g., @v2).
int32 version_tag = 10;

}
6 changes: 6 additions & 0 deletions sdk/python/feast/feature_view_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def to_proto(self) -> FeatureViewProjectionProto:
for feature in self.features:
feature_reference_proto.feature_columns.append(feature.to_proto())

if self.version_tag is not None:
feature_reference_proto.version_tag = self.version_tag

return feature_reference_proto

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

if proto.version_tag > 0:
feature_view_projection.version_tag = proto.version_tag
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

return feature_view_projection

@staticmethod
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class FeatureViewProjection(google.protobuf.message.Message):
CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int
BATCH_SOURCE_FIELD_NUMBER: builtins.int
STREAM_SOURCE_FIELD_NUMBER: builtins.int
VERSION_TAG_FIELD_NUMBER: builtins.int
feature_view_name: builtins.str
"""The feature view name"""
feature_view_name_alias: builtins.str
Expand All @@ -68,6 +69,8 @@ class FeatureViewProjection(google.protobuf.message.Message):
@property
def stream_source(self) -> feast.core.DataSource_pb2.DataSource:
"""Streaming DataSource from where this view can consume "online" feature data."""
version_tag: builtins.int
"""Optional version tag for version-qualified feature references (e.g., @v2)."""
def __init__(
self,
*,
Expand All @@ -80,8 +83,9 @@ class FeatureViewProjection(google.protobuf.message.Message):
created_timestamp_column: builtins.str = ...,
batch_source: feast.core.DataSource_pb2.DataSource | None = ...,
stream_source: feast.core.DataSource_pb2.DataSource | None = ...,
version_tag: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "stream_source", b"stream_source"]) -> builtins.bool: ...
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: ...
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: ...

global___FeatureViewProjection = FeatureViewProjection
33 changes: 0 additions & 33 deletions sdk/python/feast/utils.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1237,17 +1237,6 @@ def _get_features(

# Build feature reference list
for projection in feature_service_from_registry.feature_view_projections:
if getattr(registry, "enable_online_versioning", False):
try:
fv = registry.get_any_feature_view(
projection.name, project, allow_cache
)
ver = getattr(fv, "current_version_number", None)
if ver is not None and ver > 0:
projection = copy.copy(projection)
projection.version_tag = ver
except Exception:
pass
_feature_refs.extend(
[f"{projection.name_to_use()}:{f.name}" for f in projection.features]
)
Expand Down Expand Up @@ -1333,28 +1322,6 @@ def _get_feature_views_to_use(
fv.projection.version_tag = version_num
else:
fv = registry.get_any_feature_view(name, project, allow_cache)
# Gate: feature services must not resolve to versioned FVs when online versioning is off
cur_ver: Optional[int] = getattr(fv, "current_version_number", None)
if (
isinstance(features, FeatureService)
and cur_ver is not None
and cur_ver > 0
and not getattr(registry, "enable_online_versioning", False)
):
raise ValueError(
f"Feature service references feature view '{name}' which is at version "
f"v{cur_ver}, but online versioning is disabled. "
f"Set 'enable_online_feature_view_versioning: true' under 'registry' "
f"in feature_store.yaml."
)
# For FeatureService refs: resolve the active version when online versioning is on
if (
isinstance(features, FeatureService)
and cur_ver is not None
and cur_ver > 0
and getattr(registry, "enable_online_versioning", False)
):
version_num = cur_ver

if isinstance(fv, OnDemandFeatureView):
od_fvs_to_use.append(
Expand Down
87 changes: 64 additions & 23 deletions sdk/python/tests/integration/registration/test_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,10 +994,10 @@ def _make_store(self, tmpdir, enable_versioning=False):
)
)

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

with tempfile.TemporaryDirectory() as tmpdir:
Expand All @@ -1012,9 +1012,7 @@ def test_feature_service_apply_fails_with_versioned_fv_when_flag_off(
name="driver_service",
features=[fv_v1],
)

with pytest.raises(ValueError, match="version v1"):
store.apply([fs])
store.apply([fs]) # Should not raise

def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_on(
self, versioned_fv_and_entity
Expand All @@ -1036,10 +1034,10 @@ def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_on(
)
store.apply([fs]) # Should not raise

def test_feature_service_retrieval_fails_with_versioned_fv_when_flag_off(
def test_feature_service_retrieval_succeeds_with_versioned_fv_when_flag_off(
self, versioned_fv_and_entity
):
"""get_online_features with a feature service referencing a versioned FV, flag off -> ValueError."""
"""get_online_features with a feature service referencing a versioned FV, flag off -> succeeds."""
entity, fv_v0, fv_v1 = versioned_fv_and_entity
from feast.utils import _get_feature_views_to_use

Expand All @@ -1060,14 +1058,15 @@ def test_feature_service_retrieval_fails_with_versioned_fv_when_flag_off(
"driver_service", "test_project"
)

with pytest.raises(ValueError, match="online versioning is disabled"):
_get_feature_views_to_use(
registry=store_off.registry,
project="test_project",
features=registered_fs,
allow_cache=False,
hide_dummy_entity=False,
)
fvs, _ = _get_feature_views_to_use(
registry=store_off.registry,
project="test_project",
features=registered_fs,
allow_cache=False,
hide_dummy_entity=False,
)

assert len(fvs) == 1

def test_feature_service_with_unversioned_fv_succeeds(
self, unversioned_fv_and_entity
Expand All @@ -1088,7 +1087,8 @@ def test_feature_service_with_unversioned_fv_succeeds(
def test_feature_service_serves_versioned_fv_when_flag_on(
self, versioned_fv_and_entity
):
"""With online versioning on, FeatureService projections carry the correct version_tag."""
"""With online versioning on, FeatureService projections do not carry version_tag;
the FV in the registry carries current_version_number."""
from feast.utils import _get_feature_views_to_use

entity, fv_v0, fv_v1 = versioned_fv_and_entity
Expand Down Expand Up @@ -1121,13 +1121,19 @@ def test_feature_service_serves_versioned_fv_when_flag_on(
)

assert len(fvs) == 1
assert fvs[0].projection.version_tag == 1
assert fvs[0].projection.name_to_use() == "driver_stats@v1"
assert fvs[0].projection.version_tag is None
assert fvs[0].projection.name_to_use() == "driver_stats"

# Verify the FV in the registry has the correct version
fv_from_registry = store.registry.get_feature_view(
"driver_stats", "test_project"
)
assert fv_from_registry.current_version_number == 1

def test_feature_service_feature_refs_include_version_when_flag_on(
def test_feature_service_feature_refs_are_plain_when_flag_on(
self, versioned_fv_and_entity
):
"""With online versioning on, _get_features() produces version-qualified refs."""
"""With online versioning on, _get_features() produces plain (non-versioned) refs for FeatureService."""
from feast.utils import _get_features

entity, fv_v0, fv_v1 = versioned_fv_and_entity
Expand Down Expand Up @@ -1158,9 +1164,44 @@ def test_feature_service_feature_refs_include_version_when_flag_on(
allow_cache=False,
)

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

# Check specific ref format
assert "driver_stats@v1:trips_today" in refs
assert "driver_stats:trips_today" in refs

def test_unpin_from_versioned_to_latest(self, versioned_fv_and_entity):
"""Pin a FV to v1, then apply with version='latest' (no schema change) -> unpinned."""
entity, fv_v0, fv_v1 = versioned_fv_and_entity

with tempfile.TemporaryDirectory() as tmpdir:
store = self._make_store(tmpdir, enable_versioning=True)

# Apply v0 then v1 to create version history (v1 has schema change)
store.apply([entity, fv_v0])
store.apply([entity, fv_v1])

# Verify it's pinned to v1
reloaded = store.registry.get_feature_view("driver_stats", "test_project")
assert reloaded.current_version_number == 1

# Now re-apply the same schema with version="latest" to unpin
fv_latest = FeatureView(
name="driver_stats",
entities=[entity],
ttl=timedelta(days=1),
schema=[
Field(name="driver_id", dtype=Int64),
Field(name="trips_today", dtype=Int64),
Field(name="avg_rating", dtype=Float32),
],
version="latest",
description="v1",
)
store.apply([entity, fv_latest])

# Reload and verify unpinned
reloaded = store.registry.get_feature_view("driver_stats", "test_project")
assert reloaded.current_version_number is None
assert reloaded.version == "latest"
10 changes: 8 additions & 2 deletions sdk/python/tests/unit/test_feature_view_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,10 @@ def test_v1_with_suffix(self):
ttl=timedelta(days=1),
)
fv.current_version_number = 1
assert _table_id("my_project", fv) == "my_project_test_fv_v1"
assert (
_table_id("my_project", fv, enable_versioning=True)
== "my_project_test_fv_v1"
)

def test_v5_with_suffix(self):
from datetime import timedelta
Expand All @@ -456,7 +459,10 @@ def test_v5_with_suffix(self):
ttl=timedelta(days=1),
)
fv.current_version_number = 5
assert _table_id("my_project", fv) == "my_project_test_fv_v5"
assert (
_table_id("my_project", fv, enable_versioning=True)
== "my_project_test_fv_v5"
)


class TestValidateFeatureRefsVersioned:
Expand Down