Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1292,12 +1292,14 @@ spec:
enum:
- snowflake.online
- redis
- eg-valkey
- ikv
- datastore
- dynamodb
- bigtable
- postgres
- cassandra
- scylladb
- mysql
- hazelcast
- singlestore
Expand All @@ -1306,6 +1308,7 @@ spec:
- qdrant
- couchbase.online
- milvus
- eg-milvus
type: string
required:
- secretRef
Expand Down Expand Up @@ -1767,7 +1770,9 @@ spec:
want to use.
enum:
- sql
- sql-fallback
- snowflake.registry
- http
type: string
required:
- secretRef
Expand Down Expand Up @@ -5285,12 +5290,14 @@ spec:
enum:
- snowflake.online
- redis
- eg-valkey
- ikv
- datastore
- dynamodb
- bigtable
- postgres
- cassandra
- scylladb
- mysql
- hazelcast
- singlestore
Expand All @@ -5299,6 +5306,7 @@ spec:
- qdrant
- couchbase.online
- milvus
- eg-milvus
type: string
required:
- secretRef
Expand Down Expand Up @@ -5772,7 +5780,9 @@ spec:
you want to use.
enum:
- sql
- sql-fallback
- snowflake.registry
- http
type: string
required:
- secretRef
Expand Down
10 changes: 10 additions & 0 deletions infra/feast-operator/dist/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1300,12 +1300,14 @@ spec:
enum:
- snowflake.online
- redis
- eg-valkey
- ikv
- datastore
- dynamodb
- bigtable
- postgres
- cassandra
- scylladb
- mysql
- hazelcast
- singlestore
Expand All @@ -1314,6 +1316,7 @@ spec:
- qdrant
- couchbase.online
- milvus
- eg-milvus
type: string
required:
- secretRef
Expand Down Expand Up @@ -1775,7 +1778,9 @@ spec:
want to use.
enum:
- sql
- sql-fallback
- snowflake.registry
- http
type: string
required:
- secretRef
Expand Down Expand Up @@ -5293,12 +5298,14 @@ spec:
enum:
- snowflake.online
- redis
- eg-valkey
- ikv
- datastore
- dynamodb
- bigtable
- postgres
- cassandra
- scylladb
- mysql
- hazelcast
- singlestore
Expand All @@ -5307,6 +5314,7 @@ spec:
- qdrant
- couchbase.online
- milvus
- eg-milvus
type: string
required:
- secretRef
Expand Down Expand Up @@ -5780,7 +5788,9 @@ spec:
you want to use.
enum:
- sql
- sql-fallback
- snowflake.registry
- http
type: string
required:
- secretRef
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ class FeatureViewProjectionModel(BaseModel):
"""

name: str
name_alias: Optional[str]
name_alias: Optional[str] = None
desired_features: List[str]
features: List[FieldModel]
join_key_map: Dict[str, str]
timestamp_field: Optional[str]
date_partition_column: Optional[str]
created_timestamp_column: Optional[str]
batch_source: Optional[AnyBatchDataSource]
timestamp_field: Optional[str] = None
date_partition_column: Optional[str] = None
created_timestamp_column: Optional[str] = None
batch_source: Optional[AnyBatchDataSource] = None

def to_feature_view_projection(self) -> FeatureViewProjection:
return FeatureViewProjection(
Expand Down
1 change: 1 addition & 0 deletions sdk/python/feast/infra/registry/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ def list_project_metadata( # type: ignore[return]
try:
url = f"{self.base_url}/projects/{project}"
response_data = self._send_request("GET", url)
logger.info(f"ProjectMetadata response data: {response_data}")
return [
ProjectMetadataModel.model_validate(response_data).to_project_metadata()
]
Expand Down
5 changes: 3 additions & 2 deletions sdk/python/feast/infra/registry/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def apply_project_metadata(
self, project: str, commit: bool
) -> ProjectMetadataModel:
self._maybe_init_project_metadata(project)
return self._get_project_metadata_model(project)
return self.get_project_metadata_model(project)

def _get_entity(self, name: str, project: str) -> Entity:
return self._get_object(
Expand Down Expand Up @@ -1531,12 +1531,13 @@ def get_project_metadata(self, project: str, key: str) -> Optional[str]:
return row._mapping["metadata_value"]
return None

def _get_project_metadata_model(
def get_project_metadata_model(
self,
project: str,
allow_cache: bool = False,
) -> ProjectMetadataModel:
"""
Expedia specific function used in eg-feature-store-registry to get project metadata model.
Returns given project metdata. No supporting function in SQL Registry so implemented this here rather than using _get_last_updated_metadata and list_project_metadata.
"""

Expand Down
83 changes: 62 additions & 21 deletions sdk/python/tests/unit/infra/registry/test_sql_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,65 @@ def sqlite_registry():
registry.teardown()


def test_sql_registry(sqlite_registry):
"""
Test the SQL registry
"""
entity = Entity(
name="test_entity",
description="Test entity for testing",
tags={"test": "transaction"},
)
sqlite_registry.apply_entity(entity, "test_project")
retrieved_entity = sqlite_registry.get_entity("test_entity", "test_project")
assert retrieved_entity.name == "test_entity"
assert retrieved_entity.description == "Test entity for testing"

sqlite_registry.set_project_metadata("test_project", "test_key", "test_value")
value = sqlite_registry.get_project_metadata("test_project", "test_key")
assert value == "test_value"

sqlite_registry.delete_entity("test_entity", "test_project")
with pytest.raises(Exception):
sqlite_registry.get_entity("test_entity", "test_project")
class TestSqlRegistry:
"""Test class for SqlRegistry"""

def test_apply_and_retrieve_entity(self, sqlite_registry):
"""Test applying and retrieving an entity from the SQL registry."""
entity = Entity(
name="test_entity",
description="Test entity for testing",
tags={"test": "transaction"},
)
sqlite_registry.apply_entity(entity, "test_project")

retrieved_entity = sqlite_registry.get_entity("test_entity", "test_project")
assert retrieved_entity.name == "test_entity"
assert retrieved_entity.description == "Test entity for testing"

def test_delete_entity(self, sqlite_registry):
"""Test deleting an entity from the SQL registry."""
entity = Entity(name="test_entity", description="Test entity")
sqlite_registry.apply_entity(entity, "test_project")

sqlite_registry.delete_entity("test_entity", "test_project")

with pytest.raises(Exception):
sqlite_registry.get_entity("test_entity", "test_project")

def test_get_project_metadata_model_returns_initialized_metadata(
self, sqlite_registry
):
"""Test that get_project_metadata_model returns metadata after applying an entity."""
entity = Entity(name="test_entity", description="Test entity")
sqlite_registry.apply_entity(entity, "test_project")

project_metadata = sqlite_registry.get_project_metadata_model("test_project")

assert project_metadata.project_name == "test_project"
assert project_metadata.project_uuid is not None
assert project_metadata.last_updated_timestamp is not None

def test_get_project_metadata_model_nonexistent_project(self, sqlite_registry):
"""Test that get_project_metadata_model handles non-existent projects gracefully."""
project_metadata = sqlite_registry.get_project_metadata_model(
"nonexistent_project"
)

assert project_metadata.project_name == "nonexistent_project"
assert project_metadata is not None

def test_get_all_project_metadata_multiple_projects(self, sqlite_registry):
"""Test that get_all_project_metadata returns metadata for all projects."""
entity1 = Entity(name="entity1", description="Entity 1")
entity2 = Entity(name="entity2", description="Entity 2")
sqlite_registry.apply_entity(entity1, "project_1")
sqlite_registry.apply_entity(entity2, "project_2")

all_metadata = sqlite_registry.get_all_project_metadata()

project_names = [m.project_name for m in all_metadata]
assert "project_1" in project_names
assert "project_2" in project_names
for metadata in all_metadata:
assert metadata.project_uuid is not None
99 changes: 99 additions & 0 deletions sdk/python/tests/unit/test_pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,44 @@ def test_idempotent_feature_view_projection_conversion():
assert pydantic_obj == FeatureViewProjectionModel.model_validate(pydantic_json)


def test_feature_view_projection_backwards_compatibility():
# Test deserialization with minimal fields (missing new optional fields)
# https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
minimal_dict = {
"name": "test_projection",
"desired_features": [],
"features": [],
"join_key_map": {},
}
pydantic_obj = FeatureViewProjectionModel.model_validate(minimal_dict)
assert pydantic_obj.name == "test_projection"
assert pydantic_obj.name_alias is None
assert pydantic_obj.desired_features == []
assert pydantic_obj.features == []
assert pydantic_obj.join_key_map == {}
assert pydantic_obj.timestamp_field is None
assert pydantic_obj.date_partition_column is None
assert pydantic_obj.created_timestamp_column is None
assert pydantic_obj.batch_source is None

minimal_json = '{"name": "test_projection_json", "desired_features": [], "features": [], "join_key_map": {}}'
pydantic_obj_from_json = FeatureViewProjectionModel.model_validate_json(
minimal_json
)
assert pydantic_obj_from_json.name == "test_projection_json"
assert pydantic_obj_from_json.timestamp_field is None
assert pydantic_obj_from_json.date_partition_column is None
assert pydantic_obj_from_json.created_timestamp_column is None
assert pydantic_obj_from_json.batch_source is None

converted_python_obj = pydantic_obj.to_feature_view_projection()
assert converted_python_obj.name == "test_projection"
assert converted_python_obj.timestamp_field is None
assert converted_python_obj.date_partition_column is None
assert converted_python_obj.created_timestamp_column is None
assert converted_python_obj.batch_source is None


def test_idempotent_on_demand_feature_view_conversion():
tags = {
"tag1": "val1",
Expand Down Expand Up @@ -831,6 +869,67 @@ def test_idempotent_feature_service_conversion():
assert pydantic_obj == FeatureServiceModel.model_validate(pydantic_json)


def test_feature_service_backwards_compatibility():
# Test deserialization with feature_view_projections missing optional fields
# https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
field1 = Field(name="feature1", dtype=Float32)
field2 = Field(name="feature2", dtype=String)
field1_model = FieldModel.from_field(field1)
field2_model = FieldModel.from_field(field2)

minimal_dict = {
"name": "test_feature_service",
"features": [],
"feature_view_projections": [
{
"name": "projection1",
"desired_features": ["feature1", "feature2"],
"features": [
field1_model.model_dump(),
field2_model.model_dump(),
],
"join_key_map": {"entity_id": "entity_id"},
},
{
"name": "projection2",
"desired_features": [],
"features": [],
"join_key_map": {},
},
],
"description": "Test feature service",
"tags": {"env": "test"},
"owner": "test@example.com",
"created_timestamp": None,
"last_updated_timestamp": None,
}
pydantic_obj = FeatureServiceModel.model_validate(minimal_dict)
assert pydantic_obj.name == "test_feature_service"
assert len(pydantic_obj.feature_view_projections) == 2

proj1 = pydantic_obj.feature_view_projections[0]
assert proj1.name == "projection1"
assert proj1.timestamp_field is None
assert proj1.date_partition_column is None
assert proj1.created_timestamp_column is None
assert proj1.batch_source is None
assert len(proj1.features) == 2

proj2 = pydantic_obj.feature_view_projections[1]
assert proj2.name == "projection2"
assert proj2.timestamp_field is None
assert proj2.date_partition_column is None
assert proj2.created_timestamp_column is None
assert proj2.batch_source is None

pydantic_json = pydantic_obj.model_dump_json()
pydantic_obj_from_json = FeatureServiceModel.model_validate_json(pydantic_json)
assert pydantic_obj_from_json.name == "test_feature_service"
assert len(pydantic_obj_from_json.feature_view_projections) == 2
assert pydantic_obj_from_json.feature_view_projections[0].timestamp_field is None
assert pydantic_obj_from_json.feature_view_projections[1].batch_source is None


def test_idempotent_project_metadata_conversion():
python_obj = ProjectMetadata(
project_name="test_project",
Expand Down
Loading