Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eb042f0

Browse filesBrowse files
authored
feat: New zoned timestamp feature type (#6536) (#6537)
1 parent f0c5be8 commit eb042f0
Copy full SHA for eb042f0

13 files changed

+402-65Lines changed: 402 additions & 65 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/getting-started/concepts/feast-types.md‎

Copy file name to clipboardExpand all lines: docs/getting-started/concepts/feast-types.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Feast's type system is built on top of [protobuf](https://github.com/protocolbuf
88
Feast supports the following categories of data types:
99

1010
- **Primitive types**: numerical values (`Int32`, `Int64`, `Float32`, `Float64`), `String`, `Bytes`, `Bool`, and `UnixTimestamp`.
11+
- **Zoned timestamp type**: `ZonedTimestamp` stores a timezone-aware datetime as both the UTC instant and its originating zone, so the original wall-clock zone round-trips losslessly. This differs from `UnixTimestamp`, which is always decoded as UTC and discards the source zone. Use `ZonedTimestamp` when local time-of-day or the offset/zone itself is meaningful. It must be explicitly declared in schema (it is not inferred by any backend), and is not supported as an entity key.
1112
- **Domain-specific primitives**: `PdfBytes` (PDF binary data for RAG/document pipelines) and `ImageBytes` (image binary data for multimodal pipelines). These are semantic aliases over `Bytes` and must be explicitly declared in schema — no backend infers them.
1213
- **UUID types**: `Uuid` and `TimeUuid` for universally unique identifiers. Stored as strings at the proto level but deserialized to `uuid.UUID` objects in Python.
1314
- **Array types**: ordered lists of any primitive type, e.g. `Array(Int64)`, `Array(String)`, `Array(Uuid)`.
Collapse file

‎docs/reference/type-system.md‎

Copy file name to clipboardExpand all lines: docs/reference/type-system.md
+41-1Lines changed: 41 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Feast supports the following data types:
2424
| `Bytes` | `bytes` | Binary data |
2525
| `Bool` | `bool` | Boolean value |
2626
| `UnixTimestamp` | `datetime` | Unix timestamp (nullable) |
27+
| `ZonedTimestamp` | `datetime` | Timezone-aware datetime preserving its source zone (nullable) |
2728
| `Uuid` | `uuid.UUID` | UUID (any version) |
2829
| `TimeUuid` | `uuid.UUID` | Time-based UUID (version 1) |
2930
| `Decimal` | `decimal.Decimal` | Arbitrary-precision decimal number |
@@ -202,7 +203,8 @@ from datetime import timedelta
202203
from feast import Entity, FeatureView, Field, FileSource
203204
from feast.types import (
204205
Int32, Int64, Float32, Float64, String, Bytes, Bool, UnixTimestamp,
205-
Uuid, TimeUuid, Decimal, Array, Set, Map, ScalarMap, Json, Struct
206+
Uuid, TimeUuid, Decimal, Array, Set, Map, ScalarMap, Json, Struct,
207+
ZonedTimestamp
206208
)
207209

208210
# Define a data source
@@ -232,6 +234,7 @@ user_features = FeatureView(
232234
Field(name="profile_picture", dtype=Bytes),
233235
Field(name="is_active", dtype=Bool),
234236
Field(name="last_login", dtype=UnixTimestamp),
237+
Field(name="event_time", dtype=ZonedTimestamp),
235238
Field(name="session_id", dtype=Uuid),
236239
Field(name="event_id", dtype=TimeUuid),
237240
Field(name="price", dtype=Decimal),
@@ -362,6 +365,43 @@ unique_prices = {decimal.Decimal("9.99"), decimal.Decimal("19.99"), decimal.Deci
362365
`Decimal` is **not** inferred from any backend schema. You must declare it explicitly in your feature view schema. The pandas dtype for `Decimal` columns is `object` (holding `decimal.Decimal` instances), not a numeric dtype.
363366
{% endhint %}
364367

368+
### ZonedTimestamp Type Usage Examples
369+
370+
The `ZonedTimestamp` type stores a timezone-aware `datetime` as both the UTC instant
371+
and its originating zone, so the original wall-clock zone round-trips losslessly.
372+
By contrast, `UnixTimestamp` always decodes to UTC and discards the source zone.
373+
374+
```python
375+
from datetime import datetime, timezone
376+
from zoneinfo import ZoneInfo
377+
378+
# A datetime in a specific zone — both the instant and "America/Los_Angeles" are kept
379+
event_time = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles"))
380+
381+
# ZonedTimestamp values are returned as tz-aware datetime objects, in their own zone
382+
response = store.get_online_features(
383+
features=["event_features:event_time"],
384+
entity_rows=[{"user_id": 1001}],
385+
)
386+
result = response.to_dict()
387+
# result["event_time"][0] == event_time (same instant AND same zone, e.g. 09:00-07:00)
388+
389+
# Two values at the same instant but different zones stay distinct
390+
la = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles"))
391+
utc = datetime(2026, 6, 17, 16, 0, 0, tzinfo=timezone.utc) # same instant as `la`
392+
393+
# A naive (tz-less) datetime is interpreted as UTC
394+
naive = datetime(2026, 6, 17, 12, 0, 0) # stored zone is empty, decoded as UTC
395+
```
396+
397+
{% hint style="warning" %}
398+
`ZonedTimestamp` is **not** inferred from any backend schema — you must declare it
399+
explicitly in your feature view schema. It is not supported as an entity key. The
400+
zone is stored as an IANA name (e.g. `America/Los_Angeles`) when available, falling
401+
back to a fixed-offset string; offline stores that cannot natively carry a zone may
402+
normalize to UTC on that backend.
403+
{% endhint %}
404+
365405
### Nested Collection Type Usage Examples
366406

367407
```python
Collapse file

‎protos/feast/types/Value.proto‎

Copy file name to clipboardExpand all lines: protos/feast/types/Value.proto
+13Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ message ValueType {
6969
DECIMAL_LIST = 45;
7070
DECIMAL_SET = 46;
7171
SCALAR_MAP = 47;
72+
ZONED_TIMESTAMP = 48;
7273
}
7374
}
7475

@@ -120,13 +121,25 @@ message Value {
120121
StringList decimal_list_val = 45;
121122
StringSet decimal_set_val = 46;
122123
ScalarMap scalar_map_val = 47;
124+
ZonedTimestamp zoned_timestamp_val = 48;
123125
}
124126
}
125127

126128
enum Null {
127129
NULL = 0;
128130
}
129131

132+
// A timezone-aware datetime: the UTC instant plus its originating zone, so a
133+
// zoned datetime round-trips losslessly (unlike UNIX_TIMESTAMP, which is decoded
134+
// as UTC and discards the original zone).
135+
message ZonedTimestamp {
136+
// Epoch seconds (UTC instant), same convention as unix_timestamp_val.
137+
int64 unix_timestamp = 1;
138+
// IANA zone name (e.g. "America/Los_Angeles") or a fixed-offset string
139+
// (e.g. "-07:00", "UTC"). Empty string is treated as UTC on read.
140+
string zone = 2;
141+
}
142+
130143
message BytesList {
131144
repeated bytes val = 1;
132145
}
Collapse file

‎sdk/python/feast/feature_server_utils.py‎

Copy file name to clipboardExpand all lines: sdk/python/feast/feature_server_utils.py
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ def _value_to_native(v: Value) -> Optional[Any]:
9999
"scalar_map_val is not yet supported by convert_response_to_dict; value will be None"
100100
)
101101
return None
102+
# zoned_timestamp_val is a ZonedTimestamp message; serialize as an ISO 8601
103+
# string in its stored zone so JSONResponse can encode it (the raw message
104+
# is not JSON-serializable).
105+
elif which == "zoned_timestamp_val":
106+
return _zoned_timestamp_to_str(v.zoned_timestamp_val)
102107
# bytes_list_val / bytes_set_val — base64-encode each element
103108
elif which in ("bytes_list_val", "bytes_set_val"):
104109
return [base64.b64encode(b).decode("ascii") for b in getattr(v, which).val]
@@ -109,6 +114,20 @@ def _value_to_native(v: Value) -> Optional[Any]:
109114
return getattr(v, which)
110115

111116

117+
def _zoned_timestamp_to_str(zoned) -> Optional[str]:
118+
"""Convert a ZonedTimestamp proto to an ISO 8601 string in its stored zone.
119+
120+
A null instant (NaT sentinel) returns None. The zone is resolved via the
121+
same logic used on the read path so IANA names and fixed offsets round-trip.
122+
"""
123+
from feast.type_map import NULL_TIMESTAMP_INT_VALUE, _zone_from_name
124+
125+
if zoned.unix_timestamp == NULL_TIMESTAMP_INT_VALUE:
126+
return None
127+
tz = _zone_from_name(zoned.zone)
128+
return datetime.fromtimestamp(zoned.unix_timestamp, tz=tz).isoformat()
129+
130+
112131
def _timestamp_to_str(ts) -> str:
113132
"""Convert protobuf Timestamp to RFC 3339 format with Z suffix.
114133
Collapse file

‎sdk/python/feast/proto_json.py‎

Copy file name to clipboardExpand all lines: sdk/python/feast/proto_json.py
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ def to_json_object(printer: _Printer, message: ProtoMessage) -> JsonObject:
6666
elif which in ("list_val", "set_val"):
6767
# Nested collection: RepeatedValue containing Values
6868
repeated = getattr(message, which)
69-
value = [
69+
value: JsonObject = [
7070
printer._MessageToJsonObject(inner_val) for inner_val in repeated.val
7171
]
72+
elif which == "zoned_timestamp_val":
73+
# ZonedTimestamp is a message; render it as an ISO 8601 string in its
74+
# stored zone so the result is JSON-serializable (the raw message is not).
75+
from feast.feature_server_utils import _zoned_timestamp_to_str
76+
77+
value = _zoned_timestamp_to_str(message.zoned_timestamp_val)
7278
elif "_list_" in which:
7379
value = list(getattr(message, which).val)
7480
else:

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.