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
6 changes: 3 additions & 3 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -1485,14 +1485,14 @@
"filename": "sdk/python/tests/universal/feature_repos/repo_configuration.py",
"hashed_secret": "d90e76ef629fb00c95f4e84fec29fbda111e2392",
"is_verified": false,
"line_number": 466
"line_number": 475
},
{
"type": "Secret Keyword",
"filename": "sdk/python/tests/universal/feature_repos/repo_configuration.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 468
"line_number": 477
}
],
"sdk/python/tests/universal/feature_repos/universal/data_sources/file.py": [
Expand Down Expand Up @@ -1564,5 +1564,5 @@
}
]
},
"generated_at": "2026-07-30T09:40:48Z"
"generated_at": "2026-07-30T13:06:49Z"
}
75 changes: 74 additions & 1 deletion sdk/python/feast/infra/offline_stores/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
opt_float,
)
from feast.repo_config import FeastConfigBaseModel, RepoConfig
from feast.utils import compute_non_entity_date_range


def _read_data_source(data_source: DataSource, repo_path: str) -> Table:
Expand Down Expand Up @@ -495,6 +496,59 @@ class DuckDBOfflineStoreConfig(FeastConfigBaseModel):
staging_location_endpoint_override: Optional[str] = None


def _build_entity_df_from_sources(
config: RepoConfig,
feature_views: List[FeatureView],
start_date: datetime,
end_date: datetime,
data_source_reader: Callable[[DataSource, str], Table],
) -> pd.DataFrame:
"""Build a synthetic entity DataFrame for non-entity mode.

Scans each feature view's source within the date range to discover all unique
entity key combinations, then creates a single row per entity with
event_timestamp = end_date so the PIT join returns the latest feature values.
"""
entity_dfs: List[pd.DataFrame] = []
repo_path = str(config.repo_path) if config.repo_path else ""

for fv in feature_views:
source = fv.batch_source
assert source is not None, f"Feature view '{fv.name}' has no batch_source"
table = data_source_reader(source, repo_path)
ts_col = source.timestamp_field

if fv.projection.join_key_map:
join_keys = list(fv.projection.join_key_map.values())
elif fv.entity_columns:
join_keys = [e.name for e in fv.entity_columns]
else:
join_keys = list(fv.entities) if fv.entities else []

filtered = table.filter(
(table[ts_col] >= ibis.literal(start_date))
& (table[ts_col] <= ibis.literal(end_date))
)

if join_keys:
sub_df = filtered.select(join_keys).execute().drop_duplicates()
else:
sub_df = pd.DataFrame(index=[0])

entity_dfs.append(sub_df)

if not entity_dfs:
return pd.DataFrame(columns=["event_timestamp"])

combined = pd.concat(entity_dfs, ignore_index=True).drop_duplicates()
combined["event_timestamp"] = (
pd.Timestamp(end_date).tz_convert("UTC")
if pd.Timestamp(end_date).tzinfo
else pd.Timestamp(end_date, tz="UTC")
)
return combined


class DuckDBOfflineStore(OfflineStore):
@staticmethod
def pull_latest_from_table_or_query(
Expand Down Expand Up @@ -527,11 +581,30 @@ def get_historical_features(
config: RepoConfig,
feature_views: List[FeatureView],
feature_refs: List[str],
entity_df: Union[pd.DataFrame, str],
entity_df: Optional[Union[pd.DataFrame, str]],
registry: BaseRegistry,
project: str,
full_feature_names: bool = False,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
) -> RetrievalJob:
non_entity_mode = entity_df is None

if non_entity_mode:
start_date, end_date = compute_non_entity_date_range(
feature_views,
start_date=start_date,
end_date=end_date,
)

entity_df = _build_entity_df_from_sources(
config=config,
feature_views=feature_views,
start_date=start_date,
end_date=end_date,
data_source_reader=_read_data_source,
)

return get_historical_features_ibis(
config=config,
feature_views=feature_views,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@


@pytest.mark.integration
@pytest.mark.universal_offline_stores
@pytest.mark.ray_offline_stores_only
@pytest.mark.universal_offline_stores(only=["file", "duckdb"])
def test_non_entity_mode_basic(environment, universal_data_sources):
"""Test historical features retrieval without entity_df (non-entity mode).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ def test_historical_features_field_mapping(


@pytest.mark.integration
@pytest.mark.universal_offline_stores(only=["file"])
@pytest.mark.universal_offline_stores(only=["file", "duckdb"])
def test_historical_features_non_entity_retrieval(environment):
"""Test get_historical_features with entity_df=None using start_date/end_date.

Expand Down
Loading
Loading