From eff3c50db3c7d2c5038cefbd51112d628c23c1b0 Mon Sep 17 00:00:00 2001 From: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:00:56 +0200 Subject: [PATCH] Fix datetime parsing for YYYY-MM-DDTHH:MM format (T separator, no seconds) (#8719) The documented format `2023-02-08T10:49` was not accepted because the parser only had `%Y-%m-%d %H:%M` (space separator) but was missing `%Y-%m-%dT%H:%M` (T separator). Add the missing format variant and tests. Closes #8718 Made-with: Cursor Co-authored-by: Cursor Agent --- lib/segment/src/types.rs | 8 +++++++- tests/openapi/test_payload_indexing.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/segment/src/types.rs b/lib/segment/src/types.rs index 2f1b92fbc3..7b6f04d640 100644 --- a/lib/segment/src/types.rs +++ b/lib/segment/src/types.rs @@ -126,11 +126,14 @@ impl FromStr for DateTimePayloadType { // Attempt to parse the input string in the specified formats: // - YYYY-MM-DD'T'HH:MM:SS (without timezone or Z) // - YYYY-MM-DD HH:MM:SS (without timezone or Z) - // - YYYY-MM-DD HH:MM + // - YYYY-MM-DD'T'HH:MM (without timezone and seconds) + // - YYYY-MM-DD HH:MM (without timezone and seconds) // - YYYY-MM-DD // See: + // See: let datetime = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")) + .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M")) .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M")) .or_else(|_| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map(Into::into))?; @@ -4173,6 +4176,9 @@ mod tests { #[case::without_z_and_decimals("2020-03-01T00:00:00.12")] #[case::space_sep_without_z("2020-03-01 00:00:00")] #[case::space_sep_without_z_and_decimals("2020-03-01 00:00:00.123456")] + #[case::t_sep_without_seconds("2020-03-01T00:00")] + #[case::space_sep_without_seconds("2020-03-01 00:00")] + #[case::date_only("2020-03-01")] fn test_datetime_deserialization(#[case] datetime: &str) { let datetime = DateTimePayloadType::from_str(datetime).unwrap(); let serialized = serde_json::to_string(&datetime).unwrap(); diff --git a/tests/openapi/test_payload_indexing.py b/tests/openapi/test_payload_indexing.py index dc19920ca9..0d8f3ba95b 100644 --- a/tests/openapi/test_payload_indexing.py +++ b/tests/openapi/test_payload_indexing.py @@ -171,6 +171,8 @@ def test_datetime_indexing(collection_name): # create payload set_payload(collection_name, {datetime_key: "2015-01-01T00:00:00Z"}, [1]) set_payload(collection_name, {datetime_key: "2015-02-01T08:00:00+02:00"}, [2]) + # Use YYYY-MM-DDTHH:MM format (T separator, without seconds) — https://github.com/qdrant/qdrant/issues/8718 + set_payload(collection_name, {datetime_key: "2015-03-01T12:30"}, [3]) # Create index response = request_with_validation( @@ -185,6 +187,20 @@ def test_datetime_indexing(collection_name): ) assert response.ok + # Verify point with YYYY-MM-DDTHH:MM format was indexed (issue #8718) + response = request_with_validation( + api="/collections/{collection_name}/points/scroll", + method="POST", + path_params={"collection_name": collection_name}, + body={ + "with_vector": False, + "filter": {"must": [{"key": datetime_key, "range": {"gte": "2015-03-01T12:30", "lte": "2015-03-01T12:30"}}]}, + }, + ) + assert response.ok, response.json() + point_ids = [p["id"] for p in response.json()["result"]["points"]] + assert 3 in point_ids, f"Point 3 with YYYY-MM-DDTHH:MM format not found in index results: {point_ids}" + # test with mixed datetime format data = [ ({"gte": "2015-01-01", "lte": "2015-01-01 00:00"}, [1]), @@ -192,6 +208,9 @@ def test_datetime_indexing(collection_name): ({"gte": "2015-02-01T06:00:00", "lte": "2015-02-01T06:00:00Z"}, [2]), # date_optional_time ({"gte": "2015-02-01T06:00:00.000000000", "lte": "2015-02-01T06:00:00.000000000"}, [2]), + # YYYY-MM-DDTHH:MM (T separator, without seconds) — https://github.com/qdrant/qdrant/issues/8718 + ({"gte": "2015-01-01T00:00", "lte": "2015-01-01T00:00"}, [1]), + ({"gte": "2015-02-01T06:00", "lte": "2015-02-01T06:00"}, [2]), ] for range_, expected_ids in data: response = request_with_validation(