Repository: feast-dev/feast

Issue:
PostgreSQL Scalable Registry: Crash on multiple apply calls
## Expected Behavior 
Feast apply can run on an already-initialized repository in the event that changes need to be applied; the registry can thus be maintained over time.
## Current Behavior
Feast, when using the PostgreSQL registry, will crash due to an inability to read the metadata tables whenever apply is run on an already-initialized feature repository. The error is as follows:
`
Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/generic-api/bin/feast", line 8, in <module>
    sys.exit(cli())
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/cli.py", line 519, in apply_total_command
    apply_total(repo_config, repo, skip_source_validation)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/usage.py", line 283, in wrapper
    return func(*args, **kwargs)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/repo_operations.py", line 335, in apply_total
    apply_total_with_repo_instance(
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/repo_operations.py", line 296, in apply_total_with_repo_instance
    registry_diff, infra_diff, new_infra = store.plan(repo)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/usage.py", line 294, in wrapper
    raise exc.with_traceback(traceback)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/usage.py", line 283, in wrapper
    return func(*args, **kwargs)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/feature_store.py", line 742, in plan
    current_infra_proto = self._registry.proto().infra.__deepcopy__()
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/infra/registry/sql.py", line 675, in proto
    r.infra.CopyFrom(self.get_infra(project).to_proto())
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/infra/registry/sql.py", line 574, in get_infra
    return Infra.from_proto(infra_object)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/infra/infra_object.py", line 150, in from_proto
    infra.infra_objects += [
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/infra/infra_object.py", line 151, in <listcomp>
    InfraObject.from_infra_object_proto(infra_object_proto)
  File "/usr/local/anaconda3/envs/generic-api/lib/python3.9/site-packages/feast/infra/infra_object.py", line 73, in from_infra_object_proto
    if infra_object_proto.infra_object_class_type:
AttributeError: 'SqliteTable' object has no attribute 'infra_object_class_type'
`
## Steps to reproduce

Step 1: Create a basic local repo with `feast init -t local` (my created directory was named native_duckling in testing)
Step 2: Reconfigure the feature_repo.yaml to point to any postgres registry, e.g.
`
registry:
    registry_type: sql
    path: <insert_connection_string_here>
`
Step 3: Run feast apply. It should return:
`Created entity driver
Created feature view driver_hourly_stats_fresh
Created feature view driver_hourly_stats
Created on demand feature view transformed_conv_rate
Created on demand feature view transformed_conv_rate_fresh
Created feature service driver_activity_v2
Created feature service driver_activity_v1
Created feature service driver_activity_v3

Created sqlite table native_duckling_driver_hourly_stats_fresh
Created sqlite table native_duckling_driver_hourly_stats
`
Step 4: Make any small change to the registry.
Step 5: Run feast apply again; the error above will be generated and the apply will fail.

### Specifications

- Version: feast 0.26.0, Python 3.9
- Platform: macOS Big Sur 11.4
- Subsystem:

## Possible Solution

The issue does not appear to come up when using a file registry, but this is not a preferable solution as my team would like to use more scalable options in production settings.


Discussion:
I'm running into the exact same issue. No clue how to fix it though.
Same here with v0.27.0

