diff --git a/sdk/python/feast/cli/cli.py b/sdk/python/feast/cli/cli.py index 886c91f69ae..a549a5f41b3 100644 --- a/sdk/python/feast/cli/cli.py +++ b/sdk/python/feast/cli/cli.py @@ -13,6 +13,7 @@ # limitations under the License. import json import logging +import sys from datetime import datetime from importlib.metadata import version as importlib_version from pathlib import Path @@ -49,7 +50,7 @@ from feast.cli.ui import ui from feast.cli.validation_references import validation_references_cmd from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME -from feast.errors import FeastProviderLoginError +from feast.errors import FeastError, FeastProviderLoginError from feast.repo_config import load_repo_config from feast.repo_operations import ( apply_total, @@ -258,6 +259,9 @@ def plan_command( plan(repo_config, repo, skip_source_validation, skip_feature_view_validation) except FeastProviderLoginError as e: print(str(e)) + except FeastError as e: + print(str(e)) + sys.exit(1) @cli.command("apply", cls=NoOptionDefaultFormat) @@ -316,6 +320,9 @@ def apply_total_command( ) except FeastProviderLoginError as e: print(str(e)) + except FeastError as e: + print(str(e)) + sys.exit(1) @cli.command("teardown", cls=NoOptionDefaultFormat) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 28fe86602ad..7e8e2cf175b 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -21,6 +21,7 @@ from feast.data_source import DataSource, KafkaSource, KinesisSource from feast.diff.registry_diff import extract_objects_for_keep_delete_update_add from feast.entity import Entity +from feast.errors import ConflictingFeatureViewNames, DataSourceRepeatNamesException from feast.feature_service import FeatureService from feast.feature_store import FeatureStore from feast.feature_view import DUMMY_ENTITY, FeatureView @@ -219,6 +220,33 @@ def parse_repo(repo_root: Path) -> RepoContents: elif isinstance(obj, Project) and not any((obj is p) for p in res.projects): res.projects.append(obj) + # Early duplicate detection: validate feature view names are case-insensitively unique + # This runs before FeatureStore initialization to avoid slow cleanup on error + fv_names_seen = {} + all_feature_views = ( + res.feature_views + res.stream_feature_views + res.on_demand_feature_views + ) + for fv in all_feature_views: + lower_name = fv.name.lower() + if lower_name in fv_names_seen: + existing_fv = fv_names_seen[lower_name] + raise ConflictingFeatureViewNames( + fv.name, + existing_type=type(existing_fv).__name__, + new_type=type(fv).__name__, + ) + fv_names_seen[lower_name] = fv + + # Early duplicate detection: validate data source names are case-insensitively unique + ds_names_seen = {} + for ds in res.data_sources: + if ds.name is None: + continue + lower_name = ds.name.lower() + if lower_name in ds_names_seen: + raise DataSourceRepeatNamesException(ds.name) + ds_names_seen[lower_name] = ds + res.entities.append(DUMMY_ENTITY) return res