fix: fix start from with date object

This commit is contained in:
George Panchuk
2026-09-11 04:31:23 +07:00
parent 977fda4fc9
commit 9328de05c6
4 changed files with 73 additions and 6 deletions
+3 -1
View File
@@ -3646,7 +3646,9 @@ class RestToGrpc:
return grpc.StartFrom(integer=model)
if isinstance(model, float):
return grpc.StartFrom(float=model)
if isinstance(model, datetime):
if isinstance(model, date):
# covers datetime too, which is a subclass of date. convert_datetime turns a
# bare date into midnight UTC on that day.
ts = cls.convert_datetime(model)
return grpc.StartFrom(timestamp=ts)
if isinstance(model, str):
+7 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from qdrant_client.http.models import OrderValue
from qdrant_client.local.datetime_utils import parse
@@ -24,7 +24,7 @@ def datetime_to_microseconds(dt: datetime) -> int:
return delta.days * MICROS_PER_DAY + delta.seconds * MICROS_PER_SECOND + delta.microseconds
def to_order_value(value: str | datetime | OrderValue | None) -> OrderValue | None:
def to_order_value(value: str | date | datetime | OrderValue | None) -> OrderValue | None:
if value is None:
return None
@@ -37,6 +37,11 @@ def to_order_value(value: str | datetime | OrderValue | None) -> OrderValue | No
if isinstance(value, datetime):
return datetime_to_microseconds(value)
if isinstance(value, date):
# Must stay below the datetime branch: datetime is a subclass of date. Midnight is
# naive, so `datetime_to_microseconds` reads it as UTC, like core reads "%Y-%m-%d".
return datetime_to_microseconds(datetime.combine(value, datetime.min.time()))
if isinstance(value, str):
dt = parse(value)
if dt is not None:
+10 -2
View File
@@ -1,9 +1,9 @@
from datetime import datetime, timedelta, timezone, tzinfo
from datetime import date, datetime, timedelta, timezone, tzinfo
import pytest
from qdrant_client.local.datetime_utils import parse
from qdrant_client.local.order_by import datetime_to_microseconds
from qdrant_client.local.order_by import datetime_to_microseconds, to_order_value
@pytest.mark.parametrize( # type: ignore
@@ -144,3 +144,11 @@ def test_tzinfo_without_an_offset_counts_as_naive() -> None:
assert datetime_to_microseconds(
datetime(2024, 6, 15, 12, 30, 45, tzinfo=NoOffset())
) == datetime_to_microseconds(datetime(2024, 6, 15, 12, 30, 45, tzinfo=timezone.utc))
def test_to_order_value_reads_a_bare_date_as_utc_midnight() -> None:
"""A `date` is a member of the StartFrom union, and means the same instant as the
"%Y-%m-%d" string REST serializes it to. Local midnight would shift the window, and is
only visible on a client outside UTC."""
assert to_order_value(date(2021, 1, 1)) == 1609459200000000 # 2021-01-01T00:00:00Z
assert to_order_value("2021-01-01") == 1609459200000000
+53 -1
View File
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import date, datetime
from qdrant_client import models
from qdrant_client.client_base import QdrantBase
@@ -220,3 +220,55 @@ def test_scroll_from_naive_datetime() -> None:
compare_client_results(grpc_client, http_client, scroll_from_naive_datetime)
compare_client_results(local_client, http_client, scroll_from_naive_datetime)
DATE_START_FROM = date(2024, 6, 15)
DATED_POINTS = [
models.PointStruct(id=1, vector=[], payload={"date_field": "2024-06-13T12:00:00+00:00"}),
models.PointStruct(id=2, vector=[], payload={"date_field": "2024-06-14T23:59:59+00:00"}),
# exactly on the boundary: start_from is inclusive
models.PointStruct(id=3, vector=[], payload={"date_field": "2024-06-15T00:00:00+00:00"}),
models.PointStruct(id=4, vector=[], payload={"date_field": "2024-06-15T08:30:00+00:00"}),
models.PointStruct(id=5, vector=[], payload={"date_field": "2024-06-17T00:00:00+00:00"}),
]
def scroll_from_date(client: QdrantBase, direction: models.Direction) -> list[models.Record]:
records, next_page = client.scroll(
collection_name=COLLECTION_NAME,
limit=10,
order_by=models.OrderBy(key="date_field", direction=direction, start_from=DATE_START_FROM),
with_payload=True,
)
assert next_page is None
return records
def test_scroll_order_by_date_start_from() -> None:
"""A `date` start_from means midnight UTC on that day to local, REST and gRPC alike.
Local mode used to drop it and scroll the whole collection, and gRPC used to raise.
"""
local_client = init_local()
init_client(local_client, DATED_POINTS, vectors_config={})
http_client = init_remote()
init_client(http_client, DATED_POINTS, vectors_config={})
http_client.create_payload_index(
COLLECTION_NAME, "date_field", models.PayloadSchemaType.DATETIME, wait=True
)
grpc_client = init_remote(prefer_grpc=True)
for direction, expected_ids in [
(models.Direction.ASC, [3, 4, 5]),
(models.Direction.DESC, [3, 2, 1]),
]:
# a dropped start_from returns all five points, which congruence alone would miss
assert [record.id for record in scroll_from_date(http_client, direction)] == expected_ids
compare_client_results(grpc_client, http_client, scroll_from_date, direction=direction)
compare_client_results(local_client, http_client, scroll_from_date, direction=direction)