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
1 change: 0 additions & 1 deletion docs/reference/feature-store-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ online_store:
* **path** \(a local filepath\) — Parameter for the sqlite online store. Defines the path to the SQLite database file.
* **project\_id** — Optional parameter for the datastore online store. Sets the GCP project id used by Feast, if not set Feast will use the default GCP project id in the local environment.
* **project** — Defines a namespace for the entire feature store. Can be used to isolate multiple deployments in a single installation of Feast.

## Providers

The `provider` field defines the environment in which Feast will execute data flows. As a result, it also determines the default values for other fields.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Regression fixture for EAPC-22316 follow-up: a SortedFeatureView whose
# UNIX_TIMESTAMP sort key is requested as a feature must retain millisecond
# precision for rows that differ by less than one second (Go read-side fix in
# InterfaceToProtoValue, go/types/typeconversion.go).

from datetime import timedelta

from feast import Entity, Field, FileSource, SortedFeatureView
from feast.protos.feast.core.SortedFeatureView_pb2 import SortOrder
from feast.sort_key import SortKey
from feast.types import String, UnixTimestamp
from feast.value_type import ValueType

tags = {"team": "Feast"}
owner = "test@test.com"

sub_second_entity: Entity = Entity(
name="sub_second_entity",
join_keys=["sub_second_entity_id"],
value_type=ValueType.STRING,
tags=tags,
owner=owner,
)

sub_second_source: FileSource = FileSource(
path="sub_second_data.parquet", timestamp_field="event_timestamp"
)

sub_second_sort_key_view: SortedFeatureView = SortedFeatureView(
name="sub_second_sort_key_view",
entities=[sub_second_entity],
ttl=timedelta(days=0),
source=sub_second_source,
tags=tags,
description="Regression fixture: sort key rows <1s apart for the same entity",
owner=owner,
online=True,
sort_keys=[
SortKey(
name="event_timestamp",
value_type=ValueType.UNIX_TIMESTAMP,
default_sort_order=SortOrder.DESC,
)
],
schema=[
Field(name="value", dtype=String),
Field(name="event_timestamp", dtype=UnixTimestamp),
],
)

# Second regression fixture: the sort key is a secondary feature with a name
# other than "event_timestamp" (mirroring the real customer schema, where the
# sort key was named "viewed_at" and was distinct from the source's own
# ingestion timestamp field). This guards against the fix accidentally being
# tied to the "event_timestamp" column name rather than working for any
# UNIX_TIMESTAMP column declared as a sort key.
custom_sortkey_entity: Entity = Entity(
name="custom_sortkey_entity",
join_keys=["custom_sortkey_entity_id"],
value_type=ValueType.STRING,
tags=tags,
owner=owner,
)

sub_second_custom_sortkey_source: FileSource = FileSource(
path="sub_second_custom_sortkey_data.parquet", timestamp_field="event_timestamp"
)