Resolving patch:
diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py
--- a/sdk/python/feast/infra/registry/sql.py
+++ b/sdk/python/feast/infra/registry/sql.py
@@ -207,14 +207,14 @@ def get_stream_feature_view(
         self, name: str, project: str, allow_cache: bool = False
     ):
         return self._get_object(
-            stream_feature_views,
-            name,
-            project,
-            StreamFeatureViewProto,
-            StreamFeatureView,
-            "feature_view_name",
-            "feature_view_proto",
-            FeatureViewNotFoundException,
+            table=stream_feature_views,
+            name=name,
+            project=project,
+            proto_class=StreamFeatureViewProto,
+            python_class=StreamFeatureView,
+            id_field_name="feature_view_name",
+            proto_field_name="feature_view_proto",
+            not_found_exception=FeatureViewNotFoundException,
         )
 
     def list_stream_feature_views(
@@ -230,101 +230,105 @@ def list_stream_feature_views(
 
     def apply_entity(self, entity: Entity, project: str, commit: bool = True):
         return self._apply_object(
-            entities, project, "entity_name", entity, "entity_proto"
+            table=entities,
+            project=project,
+            id_field_name="entity_name",
+            obj=entity,
+            proto_field_name="entity_proto",
         )
 
     def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Entity:
         return self._get_object(
-            entities,
-            name,
-            project,
-            EntityProto,
-            Entity,
-            "entity_name",
-            "entity_proto",
-            EntityNotFoundException,
+            table=entities,
+            name=name,
+            project=project,
+            proto_class=EntityProto,
+            python_class=Entity,
+            id_field_name="entity_name",
+            proto_field_name="entity_proto",
+            not_found_exception=EntityNotFoundException,
         )
 
     def get_feature_view(
         self, name: str, project: str, allow_cache: bool = False
     ) -> FeatureView:
         return self._get_object(
-            feature_views,
-            name,
-            project,
-            FeatureViewProto,
-            FeatureView,
-            "feature_view_name",
-            "feature_view_proto",
-            FeatureViewNotFoundException,
+            table=feature_views,
+            name=name,
+            project=project,
+            proto_class=FeatureViewProto,
+            python_class=FeatureView,
+            id_field_name="feature_view_name",
+            proto_field_name="feature_view_proto",
+            not_found_exception=FeatureViewNotFoundException,
         )
 
     def get_on_demand_feature_view(
         self, name: str, project: str, allow_cache: bool = False
     ) -> OnDemandFeatureView:
         return self._get_object(
-            on_demand_feature_views,
-            name,
-            project,
-            OnDemandFeatureViewProto,
-            OnDemandFeatureView,
-            "feature_view_name",
-            "feature_view_proto",
-            FeatureViewNotFoundException,
+            table=on_demand_feature_views,
+            name=name,
+            project=project,
+            proto_class=OnDemandFeatureViewProto,
+            python_class=OnDemandFeatureView,
+            id_field_name="feature_view_name",
+            proto_field_name="feature_view_proto",
+            not_found_exception=FeatureViewNotFoundException,
         )
 
     def get_request_feature_view(self, name: str, project: str):
         return self._get_object(
-            request_feature_views,
-            name,
-            project,
-            RequestFeatureViewProto,
-            RequestFeatureView,
-            "feature_view_name",
-            "feature_view_proto",
-            FeatureViewNotFoundException,
+            table=request_feature_views,
+            name=name,
+            project=project,
+            proto_class=RequestFeatureViewProto,
+            python_class=RequestFeatureView,
+            id_field_name="feature_view_name",
+            proto_field_name="feature_view_proto",
+            not_found_exception=FeatureViewNotFoundException,
         )
 
     def get_feature_service(
         self, name: str, project: str, allow_cache: bool = False
     ) -> FeatureService:
         return self._get_object(
-            feature_services,
-            name,
-            project,
-            FeatureServiceProto,
-            FeatureService,
-            "feature_service_name",
-            "feature_service_proto",
-            FeatureServiceNotFoundException,
+            table=feature_services,
+            name=name,
+            project=project,
+            proto_class=FeatureServiceProto,
+            python_class=FeatureService,
+            id_field_name="feature_service_name",
+            proto_field_name="feature_service_proto",
+            not_found_exception=FeatureServiceNotFoundException,
         )
 
     def get_saved_dataset(
         self, name: str, project: str, allow_cache: bool = False
     ) -> SavedDataset:
         return self._get_object(
-            saved_datasets,
-            name,
-            project,
-            SavedDatasetProto,
-            SavedDataset,
-            "saved_dataset_name",
-            "saved_dataset_proto",
-            SavedDatasetNotFound,
+            table=saved_datasets,
+            name=name,
+            project=project,
+            proto_class=SavedDatasetProto,
+            python_class=SavedDataset,
+            id_field_name="saved_dataset_name",
+            proto_field_name="saved_dataset_proto",
+            not_found_exception=SavedDatasetNotFound,
         )
 
     def get_validation_reference(
         self, name: str, project: str, allow_cache: bool = False
     ) -> ValidationReference:
         return self._get_object(
-            validation_references,
-            name,
-            project,
-            ValidationReferenceProto,
-            ValidationReference,
-            "validation_reference_name",
-            "validation_reference_proto",
-            ValidationReferenceNotFound,
+            table=validation_references,
+            name=name,
+            project=project,
+            proto_class=ValidationReferenceProto,
+            python_class=ValidationReference,
+            id_field_name="validation_reference_name",
+            proto_field_name="validation_reference_proto",
+            not_found_exception=ValidationReferenceNotFound,
         )
 
     def list_entities(self, project: str, allow_cache: bool = False) -> List[Entity]:
@@ -364,14 +368,14 @@ def get_data_source(
         self, name: str, project: str, allow_cache: bool = False
     ) -> DataSource:
         return self._get_object(
-            data_sources,
-            name,
-            project,
-            DataSourceProto,
-            DataSource,
-            "data_source_name",
-            "data_source_proto",
-            DataSourceObjectNotFoundException,
+            table=data_sources,
+            name=name,
+            project=project,
+            proto_class=DataSourceProto,
+            python_class=DataSource,
+            id_field_name="data_source_name",
+            proto_field_name="data_source_proto",
+            not_found_exception=DataSourceObjectNotFoundException,
         )
 
     def list_data_sources(
@@ -556,22 +560,28 @@ def delete_validation_reference(self, name: str, project: str, commit: bool = Tr
 
     def update_infra(self, infra: Infra, project: str, commit: bool = True):
         self._apply_object(
-            managed_infra, project, "infra_name", infra, "infra_proto", name="infra_obj"
+            table=managed_infra,
+            project=project,
+            id_field_name="infra_name",
+            obj=infra,
+            proto_field_name="infra_proto",
+            name="infra_obj",
         )
 
     def get_infra(self, project: str, allow_cache: bool = False) -> Infra:
         infra_object = self._get_object(
-            managed_infra,
-            "infra_obj",
-            project,
-            InfraProto,
-            Infra,
-            "infra_name",
-            "infra_proto",
-            None,
+            table=managed_infra,
+            name="infra_obj",
+            project=project,
+            proto_class=InfraProto,
+            python_class=Infra,
+            id_field_name="infra_name",
+            proto_field_name="infra_proto",
+            not_found_exception=None,
         )
-        infra_object = infra_object or InfraProto()
-        return Infra.from_proto(infra_object)
+        if infra_object:
+            return infra_object
+        return Infra()
 
     def apply_user_metadata(
         self,


Test patch:
diff --git a/sdk/python/tests/unit/test_sql_registry.py b/sdk/python/tests/unit/test_sql_registry.py
--- a/sdk/python/tests/unit/test_sql_registry.py
+++ b/sdk/python/tests/unit/test_sql_registry.py
@@ -28,6 +28,8 @@
 from feast.errors import FeatureViewNotFoundException
 from feast.feature_view import FeatureView
 from feast.field import Field
+from feast.infra.infra_object import Infra
+from feast.infra.online_stores.sqlite import SqliteTable
 from feast.infra.registry.sql import SqlRegistry
 from feast.on_demand_feature_view import on_demand_feature_view
 from feast.repo_config import RegistryConfig
@@ -258,10 +260,20 @@ def test_apply_feature_view_success(sql_registry):
         and feature_view.features[3].dtype == Array(Bytes)
         and feature_view.entities[0] == "fs1_my_entity_1"
     )
+    assert feature_view.ttl == timedelta(minutes=5)
 
     # After the first apply, the created_timestamp should be the same as the last_update_timestamp.
     assert feature_view.created_timestamp == feature_view.last_updated_timestamp
 
+    # Modify the feature view and apply again to test if diffing the online store table works
+    fv1.ttl = timedelta(minutes=6)
+    sql_registry.apply_feature_view(fv1, project)
+    feature_views = sql_registry.list_feature_views(project)
+    assert len(feature_views) == 1
+    feature_view = sql_registry.get_feature_view("my_feature_view_1", project)
+    assert feature_view.ttl == timedelta(minutes=6)
+
+    # Delete feature view
     sql_registry.delete_feature_view("my_feature_view_1", project)
     feature_views = sql_registry.list_feature_views(project)
     assert len(feature_views) == 0
@@ -570,6 +582,22 @@ def test_update_infra(sql_registry):
     project = "project"
     infra = sql_registry.get_infra(project=project)
 
+    assert len(infra.infra_objects) == 0
+
     # Should run update infra successfully
     sql_registry.update_infra(infra, project)
+
+    # Should run update infra successfully when adding
+    new_infra = Infra()
+    new_infra.infra_objects.append(
+        SqliteTable(
+            path="/tmp/my_path.db",
+            name="my_table",
+        )
+    )
+    sql_registry.update_infra(new_infra, project)
+    infra = sql_registry.get_infra(project=project)
+    assert len(infra.infra_objects) == 1
+
+    # Try again since second time, infra should be not-empty
     sql_registry.teardown()