Skip to content
Prev Previous commit
Next Next commit
Add skip_validation parameter to FeatureStore.apply() and plan()
Co-authored-by: franciscojavierarceo <4163062+franciscojavierarceo@users.noreply.github.com>
  • Loading branch information
Copilot and franciscojavierarceo committed Jan 14, 2026
commit a3f7c497728e003ee43ee363e953958e42fc38a1
18 changes: 14 additions & 4 deletions sdk/python/feast/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,13 @@ def endpoint(ctx: click.Context):
is_flag=True,
help="Don't validate the data sources by checking for that the tables exist.",
)
@click.option(
"--skip-validation",
is_flag=True,
help="Don't validate feature views. Use with caution as this skips important checks.",
)
@click.pass_context
def plan_command(ctx: click.Context, skip_source_validation: bool):
def plan_command(ctx: click.Context, skip_source_validation: bool, skip_validation: bool):
"""
Create or update a feature store deployment
"""
Expand All @@ -247,7 +252,7 @@ def plan_command(ctx: click.Context, skip_source_validation: bool):
cli_check_repo(repo, fs_yaml_file)
repo_config = load_repo_config(repo, fs_yaml_file)
try:
plan(repo_config, repo, skip_source_validation)
plan(repo_config, repo, skip_source_validation, skip_validation)
except FeastProviderLoginError as e:
print(str(e))

Expand All @@ -258,8 +263,13 @@ def plan_command(ctx: click.Context, skip_source_validation: bool):
is_flag=True,
help="Don't validate the data sources by checking for that the tables exist.",
)
@click.option(
"--skip-validation",
is_flag=True,
help="Don't validate feature views. Use with caution as this skips important checks.",
)
@click.pass_context
def apply_total_command(ctx: click.Context, skip_source_validation: bool):
def apply_total_command(ctx: click.Context, skip_source_validation: bool, skip_validation: bool):
"""
Create or update a feature store deployment
"""
Expand All @@ -269,7 +279,7 @@ def apply_total_command(ctx: click.Context, skip_source_validation: bool):

repo_config = load_repo_config(repo, fs_yaml_file)
try:
apply_total(repo_config, repo, skip_source_validation)
apply_total(repo_config, repo, skip_source_validation, skip_validation)
except FeastProviderLoginError as e:
print(str(e))

