Skip to content

Commit 01e4e77

Browse files
feat: Add --no-promote flag to feast apply and fix versioned ref parsing
Add --no-promote flag that saves new version snapshots without promoting them to active, enabling phased rollouts without a transition window where unversioned consumers briefly see the new schema. Also audit and fix brittle feature reference parsing across the codebase to properly handle @v<N> version-qualified syntax: - Fix ibis offline store prefix matching to use name_to_use() - Fix Go ParseFeatureReference to strip @v<N> from view name - Fix passthrough_provider saved dataset column naming Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Signed-off-by: Francisco Javier Arceo <[email protected]>
1 parent 43674ac commit 01e4e77

File tree

17 files changed

+843
-429
lines changed

17 files changed

+843
-429
lines changed

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
* [\[Alpha\] Vector Database](reference/alpha-vector-database.md)
167167
* [\[Alpha\] Data quality monitoring](reference/dqm.md)
168168
* [\[Alpha\] Streaming feature computation with Denormalized](reference/denormalized.md)
169+
* [\[Alpha\] Feature View Versioning](reference/alpha-feature-view-versioning.md)
169170
* [OpenLineage Integration](reference/openlineage.md)
170171
* [Feast CLI reference](reference/feast-cli-commands.md)
171172
* [Python API reference](http://rtd.feast.dev)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Feature names must be unique within a [feature view](feature-view.md#feature-vie
160160

161161
Each field can have additional metadata associated with it, specified as key-value [tags](https://rtd.feast.dev/en/master/feast.html#feast.field.Field).
162162

163-
## Versioning
163+
## \[Alpha\] Versioning
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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# \[Alpha\] Feature View Versioning
2+
3+
{% hint style="warning" %}
4+
**Warning**: This is an _experimental_ feature. It is stable but there are still rough edges. Contributions are welcome!
5+
{% endhint %}
6+
7+
## Overview
8+
9+
Feature view versioning automatically tracks schema and UDF changes to feature views. Every time `feast apply` detects a change, a versioned snapshot is saved to the registry. This enables:
10+
11+
- **Audit trail** — see what a feature view looked like at any point in time
12+
- **Safe rollback** — pin serving to a prior version with `version="v0"` in your definition
13+
- **Multi-version serving** — serve both old and new schemas simultaneously using `@v<N>` syntax
14+
- **Staged publishing** — use `feast apply --no-promote` to publish a new version without making it the default
15+
16+
## Quick Start
17+
18+
Version history tracking is **always active** with no configuration required. Every `feast apply` that changes a feature view automatically records a version snapshot.
19+
20+
### List versions
21+
22+
```bash
23+
feast version-history driver_stats
24+
```
25+
26+
### Pin to a prior version
27+
28+
```python
29+
driver_stats = FeatureView(
30+
name="driver_stats",
31+
version="v0", # Pin to v0
32+
...
33+
)
34+
```
35+
36+
### Staged publishing (no-promote)
37+
38+
```bash
39+
# Publish v2 without promoting it to active
40+
feast apply --no-promote
41+
42+
# Populate v2 online table
43+
feast materialize --views driver_stats --version v2 ...
44+
45+
# Migrate consumers to @v2 refs, then promote
46+
feast apply
47+
```
48+
49+
### Version-qualified online reads
50+
51+
To enable version-qualified reads (e.g., `driver_stats@v2:trips_today`), add the following to your `feature_store.yaml`:
52+
53+
```yaml
54+
registry:
55+
path: data/registry.db
56+
enable_online_feature_view_versioning: true
57+
```
58+
59+
Then query specific versions:
60+
61+
```python
62+
features = store.get_online_features(
63+
features=["driver_stats@v2:trips_today"],
64+
entity_rows=[{"driver_id": 1001}],
65+
)
66+
```
67+
68+
## Online Store Support
69+
70+
{% hint style="info" %}
71+
**Currently, version-qualified online reads (`@v<N>`) are only supported with the SQLite online store.** Support for additional online stores (Redis, DynamoDB, Bigtable, Postgres, etc.) will be added based on community priority.
72+
73+
If you need versioned online reads for a specific online store, please [open a GitHub issue](https://github.com/feast-dev/feast/issues/new) describing your use case and which store you need. This helps us prioritize development.
74+
{% endhint %}
75+
76+
Version history tracking in the registry (listing versions, pinning, `--no-promote`) works with **all** registry backends (file, SQL, Snowflake).
77+
78+
## Full Details
79+
80+
For the complete design, concurrency semantics, and feature service interactions, see the [Feature View Versioning RFC](../rfcs/feature-view-versioning.md).
81+
82+
## Known Limitations
83+
84+
- **Online store coverage** — Version-qualified reads (`@v<N>`) are SQLite-only today. Other online stores are follow-up work.
85+
- **Offline store versioning** — Versioned historical retrieval is not yet supported.
86+
- **Version deletion** — There is no mechanism to prune old versions from the registry.
87+
- **Cross-version joins** — Joining features from different versions of the same feature view in `get_historical_features` is not supported.
88+
- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not served until promoted.

docs/rfcs/feature-view-versioning.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,60 @@ If two concurrent applies both try to forward-declare the same version:
364364
- For single-developer or CI/CD workflows, the file registry works fine.
365365
- For multi-client environments with concurrent applies, use the SQL registry for proper conflict detection.
366366

367+
## Staged Publishing (`--no-promote`)
368+
369+
By default, `feast apply` atomically saves a version snapshot **and** promotes it to the active definition. This works well for additive changes, but for breaking schema changes you may want to stage the new version without disrupting unversioned consumers.
370+
371+
### The Problem
372+
373+
Without `--no-promote`, a phased rollout looks like:
374+
375+
1. `feast apply` — saves v2 and promotes it (all unversioned consumers now hit v2)
376+
2. Immediately pin back to v1 — `version="v1"` in the definition, then `feast apply` again
377+
378+
This leaves a transition window where unversioned consumers briefly see the new schema. Authors can also forget the pin-back step.
379+
380+
### The Solution
381+
382+
The `--no-promote` flag saves the version snapshot without updating the active feature view definition. The new version is accessible only via explicit `@v<N>` reads and `--version` materialization.
383+
384+
**CLI usage:**
385+
386+
```bash
387+
feast apply --no-promote
388+
```
389+
390+
**Python SDK equivalent:**
391+
392+
```python
393+
store.apply([entity, feature_view], no_promote=True)
394+
```
395+
396+
### Phased Rollout Workflow
397+
398+
1. **Stage the new version:**
399+
```bash
400+
feast apply --no-promote
401+
```
402+
This publishes v2 without promoting it. All unversioned consumers continue using v1.
403+
404+
2. **Populate the v2 online table:**
405+
```bash
406+
feast materialize --views driver_stats --version v2 ...
407+
```
408+
409+
3. **Migrate consumers one at a time:**
410+
- Consumer A switches to `driver_stats@v2:trips_today`
411+
- Consumer B switches to `driver_stats@v2:avg_rating`
412+
413+
4. **Promote v2 as the default:**
414+
```bash
415+
feast apply
416+
```
417+
Or pin to v2: set `version="v2"` in the definition and run `feast apply`.
418+
419+
> **Note:** By default, `feast apply` (without `--no-promote`) promotes the new version immediately. Use `--no-promote` only when you need a controlled, phased rollout.
420+
367421
## Feature Services
368422

369423
Feature services work with versioned feature views when the online versioning flag is enabled:
@@ -375,6 +429,7 @@ Feature services work with versioned feature views when the online versioning fl
375429
- `get_online_features()` will fail at retrieval time with a descriptive error message.
376430
- **No `@v<N>` syntax in feature services.** Version-qualified reads (`driver_stats@v2:trips_today`) using the `@v<N>` syntax require string-based feature references passed directly to `get_online_features()`. Feature services always resolve to the active (latest) version of each referenced feature view.
377431
- **Future work: per-reference version pinning.** A future enhancement could allow feature services to pin individual feature view references to specific versions (e.g., `FeatureService(features=[driver_stats["v2"]])`).
432+
- **`--no-promote` versions are not served.** Feature services always resolve to the active (promoted) version. Versions published with `--no-promote` are not visible to feature services until promoted via a regular `feast apply` or explicit pin.
378433

379434
## Limitations & Future Work
380435

go/internal/feast/onlineserving/serving.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ func ParseFeatureReference(featureRef string) (featureViewName, featureName stri
492492
featureViewName = parsedFeatureName[0]
493493
featureName = parsedFeatureName[1]
494494
}
495+
496+
// Strip @v<N> version qualifier from feature view name
497+
if atIdx := strings.Index(featureViewName, "@"); atIdx >= 0 {
498+
featureViewName = featureViewName[:atIdx]
499+
}
495500
return
496501
}
497502

sdk/python/feast/cli/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,20 @@ def plan_command(
276276
is_flag=True,
277277
help="Disable progress bars during apply operation.",
278278
)
279+
@click.option(
280+
"--no-promote",
281+
is_flag=True,
282+
default=False,
283+
help="Save new versions without promoting them to active. "
284+
"New versions are accessible via @v<N> reads and --version materialization.",
285+
)
279286
@click.pass_context
280287
def apply_total_command(
281288
ctx: click.Context,
282289
skip_source_validation: bool,
283290
skip_feature_view_validation: bool,
284291
no_progress: bool,
292+
no_promote: bool,
285293
):
286294
"""
287295
Create or update a feature store deployment
@@ -304,6 +312,7 @@ def apply_total_command(
304312
repo,
305313
skip_source_validation,
306314
skip_feature_view_validation,
315+
no_promote=no_promote,
307316
)
308317
except FeastProviderLoginError as e:
309318
print(str(e))

0 commit comments

Comments
 (0)