Skip to content

Commit 343eb2c

Browse files
Normalize extended ISO 8601 datetime strings in query values for MongoDB compatibility
1 parent 1530a1c commit 343eb2c

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

src/Database/Adapter/Mongo.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,6 +2473,20 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr
24732473
*/
24742474
protected function buildFilter(Query $query): array
24752475
{
2476+
// Normalize extended ISO 8601 datetime strings in query values to UTCDateTime
2477+
// so they can be correctly compared against datetime fields stored in MongoDB.
2478+
$values = $query->getValues();
2479+
foreach ($values as $k => $value) {
2480+
if (is_string($value) && $this->isExtendedISODatetime($value)) {
2481+
try {
2482+
$values[$k] = $this->toMongoDatetime($value);
2483+
} catch (\Throwable $th) {
2484+
// Leave value as-is if it cannot be parsed as a datetime
2485+
}
2486+
}
2487+
}
2488+
$query->setValues($values);
2489+
24762490
if ($query->getAttribute() === '$id') {
24772491
$query->setAttribute('_uid');
24782492
} elseif ($query->getAttribute() === '$sequence') {

tests/e2e/Adapter/Scopes/SchemalessTests.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,4 +3242,132 @@ public function testSchemalessMongoDotNotationIndexes(): void
32423242

32433243
$database->deleteCollection($col);
32443244
}
3245+
3246+
public function testQueryWithDatetime(): void
3247+
{
3248+
/** @var Database $database */
3249+
$database = static::getDatabase();
3250+
3251+
if ($database->getAdapter()->getSupportForAttributes()) {
3252+
$this->expectNotToPerformAssertions();
3253+
return;
3254+
}
3255+
3256+
$col = uniqid('sl_query_datetime');
3257+
$database->createCollection($col);
3258+
3259+
$permissions = [
3260+
Permission::read(Role::any()),
3261+
Permission::write(Role::any()),
3262+
Permission::update(Role::any()),
3263+
Permission::delete(Role::any())
3264+
];
3265+
3266+
// Documents with datetime field (ISO 8601) for query tests
3267+
// Dates: Jan 15 2024, Feb 20 2024, Mar 25 2024, Jun 15 2024, Dec 31 2024
3268+
$docs = [
3269+
new Document([
3270+
'$id' => 'dt1',
3271+
'$permissions' => $permissions,
3272+
'name' => 'January',
3273+
'datetime' => '2024-01-15T10:30:00.000+00:00'
3274+
]),
3275+
new Document([
3276+
'$id' => 'dt2',
3277+
'$permissions' => $permissions,
3278+
'name' => 'February',
3279+
'datetime' => '2024-02-20T14:45:30.123Z'
3280+
]),
3281+
new Document([
3282+
'$id' => 'dt3',
3283+
'$permissions' => $permissions,
3284+
'name' => 'March',
3285+
// Use a valid extended ISO 8601 datetime that will be normalized
3286+
// to MongoDB UTCDateTime for comparison queries.
3287+
'datetime' => '2024-03-25T08:15:45.000+00:00'
3288+
]),
3289+
new Document([
3290+
'$id' => 'dt4',
3291+
'$permissions' => $permissions,
3292+
'name' => 'June',
3293+
'datetime' => '2024-06-15T12:00:00.000Z'
3294+
]),
3295+
new Document([
3296+
'$id' => 'dt5',
3297+
'$permissions' => $permissions,
3298+
'name' => 'December',
3299+
'datetime' => '2024-12-31T23:59:59.999+00:00'
3300+
]),
3301+
];
3302+
3303+
$createdCount = $database->createDocuments($col, $docs);
3304+
$this->assertEquals(5, $createdCount);
3305+
3306+
// Query: equal - find document with exact datetime (Jan 15 2024)
3307+
$equalResults = $database->find($col, [
3308+
Query::equal('datetime', ['2024-01-15T10:30:00.000+00:00'])
3309+
]);
3310+
$this->assertCount(1, $equalResults);
3311+
$this->assertEquals('dt1', $equalResults[0]->getId());
3312+
$this->assertEquals('January', $equalResults[0]->getAttribute('name'));
3313+
3314+
// Query: greaterThan - datetimes after 2024-03-01 (dt3, dt4, dt5)
3315+
$greaterResults = $database->find($col, [
3316+
Query::greaterThan('datetime', '2024-03-01T00:00:00.000Z')
3317+
]);
3318+
$this->assertCount(3, $greaterResults);
3319+
$greaterIds = array_map(fn ($d) => $d->getId(), $greaterResults);
3320+
$this->assertContains('dt3', $greaterIds);
3321+
$this->assertContains('dt4', $greaterIds);
3322+
$this->assertContains('dt5', $greaterIds);
3323+
3324+
// Query: lessThan - datetimes before 2024-03-01 (dt1, dt2)
3325+
$lessResults = $database->find($col, [
3326+
Query::lessThan('datetime', '2024-03-01T00:00:00.000Z')
3327+
]);
3328+
$this->assertCount(2, $lessResults);
3329+
$lessIds = array_map(fn ($d) => $d->getId(), $lessResults);
3330+
$this->assertContains('dt1', $lessIds);
3331+
$this->assertContains('dt2', $lessIds);
3332+
3333+
// Query: greaterThanEqual - datetimes on or after 2024-02-20 (dt2, dt3, dt4, dt5)
3334+
$gteResults = $database->find($col, [
3335+
Query::greaterThanEqual('datetime', '2024-02-20T14:45:30.123Z')
3336+
]);
3337+
$this->assertCount(4, $gteResults);
3338+
$gteIds = array_map(fn ($d) => $d->getId(), $gteResults);
3339+
$this->assertContains('dt2', $gteIds);
3340+
$this->assertContains('dt3', $gteIds);
3341+
$this->assertContains('dt4', $gteIds);
3342+
$this->assertContains('dt5', $gteIds);
3343+
3344+
// Query: lessThanEqual - datetimes on or before 2024-06-15 (dt1, dt2, dt3, dt4)
3345+
$lteResults = $database->find($col, [
3346+
Query::lessThanEqual('datetime', '2024-06-15T12:00:00.000Z')
3347+
]);
3348+
$this->assertCount(4, $lteResults);
3349+
$lteIds = array_map(fn ($d) => $d->getId(), $lteResults);
3350+
$this->assertContains('dt1', $lteIds);
3351+
$this->assertContains('dt2', $lteIds);
3352+
$this->assertContains('dt3', $lteIds);
3353+
$this->assertContains('dt4', $lteIds);
3354+
3355+
// Query: between - datetimes in range [2024-02-01, 2024-07-01) (dt2, dt3, dt4)
3356+
$betweenResults = $database->find($col, [
3357+
Query::between('datetime', '2024-02-01T00:00:00.000Z', '2024-07-01T00:00:00.000Z')
3358+
]);
3359+
$this->assertCount(3, $betweenResults);
3360+
$betweenIds = array_map(fn ($d) => $d->getId(), $betweenResults);
3361+
$this->assertContains('dt2', $betweenIds);
3362+
$this->assertContains('dt3', $betweenIds);
3363+
$this->assertContains('dt4', $betweenIds);
3364+
3365+
// Query: equal with no match
3366+
$noneResults = $database->find($col, [
3367+
Query::equal('datetime', ['2020-01-01T00:00:00.000Z'])
3368+
]);
3369+
$this->assertCount(0, $noneResults);
3370+
3371+
// $database->deleteCollection($col);
3372+
}
32453373
}

0 commit comments

Comments
 (0)