Skip to content

Commit 1e64c77

Browse files
fix: Fix for materializing entityless feature views in Snowflake (feast-dev#3961)
1 parent 42f37bb commit 1e64c77

2 files changed

Lines changed: 68 additions & 6 deletions

File tree

sdk/python/feast/infra/materialization/snowflake_engine.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import feast
1515
from feast.batch_feature_view import BatchFeatureView
1616
from feast.entity import Entity
17-
from feast.feature_view import FeatureView
17+
from feast.feature_view import DUMMY_ENTITY_ID, FeatureView
1818
from feast.infra.materialization.batch_materialization_engine import (
1919
BatchMaterializationEngine,
2020
MaterializationJob,
@@ -274,7 +274,11 @@ def _materialize_one(
274274

275275
fv_latest_values_sql = offline_job.to_sql()
276276

277-
if feature_view.entity_columns:
277+
if (
278+
feature_view.entity_columns[0].name == DUMMY_ENTITY_ID
279+
): # entityless Feature View's placeholder entity
280+
entities_to_write = 1
281+
else:
278282
join_keys = [entity.name for entity in feature_view.entity_columns]
279283
unique_entities = '"' + '", "'.join(join_keys) + '"'
280284

@@ -287,10 +291,6 @@ def _materialize_one(
287291

288292
with GetSnowflakeConnection(self.repo_config.offline_store) as conn:
289293
entities_to_write = conn.cursor().execute(query).fetchall()[0][0]
290-
else:
291-
entities_to_write = (
292-
1 # entityless feature view has a placeholder entity
293-
)
294294

295295
if feature_view.batch_source.field_mapping is not None:
296296
fv_latest_mapped_values_sql = _run_snowflake_field_mapping(

sdk/python/tests/integration/materialization/test_snowflake.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,65 @@ def test_snowflake_materialization_consistency_internal_with_lists(
185185
finally:
186186
fs.teardown()
187187
snowflake_environment.data_source_creator.teardown()
188+
189+
190+
@pytest.mark.integration
191+
def test_snowflake_materialization_entityless_fv():
192+
snowflake_config = IntegrationTestRepoConfig(
193+
online_store=SNOWFLAKE_ONLINE_CONFIG,
194+
offline_store_creator=SnowflakeDataSourceCreator,
195+
batch_engine=SNOWFLAKE_ENGINE_CONFIG,
196+
)
197+
snowflake_environment = construct_test_environment(snowflake_config, None)
198+
199+
df = create_basic_driver_dataset()
200+
entityless_df = df.drop("driver_id", axis=1)
201+
ds = snowflake_environment.data_source_creator.create_data_source(
202+
entityless_df,
203+
snowflake_environment.feature_store.project,
204+
field_mapping={"ts_1": "ts"},
205+
)
206+
207+
fs = snowflake_environment.feature_store
208+
209+
# We include the driver entity so we can provide an entity ID when fetching features
210+
driver = Entity(
211+
name="driver_id",
212+
join_keys=["driver_id"],
213+
)
214+
215+
overall_stats_fv = FeatureView(
216+
name="overall_hourly_stats",
217+
entities=[],
218+
ttl=timedelta(weeks=52),
219+
source=ds,
220+
)
221+
222+
try:
223+
fs.apply([overall_stats_fv, driver])
224+
225+
# materialization is run in two steps and
226+
# we use timestamp from generated dataframe as a split point
227+
split_dt = df["ts_1"][4].to_pydatetime() - timedelta(seconds=1)
228+
229+
print(f"Split datetime: {split_dt}")
230+
231+
now = datetime.utcnow()
232+
233+
start_date = (now - timedelta(hours=5)).replace(tzinfo=utc)
234+
end_date = split_dt
235+
fs.materialize(
236+
feature_views=[overall_stats_fv.name],
237+
start_date=start_date,
238+
end_date=end_date,
239+
)
240+
241+
response_dict = fs.get_online_features(
242+
[f"{overall_stats_fv.name}:value"],
243+
[{"driver_id": 1}], # Included because we need an entity
244+
).to_dict()
245+
assert response_dict["value"] == [0.3]
246+
247+
finally:
248+
fs.teardown()
249+
snowflake_environment.data_source_creator.teardown()

0 commit comments

Comments
 (0)