diff --git a/docs/reference/beta-on-demand-feature-view.md b/docs/reference/beta-on-demand-feature-view.md index 2482bbc1c8f..efb7023d567 100644 --- a/docs/reference/beta-on-demand-feature-view.md +++ b/docs/reference/beta-on-demand-feature-view.md @@ -309,4 +309,44 @@ There are new CLI commands to manage on demand feature views: feast on-demand-feature-views list: Lists all registered on demand feature views after feast apply is run. feast on-demand-feature-views describe [NAME]: Describes the definition of an on demand feature view. +## Troubleshooting + +### Validation Issues with Complex Transformations + +When defining On Demand Feature Views with complex transformations, you may encounter validation errors during `feast apply`. Feast validates ODFVs by constructing random inputs and running the transformation function to infer the output schema. This validation can sometimes be overly strict or fail for complex transformation logic. + +If you encounter validation errors that you believe are incorrect, you can skip feature view validation: + +**Python SDK:** +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +store.apply([my_odfv], skip_feature_view_validation=True) +``` + +**CLI:** +```bash +feast apply --skip-feature-view-validation +``` + +{% hint style="warning" %} +Skipping validation bypasses important checks. Use this option only when the validation system is being overly strict. We encourage you to report validation issues on the [Feast GitHub repository](https://github.com/feast-dev/feast/issues) so the team can improve the validation system. +{% endhint %} + +**When to use skip_feature_view_validation:** +- Your ODFV transformation uses complex logic that fails the random input validation +- You've verified your transformation works correctly with real data +- The validation error doesn't reflect an actual issue with your transformation + +**What validation is skipped:** +- Feature view name uniqueness checks +- ODFV transformation validation via `_construct_random_input()` +- Schema inference for features + +**What is NOT skipped:** +- Data source validation (use `--skip-source-validation` to skip) +- Registry operations +- Infrastructure updates + diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index 55d3c27ac84..eb6fa90d280 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -52,13 +52,32 @@ Creates or updates a feature store deployment feast apply ``` +**Options:** +* `--skip-source-validation`: Skip validation of data sources (don't check if tables exist) +* `--skip-feature-view-validation`: Skip validation of feature views. Use with caution as this skips important checks + +```bash +# Skip only data source validation +feast apply --skip-source-validation + +# Skip only feature view validation +feast apply --skip-feature-view-validation + +# Skip both validations +feast apply --skip-source-validation --skip-feature-view-validation +``` + **What does Feast apply do?** 1. Feast will scan Python files in your feature repository and find all Feast object definitions, such as feature views, entities, and data sources. -2. Feast will validate your feature definitions (e.g. for uniqueness of features) +2. Feast will validate your feature definitions (e.g. for uniqueness of features). This validation can be skipped using the `--skip-feature-view-validation` flag if the type/validation system is being overly strict. 3. Feast will sync the metadata about Feast objects to the registry. If a registry does not exist, then it will be instantiated. The standard registry is a simple protobuf binary file that is stored on disk \(locally or in an object store\). 4. Feast CLI will create all necessary feature store infrastructure. The exact infrastructure that is deployed or configured depends on the `provider` configuration that you have set in `feature_store.yaml`. For example, setting `local` as your provider will result in a `sqlite` online store being created. +{% hint style="info" %} +The `--skip-feature-view-validation` flag is particularly useful for On-Demand Feature Views (ODFVs) with complex transformations that may fail validation. However, use it with caution and please report any validation issues to the Feast team on GitHub. +{% endhint %} + {% hint style="warning" %} `feast apply` \(when configured to use cloud provider like `gcp` or `aws`\) will create cloud infrastructure. This may incur costs. {% endhint %} diff --git a/sdk/python/feast/cli/cli.py b/sdk/python/feast/cli/cli.py index 60ea6292488..ab756d47496 100644 --- a/sdk/python/feast/cli/cli.py +++ b/sdk/python/feast/cli/cli.py @@ -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-feature-view-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_feature_view_validation: bool +): """ Create or update a feature store deployment """ @@ -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_feature_view_validation) except FeastProviderLoginError as e: print(str(e)) @@ -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-feature-view-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_feature_view_validation: bool +): """ Create or update a feature store deployment """ @@ -269,7 +283,9 @@ 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_feature_view_validation + ) except FeastProviderLoginError as e: print(str(e)) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 5118cd4fde2..0eff5034683 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -723,7 +723,9 @@ def _get_feature_views_to_materialize( return feature_views_to_materialize def plan( - self, desired_repo_contents: RepoContents + self, + desired_repo_contents: RepoContents, + skip_feature_view_validation: bool = False, ) -> Tuple[RegistryDiff, InfraDiff, Infra]: """Dry-run registering objects to metadata store. @@ -733,6 +735,8 @@ def plan( Args: desired_repo_contents: The desired repo state. + skip_feature_view_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. @@ -767,11 +771,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_feature_view_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, @@ -835,6 +840,7 @@ def apply( ], objects_to_delete: Optional[List[FeastObject]] = None, partial: bool = True, + skip_feature_view_validation: bool = False, ): """Register objects to metadata store and update related infrastructure. @@ -853,6 +859,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_feature_view_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. @@ -954,11 +962,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_feature_view_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, diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 9bc11e625f5..b0809bdd399 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -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_feature_view_validation: bool = False, +): os.chdir(repo_path) repo = _get_repo_contents(repo_path, repo_config.project, repo_config) for project in repo.projects: @@ -234,7 +239,9 @@ 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_feature_view_validation=skip_feature_view_validation + ) click.echo(registry_diff.to_string()) click.echo(infra_diff.to_string()) @@ -334,6 +341,7 @@ def apply_total_with_repo_instance( registry: BaseRegistry, repo: RepoContents, skip_source_validation: bool, + skip_feature_view_validation: bool = False, ): if not skip_source_validation: provider = store._get_provider() @@ -351,13 +359,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_feature_view_validation=skip_feature_view_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_feature_view_validation=skip_feature_view_validation, + ) log_infra_changes(views_to_keep, views_to_delete) @@ -396,7 +411,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_feature_view_validation: bool = False, +): os.chdir(repo_path) repo = _get_repo_contents(repo_path, repo_config.project, repo_config) for project in repo.projects: @@ -411,7 +431,12 @@ 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_feature_view_validation, ) diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py new file mode 100644 index 00000000000..8fb916e3776 --- /dev/null +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -0,0 +1,71 @@ +""" +Tests for skip_feature_view_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_feature_view_validation_parameter(): + """Test that FeatureStore.apply() method has skip_feature_view_validation parameter""" + # Get the signature of the apply method + sig = inspect.signature(FeatureStore.apply) + + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_feature_view_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_feature_view_validation_parameter(): + """Test that FeatureStore.plan() method has skip_feature_view_validation parameter""" + # Get the signature of the plan method + sig = inspect.signature(FeatureStore.plan) + + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_feature_view_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_feature_view_validation_use_case_documentation(): + """ + Documentation test: This test documents the key use case for skip_feature_view_validation. + + The skip_feature_view_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_feature_view_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_feature_view_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