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: Handle version race conditions gracefully with retry and forwar…
…d declaration

Auto-increment path (version="latest") now retries up to 3 times on
IntegrityError in the SQL registry when concurrent applies race on the
same version number. Explicit version path (version="v<N>") now creates
the version if it doesn't exist (forward declaration) instead of raising
FeatureViewVersionNotFound, with ConcurrentVersionConflict on race.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Loading branch information
franciscojavierarceo and claude committed Mar 18, 2026
commit c5d4b491d67a514c1a96bd807072c423a58a6b20
335 changes: 335 additions & 0 deletions docs/rfcs/feature-view-versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
# RFC: Feature View Versioning

**Status:** In Review
**Authors:** @farceo
**Branch:** `featureview-versioning`
**Date:** 2026-03-17

## Summary

This RFC proposes adding automatic version tracking to Feast feature views. Every time `feast apply` detects a schema or UDF change to a feature view, a versioned snapshot is saved to the registry. Users can list version history, pin serving to a prior version, and optionally query specific versions at read time using `@v<N>` syntax.

## Motivation

Today, when a feature view's schema changes, the old definition is silently overwritten. This creates several problems:

1. **No audit trail.** Teams can't answer "what did this feature view look like last week?" or "who changed the schema and when?"
2. **No safe rollback.** If a schema change breaks a downstream model, there's no way to revert to the previous definition without manually reconstructing it.
3. **No multi-version serving.** During migrations, teams often need to serve both the old and new schema simultaneously (e.g., model A uses v1 features, model B uses v2 features). This is currently impossible without creating entirely separate feature views.

## Diagrams

### Lifecycle Flow

Shows what happens during `feast apply` and `get_online_features`, and how version
history, pinning, and version-qualified reads fit together.

```
feast apply
|
v
+------------------------+
| Compare new definition |
| against active FV |
+------------------------+
| |
schema/UDF metadata only
changed changed
| |
v v
+--------------+ +------------------+
| Save old as | | Update in place, |
| version N | | no new version |
| Save new as | +------------------+
| version N+1 |
+--------------+
|
+------------+------------+
| |
v v
+----------------+ +-------------------+
| Registry | | Online Store |
| (version | | (only if flag on) |
| history) | +-------------------+
+----------------+ |
| +------+------+
| | |
v v v
+----------------+ +--------+ +-----------+
| feast versions | | proj_ | | proj_ |
| feast pin v2 | | fv | | fv_v1 |
| list / get | | (v0) | | fv_v2 ... |
+----------------+ +--------+ +-----------+
Always available Unversioned Versioned
table tables


get_online_features
|
v
+---------------------+
| Parse feature refs |
+---------------------+
| |
"fv:feature" "fv@v2:feature"
(no version) (version-qualified)
| |
v v
+------------+ +------------------+
| Read from | | flag enabled? |
| active FV | +------------------+
| table | | |
+------------+ yes no
| |
v v
+------------+ +-------+
| Look up v2 | | raise |
| snapshot, | | error |
| read from | +-------+
| proj_fv_v2 |
+------------+
```

### Architecture / Storage

Shows how version data is stored in the registry and online store, and the
relationship between the active definition and historical snapshots.

```
+--feature_store.yaml------------------------------------------+
| registry: |
| path: data/registry.db |
| enable_online_feature_view_versioning: true (optional) |
+--------------------------------------------------------------+
| |
v v
+--Registry (file or SQL)--+ +--Online Store (SQLite, ...)---+
| | | |
| Active Feature Views | | Unversioned tables (v0) |
| +--------------------+ | | +-------------------------+ |
| | driver_stats | | | | proj_driver_stats | |
| | version: latest | | | | driver_id | trips | . | |
| | current_ver: 2 | | | +-------------------------+ |
| | schema: [...] | | | |
| +--------------------+ | | Versioned tables (v1+) |
| | | +-------------------------+ |
| Version History | | | proj_driver_stats_v1 | |
| +--------------------+ | | | driver_id | trips | . | |
| | v0: proto snapshot | | | +-------------------------+ |
| | created: Jan 15 | | | +-------------------------+ |
| | v1: proto snapshot | | | | proj_driver_stats_v2 | |
| | created: Jan 16 | | | | driver_id | trips | . | |
| | v2: proto snapshot | | | +-------------------------+ |
| | created: Jan 20 | | | |
| +--------------------+ | +-------------------------------+
| |
| Always active. | Only created when flag is on
| No flag needed. | and feast materialize is run.
+--------------------------+
```