sub_second_custom_sortkey_view: SortedFeatureView = SortedFeatureView(
name="sub_second_custom_sortkey_view",
entities=[custom_sortkey_entity],
ttl=timedelta(days=0),
source=sub_second_custom_sortkey_source,
tags=tags,
description="Regression fixture: sort key named 'viewed_at' (not 'event_timestamp'), rows <1s apart",
owner=owner,
online=True,
sort_keys=[
SortKey(
name="viewed_at",
value_type=ValueType.UNIX_TIMESTAMP,
default_sort_order=SortOrder.DESC,
)
],
schema=[
Field(name="value", dtype=String),
Field(name="viewed_at", dtype=UnixTimestamp),
],
)
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func TestGetOnlineFeaturesRange_Http_withOnlyEqualsFilter(t *testing.T) {
"sort_key_filters": [
{
"sort_key_name": "event_timestamp",
"equals": 1744769171
"equals": 1744769171919
}
],
"limit": 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@
{
"values": [
[
"2025-04-16 02:06:11Z"
"2025-04-16 02:06:11.919Z"
]
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@
}, {
"values" : [ [ null, null, null, null, null, null, null, null, null, null ], [ null, null, null, null, null, null, null, null, null, null ], [ null, null, null, null, null, null, null, null, null, null ] ]
}, {
"values" : [ [ "2025-04-16 02:06:10Z", "2025-04-16 02:06:09Z", "2025-04-16 02:06:08Z", "2025-04-16 02:06:07Z", "2025-04-16 02:06:06Z", "2025-04-16 02:06:05Z", "2025-04-16 02:06:04Z", "2025-04-16 02:06:03Z", "2025-04-16 02:06:02Z", "2025-04-16 02:06:01Z" ], [ "2025-04-16 02:06:11Z", "2025-04-16 02:06:10Z", "2025-04-16 02:06:09Z", "2025-04-16 02:06:08Z", "2025-04-16 02:06:07Z", "2025-04-16 02:06:06Z", "2025-04-16 02:06:05Z", "2025-04-16 02:06:04Z", "2025-04-16 02:06:03Z", "2025-04-16 02:06:02Z" ], [ "2025-04-16 02:06:11Z", "2025-04-16 02:06:10Z", "2025-04-16 02:06:09Z", "2025-04-16 02:06:08Z", "2025-04-16 02:06:07Z", "2025-04-16 02:06:06Z", "2025-04-16 02:06:05Z", "2025-04-16 02:06:04Z", "2025-04-16 02:06:03Z", "2025-04-16 02:06:02Z" ] ]
"values" : [ [ "2025-04-16 02:06:10.919Z", "2025-04-16 02:06:09.919Z", "2025-04-16 02:06:08.919Z", "2025-04-16 02:06:07.919Z", "2025-04-16 02:06:06.919Z", "2025-04-16 02:06:05.919Z", "2025-04-16 02:06:04.919Z", "2025-04-16 02:06:03.919Z", "2025-04-16 02:06:02.919Z", "2025-04-16 02:06:01.919Z" ], [ "2025-04-16 02:06:11.919Z", "2025-04-16 02:06:10.919Z", "2025-04-16 02:06:09.919Z", "2025-04-16 02:06:08.919Z", "2025-04-16 02:06:07.919Z", "2025-04-16 02:06:06.919Z", "2025-04-16 02:06:05.919Z", "2025-04-16 02:06:04.919Z", "2025-04-16 02:06:03.919Z", "2025-04-16 02:06:02.919Z" ], [ "2025-04-16 02:06:11.919Z", "2025-04-16 02:06:10.919Z", "2025-04-16 02:06:09.919Z", "2025-04-16 02:06:08.919Z", "2025-04-16 02:06:07.919Z", "2025-04-16 02:06:06.919Z", "2025-04-16 02:06:05.919Z", "2025-04-16 02:06:04.919Z", "2025-04-16 02:06:03.919Z", "2025-04-16 02:06:02.919Z" ] ]
} ]
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestGetOnlineFeaturesRange_withOnlyEqualsFilter(t *testing.T) {
{
SortKeyName: "event_timestamp",
Query: &serving.SortKeyFilter_Equals{
Equals: &types.Value{Val: &types.Value_UnixTimestampVal{UnixTimestampVal: 1744769171}},
Equals: &types.Value{Val: &types.Value_UnixTimestampVal{UnixTimestampVal: 1744769171919}},
},
},
},
Expand Down Expand Up @@ -370,6 +370,181 @@ func TestGetOnlineFeaturesRange_withFeatureViewThrowsError(t *testing.T) {
"Expected error message for non-existent sorted feature view")
}

