Skip to content
22 changes: 18 additions & 4 deletions sdk/python/feast/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,15 @@ 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot since we have skip_source_validation we should probably make skip_validation more descriptive or explicit about what validation it is skipping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in commit d4fe780. I've renamed the parameter from skip_validation to skip_feature_view_validation throughout the codebase to make it explicit that it skips feature view validation, which is now consistent with the existing skip_source_validation parameter.

Changes:

  • CLI flags: --skip-validation--skip-feature-view-validation
  • Python SDK parameters: skip_validationskip_feature_view_validation
  • Updated in: FeatureStore.apply(), FeatureStore.plan(), repo_operations.py, CLI commands, and tests

Usage:

# Python SDK
fs.apply([odfv], skip_feature_view_validation=True)
fs.plan(repo_contents, skip_feature_view_validation=True)
# CLI
feast apply --skip-feature-view-validation
feast plan --skip-feature-view-validation

):
"""
Create or update a feature store deployment
"""
Expand All @@ -247,7 +254,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 +265,15 @@ 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 +283,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
30 changes: 24 additions & 6 deletions sdk/python/feast/repo_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,12 @@ 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 +239,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 +339,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 +357,20 @@ 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 +409,12 @@ 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 +429,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
71 changes: 71 additions & 0 deletions sdk/python/tests/unit/test_skip_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Tests for skip_validation parameter in FeatureStore.apply() and FeatureStore.plan()

This feature allows users to skip Feature View validation when the validation system
is being overly strict. This is particularly important for:
- Feature transformations that go through validation (e.g., _construct_random_input in ODFVs)
- Cases where the type/validation system is being too restrictive

Users should be encouraged to report issues on GitHub when they need to use this flag.
"""

import inspect

from feast.feature_store import FeatureStore


def test_apply_has_skip_validation_parameter():
"""Test that FeatureStore.apply() method has skip_validation parameter"""
# Get the signature of the apply method
sig = inspect.signature(FeatureStore.apply)

# Check that skip_validation parameter exists
assert "skip_validation" in sig.parameters

# Check that it has a default value of False
param = sig.parameters["skip_validation"]
assert param.default is False

# Check that it's a boolean type hint (if type hints are present)
if param.annotation != inspect.Parameter.empty:
assert param.annotation == bool


def test_plan_has_skip_validation_parameter():
"""Test that FeatureStore.plan() method has skip_validation parameter"""
# Get the signature of the plan method
sig = inspect.signature(FeatureStore.plan)

# Check that skip_validation parameter exists
assert "skip_validation" in sig.parameters

# Check that it has a default value of False
param = sig.parameters["skip_validation"]
assert param.default is False

# Check that it's a boolean type hint (if type hints are present)
if param.annotation != inspect.Parameter.empty:
assert param.annotation == bool


def test_skip_validation_use_case_documentation():
"""
Documentation test: This test documents the key use case for skip_validation.

The skip_validation flag is particularly important for On-Demand Feature Views (ODFVs)
that use feature transformations. During the apply() process, ODFVs call infer_features()
which internally uses _construct_random_input() to validate the transformation.

Sometimes this validation can be overly strict or fail for complex transformations.
In such cases, users can use skip_validation=True to bypass this check.

Example use case from the issue:
- User has an ODFV with a complex transformation
- The _construct_random_input validation fails or is too restrictive
- User can now call: fs.apply([odfv], skip_validation=True)
- The ODFV is registered without going through the validation

Note: Users should be encouraged to report such cases on GitHub so the Feast team
can improve the validation system.
"""
pass # This is a documentation test
Loading