Skip to content

fix: Preserve distinct entity_df rows sharing a join key and timestamp in Dask offline store - #6786

Open
piyush182004 wants to merge 1 commit into
feast-dev:masterfrom
piyush182004:fix/dask-entity-row-collapse
Open

fix: Preserve distinct entity_df rows sharing a join key and timestamp in Dask offline store#6786
piyush182004 wants to merge 1 commit into
feast-dev:masterfrom
piyush182004:fix/dask-entity-row-collapse

Conversation

@piyush182004

@piyush182004 piyush182004 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

ISSUE:- #6787

What this PR does / why we need it:

get_historical_features() on the Dask/File offline store returned fewer rows than were passed in whenever the entity_df contained two or more rows that shared a join key and event timestamp but differed in other columns — for example, two orders placed by the same customer in the same logged second, each with its own order id and label.

_drop_duplicates() deduplicated the joined result on (join keys, event timestamp) only. Since that pair does not uniquely identify an input row, distinct entity_df rows sharing it were collapsed into one by drop_duplicates(keep="last"), silently discarding every other column of the dropped rows — including labels a training pipeline would join on.

Reproduced with a 4-row entity_df (two rows sharing a join key and timestamp): before this change, only 2 of 4 rows survived. The DuckDB offline store, and every SQL-based store (BigQuery, Snowflake, Redshift, Postgres, Spark, ClickHouse, Trino, Couchbase, Athena), already avoid this by carrying a per-row entity_row_unique_id through their generated queries; Dask/File had no equivalent.

The fix tags each input row with a synthetic unique id before the per-feature-view join, dedupes on that id instead of (join keys, timestamp), and drops the id column before returning. Non-entity mode (entity_df=None, used to snapshot all entities as of a timestamp) is unaffected and covered by an existing test.

Verification

Full offline-store + local-feast-tests suite: 260 passed, 0 failed. Added 2 regression tests covering the collapse scenario and the exact row-count invariant.

Which issue(s) this PR fixes:

Checks

  • I've made sure the tests are passing.
  • My commits are signed off (git commit -s)
  • My PR title follows conventional commits format

Testing Strategy

  • Unit tests

…p in Dask offline store

get_historical_features() on the Dask/File offline store returned fewer
rows than were passed in whenever the entity_df contained two or more rows
that shared a join key and event timestamp but differed in other columns -
for example two orders placed by the same customer in the same logged
second, each with its own order id and label.

_drop_duplicates() deduplicated the joined result on
(join keys, event timestamp) only. Since that pair does not uniquely
identify an *input* row, distinct entity_df rows sharing it were collapsed
into one by drop_duplicates(keep="last"), silently discarding every other
column of the dropped rows - including labels a training pipeline would
join on.

Reproduced with a 4-row entity_df (two rows sharing driver_id and
timestamp): before this change, only 2 of 4 rows survived. The DuckDB
offline store, and every SQL-based store (BigQuery, Snowflake, Redshift,
Postgres, Spark, ClickHouse, Trino, Couchbase, Athena), already avoid this
by carrying a per-row "entity_row_unique_id" through their generated
queries; Dask/File had no equivalent.

The fix tags each input row with a synthetic unique id before the
per-feature-view join, dedupes on that id instead of
(join keys, timestamp), and drops the id column before returning. The
non-entity mode (entity_df=None, used to snapshot all entities as of a
timestamp) is unaffected: it still dedupes on (join keys, timestamp) since
there is a single synthetic entity_df row shared by every real entity in
that mode.

Related but distinct from feast-dev#3360 (closed wontfix), which is about
tie-breaking between 3+ created_timestamp versions on the *feature source*
side; this is about the *entity_df* (request) side never being fanned out
correctly in the first place.

Signed-off-by: Piyush Mondal <piyushmondal182004@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Preserves distinct entity rows during Dask historical feature retrieval.

Changes:

  • Adds synthetic row IDs for deduplication.
  • Adds regression tests for duplicate join keys and timestamps.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
sdk/python/feast/infra/offline_stores/dask.py Implements row-ID-based deduplication.
sdk/python/tests/unit/infra/offline_stores/test_dask_entity_row_dedup.py Tests row preservation and counts.
Suppressed comments (1)

sdk/python/feast/infra/offline_stores/dask.py:371

  • This unconditionally deletes a caller column named __entity_row_unique_id__: line 229 first overwrites that column, and this cleanup then removes it from the result. Since entity dataframe payload columns are expected to survive retrieval, choose a collision-free temporary name (or preserve and restore the original column) and propagate that name through deduplication.
            entity_df_with_features = entity_df_with_features.drop(
                columns=[_ENTITY_ROW_ID_COL]
            )

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +228 to +229
entity_df_with_features = entity_df_with_features.reset_index(drop=True)
entity_df_with_features[_ENTITY_ROW_ID_COL] = entity_df_with_features.index
@codecov-commenter

codecov-commenter commented Aug 26, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 47.09%. Comparing base (8ab92e8) to head (02938bb).
⚠️ Report is 1 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #6786   +/-   ##
=======================================
  Coverage   47.08%   47.09%           
=======================================
  Files         419      419           
  Lines       51878    51883    +5     
  Branches     7525     7525           
=======================================
+ Hits        24429    24434    +5     
  Misses      25700    25700           
  Partials     1749     1749           
Flag Coverage Δ
go-feature-server 30.58% <ø> (ø)
python-unit 48.40% <100.00%> (+<0.01%) ⬆️
Files with missing lines Coverage Δ
sdk/python/feast/infra/offline_stores/dask.py 52.83% <100.00%> (+0.44%) ⬆️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8ab92e8...02938bb. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants