6 files changed
@@ -309,4 +309,44 @@ There are new CLI commands to manage on demand feature views: | |||
| 309 | 309 | feast on-demand-feature-views list: Lists all registered on demand feature views after feast apply is run. | |
| 310 | 310 | feast on-demand-feature-views describe [NAME]: Describes the definition of an on demand feature view. | |
| 311 | 311 | ||
| 312 | + ## Troubleshooting | ||
| 313 | + | ||
| 314 | + ### Validation Issues with Complex Transformations | ||
| 315 | + | ||
| 316 | + 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. | ||
| 317 | + | ||
| 318 | + If you encounter validation errors that you believe are incorrect, you can skip feature view validation: | ||
| 319 | + | ||
| 320 | + **Python SDK:** | ||
| 321 | + ```python | ||
| 322 | + from feast import FeatureStore | ||
| 323 | + | ||
| 324 | + store = FeatureStore(repo_path=".") | ||
| 325 | + store.apply([my_odfv], skip_feature_view_validation=True) | ||
| 326 | + ``` | ||
| 327 | + | ||
| 328 | + **CLI:** | ||
| 329 | + ```bash | ||
| 330 | + feast apply --skip-feature-view-validation | ||
| 331 | + ``` | ||
| 332 | + | ||
| 333 | + {% hint style="warning" %} | ||
| 334 | + 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. | ||
| 335 | + {% endhint %} | ||
| 336 | + | ||
| 337 | + **When to use skip_feature_view_validation:** | ||
| 338 | + - Your ODFV transformation uses complex logic that fails the random input validation | ||
| 339 | + - You've verified your transformation works correctly with real data | ||
| 340 | + - The validation error doesn't reflect an actual issue with your transformation | ||
| 341 | + | ||
| 342 | + **What validation is skipped:** | ||
| 343 | + - Feature view name uniqueness checks | ||
| 344 | + - ODFV transformation validation via `_construct_random_input()` | ||
| 345 | + - Schema inference for features | ||
| 346 | + | ||
| 347 | + **What is NOT skipped:** | ||
| 348 | + - Data source validation (use `--skip-source-validation` to skip) | ||
| 349 | + - Registry operations | ||
| 350 | + - Infrastructure updates | ||
| 351 | + | ||
| 312 | 352 | ||
@@ -52,13 +52,32 @@ Creates or updates a feature store deployment | |||
| 52 | 52 | feast apply | |
| 53 | 53 | ``` | |
| 54 | 54 | ||
| 55 | + **Options:** | ||
| 56 | + * `--skip-source-validation`: Skip validation of data sources (don't check if tables exist) | ||
| 57 | + * `--skip-feature-view-validation`: Skip validation of feature views. Use with caution as this skips important checks | ||
| 58 | + | ||
| 59 | + ```bash | ||
| 60 | + # Skip only data source validation | ||
| 61 | + feast apply --skip-source-validation | ||
| 62 | + | ||
| 63 | + # Skip only feature view validation | ||
| 64 | + feast apply --skip-feature-view-validation | ||
| 65 | + | ||
| 66 | + # Skip both validations | ||
| 67 | + feast apply --skip-source-validation --skip-feature-view-validation | ||
| 68 | + ``` | ||
| 69 | + | ||
| 55 | 70 | **What does Feast apply do?** | |
| 56 | 71 | ||
| 57 | 72 | 1. Feast will scan Python files in your feature repository and find all Feast object definitions, such as feature views, entities, and data sources. | |
| 58 | - 2. Feast will validate your feature definitions (e.g. for uniqueness of features) | ||
| 73 | + 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. | ||
| 59 | 74 | 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\). | |
| 60 | 75 | 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. | |
| 61 | 76 | ||
| 77 | + {% hint style="info" %} | ||
| 78 | + 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. | ||
| 79 | + {% endhint %} | ||
| 80 | + | ||
| 62 | 81 | {% hint style="warning" %} | |
| 63 | 82 | `feast apply` \(when configured to use cloud provider like `gcp` or `aws`\) will create cloud infrastructure. This may incur costs. | |
| 64 | 83 | {% endhint %} | |
@@ -237,8 +237,15 @@ def endpoint(ctx: click.Context): | |||
| 237 | 237 | is_flag=True, | |
| 238 | 238 | help="Don't validate the data sources by checking for that the tables exist.", | |
| 239 | 239 | ) | |
| 240 | + @click.option( | ||
| 241 | + "--skip-feature-view-validation", | ||
| 242 | + is_flag=True, | ||
| 243 | + help="Don't validate feature views. Use with caution as this skips important checks.", | ||
| 244 | + ) | ||
| 240 | 245 | @click.pass_context | |
| 241 | - def plan_command(ctx: click.Context, skip_source_validation: bool): | ||
| 246 | + def plan_command( | ||
| 247 | + ctx: click.Context, skip_source_validation: bool, skip_feature_view_validation: bool | ||
| 248 | + ): | ||
| 242 | 249 | """ | |
| 243 | 250 | Create or update a feature store deployment | |
| 244 | 251 | """ | |
@@ -247,7 +254,7 @@ def plan_command(ctx: click.Context, skip_source_validation: bool): | |||
| 247 | 254 | cli_check_repo(repo, fs_yaml_file) | |
| 248 | 255 | repo_config = load_repo_config(repo, fs_yaml_file) | |
| 249 | 256 | try: | |
| 250 | - plan(repo_config, repo, skip_source_validation) | ||
| 257 | + plan(repo_config, repo, skip_source_validation, skip_feature_view_validation) | ||
| 251 | 258 | except FeastProviderLoginError as e: | |
| 252 | 259 | print(str(e)) | |
| 253 | 260 | ||
@@ -258,8 +265,15 @@ def plan_command(ctx: click.Context, skip_source_validation: bool): | |||
| 258 | 265 | is_flag=True, | |
| 259 | 266 | help="Don't validate the data sources by checking for that the tables exist.", | |
| 260 | 267 | ) | |
| 268 | + @click.option( | ||
| 269 | + "--skip-feature-view-validation", | ||
| 270 | + is_flag=True, | ||
| 271 | + help="Don't validate feature views. Use with caution as this skips important checks.", | ||
| 272 | + ) | ||
| 261 | 273 | @click.pass_context | |
| 262 | - def apply_total_command(ctx: click.Context, skip_source_validation: bool): | ||
| 274 | + def apply_total_command( | ||
| 275 | + ctx: click.Context, skip_source_validation: bool, skip_feature_view_validation: bool | ||
| 276 | + ): | ||
| 263 | 277 | """ | |
| 264 | 278 | Create or update a feature store deployment | |
| 265 | 279 | """ | |
@@ -269,7 +283,9 @@ def apply_total_command(ctx: click.Context, skip_source_validation: bool): | |||
| 269 | 283 | ||
| 270 | 284 | repo_config = load_repo_config(repo, fs_yaml_file) | |
| 271 | 285 | try: | |
| 272 | - apply_total(repo_config, repo, skip_source_validation) | ||
| 286 | + apply_total( | ||
| 287 | + repo_config, repo, skip_source_validation, skip_feature_view_validation | ||
| 288 | + ) | ||
| 273 | 289 | except FeastProviderLoginError as e: | |
| 274 | 290 | print(str(e)) | |
| 275 | 291 | ||
@@ -723,7 +723,9 @@ def _get_feature_views_to_materialize( | |||
| 723 | 723 | return feature_views_to_materialize | |
| 724 | 724 | ||
| 725 | 725 | def plan( | |
| 726 | - self, desired_repo_contents: RepoContents | ||
| 726 | + self, | ||
| 727 | + desired_repo_contents: RepoContents, | ||
| 728 | + skip_feature_view_validation: bool = False, | ||
| 727 | 729 | ) -> Tuple[RegistryDiff, InfraDiff, Infra]: | |
| 728 | 730 | """Dry-run registering objects to metadata store. | |
| 729 | 731 | ||
@@ -733,6 +735,8 @@ def plan( | |||
| 733 | 735 | ||
| 734 | 736 | Args: | |
| 735 | 737 | desired_repo_contents: The desired repo state. | |
| 738 | + skip_feature_view_validation: If True, skip validation of feature views. This can be useful when the validation | ||
| 739 | + system is being overly strict. Use with caution and report any issues on GitHub. Default is False. | ||
| 736 | 740 | ||
| 737 | 741 | Raises: | |
| 738 | 742 | ValueError: The 'objects' parameter could not be parsed properly. | |
@@ -767,11 +771,12 @@ def plan( | |||
| 767 | 771 | ... permissions=list())) # register entity and feature view | |
| 768 | 772 | """ | |
| 769 | 773 | # Validate and run inference on all the objects to be registered. | |
| 770 | - self._validate_all_feature_views( | ||
| 771 | - desired_repo_contents.feature_views, | ||
| 772 | - desired_repo_contents.on_demand_feature_views, | ||
| 773 | - desired_repo_contents.stream_feature_views, | ||
| 774 | - ) | ||
| 774 | + if not skip_feature_view_validation: | ||
| 775 | + self._validate_all_feature_views( | ||
| 776 | + desired_repo_contents.feature_views, | ||
| 777 | + desired_repo_contents.on_demand_feature_views, | ||
| 778 | + desired_repo_contents.stream_feature_views, | ||
| 779 | + ) | ||
| 775 | 780 | _validate_data_sources(desired_repo_contents.data_sources) | |
| 776 | 781 | self._make_inferences( | |
| 777 | 782 | desired_repo_contents.data_sources, | |
@@ -835,6 +840,7 @@ def apply( | |||
| 835 | 840 | ], | |
| 836 | 841 | objects_to_delete: Optional[List[FeastObject]] = None, | |
| 837 | 842 | partial: bool = True, | |
| 843 | + skip_feature_view_validation: bool = False, | ||
| 838 | 844 | ): | |
| 839 | 845 | """Register objects to metadata store and update related infrastructure. | |
| 840 | 846 | ||
@@ -853,6 +859,8 @@ def apply( | |||
| 853 | 859 | provider's infrastructure. This deletion will only be performed if partial is set to False. | |
| 854 | 860 | partial: If True, apply will only handle the specified objects; if False, apply will also delete | |
| 855 | 861 | all the objects in objects_to_delete, and tear down any associated cloud resources. | |
| 862 | + skip_feature_view_validation: If True, skip validation of feature views. This can be useful when the validation | ||
| 863 | + system is being overly strict. Use with caution and report any issues on GitHub. Default is False. | ||
| 856 | 864 | ||
| 857 | 865 | Raises: | |
| 858 | 866 | ValueError: The 'objects' parameter could not be parsed properly. | |
@@ -954,11 +962,12 @@ def apply( | |||
| 954 | 962 | entities_to_update.append(DUMMY_ENTITY) | |
| 955 | 963 | ||
| 956 | 964 | # Validate all feature views and make inferences. | |
| 957 | - self._validate_all_feature_views( | ||
| 958 | - views_to_update, | ||
| 959 | - odfvs_to_update, | ||
| 960 | - sfvs_to_update, | ||
| 961 | - ) | ||
| 965 | + if not skip_feature_view_validation: | ||
| 966 | + self._validate_all_feature_views( | ||
| 967 | + views_to_update, | ||
| 968 | + odfvs_to_update, | ||
| 969 | + sfvs_to_update, | ||
| 970 | + ) | ||
| 962 | 971 | self._make_inferences( | |
| 963 | 972 | data_sources_to_update, | |
| 964 | 973 | entities_to_update, | |
@@ -220,7 +220,12 @@ def parse_repo(repo_root: Path) -> RepoContents: | |||
| 220 | 220 | return res | |
| 221 | 221 | ||
| 222 | 222 | ||
| 223 | - def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): | ||
| 223 | + def plan( | ||
| 224 | + repo_config: RepoConfig, | ||
| 225 | + repo_path: Path, | ||
| 226 | + skip_source_validation: bool, | ||
| 227 | + skip_feature_view_validation: bool = False, | ||
| 228 | + ): | ||
| 224 | 229 | os.chdir(repo_path) | |
| 225 | 230 | repo = _get_repo_contents(repo_path, repo_config.project, repo_config) | |
| 226 | 231 | for project in repo.projects: | |
@@ -234,7 +239,9 @@ def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool) | |||
| 234 | 239 | for data_source in data_sources: | |
| 235 | 240 | provider.validate_data_source(store.config, data_source) | |
| 236 | 241 | ||
| 237 | - registry_diff, infra_diff, _ = store.plan(repo) | ||
| 242 | + registry_diff, infra_diff, _ = store.plan( | ||
| 243 | + repo, skip_feature_view_validation=skip_feature_view_validation | ||
| 244 | + ) | ||
| 238 | 245 | click.echo(registry_diff.to_string()) | |
| 239 | 246 | click.echo(infra_diff.to_string()) | |
| 240 | 247 | ||
@@ -334,6 +341,7 @@ def apply_total_with_repo_instance( | |||
| 334 | 341 | registry: BaseRegistry, | |
| 335 | 342 | repo: RepoContents, | |
| 336 | 343 | skip_source_validation: bool, | |
| 344 | + skip_feature_view_validation: bool = False, | ||
| 337 | 345 | ): | |
| 338 | 346 | if not skip_source_validation: | |
| 339 | 347 | provider = store._get_provider() | |
@@ -351,13 +359,20 @@ def apply_total_with_repo_instance( | |||
| 351 | 359 | ) = extract_objects_for_apply_delete(project_name, registry, repo) | |
| 352 | 360 | ||
| 353 | 361 | if store._should_use_plan(): | |
| 354 | - registry_diff, infra_diff, new_infra = store.plan(repo) | ||
| 362 | + registry_diff, infra_diff, new_infra = store.plan( | ||
| 363 | + repo, skip_feature_view_validation=skip_feature_view_validation | ||
| 364 | + ) | ||
| 355 | 365 | click.echo(registry_diff.to_string()) | |
| 356 | 366 | ||
| 357 | 367 | store._apply_diffs(registry_diff, infra_diff, new_infra) | |
| 358 | 368 | click.echo(infra_diff.to_string()) | |
| 359 | 369 | else: | |
| 360 | - store.apply(all_to_apply, objects_to_delete=all_to_delete, partial=False) | ||
| 370 | + store.apply( | ||
| 371 | + all_to_apply, | ||
| 372 | + objects_to_delete=all_to_delete, | ||
| 373 | + partial=False, | ||
| 374 | + skip_feature_view_validation=skip_feature_view_validation, | ||
| 375 | + ) | ||
| 361 | 376 | log_infra_changes(views_to_keep, views_to_delete) | |
| 362 | 377 | ||
| 363 | 378 | ||
@@ -396,7 +411,12 @@ def create_feature_store( | |||
| 396 | 411 | return FeatureStore(repo_path=str(repo), fs_yaml_file=fs_yaml_file) | |
| 397 | 412 | ||
| 398 | 413 | ||
| 399 | - def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): | ||
| 414 | + def apply_total( | ||
| 415 | + repo_config: RepoConfig, | ||
| 416 | + repo_path: Path, | ||
| 417 | + skip_source_validation: bool, | ||
| 418 | + skip_feature_view_validation: bool = False, | ||
| 419 | + ): | ||
| 400 | 420 | os.chdir(repo_path) | |
| 401 | 421 | repo = _get_repo_contents(repo_path, repo_config.project, repo_config) | |
| 402 | 422 | for project in repo.projects: | |
@@ -411,7 +431,12 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation | |||
| 411 | 431 | # 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. | |
| 412 | 432 | print(f"Applying changes for project {project.name}") | |
| 413 | 433 | apply_total_with_repo_instance( | |
| 414 | - store, project.name, registry, repo, skip_source_validation | ||
| 434 | + store, | ||
| 435 | + project.name, | ||
| 436 | + registry, | ||
| 437 | + repo, | ||
| 438 | + skip_source_validation, | ||
| 439 | + skip_feature_view_validation, | ||
| 415 | 440 | ) | |
| 416 | 441 | ||
| 417 | 442 | ||
@@ -0,0 +1,71 @@ | |||
| 1 | + """ | ||
| 2 | + Tests for skip_feature_view_validation parameter in FeatureStore.apply() and FeatureStore.plan() | ||
| 3 | + | ||
| 4 | + This feature allows users to skip Feature View validation when the validation system | ||
| 5 | + is being overly strict. This is particularly important for: | ||
| 6 | + - Feature transformations that go through validation (e.g., _construct_random_input in ODFVs) | ||
| 7 | + - Cases where the type/validation system is being too restrictive | ||
| 8 | + | ||
| 9 | + Users should be encouraged to report issues on GitHub when they need to use this flag. | ||
| 10 | + """ | ||
| 11 | + | ||
| 12 | + import inspect | ||
| 13 | + | ||
| 14 | + from feast.feature_store import FeatureStore | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + def test_apply_has_skip_feature_view_validation_parameter(): | ||
| 18 | + """Test that FeatureStore.apply() method has skip_feature_view_validation parameter""" | ||
| 19 | + # Get the signature of the apply method | ||
| 20 | + sig = inspect.signature(FeatureStore.apply) | ||
| 21 | + | ||
| 22 | + # Check that skip_feature_view_validation parameter exists | ||
| 23 | + assert "skip_feature_view_validation" in sig.parameters | ||
| 24 | + | ||
| 25 | + # Check that it has a default value of False | ||
| 26 | + param = sig.parameters["skip_feature_view_validation"] | ||
| 27 | + assert param.default is False | ||
| 28 | + | ||
| 29 | + # Check that it's a boolean type hint (if type hints are present) | ||
| 30 | + if param.annotation != inspect.Parameter.empty: | ||
| 31 | + assert param.annotation == bool | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + def test_plan_has_skip_feature_view_validation_parameter(): | ||
| 35 | + """Test that FeatureStore.plan() method has skip_feature_view_validation parameter""" | ||
| 36 | + # Get the signature of the plan method | ||
| 37 | + sig = inspect.signature(FeatureStore.plan) | ||
| 38 | + | ||
| 39 | + # Check that skip_feature_view_validation parameter exists | ||
| 40 | + assert "skip_feature_view_validation" in sig.parameters | ||
| 41 | + | ||
| 42 | + # Check that it has a default value of False | ||
| 43 | + param = sig.parameters["skip_feature_view_validation"] | ||
| 44 | + assert param.default is False | ||
| 45 | + | ||
| 46 | + # Check that it's a boolean type hint (if type hints are present) | ||
| 47 | + if param.annotation != inspect.Parameter.empty: | ||
| 48 | + assert param.annotation == bool | ||
| 49 | + | ||
| 50 | + | ||
| 51 | + def test_skip_feature_view_validation_use_case_documentation(): | ||
| 52 | + """ | ||
| 53 | + Documentation test: This test documents the key use case for skip_feature_view_validation. | ||
| 54 | + | ||
| 55 | + The skip_feature_view_validation flag is particularly important for On-Demand Feature Views (ODFVs) | ||
| 56 | + that use feature transformations. During the apply() process, ODFVs call infer_features() | ||
| 57 | + which internally uses _construct_random_input() to validate the transformation. | ||
| 58 | + | ||
| 59 | + Sometimes this validation can be overly strict or fail for complex transformations. | ||
| 60 | + In such cases, users can use skip_feature_view_validation=True to bypass this check. | ||
| 61 | + | ||
| 62 | + Example use case from the issue: | ||
| 63 | + - User has an ODFV with a complex transformation | ||
| 64 | + - The _construct_random_input validation fails or is too restrictive | ||
| 65 | + - User can now call: fs.apply([odfv], skip_feature_view_validation=True) | ||
| 66 | + - The ODFV is registered without going through the validation | ||
| 67 | + | ||
| 68 | + Note: Users should be encouraged to report such cases on GitHub so the Feast team | ||
| 69 | + can improve the validation system. | ||
| 70 | + """ | ||
| 71 | + pass # This is a documentation test | ||
0 commit comments