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
feat: Implement version-qualified feature references (@v<N>)
Extends feature view versioning with support for reading features from specific
versions at query time using the syntax: "driver_stats@v2:trips_today"

Core changes:
- Add _parse_feature_ref() to parse version-qualified feature references
- Update all feature reference parsing to use _parse_feature_ref()
- Add get_feature_view_by_version() to BaseRegistry and all implementations
- Add FeatureViewProjection.version_tag for multi-version query support
- Add version-aware _table_id() in SQLite online store (v0→unversioned, v1+→_v{N})
- Add VersionedOnlineReadNotSupported error for unsupported stores

Features:
- "driver_stats:trips" = "driver_stats@latest:trips" (backward compatible)
- "driver_stats@v2:trips" reads from v2 snapshot using _v2 table suffix
- Multiple versions in same query: ["driver@v1:trips", "driver@v2:daily"]
- Version parameter added to all decorator functions for consistency

Backward compatibility:
- Unversioned table serves as v0, only v1+ get _v{N} suffix
- All existing queries work unchanged
- SQLite-only for now, other stores raise clear error

Documentation:
- Updated feature-view.md with @Version syntax examples
- Updated feature-retrieval.md reference format
- Added version examples to how-to guides

Tests: 47 unit + 11 integration tests pass, no regressions

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
  • Loading branch information
