Skip to content

Commit dd31cdb

Browse files
feat: Make feature view versioning opt-in via registry config
Versioning was always active on every `feast apply`. This adds an `enable_feature_view_versioning` boolean (default False) to RegistryConfig so version history, version pins, and version-qualified refs are only available when explicitly enabled. Existing behaviour is fully preserved when the flag is set to true. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent af47911 commit dd31cdb

13 files changed

Lines changed: 180 additions & 31 deletions

File tree

docs/getting-started/concepts/feature-retrieval.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ online_features = fs.get_online_features(
100100
```
101101

102102
{% hint style="info" %}
103-
Version-qualified reads (`@v<N>`) are currently supported only on the SQLite online store. See the [feature view versioning docs](feature-view.md#version-qualified-feature-references) for details.
103+
Version-qualified reads (`@v<N>`) require `enable_feature_view_versioning: true` in your registry config and are currently supported only on the SQLite online store. See the [feature view versioning docs](feature-view.md#version-qualified-feature-references) for details.
104104
{% endhint %}
105105

106106
It is possible to retrieve features from multiple feature views with a single request, and Feast is able to join features from multiple tables in order to build a training dataset. However, it is not possible to reference (or retrieve) features from multiple projects at the same time.

docs/getting-started/concepts/feature-view.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,21 @@ Each field can have additional metadata associated with it, specified as key-val
164164

165165
Feature views support automatic version tracking. Every time `feast apply` detects a change to a feature view, a version snapshot is saved to the registry's version history. This enables auditing what changed, reverting to a prior definition, or pinning serving to a known-good version.
166166

167+
{% hint style="warning" %}
168+
Versioning is **opt-in** and disabled by default. To enable it, add `enable_feature_view_versioning: true` to your registry config in `feature_store.yaml`:
169+
170+
```yaml
171+
registry:
172+
path: data/registry.db
173+
enable_feature_view_versioning: true
174+
```
175+
176+
When disabled, `feast apply` works normally but does not create version history. Version-qualified refs (e.g., `fv@v2:feature`) and version pinning (e.g., `version="v0"`) will raise errors.
177+
{% endhint %}
178+
167179
### How it works
168180

169-
Version tracking is fully automatic. You don't need to set any version parameter — just use `feast apply` as usual:
181+
Once versioning is enabled, version tracking is fully automatic. You don't need to set any version parameter — just use `feast apply` as usual:
170182

171183
1. **First apply** — Your feature view definition is saved as **v0**.
172184
2. **Change something and re-apply** — Feast detects the change, saves the old definition as a snapshot, and saves the new one as **v1**. The version number auto-increments on each real change.

docs/how-to-guides/feast-snowflake-gcp-aws/read-features-from-the-online-store.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Create a list of features that you would like to retrieve. This list typically c
2222
features = [
2323
"driver_hourly_stats:conv_rate",
2424
"driver_hourly_stats:acc_rate",
25-
# Optionally, reference a specific version: "driver_hourly_stats@v2:conv_rate"
25+
# Optionally, reference a specific version (requires enable_feature_view_versioning):
26+
# "driver_hourly_stats@v2:conv_rate"
2627
]
2728
```
2829

docs/reference/feast-cli-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ NAME ENTITIES TYPE
176176
driver_hourly_stats {'driver'} FeatureView
177177
```
178178

179-
List version history for a feature view
179+
List version history for a feature view (requires `enable_feature_view_versioning: true` in registry config)
180180

181181
```text
182182
feast feature-views versions FEATURE_VIEW_NAME

docs/reference/registries/metadata.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The metadata info of Feast `feature_store.yaml` is:
2020
| registry.warehouse | N | string | snowflake warehouse name |
2121
| registry.database | N | string | snowflake db name |
2222
| registry.schema | N | string | snowflake schema name |
23+
| registry.enable_feature_view_versioning | N | boolean | enable feature view version history tracking (default: false) |
2324
| online_store | Y | | |
2425
| offline_store | Y | NA | | |
2526
| offline_store.type | Y | string | storage type |

sdk/python/feast/feature_view.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ def to_proto_meta(self):
510510
meta.materialization_intervals.append(interval_proto)
511511
if self.current_version_number is not None:
512512
meta.current_version_number = self.current_version_number
513+
else:
514+
meta.current_version_number = -1
513515
return meta
514516

515517
def get_ttl_duration(self):
@@ -671,18 +673,16 @@ def _from_proto_internal(
671673

672674
# Restore version fields.
673675
feature_view.version = feature_view_proto.spec.version or "latest"
674-
# proto3 int32 defaults to 0, so use spec.version to distinguish
675-
# "actually version 0" from "no version set". An empty spec.version
676-
# means the proto predates versioning, so current_version_number
677-
# should be None.
678-
if feature_view_proto.meta.current_version_number:
679-
feature_view.current_version_number = (
680-
feature_view_proto.meta.current_version_number
681-
)
682-
elif (
683-
feature_view_proto.meta.current_version_number == 0
684-
and feature_view_proto.spec.version
685-
):
676+
# A sentinel of -1 means "not set" (versioning disabled).
677+
# A value of 0 means explicitly version 0.
678+
# For protos predating the sentinel (current_version_number == 0
679+
# with no spec.version), treat as None for backward compat.
680+
cvn = feature_view_proto.meta.current_version_number
681+
if cvn == -1:
682+
feature_view.current_version_number = None
683+
elif cvn > 0:
684+
feature_view.current_version_number = cvn
685+
elif cvn == 0 and feature_view_proto.spec.version:
686686
feature_view.current_version_number = 0
687687
else:
688688
feature_view.current_version_number = None

sdk/python/feast/infra/registry/registry.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ def __init__(
229229
else False
230230
)
231231

232+
self.enable_versioning = (
233+
registry_config.enable_feature_view_versioning
234+
if registry_config is not None
235+
else False
236+
)
237+
232238
self.cache_mode = (
233239
registry_config.cache_mode if registry_config is not None else "sync"
234240
)
@@ -622,6 +628,11 @@ def _save_version_record(
622628
def list_feature_view_versions(
623629
self, name: str, project: str
624630
) -> List[Dict[str, Any]]:
631+
if not self.enable_versioning:
632+
raise ValueError(
633+
"Feature view versioning is not enabled. "
634+
"Set 'enable_feature_view_versioning: true' under 'registry' in feature_store.yaml."
635+
)
625636
history = self.cached_registry_proto.feature_view_version_history
626637
results = []
627638
for record in history.records:
@@ -641,6 +652,11 @@ def list_feature_view_versions(
641652
def get_feature_view_by_version(
642653
self, name: str, project: str, version_number: int, allow_cache: bool = False
643654
) -> BaseFeatureView:
655+
if not self.enable_versioning:
656+
raise ValueError(
657+
"Feature view versioning is not enabled. "
658+
"Set 'enable_feature_view_versioning: true' under 'registry' in feature_store.yaml."
659+
)
644660
record = self._get_version_record(name, project, version_number)
645661
if record is None:
646662
raise FeatureViewVersionNotFound(name, version_tag(version_number), project)
@@ -663,6 +679,13 @@ def apply_feature_view(
663679
fv_type_str = self._infer_fv_type_string(feature_view)
664680
is_latest, pin_version = parse_version(feature_view.version)
665681

682+
if not self.enable_versioning and not is_latest:
683+
raise ValueError(
684+
f"Cannot pin '{feature_view.name}' to '{feature_view.version}': "
685+
f"versioning is disabled. Set 'enable_feature_view_versioning: true' "
686+
f"under 'registry' in feature_store.yaml."
687+
)
688+
666689
if not is_latest:
667690
# Pin to a specific version
668691
record = self._get_version_record(feature_view.name, project, pin_version)
@@ -770,7 +793,7 @@ def apply_feature_view(
770793
break
771794

772795
# Version history tracking
773-
if is_latest:
796+
if self.enable_versioning and is_latest:
774797
if old_proto_bytes is not None:
775798
# FV changed: save old as a version if first time, then save new
776799
next_ver = self._next_version_number(feature_view.name, project)

sdk/python/feast/infra/registry/sql.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ def __init__(
294294
registry_config.thread_pool_executor_worker_count
295295
)
296296
self.purge_feast_metadata = registry_config.purge_feast_metadata
297+
self.enable_versioning = registry_config.enable_feature_view_versioning
297298
super().__init__(
298299
project=project,
299300
cache_ttl_seconds=registry_config.cache_ttl_seconds,
@@ -625,6 +626,13 @@ def apply_feature_view(
625626

626627
is_latest, pin_version = parse_version(feature_view.version)
627628

629+
if not self.enable_versioning and not is_latest:
630+
raise ValueError(
631+
f"Cannot pin '{feature_view.name}' to '{feature_view.version}': "
632+
f"versioning is disabled. Set 'enable_feature_view_versioning: true' "
633+
f"under 'registry' in feature_store.yaml."
634+
)
635+
628636
if not is_latest:
629637
# Pin to a specific version: load snapshot and apply it
630638
snapshot = self._get_version_snapshot(
@@ -705,6 +713,9 @@ def apply_feature_view(
705713
else:
706714
return # shouldn't happen
707715

716+
if not self.enable_versioning:
717+
return
718+
708719
if old_proto_bytes is not None:
709720
# Deserialize both versions to compare schema/UDF changes
710721
proto_class, fv_class = self._proto_class_for_type(fv_type_str)
@@ -1100,6 +1111,11 @@ def _get_version_snapshot(
11001111
def get_feature_view_by_version(
11011112
self, name: str, project: str, version_number: int, allow_cache: bool = False
11021113
) -> BaseFeatureView:
1114+
if not self.enable_versioning:
1115+
raise ValueError(
1116+
"Feature view versioning is not enabled. "
1117+
"Set 'enable_feature_view_versioning: true' under 'registry' in feature_store.yaml."
1118+
)
11031119
snapshot = self._get_version_snapshot(name, project, version_number)
11041120
if snapshot is None:
11051121
raise FeatureViewVersionNotFound(name, version_tag(version_number), project)
@@ -1113,6 +1129,11 @@ def get_feature_view_by_version(
11131129
def list_feature_view_versions(
11141130
self, name: str, project: str
11151131
) -> List[Dict[str, Any]]:
1132+
if not self.enable_versioning:
1133+
raise ValueError(
1134+
"Feature view versioning is not enabled. "
1135+
"Set 'enable_feature_view_versioning: true' under 'registry' in feature_store.yaml."
1136+
)
11161137
with self.read_engine.begin() as conn:
11171138
stmt = (
11181139
select(feature_view_version_history)

sdk/python/feast/on_demand_feature_view.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ def to_proto(self) -> OnDemandFeatureViewProto:
507507
meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp)
508508
if self.current_version_number is not None:
509509
meta.current_version_number = self.current_version_number
510+
else:
511+
meta.current_version_number = -1
510512
sources = {}
511513
for source_name, fv_projection in self.source_feature_view_projections.items():
512514
sources[source_name] = OnDemandSource(
@@ -600,14 +602,12 @@ def from_proto(
600602
on_demand_feature_view_obj.version = (
601603
on_demand_feature_view_proto.spec.version or "latest"
602604
)
603-
if on_demand_feature_view_proto.meta.current_version_number:
604-
on_demand_feature_view_obj.current_version_number = (
605-
on_demand_feature_view_proto.meta.current_version_number
606-
)
607-
elif (
608-
on_demand_feature_view_proto.meta.current_version_number == 0
609-
and on_demand_feature_view_proto.spec.version
610-
):
605+
cvn = on_demand_feature_view_proto.meta.current_version_number
606+
if cvn == -1:
607+
on_demand_feature_view_obj.current_version_number = None
608+
elif cvn > 0:
609+
on_demand_feature_view_obj.current_version_number = cvn
610+
elif cvn == 0 and on_demand_feature_view_proto.spec.version:
611611
on_demand_feature_view_obj.current_version_number = 0
612612
else:
613613
on_demand_feature_view_obj.current_version_number = None

sdk/python/feast/repo_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ class RegistryConfig(FeastBaseModel):
167167
Once this is set to True, it cannot be reverted back to False. Reverting back to False will
168168
only reset the project but not all the projects"""
169169

170+
enable_feature_view_versioning: StrictBool = False
171+
""" bool: Enable feature view version history tracking. When True, schema changes
172+
create version snapshots and version-qualified refs (e.g., 'fv@v2:feature') are
173+
supported. Defaults to False. """
174+
170175
@field_validator("path")
171176
def validate_path(cls, path: str, values: ValidationInfo) -> str:
172177
if values.data.get("registry_type") == "sql":

0 commit comments

Comments
 (0)