// TestGetOnlineFeaturesRange_SubSecondSortKeyValuesRemainDistinct is a
// regression test for the EAPC-22316 follow-up: InterfaceToProtoValue in
// go/types/typeconversion.go was truncating UNIX_TIMESTAMP sort key values
// read back from Cassandra to whole seconds via time.Time.Unix(), causing
// rows for the same entity that differ by less than a second to appear
// identical when the sort key is requested as a feature value. The fixture
// (sub_second_sort_key_view, seeded from sub_second_data.parquet) has three
// rows for one entity: two only 18ms apart within the same second, and one
// two seconds later - mirroring the real-world repro that caught this bug.
func TestGetOnlineFeaturesRange_SubSecondSortKeyValuesRemainDistinct(t *testing.T) {
entities := make(map[string]*types.RepeatedValue)
entities["sub_second_entity_id"] = &types.RepeatedValue{
Val: []*types.Value{
{Val: &types.Value_StringVal{StringVal: "entity-1"}},
},
}

request := &serving.GetOnlineFeaturesRangeRequest{
Kind: &serving.GetOnlineFeaturesRangeRequest_Features{
Features: &serving.FeatureList{
Val: []string{
"sub_second_sort_key_view:event_timestamp",
"sub_second_sort_key_view:value",
},
},
},
Entities: entities,
SortKeyFilters: []*serving.SortKeyFilter{
{
SortKeyName: "event_timestamp",
Query: &serving.SortKeyFilter_Range{
Range: &serving.SortKeyFilter_RangeQuery{
RangeStart: &types.Value{Val: &types.Value_UnixTimestampVal{UnixTimestampVal: 0}},
},
},
},
},
Limit: 10,
IncludeMetadata: true,
}

response, err := client.GetOnlineFeaturesRange(ctx, request)
require.NoError(t, err)
require.NotNil(t, response)
require.Len(t, response.Results, 2, "expected event_timestamp and value results")

eventTimestampIdx := -1
for i, name := range response.Metadata.FeatureNames.Val {
if name == "event_timestamp" {
eventTimestampIdx = i
}
}
require.NotEqual(t, -1, eventTimestampIdx, "expected to find event_timestamp in the response")

eventTimestampVector := response.Results[eventTimestampIdx]
require.Len(t, eventTimestampVector.Values, 1, "expected results for the single requested entity")

timestamps := eventTimestampVector.Values[0].Val
require.Len(t, timestamps, 3, "expected all 3 sub-second-apart rows to be returned")

seen := make(map[int64]bool)
for _, v := range timestamps {
ts := v.GetUnixTimestampVal()
assert.False(t, seen[ts], "sort key value %d was returned more than once; sub-second rows collapsed", ts)
seen[ts] = true
}
assert.Len(t, seen, 3, "expected 3 distinct millisecond-precision sort key values")

expected := map[int64]bool{
1717244257886: true, // 2024-06-01 12:17:37.886
1717244257904: true, // 2024-06-01 12:17:37.904
1717244259035: true, // 2024-06-01 12:17:39.035
}
for ts := range seen {
assert.True(t, expected[ts], "unexpected sort key value %d", ts)
}

// EventTimestamps come from the _ts:<fv> hash field (a timestamppb.Timestamp).
// grpc_server calls GetTimestampSeconds, so values must be whole-second Unix timestamps.
require.Len(t, eventTimestampVector.EventTimestamps, 1, "expected EventTimestamps for 1 entity")
require.Len(t, eventTimestampVector.EventTimestamps[0].Val, 3, "expected EventTimestamp for each of the 3 rows")
for _, tsVal := range eventTimestampVector.EventTimestamps[0].Val {
secs := tsVal.GetUnixTimestampVal()
assert.True(t, secs > 1_000_000_000 && secs < 2_000_000_000,
"EventTimestamp must be seconds-precision (~2001–2033), got %d", secs)
}
}

