Skip to content

feat: Add feature view versioning to HBase online store - #6755

Open
arose26 wants to merge 1 commit into
feast-dev:masterfrom
arose26:feat/hbase-versioned-tables
Open

feat: Add feature view versioning to HBase online store#6755
arose26 wants to merge 1 commit into
feast-dev:masterfrom
arose26:feat/hbase-versioned-tables

Conversation

@arose26

@arose26 arose26 commented Aug 18, 2026

Copy link
Copy Markdown

Closes #6175. Part of #2728.

What this does

Threads registry.enable_online_feature_view_versioning through HbaseOnlineStore._table_id,
so that with versioning enabled each feature view version gets its own HBase table
(test_project:driver_stats_v2) instead of all versions sharing one. The store already
funnelled all five call sites — online_write_batch, online_read, update (both the keep
and delete loops) and teardown — through that one method, so the change is localised there.

Why not compute_table_id

The merged Milvus (#6330) and FAISS (#6256) stores use compute_table_id, which joins as
{project}_{name}[_v{N}]. HBase addresses tables as namespace:table, and this store has
always produced f"{project}:{table.name}". So the name is built here from
compute_versioned_name instead, preserving the : separator and putting the version on the
table half where it belongs. With versioning disabled the result is byte-identical to today's,
which the enable_versioning=False cases pin.

Row keys are left unversioned

_hbase_row_key still returns {entity_id}#{feature_view_name}. The existing comment explains
the suffix disambiguates feature views that share a table; now that the table itself is
version-scoped, adding the version to the key would be redundant. Leaving it alone keeps
existing row keys stable, and — more importantly — keeps the write and read paths computing
identical keys, which test_write_and_read_agree_on_row_keys pins directly.

Why HBase is not added to the versioned-read allowlist

I checked the read path rather than assuming it. HbaseOnlineStore.online_read builds its
result by iterating the rows HBase returned, not the entity keys requested:

rows = hbase.rows(table_name, row_keys=row_keys)
for _, row in rows:
    ...
    result.append((res_ts, res))

HBaseConnector.rows passes straight through to happybase's Table.rows, which omits keys
that do not exist. So a miss shortens the list rather than yielding (None, None) in place,
and the result stops corresponding positionally to entity_keys — the contract sqlite.py
implements. Demonstrated with mocks, no HBase required:

requested 3 entity keys, got 1 results
runnable reproduction
import struct
from datetime import timedelta
from unittest.mock import MagicMock, patch

from feast import Entity
from feast.feature_view import FeatureView
from feast.field import Field
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
from feast.types import Float32
from feast.value_type import ValueType
from feast.infra.online_stores.hbase_online_store.hbase import HbaseOnlineStore

fv = FeatureView(
    name="driver_stats",
    entities=[Entity(name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64)],
    ttl=timedelta(days=1),
    schema=[Field(name="trips_today", dtype=Float32)],
)
config = MagicMock()
config.project = "test_project"
config.entity_key_serialization_version = 3
config.registry.enable_online_feature_view_versioning = False

entity_keys = [
    EntityKeyProto(join_keys=["driver_id"], entity_values=[ValueProto(int64_val=i)])
    for i in (1, 2, 3)
]
value = ValueProto(float_val=1.0)
found = [(b"k#driver_stats", {b"data:trips_today": value.SerializeToString(),
                              b"data:event_ts": struct.pack(">L", 1704110400)})]

store = HbaseOnlineStore()
with patch.object(HbaseOnlineStore, "_get_conn"), patch(
    "feast.infra.online_stores.hbase_online_store.hbase.HBaseConnector"
) as C:
    C.return_value.rows.return_value = found
    result = store.online_read(config, fv, entity_keys, ["trips_today"])

print(f"requested {len(entity_keys)} entity keys, got {len(result)} results")

That gap is pre-existing and orthogonal to versioning, so I have left it out of scope rather
than widening this PR — versioned scalar reads stay correctly gated behind
VersionedOnlineReadNotSupported until it is addressed. Happy to open a separate issue, or to
take it in a follow-up.

Tests

New sdk/python/tests/unit/infra/online_store/test_hbase_versioning.py, 15 tests,
MagicMock-based in the same style as the merged test_milvus_versioning.py, and
importorskip-guarded on happybase.

  • naming: unversioned unchanged; version ignored while the flag is off; _v2 when on;
    projection.version_tag takes priority over current_version_number; version 0 gets no
    suffix; the : namespace separator is preserved
  • routing: write, read, update (create and delete) and teardown all operate on the
    versioned table — write is parametrized so the unversioned case is asserted as the control
  • invariants: row keys stay unversioned, write and read agree on them, and the event_ts
    decode path still round-trips

Verified red-before/green-after: with the source change reverted, the four routing tests fail
while the unversioned control and both invariant tests still pass, so they test the change
rather than the setup.

Regression check across the 55 unit test files touching online stores or versioning: the set of
failing and erroring tests is identical with and without this change (diff of the sorted
FAILED/ERROR lines is empty). Those pre-existing failures are missing optional dependencies
in my environment. ruff check, ruff format and mypy are clean on both files.


🤖 Written with Claude Code (Claude Opus 5), reviewed by @arose26.

Thread registry.enable_online_feature_view_versioning through _table_id so
that, when it is enabled, each feature view version gets its own HBase table
(test_project:driver_stats_v2) instead of all versions sharing one.

The version is appended via compute_versioned_name rather than by calling
compute_table_id, because HBase addresses tables as namespace:table while
compute_table_id joins with an underscore. Building the name here keeps the
':' separator, and with versioning disabled the table name is unchanged.

Row keys stay unversioned. They are already suffixed with the feature view
name to disambiguate views sharing a table, and since the table itself is now
version-scoped the version would be redundant there; leaving them alone keeps
existing keys stable and keeps the write and read paths computing identical
keys, which a test pins.

HbaseOnlineStore is deliberately not added to the versioned-read allowlist:
online_read builds its result by iterating the rows HBase returned rather than
the entity keys requested, so a missing row shortens the list instead of
yielding (None, None) in place. That contract gap is pre-existing and
independent of versioning. See the PR description.

Part of feast-dev#2728. Closes feast-dev#6175

Signed-off-by: arose26 <145766958+arose26@users.noreply.github.com>
@arose26
arose26 requested a review from a team as a code owner August 18, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add feature view versioning support to HBase online store

1 participant