Skip to content
Merged
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
34 changes: 34 additions & 0 deletions mock_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,40 @@ def Search(
return weaviate_client.collections.use("YearZeroCollection")


@pytest.fixture(scope="function")
def empty_date_collection(
weaviate_client: weaviate.WeaviateClient, start_grpc_server: grpc.Server
) -> weaviate.collections.Collection:
class MockWeaviateService(weaviate_pb2_grpc.WeaviateServicer):
def Search(
self, request: search_get_pb2.SearchRequest, context: grpc.ServicerContext
) -> search_get_pb2.SearchReply:
# An empty date_value explicitly sets the oneof, so it passes the HasField guard;
# a repeated string has no per-element presence at all.
date_props: Mapping[str, properties_pb2.Value] = {
"date": properties_pb2.Value(date_value=""),
"dates": properties_pb2.Value(
list_value=properties_pb2.ListValue(
date_values=properties_pb2.DateValues(
values=["", "2023-01-15T14:30:45.123456Z"]
)
)
),
}
return search_get_pb2.SearchReply(
results=[
search_get_pb2.SearchResult(
properties=search_get_pb2.PropertiesResult(
non_ref_props=properties_pb2.Properties(fields=date_props)
)
),
]
)

weaviate_pb2_grpc.add_WeaviateServicer_to_server(MockWeaviateService(), start_grpc_server)
return weaviate_client.collections.use("EmptyDateCollection")


@pytest.fixture(scope="function")
def timeouts_collection(
weaviate_timeouts_client: weaviate.WeaviateClient, start_grpc_server: grpc.Server
Expand Down
12 changes: 12 additions & 0 deletions mock_tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,18 @@ def test_year_zero(year_zero_collection: weaviate.collections.Collection) -> Non
assert str(recwarn[0].message).startswith("Con004")


def test_empty_date(empty_date_collection: weaviate.collections.Collection) -> None:
with pytest.warns(UserWarning) as recwarn:
objs = empty_date_collection.query.fetch_objects().objects
assert objs[0].properties["date"] is None
assert objs[0].properties["dates"] == [
None,
datetime.datetime(2023, 1, 15, 14, 30, 45, 123456, tzinfo=datetime.timezone.utc),
]

assert str(recwarn[0].message).startswith("Con006")


@pytest.mark.parametrize("output", ["minimal", "verbose"])
def test_node_with_timeout(
httpserver: HTTPServer, start_grpc_server: grpc.Server, output: Literal["minimal", "verbose"]
Expand Down
10 changes: 9 additions & 1 deletion test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid as uuid_lib
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from typing import Optional
from unittest.mock import Mock, patch

import pytest
Expand Down Expand Up @@ -458,12 +459,19 @@ def test_is_weaviate_too_old(version: str, too_old: bool):
),
# Test handling year 0 (should return datetime.min)
("0000-01-15T14:30:45.123456Z", datetime.min),
# Test empty string (protobuf default for unset string field) -> None
("", None),
],
)
def test_datetime_from_weaviate_str(input_str: str, expected: datetime) -> None:
def test_datetime_from_weaviate_str(input_str: str, expected: Optional[datetime]) -> None:
assert _datetime_from_weaviate_str(input_str) == expected


def test_datetime_from_weaviate_str_empty_string_warns() -> None:
with pytest.warns(UserWarning, match="Con006"):
assert _datetime_from_weaviate_str("") is None


@pytest.mark.parametrize(
"current_version,latest_version,too_old",
[
Expand Down
7 changes: 6 additions & 1 deletion weaviate/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,12 @@ def _datetime_to_string(value: TIME) -> str:
return value.isoformat(sep="T", timespec="microseconds")


def _datetime_from_weaviate_str(string: str) -> datetime.datetime:
def _datetime_from_weaviate_str(string: str) -> Optional[datetime.datetime]:
# An unset date property arrives as null_value, never as "", so an empty string here
# can only be a malformed value from the server. Warn rather than drop it silently.
if not string:
_Warnings.datetime_empty_string()
return None
Comment on lines +764 to +769
if string[-1] != "Z":
string = "".join(string.rsplit(":", 1))

Expand Down
10 changes: 10 additions & 0 deletions weaviate/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,16 @@ def datetime_year_zero(date: str) -> None:
stacklevel=1,
)

@staticmethod
def datetime_empty_string() -> None:
warnings.warn(
message="""Con006: Received an empty date string from Weaviate. This indicates a malformed or corrupt
date value, as an unset property is returned as null and never reaches this path.
None will be returned.""",
category=UserWarning,
stacklevel=1,
)

@staticmethod
def batch_refresh_failed(err: str) -> None:
warnings.warn(
Expand Down
Loading