Expand Down
29 changes: 18 additions & 11 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ def _get_feature_views_to_materialize(
return feature_views_to_materialize

def plan(
self, desired_repo_contents: RepoContents
self, desired_repo_contents: RepoContents, skip_validation: bool = False
) -> Tuple[RegistryDiff, InfraDiff, Infra]:
"""Dry-run registering objects to metadata store.

Expand All @@ -733,6 +733,8 @@ def plan(

Args:
desired_repo_contents: The desired repo state.
skip_validation: If True, skip validation of feature views. This can be useful when the validation
system is being overly strict. Use with caution and report any issues on GitHub. Default is False.

Raises:
ValueError: The 'objects' parameter could not be parsed properly.
Expand Down Expand Up @@ -767,11 +769,12 @@ def plan(
... permissions=list())) # register entity and feature view
"""
# Validate and run inference on all the objects to be registered.
self._validate_all_feature_views(
desired_repo_contents.feature_views,
desired_repo_contents.on_demand_feature_views,
desired_repo_contents.stream_feature_views,
)
if not skip_validation:
self._validate_all_feature_views(
desired_repo_contents.feature_views,
desired_repo_contents.on_demand_feature_views,
desired_repo_contents.stream_feature_views,
)
_validate_data_sources(desired_repo_contents.data_sources)
self._make_inferences(
desired_repo_contents.data_sources,
Expand Down Expand Up @@ -835,6 +838,7 @@ def apply(
],
objects_to_delete: Optional[List[FeastObject]] = None,
partial: bool = True,
skip_validation: bool = False,
):
"""Register objects to metadata store and update related infrastructure.

Expand All @@ -849,6 +853,8 @@ def apply(
provider's infrastructure. This deletion will only be performed if partial is set to False.
partial: If True, apply will only handle the specified objects; if False, apply will also delete
all the objects in objects_to_delete, and tear down any associated cloud resources.
skip_validation: If True, skip validation of feature views. This can be useful when the validation
system is being overly strict. Use with caution and report any issues on GitHub. Default is False.

Raises:
ValueError: The 'objects' parameter could not be parsed properly.
Expand Down Expand Up @@ -950,11 +956,12 @@ def apply(
entities_to_update.append(DUMMY_ENTITY)

# Validate all feature views and make inferences.
self._validate_all_feature_views(
views_to_update,
odfvs_to_update,
sfvs_to_update,
)
if not skip_validation:
self._validate_all_feature_views(
views_to_update,
odfvs_to_update,
sfvs_to_update,
)
self._make_inferences(
data_sources_to_update,
entities_to_update,
Expand Down
13 changes: 7 additions & 6 deletions sdk/python/feast/repo_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def parse_repo(repo_root: Path) -> RepoContents:
return res


def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool):
def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool, skip_validation: bool = False):
os.chdir(repo_path)
repo = _get_repo_contents(repo_path, repo_config.project, repo_config)
for project in repo.projects:
Expand All @@ -234,7 +234,7 @@ def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool)
for data_source in data_sources:
provider.validate_data_source(store.config, data_source)

registry_diff, infra_diff, _ = store.plan(repo)
registry_diff, infra_diff, _ = store.plan(repo, skip_validation=skip_validation)
click.echo(registry_diff.to_string())
click.echo(infra_diff.to_string())

Expand Down Expand Up @@ -334,6 +334,7 @@ def apply_total_with_repo_instance(
registry: BaseRegistry,
repo: RepoContents,
skip_source_validation: bool,
skip_validation: bool = False,
):
if not skip_source_validation:
provider = store._get_provider()
Expand All @@ -351,13 +352,13 @@ def apply_total_with_repo_instance(
) = extract_objects_for_apply_delete(project_name, registry, repo)

if store._should_use_plan():
registry_diff, infra_diff, new_infra = store.plan(repo)
registry_diff, infra_diff, new_infra = store.plan(repo, skip_validation=skip_validation)
click.echo(registry_diff.to_string())

store._apply_diffs(registry_diff, infra_diff, new_infra)
click.echo(infra_diff.to_string())
else:
store.apply(all_to_apply, objects_to_delete=all_to_delete, partial=False)
store.apply(all_to_apply, objects_to_delete=all_to_delete, partial=False, skip_validation=skip_validation)
log_infra_changes(views_to_keep, views_to_delete)


Expand Down Expand Up @@ -396,7 +397,7 @@ def create_feature_store(
return FeatureStore(repo_path=str(repo), fs_yaml_file=fs_yaml_file)


def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool):
def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool, skip_validation: bool = False):
os.chdir(repo_path)
repo = _get_repo_contents(repo_path, repo_config.project, repo_config)
for project in repo.projects:
Expand All @@ -411,7 +412,7 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation
# TODO: When we support multiple projects in a single repo, we should filter repo contents by project. Currently there is no way to associate Feast objects to project.
print(f"Applying changes for project {project.name}")
apply_total_with_repo_instance(
store, project.name, registry, repo, skip_source_validation
store, project.name, registry, repo, skip_source_validation, skip_validation
)


Expand Down
200 changes: 200 additions & 0 deletions sdk/python/tests/unit/test_skip_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""
Tests for skip_validation parameter in FeatureStore.apply() and FeatureStore.plan()
"""
from datetime import timedelta
from typing import Any, Dict

import pandas as pd

from feast import Entity, FeatureView, Field
from feast.data_source import RequestSource
from feast.feature_store import FeatureStore
from feast.infra.offline_stores.file_source import FileSource
from feast.on_demand_feature_view import on_demand_feature_view
from feast.types import Float32, Int64


def test_apply_with_skip_validation(tmp_path):
"""Test that FeatureStore.apply() works with skip_validation=True"""

# Create a temporary feature store
fs = FeatureStore(
config=f"""
project: test_skip_validation
registry: {tmp_path / "registry.db"}
provider: local
online_store:
type: sqlite
path: {tmp_path / "online_store.db"}
"""
)

# Create a basic feature view
batch_source = FileSource(
path=str(tmp_path / "data.parquet"),
timestamp_field="event_timestamp",
)

entity = Entity(name="test_entity", join_keys=["entity_id"])

fv = FeatureView(
name="test_fv",
entities=[entity],
schema=[
Field(name="feature1", dtype=Int64),
Field(name="entity_id", dtype=Int64),
],
source=batch_source,
ttl=timedelta(days=1),
)

# Apply with skip_validation=False (default)
fs.apply([entity, fv], skip_validation=False)

# Verify the feature view was applied
feature_views = fs.list_feature_views()
assert len(feature_views) == 1
assert feature_views[0].name == "test_fv"

# Apply again with skip_validation=True
fv2 = FeatureView(
name="test_fv2",
entities=[entity],
schema=[
Field(name="feature2", dtype=Float32),
Field(name="entity_id", dtype=Int64),
],
source=batch_source,
ttl=timedelta(days=1),
)

fs.apply([fv2], skip_validation=True)

# Verify both feature views are present
feature_views = fs.list_feature_views()
assert len(feature_views) == 2

fs.teardown()


def test_apply_odfv_with_skip_validation(tmp_path):
"""Test that skip_validation works for OnDemandFeatureViews to bypass _construct_random_input validation"""

# Create a temporary feature store
fs = FeatureStore(
config=f"""
project: test_skip_odfv
registry: {tmp_path / "registry.db"}
provider: local
online_store:
type: sqlite
path: {tmp_path / "online_store.db"}
"""
)

# Create a basic feature view
batch_source = FileSource(
path=str(tmp_path / "data.parquet"),
timestamp_field="event_timestamp",
)

entity = Entity(name="test_entity", join_keys=["entity_id"])

fv = FeatureView(
name="base_fv",
entities=[entity],
schema=[
Field(name="input_feature", dtype=Int64),
Field(name="entity_id", dtype=Int64),
],
source=batch_source,
ttl=timedelta(days=1),
)

# Create a request source
request_source = RequestSource(
name="request_source",
schema=[Field(name="request_input", dtype=Int64)],
)

# Define an ODFV with transformation
@on_demand_feature_view(
sources=[fv, request_source],
schema=[Field(name="output_feature", dtype=Int64)],
)
def test_odfv(inputs: Dict[str, Any]) -> Dict[str, Any]:
return {
"output_feature": inputs["input_feature"] + inputs["request_input"]
}

# Apply with skip_validation=True to bypass infer_features() validation
# This is the key use case mentioned in the issue
fs.apply([entity, fv, test_odfv], skip_validation=True)

# Verify the ODFV was applied
odfvs = fs.list_on_demand_feature_views()
assert len(odfvs) == 1
assert odfvs[0].name == "test_odfv"

fs.teardown()


def test_plan_with_skip_validation(tmp_path):
"""Test that FeatureStore.plan() works with skip_validation=True"""
from feast.feature_store import RepoContents

# Create a temporary feature store
fs = FeatureStore(
config=f"""
project: test_plan_skip
registry: {tmp_path / "registry.db"}
provider: local
online_store:
type: sqlite
path: {tmp_path / "online_store.db"}
"""
)

# Create a basic feature view
batch_source = FileSource(
path=str(tmp_path / "data.parquet"),
timestamp_field="event_timestamp",
)

entity = Entity(name="test_entity", join_keys=["entity_id"])

fv = FeatureView(
name="test_fv",
entities=[entity],
schema=[
Field(name="feature1", dtype=Int64),
Field(name="entity_id", dtype=Int64),
],
source=batch_source,
ttl=timedelta(days=1),
)

# Create repo contents
repo_contents = RepoContents(
feature_views=[fv],
entities=[entity],
data_sources=[batch_source],
on_demand_feature_views=[],
stream_feature_views=[],
feature_services=[],
permissions=[],
)

# Plan with skip_validation=False (default)
registry_diff, infra_diff, new_infra = fs.plan(repo_contents, skip_validation=False)

# Verify the diff shows the feature view will be added
assert len(registry_diff.fv_to_add) == 1

# Plan with skip_validation=True
registry_diff, infra_diff, new_infra = fs.plan(repo_contents, skip_validation=True)

# Verify the diff still works correctly
assert len(registry_diff.fv_to_add) == 1

fs.teardown()