// TestGetOnlineFeaturesRange_CustomNamedSortKeyValuesRemainDistinct guards
// against the EAPC-22316 fix being tied to the "event_timestamp" column name
// rather than working for any UNIX_TIMESTAMP column declared as a sort key.
// The fixture (sub_second_custom_sortkey_view, seeded from
// sub_second_custom_sortkey_data.parquet) declares "viewed_at" - a name
// distinct from the source's own "event_timestamp" watermark field - as the
// sort key, mirroring the real customer schema. Same three-row shape: two
// rows 18ms apart within the same second, one two seconds later.
func TestGetOnlineFeaturesRange_CustomNamedSortKeyValuesRemainDistinct(t *testing.T) {
entities := make(map[string]*types.RepeatedValue)
entities["custom_sortkey_entity_id"] = &types.RepeatedValue{
Val: []*types.Value{
{Val: &types.Value_StringVal{StringVal: "entity-1"}},
},
}

request := &serving.GetOnlineFeaturesRangeRequest{
Kind: &serving.GetOnlineFeaturesRangeRequest_Features{
Features: &serving.FeatureList{
Val: []string{
"sub_second_custom_sortkey_view:viewed_at",
"sub_second_custom_sortkey_view:value",
},
},
},
Entities: entities,
SortKeyFilters: []*serving.SortKeyFilter{
{
SortKeyName: "viewed_at",
Query: &serving.SortKeyFilter_Range{
Range: &serving.SortKeyFilter_RangeQuery{
RangeStart: &types.Value{Val: &types.Value_UnixTimestampVal{UnixTimestampVal: 0}},
},
},
},
},
Limit: 10,
IncludeMetadata: true,
}

response, err := client.GetOnlineFeaturesRange(ctx, request)
require.NoError(t, err)
require.NotNil(t, response)
require.Len(t, response.Results, 2, "expected viewed_at and value results")

viewedAtIdx := -1
for i, name := range response.Metadata.FeatureNames.Val {
if name == "viewed_at" {
viewedAtIdx = i
}
}
require.NotEqual(t, -1, viewedAtIdx, "expected to find viewed_at in the response")

viewedAtVector := response.Results[viewedAtIdx]
require.Len(t, viewedAtVector.Values, 1, "expected results for the single requested entity")

timestamps := viewedAtVector.Values[0].Val
require.Len(t, timestamps, 3, "expected all 3 sub-second-apart rows to be returned")

seen := make(map[int64]bool)
for _, v := range timestamps {
ts := v.GetUnixTimestampVal()
assert.False(t, seen[ts], "sort key value %d was returned more than once; sub-second rows collapsed", ts)
seen[ts] = true
}
assert.Len(t, seen, 3, "expected 3 distinct millisecond-precision sort key values")

expected := map[int64]bool{
1717244257886: true, // 2024-06-01 12:17:37.886
1717244257904: true, // 2024-06-01 12:17:37.904
1717244259035: true, // 2024-06-01 12:17:39.035
}
for ts := range seen {
assert.True(t, expected[ts], "unexpected sort key value %d", ts)
}

// EventTimestamps come from the _ts:<fv> hash field (a timestamppb.Timestamp).
// grpc_server calls GetTimestampSeconds, so values must be whole-second Unix timestamps.
require.Len(t, viewedAtVector.EventTimestamps, 1, "expected EventTimestamps for 1 entity")
require.Len(t, viewedAtVector.EventTimestamps[0].Val, 3, "expected EventTimestamp for each of the 3 rows")
for _, tsVal := range viewedAtVector.EventTimestamps[0].Val {
secs := tsVal.GetUnixTimestampVal()
assert.True(t, secs > 1_000_000_000 && secs < 2_000_000_000,
"EventTimestamp must be seconds-precision (~2001–2033), got %d", secs)
}
}

func assertResponseData(t *testing.T, response *serving.GetOnlineFeaturesRangeResponse, featureNames []string, entitiesRequested int, includeMetadata bool) {
assert.NotNil(t, response)
assert.Equal(t, 1, len(response.Entities), "Should have 1 list of entity")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Regression fixture for EAPC-22316: UNIX_TIMESTAMP sort keys must retain
# millisecond precision for rows that differ by less than one second.

from datetime import timedelta

from feast import Entity, Field, FileSource, SortedFeatureView
from feast.protos.feast.core.SortedFeatureView_pb2 import SortOrder
from feast.sort_key import SortKey
from feast.types import String, UnixTimestamp
from feast.value_type import ValueType

tags = {"team": "Feast"}
owner = "test@test.com"

sub_second_entity: Entity = Entity(
name="sub_second_entity",
join_keys=["sub_second_entity_id"],
value_type=ValueType.STRING,
tags=tags,
owner=owner,
)

sub_second_source: FileSource = FileSource(
path="sub_second_data.parquet", timestamp_field="event_timestamp"
)

sub_second_sort_key_view: SortedFeatureView = SortedFeatureView(
name="sub_second_sort_key_view",
entities=[sub_second_entity],
ttl=timedelta(days=0),
source=sub_second_source,
tags=tags,
description="Regression fixture: sort key rows <1s apart for the same entity",
owner=owner,
online=True,
sort_keys=[
SortKey(
name="event_timestamp",
value_type=ValueType.UNIX_TIMESTAMP,
default_sort_order=SortOrder.DESC,
)
],
schema=[
Field(name="value", dtype=String),
Field(name="event_timestamp", dtype=UnixTimestamp),
],
)
Binary file not shown.
Loading
Loading