franciscojavierarceo and claude committed Mar 14, 2026
commit 94afe6ef61aedb0c808097f80a3b9c6cebe7aef6
14 changes: 11 additions & 3 deletions docs/getting-started/concepts/feature-retrieval.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,19 @@ feature_store.get_historical_features(features=feature_service, entity_df=entity

This mechanism of retrieving features is only recommended as you're experimenting. Once you want to launch experiments or serve models, feature services are recommended.

Feature references uniquely identify feature values in Feast. The structure of a feature reference in string form is as follows: `<feature_view>:<feature>`
Feature references uniquely identify feature values in Feast. The structure of a feature reference in string form is as follows: `<feature_view>[@version]:<feature>`

The `@version` part is optional. When omitted, the latest (active) version is used. You can specify a version like `@v2` to read from a specific historical version snapshot.

Feature references are used for the retrieval of features from Feast:

```python
online_features = fs.get_online_features(
features=[
'driver_locations:lon',
'drivers_activity:trips_today'
'driver_locations:lon', # latest version (default)
'drivers_activity:trips_today', # latest version (default)
'drivers_activity@v2:trips_today', # specific version
'drivers_activity@latest:trips_today', # explicit latest
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although unlikely, it can still happen that a feature name has a @, will the apply be able to identify and abort safely?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added validation for this so that new feast apply with @ in the feature view names are rejected but old ones are used.

For the latter case we have to make some assumptions about format because we support @vN, @versionN, and @latest so those rare edge cases could cause headaches but we've documented it. Thanks for calling it out!

],
entity_rows=[
# {join_key: entity_value}
Expand All @@ -95,6 +99,10 @@ online_features = fs.get_online_features(
)
```

{% hint style="info" %}
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.
{% endhint %}

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.

{% hint style="info" %}
Expand Down
35 changes: 26 additions & 9 deletions docs/getting-started/concepts/feature-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,19 +251,34 @@ for v in versions:
print(f"{v['version']} created at {v['created_timestamp']}")
```

### Concurrent access and multi-team usage
### Version-qualified feature references

Versioning provides **definition management and rollback** — it does not support concurrent multi-version serving. Key constraints:
You can read features from a **specific version** of a feature view by using version-qualified feature references with the `@v<N>` syntax:

* Only one version can be active per feature view name per project at any time
* Pinning to a version (e.g., `version="v2"`) is a global operation that changes the active definition for **all** consumers in that project
* On-demand feature views resolve their source feature views by name against the currently active definition
* `get_online_features` and `get_historical_features` always use the active definition
```python
online_features = store.get_online_features(
features=[
"driver_stats:trips_today", # latest version (default)
"driver_stats@v2:trips_today", # specific version
"driver_stats@latest:trips_today", # explicit latest
],
entity_rows=[{"driver_id": 1001}],
)
```

**For concurrent A/B testing** of different feature definitions, use one of these approaches:
**How it works:**

* **Separate Feast projects**: `project="team_a"` and `project="team_b"` can each maintain independent feature view definitions while sharing the same underlying data sources
* **Distinct feature view names**: Create `driver_stats_experiment_v2` alongside `driver_stats` to test a new definition without affecting the original
* `driver_stats:trips_today` is equivalent to `driver_stats@latest:trips_today` — it reads from the currently active version
* `driver_stats@v2:trips_today` reads from the v2 snapshot stored in version history, using a version-specific online store table
* Multiple versions of the same feature view can be queried in a single request (e.g., `driver_stats@v1:trips` and `driver_stats@v2:trips_daily`)

**Backward compatibility:**

* The unversioned online store table (e.g., `project_driver_stats`) is treated as v0
* Only versions >= 1 get `_v{N}` suffixed tables (e.g., `project_driver_stats_v1`)
* Pre-versioning users' existing data continues to work without changes — `@latest` resolves to the active version, which for existing unversioned FVs is v0

**Materialization:** Each version requires its own materialization. After applying a new version, run `feast materialize` to populate the versioned table before querying it with `@v<N>`.

### Supported feature view types

Expand All @@ -273,6 +288,8 @@ Versioning is supported on all three feature view types:
* `StreamFeatureView`
* `OnDemandFeatureView`

**Note:** Version-qualified reads (`@v<N>`) are currently supported only on the **SQLite** online store. Other online stores will raise a clear error if versioned queries are attempted. Support for additional stores is tracked in [#6200](https://github.com/feast-dev/feast/issues/6200).

## Schema Validation

Feature views support an optional `enable_validation` parameter that enables schema validation during materialization and historical feature retrieval. When enabled, Feast verifies that:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ feature_refs = [
"driver_trips:maximum_daily_rides",
"driver_trips:rating",
"driver_trips:rating:trip_completed",
# Optionally, reference a specific version: "driver_trips@v2:average_daily_rides"
]
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Create a list of features that you would like to retrieve. This list typically c
```python
features = [
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate"
"driver_hourly_stats:acc_rate",
# Optionally, reference a specific version: "driver_hourly_stats@v2:conv_rate"
]
```

Expand Down
2 changes: 2 additions & 0 deletions sdk/python/feast/batch_feature_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def batch_feature_view(
owner: str = "",
schema: Optional[List[Field]] = None,
enable_validation: bool = False,
version: str = "latest",
):
"""
Creates a BatchFeatureView object with the given user-defined function (UDF) as the transformation.
Expand Down Expand Up @@ -222,6 +223,7 @@ def decorator(user_function):
udf=user_function,
udf_string=udf_string,
enable_validation=enable_validation,
version=version,
)
functools.update_wrapper(wrapper=batch_feature_view_obj, wrapped=user_function)
return batch_feature_view_obj
Expand Down
9 changes: 9 additions & 0 deletions sdk/python/feast/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ def __init__(self, name, version, project=None):
super().__init__(f"Version {version} of feature view {name} does not exist")


class VersionedOnlineReadNotSupported(FeastError):
def __init__(self, store_name: str, version: int):
super().__init__(
f"Versioned feature reads (@v{version}) are not yet supported by {store_name}. "
f"Currently only SQLite supports version-qualified feature references. "
f"See https://github.com/feast-dev/feast/issues/6200"
Comment thread
franciscojavierarceo marked this conversation as resolved.
Outdated
)


class FeatureViewPinConflict(FeastError):
def __init__(self, name, version):
super().__init__(
Expand Down
6 changes: 5 additions & 1 deletion sdk/python/feast/feature_view_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ class FeatureViewProjection:
date_partition_column: Optional[str] = None
created_timestamp_column: Optional[str] = None
batch_source: Optional[DataSource] = None
version_tag: Optional[int] = None

def name_to_use(self):
return self.name_alias or self.name
base = self.name_alias or self.name
if self.version_tag is not None:
return f"{base}@v{self.version_tag}"
return base
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

def to_proto(self) -> FeatureViewProjectionProto:
batch_source = None
Expand Down
18 changes: 18 additions & 0 deletions sdk/python/feast/infra/online_stores/online_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from feast import Entity, utils
from feast.batch_feature_view import BatchFeatureView
from feast.errors import VersionedOnlineReadNotSupported
from feast.feature_service import FeatureService
from feast.feature_view import FeatureView
from feast.infra.infra_object import InfraObject
Expand Down Expand Up @@ -185,6 +186,9 @@ def get_online_features(
native_entity_values=True,
)

# Check for versioned reads on unsupported stores
self._check_versioned_read_support(grouped_refs)

for table, requested_features in grouped_refs:
# Get the correct set of entity values with the correct join keys.
table_entity_values, idxs, output_len = utils._get_unique_entities(
Expand Down Expand Up @@ -231,6 +235,17 @@ def get_online_features(
)
return OnlineResponse(online_features_response)

def _check_versioned_read_support(self, grouped_refs):
"""Raise an error if versioned reads are attempted on unsupported stores."""
from feast.infra.online_stores.sqlite import SqliteOnlineStore

if isinstance(self, SqliteOnlineStore):
return
for table, _ in grouped_refs:
version = getattr(table, "current_version_number", None)
if version is not None and version > 0:
raise VersionedOnlineReadNotSupported(self.__class__.__name__, version)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

async def get_online_features_async(
self,
config: RepoConfig,
Expand Down Expand Up @@ -273,6 +288,9 @@ async def get_online_features_async(
native_entity_values=True,
)

# Check for versioned reads on unsupported stores
self._check_versioned_read_support(grouped_refs)

async def query_table(table, requested_features):
# Get the correct set of entity values with the correct join keys.
table_entity_values, idxs, output_len = utils._get_unique_entities(
Expand Down
6 changes: 5 additions & 1 deletion sdk/python/feast/infra/online_stores/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,11 @@ def _initialize_conn(


def _table_id(project: str, table: FeatureView) -> str:
return f"{project}_{table.name}"
name = table.name
version = getattr(table, "current_version_number", None)
if version is not None and version > 0:
name = f"{table.name}_v{version}"
return f"{project}_{name}"
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated


class SqliteTable(InfraObject):
Expand Down
22 changes: 22 additions & 0 deletions sdk/python/feast/infra/registry/base_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,28 @@ def list_feature_view_versions(
"list_feature_view_versions is not implemented for this registry"
)

def get_feature_view_by_version(
self, name: str, project: str, version_number: int, allow_cache: bool = False
) -> BaseFeatureView:
"""
Retrieve a feature view snapshot for a specific version number.

Args:
name: Name of feature view
project: Feast project that this feature view belongs to
version_number: The version number to retrieve
allow_cache: Whether to allow returning from a cached registry

Returns:
The feature view snapshot at the specified version.

Raises:
FeatureViewVersionNotFound: if the version doesn't exist.
"""
raise NotImplementedError(
"get_feature_view_by_version is not implemented for this registry"
)

@abstractmethod
def apply_materialization(
self,
Expand Down
37 changes: 37 additions & 0 deletions sdk/python/feast/infra/registry/proto_registry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
EntityNotFoundException,
FeatureServiceNotFoundException,
FeatureViewNotFoundException,
FeatureViewVersionNotFound,
PermissionObjectNotFoundException,
ProjectObjectNotFoundException,
SavedDatasetNotFound,
Expand Down Expand Up @@ -147,6 +148,42 @@ def get_any_feature_view(
raise FeatureViewNotFoundException(name, project)


def get_feature_view_by_version(
registry_proto: RegistryProto, name: str, project: str, version_number: int
) -> BaseFeatureView:
"""Retrieve a feature view snapshot for a specific version from version history."""
from feast.protos.feast.core.FeatureView_pb2 import (
FeatureView as FeatureViewProto,
)
from feast.protos.feast.core.OnDemandFeatureView_pb2 import (
OnDemandFeatureView as OnDemandFeatureViewProto,
)
from feast.protos.feast.core.StreamFeatureView_pb2 import (
StreamFeatureView as StreamFeatureViewProto,
)
from feast.version_utils import version_tag

type_map = {
"feature_view": (FeatureViewProto, FeatureView),
"stream_feature_view": (StreamFeatureViewProto, StreamFeatureView),
"on_demand_feature_view": (OnDemandFeatureViewProto, OnDemandFeatureView),
}

for record in registry_proto.feature_view_version_history.records:
if (
record.feature_view_name == name
and record.project_id == project
and record.version_number == version_number
):
proto_class, python_class = type_map[record.feature_view_type]
snap_proto = proto_class.FromString(record.feature_view_proto)
fv = python_class.from_proto(snap_proto)
fv.current_version_number = version_number
return fv

raise FeatureViewVersionNotFound(name, version_tag(version_number), project)


def get_feature_view(
registry_proto: RegistryProto, name: str, project: str
) -> FeatureView:
Expand Down
12 changes: 12 additions & 0 deletions sdk/python/feast/infra/registry/registry.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,18 @@ def list_feature_view_versions(
results.sort(key=lambda r: r["version_number"])
return results

def get_feature_view_by_version(
self, name: str, project: str, version_number: int, allow_cache: bool = False
) -> BaseFeatureView:
record = self._get_version_record(name, project, version_number)
if record is None:
raise FeatureViewVersionNotFound(name, version_tag(version_number), project)
proto_class, python_class = self._proto_class_for_type(record.feature_view_type)
snap_proto = proto_class.FromString(record.feature_view_proto)
fv = python_class.from_proto(snap_proto)
fv.current_version_number = version_number
return fv

def apply_feature_view(
self, feature_view: BaseFeatureView, project: str, commit: bool = True
):
Expand Down
13 changes: 13 additions & 0 deletions sdk/python/feast/infra/registry/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,19 @@ def _get_version_snapshot(
)
return None

def get_feature_view_by_version(
self, name: str, project: str, version_number: int, allow_cache: bool = False
) -> BaseFeatureView:
snapshot = self._get_version_snapshot(name, project, version_number)
if snapshot is None:
raise FeatureViewVersionNotFound(name, version_tag(version_number), project)
snap_type, snap_proto_bytes = snapshot
proto_class, python_class = self._proto_class_for_type(snap_type)
snap_proto = proto_class.FromString(snap_proto_bytes)
fv = python_class.from_proto(snap_proto)
fv.current_version_number = version_number
return fv

def list_feature_view_versions(
self, name: str, project: str
) -> List[Dict[str, Any]]:
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/feast/on_demand_feature_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ def on_demand_feature_view(
write_to_online_store: bool = False,
singleton: bool = False,
explode: bool = False,
version: str = "latest",
):
"""
Creates an OnDemandFeatureView object with the given user function as udf.
Expand Down Expand Up @@ -1191,6 +1192,7 @@ def decorator(user_function):
singleton=singleton,
udf=user_function,
udf_string=udf_string,
version=version,
)
functools.update_wrapper(
wrapper=on_demand_feature_view_obj, wrapped=user_function
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/feast/stream_feature_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ def stream_feature_view(
mode: Optional[str] = "spark",
timestamp_field: Optional[str] = "",
enable_validation: bool = False,
version: str = "latest",
):
"""
Creates an StreamFeatureView object with the given user function as udf.
Expand Down Expand Up @@ -471,6 +472,7 @@ def decorator(user_function):
mode=mode,
timestamp_field=timestamp_field,
enable_validation=enable_validation,
version=version,
)
functools.update_wrapper(wrapper=stream_feature_view_obj, wrapped=user_function)
return stream_feature_view_obj
Expand Down
Loading
Loading