## Design

### Core Concepts

- **Version number**: An auto-incrementing integer (v0, v1, v2, ...) assigned to each schema-significant change.
- **Version snapshot**: A serialized copy of the full feature view proto at that version, stored in the registry's version history table.
- **Version pin**: Setting `version="v2"` on a feature view replaces the active definition with the v2 snapshot — essentially a revert.
- **Version-qualified ref**: The `@v<N>` syntax in feature references (e.g., `driver_stats@v2:trips_today`) for reading from a specific version's online store table.

### What Triggers a New Version

Only **schema and UDF changes** create new versions. Metadata-only changes (description, tags, owner, TTL, online/offline flags) update the active definition in place without creating a version.

Schema-significant changes include:
- Adding, removing, or retyping feature columns
- Changing entities or entity columns
- Changing the UDF code (StreamFeatureView, OnDemandFeatureView)

This keeps version history meaningful — a new version number always means a real structural change.

### What Does NOT Trigger a New Version

- Re-applying an identical definition (idempotent)
- Changing `description`, `tags`, `owner`
- Changing `ttl`, `online`, `offline` flags
- Changing data source paths/locations (treated as deployment config)

### Version History Is Always-On

Version history tracking is lightweight registry metadata — just a serialized proto snapshot per version. There is no performance cost to the online path and no additional infrastructure required. For this reason, version history is **always active** with no opt-in flag needed.

Out of the box, every `feast apply` that changes a feature view will:
- Record a version snapshot
- Support `feast feature-views versions <name>` to list history
- Support `registry.list_feature_view_versions(name, project)` programmatically
- Support `registry.get_feature_view_by_version(name, project, version_number)` for snapshot retrieval
- Support version pinning via `version="v2"` in feature view definitions

### Online Versioning Is Opt-In

The expensive/risky part of versioning is creating **separate online store tables per version** and routing reads to them. This is gated behind a config flag:

```yaml
registry:
path: data/registry.db
enable_online_feature_view_versioning: true
```

When enabled, version-qualified refs like `driver_stats@v2:trips_today` in `get_online_features()` will:
1. Look up the v2 snapshot from version history
2. Read from a version-specific online store table (`project_driver_stats_v2`)

When disabled (the default), using `@v<N>` refs raises a clear error. All other versioning features (history, listing, pinning, snapshot retrieval) work regardless.

### Storage

**File-based registry**: Version history is stored as a repeated `FeatureViewVersionRecord` message in the registry proto, alongside the existing feature view definitions.

**SQL registry**: A dedicated `feature_view_version_history` table with columns for name, project, version number, type, proto bytes, and creation timestamp.

### Version Pinning

Pinning replaces the active feature view with a historical snapshot:

```python
driver_stats = FeatureView(
name="driver_stats",
entities=[driver],
schema=[...],
source=my_source,
version="v2", # revert to v2's definition
)
```

Safety constraints:
- The user's feature view definition (minus the version field) must match the currently active definition. If the user changed both the schema and the version pin simultaneously, `feast apply` raises `FeatureViewPinConflict`. This prevents accidental "I thought I was reverting but I also changed things."
- Pinning does not modify version history — v0, v1, v2 snapshots remain intact.
- After a pin, removing the version field (or setting `version="latest"`) returns to auto-incrementing behavior. If the next `feast apply` detects a schema change, a new version is created.

### Version-Qualified Feature References

The `@v<N>` syntax extends the existing `feature_view:feature` reference format:

```python
features = store.get_online_features(
features=[
"driver_stats:trips_today", # latest (default)
"driver_stats@v2:trips_today", # read from v2
"driver_stats@v1:avg_rating", # read from v1
],
entity_rows=[{"driver_id": 1001}],
)
```

Online store table naming:
- v0 uses the existing unversioned table (`project_driver_stats`) for backward compatibility
- v1+ use suffixed tables (`project_driver_stats_v1`, `project_driver_stats_v2`)

Each version requires its own materialization. `@latest` always resolves to the active version.

### Supported Feature View Types

Versioning works on all three feature view types:
- `FeatureView` / `BatchFeatureView`
- `StreamFeatureView`
- `OnDemandFeatureView`

### Online Store Support

Version-qualified reads (`@v<N>`) are currently implemented for the **SQLite** online store. Other online stores will raise a clear error. Expanding to additional stores is follow-up work.

