# Copyright 2021 The Feast Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import sys from datetime import timedelta import pandas as pd import pytest from pytest_lazyfixture import lazy_fixture from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs from feast import FileSource, RequestSource from feast.data_format import ParquetFormat from feast.entity import Entity 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 from feast.types import Array, Bytes, Float32, Int32, Int64, String from feast.value_type import ValueType from tests.integration.feature_repos.universal.entities import driver POSTGRES_USER = "test" POSTGRES_PASSWORD = "test" POSTGRES_DB = "test" logger = logging.getLogger(__name__) @pytest.fixture(scope="session") def pg_registry(): container = ( DockerContainer("postgres:latest") .with_exposed_ports(5432) .with_env("POSTGRES_USER", POSTGRES_USER) .with_env("POSTGRES_PASSWORD", POSTGRES_PASSWORD) .with_env("POSTGRES_DB", POSTGRES_DB) ) container.start() log_string_to_wait_for = "database system is ready to accept connections" waited = wait_for_logs( container=container, predicate=log_string_to_wait_for, timeout=30, interval=10, ) logger.info("Waited for %s seconds until postgres container was up", waited) container_port = container.get_exposed_port(5432) registry_config = RegistryConfig( registry_type="sql", path=f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@127.0.0.1:{container_port}/{POSTGRES_DB}", ) yield SqlRegistry(registry_config, "project", None) container.stop() @pytest.fixture(scope="session") def mysql_registry(): container = ( DockerContainer("mysql:latest") .with_exposed_ports(3306) .with_env("MYSQL_RANDOM_ROOT_PASSWORD", "true") .with_env("MYSQL_USER", POSTGRES_USER) .with_env("MYSQL_PASSWORD", POSTGRES_PASSWORD) .with_env("MYSQL_DATABASE", POSTGRES_DB) ) container.start() # The log string uses '8.0.*' since the version might be changed as new Docker images are pushed. log_string_to_wait_for = "/usr/sbin/mysqld: ready for connections. Version: '8.0.*' socket: '/var/run/mysqld/mysqld.sock' port: 3306" waited = wait_for_logs( container=container, predicate=log_string_to_wait_for, timeout=60, interval=10, ) logger.info("Waited for %s seconds until mysql container was up", waited) container_port = container.get_exposed_port(3306) registry_config = RegistryConfig( registry_type="sql", path=f"mysql+mysqldb://{POSTGRES_USER}:{POSTGRES_PASSWORD}@127.0.0.1:{container_port}/{POSTGRES_DB}", ) yield SqlRegistry(registry_config, "project", None) container.stop() @pytest.fixture(scope="session") def sqlite_registry(): registry_config = RegistryConfig( registry_type="sql", path="sqlite://", ) yield SqlRegistry(registry_config, "project", None) @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_apply_entity_success(sql_registry): entity = Entity( name="driver_car_id", description="Car driver id", tags={"team": "matchmaking"}, ) project = "project" # Register Entity sql_registry.apply_entity(entity, project) project_metadata = sql_registry.list_project_metadata(project=project) assert len(project_metadata) == 1 project_uuid = project_metadata[0].project_uuid assert len(project_metadata[0].project_uuid) == 36 assert_project_uuid(project, project_uuid, sql_registry) entities = sql_registry.list_entities(project) assert_project_uuid(project, project_uuid, sql_registry) entity = entities[0] assert ( len(entities) == 1 and entity.name == "driver_car_id" and entity.description == "Car driver id" and "team" in entity.tags and entity.tags["team"] == "matchmaking" ) entity = sql_registry.get_entity("driver_car_id", project) assert ( entity.name == "driver_car_id" and entity.description == "Car driver id" and "team" in entity.tags and entity.tags["team"] == "matchmaking" ) # After the first apply, the created_timestamp should be the same as the last_update_timestamp. assert entity.created_timestamp == entity.last_updated_timestamp sql_registry.delete_entity("driver_car_id", project) assert_project_uuid(project, project_uuid, sql_registry) entities = sql_registry.list_entities(project) assert_project_uuid(project, project_uuid, sql_registry) assert len(entities) == 0 sql_registry.teardown() def assert_project_uuid(project, project_uuid, sql_registry): project_metadata = sql_registry.list_project_metadata(project=project) assert len(project_metadata) == 1 assert project_metadata[0].project_uuid == project_uuid @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_apply_feature_view_success(sql_registry): # Create Feature Views batch_source = FileSource( file_format=ParquetFormat(), path="file://feast/*", timestamp_field="ts_col", created_timestamp_column="timestamp", ) entity = Entity(name="fs1_my_entity_1", join_keys=["test"]) fv1 = FeatureView( name="my_feature_view_1", schema=[ Field(name="fs1_my_feature_1", dtype=Int64), Field(name="fs1_my_feature_2", dtype=String), Field(name="fs1_my_feature_3", dtype=Array(String)), Field(name="fs1_my_feature_4", dtype=Array(Bytes)), ], entities=[entity], tags={"team": "matchmaking"}, source=batch_source, ttl=timedelta(minutes=5), ) project = "project" # Register Feature View sql_registry.apply_feature_view(fv1, project) feature_views = sql_registry.list_feature_views(project) # List Feature Views assert ( len(feature_views) == 1 and feature_views[0].name == "my_feature_view_1" and feature_views[0].features[0].name == "fs1_my_feature_1" and feature_views[0].features[0].dtype == Int64 and feature_views[0].features[1].name == "fs1_my_feature_2" and feature_views[0].features[1].dtype == String and feature_views[0].features[2].name == "fs1_my_feature_3" and feature_views[0].features[2].dtype == Array(String) and feature_views[0].features[3].name == "fs1_my_feature_4" and feature_views[0].features[3].dtype == Array(Bytes) and feature_views[0].entities[0] == "fs1_my_entity_1" ) feature_view = sql_registry.get_feature_view("my_feature_view_1", project) assert ( feature_view.name == "my_feature_view_1" and feature_view.features[0].name == "fs1_my_feature_1" and feature_view.features[0].dtype == Int64 and feature_view.features[1].name == "fs1_my_feature_2" and feature_view.features[1].dtype == String and feature_view.features[2].name == "fs1_my_feature_3" and feature_view.features[2].dtype == Array(String) and feature_view.features[3].name == "fs1_my_feature_4" 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 sql_registry.teardown() @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_apply_on_demand_feature_view_success(sql_registry): # Create Feature Views driver_stats = FileSource( name="driver_stats_source", path="data/driver_stats_lat_lon.parquet", timestamp_field="event_timestamp", created_timestamp_column="created", description="A table describing the stats of a driver based on hourly logs", owner="test2@gmail.com", ) driver_daily_features_view = FeatureView( name="driver_daily_features", entities=[driver()], ttl=timedelta(seconds=8640000000), schema=[ Field(name="daily_miles_driven", dtype=Float32), Field(name="lat", dtype=Float32), Field(name="lon", dtype=Float32), Field(name="string_feature", dtype=String), ], online=True, source=driver_stats, tags={"production": "True"}, owner="test2@gmail.com", ) @on_demand_feature_view( sources=[driver_daily_features_view], schema=[Field(name="first_char", dtype=String)], ) def location_features_from_push(inputs: pd.DataFrame) -> pd.DataFrame: df = pd.DataFrame() df["first_char"] = inputs["string_feature"].str[:1].astype("string") return df project = "project" with pytest.raises(FeatureViewNotFoundException): sql_registry.get_user_metadata(project, location_features_from_push) # Register Feature View sql_registry.apply_feature_view(location_features_from_push, project) assert not sql_registry.get_user_metadata(project, location_features_from_push) b = "metadata".encode("utf-8") sql_registry.apply_user_metadata(project, location_features_from_push, b) assert sql_registry.get_user_metadata(project, location_features_from_push) == b feature_views = sql_registry.list_on_demand_feature_views(project) # List Feature Views assert ( len(feature_views) == 1 and feature_views[0].name == "location_features_from_push" and feature_views[0].features[0].name == "first_char" and feature_views[0].features[0].dtype == String ) feature_view = sql_registry.get_on_demand_feature_view( "location_features_from_push", project ) assert ( feature_view.name == "location_features_from_push" and feature_view.features[0].name == "first_char" and feature_view.features[0].dtype == String ) sql_registry.delete_feature_view("location_features_from_push", project) feature_views = sql_registry.list_on_demand_feature_views(project) assert len(feature_views) == 0 sql_registry.teardown() @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_modify_feature_views_success(sql_registry): # Create Feature Views batch_source = FileSource( file_format=ParquetFormat(), path="file://feast/*", timestamp_field="ts_col", created_timestamp_column="timestamp", ) request_source = RequestSource( name="request_source", schema=[Field(name="my_input_1", dtype=Int32)], ) entity = Entity(name="fs1_my_entity_1", join_keys=["test"]) fv1 = FeatureView( name="my_feature_view_1", schema=[Field(name="fs1_my_feature_1", dtype=Int64)], entities=[entity], tags={"team": "matchmaking"}, source=batch_source, ttl=timedelta(minutes=5), ) @on_demand_feature_view( schema=[ Field(name="odfv1_my_feature_1", dtype=String), Field(name="odfv1_my_feature_2", dtype=Int32), ], sources=[request_source], ) def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: data = pd.DataFrame() data["odfv1_my_feature_1"] = feature_df["my_input_1"].astype("category") data["odfv1_my_feature_2"] = feature_df["my_input_1"].astype("int32") return data project = "project" # Register Feature Views sql_registry.apply_feature_view(odfv1, project) sql_registry.apply_feature_view(fv1, project) # Modify odfv by changing a single feature dtype @on_demand_feature_view( schema=[ Field(name="odfv1_my_feature_1", dtype=Float32), Field(name="odfv1_my_feature_2", dtype=Int32), ], sources=[request_source], ) def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: data = pd.DataFrame() data["odfv1_my_feature_1"] = feature_df["my_input_1"].astype("float") data["odfv1_my_feature_2"] = feature_df["my_input_1"].astype("int32") return data # Apply the modified odfv sql_registry.apply_feature_view(odfv1, project) # Check odfv on_demand_feature_views = sql_registry.list_on_demand_feature_views(project) assert ( len(on_demand_feature_views) == 1 and on_demand_feature_views[0].name == "odfv1" and on_demand_feature_views[0].features[0].name == "odfv1_my_feature_1" and on_demand_feature_views[0].features[0].dtype == Float32 and on_demand_feature_views[0].features[1].name == "odfv1_my_feature_2" and on_demand_feature_views[0].features[1].dtype == Int32 ) request_schema = on_demand_feature_views[0].get_request_data_schema() assert ( list(request_schema.keys())[0] == "my_input_1" and list(request_schema.values())[0] == ValueType.INT32 ) feature_view = sql_registry.get_on_demand_feature_view("odfv1", project) assert ( feature_view.name == "odfv1" and feature_view.features[0].name == "odfv1_my_feature_1" and feature_view.features[0].dtype == Float32 and feature_view.features[1].name == "odfv1_my_feature_2" and feature_view.features[1].dtype == Int32 ) request_schema = feature_view.get_request_data_schema() assert ( list(request_schema.keys())[0] == "my_input_1" and list(request_schema.values())[0] == ValueType.INT32 ) # Make sure fv1 is untouched feature_views = sql_registry.list_feature_views(project) # List Feature Views assert ( len(feature_views) == 1 and feature_views[0].name == "my_feature_view_1" and feature_views[0].features[0].name == "fs1_my_feature_1" and feature_views[0].features[0].dtype == Int64 and feature_views[0].entities[0] == "fs1_my_entity_1" ) feature_view = sql_registry.get_feature_view("my_feature_view_1", project) assert ( feature_view.name == "my_feature_view_1" and feature_view.features[0].name == "fs1_my_feature_1" and feature_view.features[0].dtype == Int64 and feature_view.entities[0] == "fs1_my_entity_1" ) sql_registry.teardown() @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_apply_data_source(sql_registry): # Create Feature Views batch_source = FileSource( name="test_source", file_format=ParquetFormat(), path="file://feast/*", timestamp_field="ts_col", created_timestamp_column="timestamp", ) entity = Entity(name="fs1_my_entity_1", join_keys=["test"]) fv1 = FeatureView( name="my_feature_view_1", schema=[ Field(name="fs1_my_feature_1", dtype=Int64), Field(name="fs1_my_feature_2", dtype=String), Field(name="fs1_my_feature_3", dtype=Array(String)), Field(name="fs1_my_feature_4", dtype=Array(Bytes)), ], entities=[entity], tags={"team": "matchmaking"}, source=batch_source, ttl=timedelta(minutes=5), ) project = "project" # Register data source and feature view sql_registry.apply_data_source(batch_source, project, commit=False) sql_registry.apply_feature_view(fv1, project, commit=True) registry_feature_views = sql_registry.list_feature_views(project) registry_data_sources = sql_registry.list_data_sources(project) assert len(registry_feature_views) == 1 assert len(registry_data_sources) == 1 registry_feature_view = registry_feature_views[0] assert registry_feature_view.batch_source == batch_source registry_data_source = registry_data_sources[0] assert registry_data_source == batch_source # Check that change to batch source propagates batch_source.timestamp_field = "new_ts_col" sql_registry.apply_data_source(batch_source, project, commit=False) sql_registry.apply_feature_view(fv1, project, commit=True) registry_feature_views = sql_registry.list_feature_views(project) registry_data_sources = sql_registry.list_data_sources(project) assert len(registry_feature_views) == 1 assert len(registry_data_sources) == 1 registry_feature_view = registry_feature_views[0] assert registry_feature_view.batch_source == batch_source registry_batch_source = sql_registry.list_data_sources(project)[0] assert registry_batch_source == batch_source sql_registry.teardown() @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_registry_cache(sql_registry): # Create Feature Views batch_source = FileSource( name="test_source", file_format=ParquetFormat(), path="file://feast/*", timestamp_field="ts_col", created_timestamp_column="timestamp", ) entity = Entity(name="fs1_my_entity_1", join_keys=["test"]) fv1 = FeatureView( name="my_feature_view_1", schema=[ Field(name="fs1_my_feature_1", dtype=Int64), Field(name="fs1_my_feature_2", dtype=String), Field(name="fs1_my_feature_3", dtype=Array(String)), Field(name="fs1_my_feature_4", dtype=Array(Bytes)), ], entities=[entity], tags={"team": "matchmaking"}, source=batch_source, ttl=timedelta(minutes=5), ) project = "project" # Register data source and feature view sql_registry.apply_data_source(batch_source, project) sql_registry.apply_feature_view(fv1, project) registry_feature_views_cached = sql_registry.list_feature_views( project, allow_cache=True ) registry_data_sources_cached = sql_registry.list_data_sources( project, allow_cache=True ) # Not refreshed cache, so cache miss assert len(registry_feature_views_cached) == 0 assert len(registry_data_sources_cached) == 0 sql_registry.refresh(project) # Now objects exist registry_feature_views_cached = sql_registry.list_feature_views( project, allow_cache=True ) registry_data_sources_cached = sql_registry.list_data_sources( project, allow_cache=True ) assert len(registry_feature_views_cached) == 1 assert len(registry_data_sources_cached) == 1 registry_feature_view = registry_feature_views_cached[0] assert registry_feature_view.batch_source == batch_source registry_data_source = registry_data_sources_cached[0] assert registry_data_source == batch_source sql_registry.teardown() @pytest.mark.skipif( sys.platform == "darwin" and "GITHUB_REF" in os.environ, reason="does not run on mac github actions", ) @pytest.mark.parametrize( "sql_registry", [ lazy_fixture("mysql_registry"), lazy_fixture("pg_registry"), lazy_fixture("sqlite_registry"), ], ) def test_update_infra(sql_registry): # Create infra object 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()