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 <agent@cursor.com>
This commit is contained in:
qdrant-cloud-bot
2026-04-20 10:00:56 +02:00
committed by timvisee
parent 45377af629
commit eff3c50db3
2 changed files with 26 additions and 1 deletions

View File

@@ -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: <https://github.com/qdrant/qdrant/issues/3529>
// See: <https://github.com/qdrant/qdrant/issues/8718>
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();

View File

@@ -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(