## API Surface

### Python SDK

```python
# List version history
versions = store.list_feature_view_versions("driver_stats")
# [{"version": "v0", "version_number": 0, "created_timestamp": ..., ...}, ...]

# Get a specific version's definition
fv_v1 = store.registry.get_feature_view_by_version("driver_stats", project, 1)

# Pin to a version
FeatureView(name="driver_stats", ..., version="v2")

# Version-qualified online read (requires enable_online_feature_view_versioning)
store.get_online_features(features=["driver_stats@v2:trips_today"], ...)
```

### CLI

```bash
# List versions
feast feature-views versions driver_stats

# Output:
# VERSION TYPE CREATED VERSION_ID
# v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-...
# v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-...
```

### Configuration

```yaml
# feature_store.yaml
registry:
path: data/registry.db
# Optional: enable versioned online tables and @v<N> reads (default: false)
enable_online_feature_view_versioning: true
```

## Migration & Backward Compatibility

- **Zero breaking changes.** All existing feature views continue to work. The `version` parameter defaults to `"latest"` and `current_version_number` defaults to `None`.
- **Existing online data is preserved.** The unversioned online store table is treated as v0. No data migration needed.
- **Version history starts on first apply.** Pre-existing feature views get a v0 snapshot on their next `feast apply`.
- **Proto backward compatibility.** The new `version` and `current_version_number` fields use proto defaults (empty string and 0) so old protos deserialize correctly.

## Concurrency

Two concurrent `feast apply` calls on the same feature view can race on version number assignment. The behavior depends on the version mode and registry backend.

### `version="latest"` (auto-increment)

The registry computes `MAX(version_number) + 1` and saves the new snapshot. If two concurrent applies race on the same version number:

- **SQL registry**: The unique constraint on `(feature_view_name, project_id, version_number)` causes an `IntegrityError`. The registry catches this and retries up to 3 times, re-reading `MAX + 1` each time. Since the client said "latest", the exact version number doesn't matter.
- **File registry**: Last-write-wins. The file registry uses an in-memory proto with no database-level constraints, so concurrent writes may overwrite each other. This is a pre-existing limitation for all file registry operations.

### `version="v<N>"` (explicit version)

The registry checks whether version N already exists:

- **Exists** → pin/revert to that version's snapshot (unchanged behavior)
- **Doesn't exist** → forward declaration: create version N with the provided definition

If two concurrent applies both try to forward-declare the same version:

- **SQL registry**: The first one succeeds; the second gets a `ConcurrentVersionConflict` error with a clear message to pull latest and retry.
- **File registry**: Last-write-wins (same pre-existing limitation).

### Recommendations

- For single-developer or CI/CD workflows, the file registry works fine.
- For multi-client environments with concurrent applies, use the SQL registry for proper conflict detection.

## Limitations & Future Work

- **Online store coverage.** Version-qualified reads are only on SQLite today. Redis, DynamoDB, Bigtable, Postgres, etc. are follow-up work.
- **Offline store versioning.** This RFC covers online reads only. Versioned historical retrieval is out of scope.
- **Version deletion.** There is no mechanism to prune old versions. This could be added later if registries grow large.
- **Cross-version joins.** Joining features from different versions of the same feature view in `get_historical_features` is not supported.

## Open Questions

1. **Should version history have a retention policy?** For long-lived feature views with frequent schema changes, version history could grow unbounded. A `max_versions` config or TTL-based pruning could help.
2. **Should version-qualified refs work in `get_historical_features`?** The current implementation is online-only. Offline versioned reads would require point-in-time-correct version resolution.
3. **Should we support version aliases?** e.g., `driver_stats@stable:trips` mapping to a pinned version number via config.

## References

- Branch: `featureview-versioning`
- Documentation: `docs/getting-started/concepts/feature-view.md` (Versioning section)
- Tests: `sdk/python/tests/integration/registration/test_versioning.py`, `sdk/python/tests/unit/test_feature_view_versioning.py`
5 changes: 5 additions & 0 deletions sdk/python/feast/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ def __init__(self, name, version):
)


class ConcurrentVersionConflict(FeastError):
def __init__(self, msg: str):
super().__init__(msg)


class OnDemandFeatureViewNotFoundException(FeastObjectNotFoundException):
def __init__(self, name, project=None):
if project:
Expand Down
Loading