Skip to content

Commit a4398c1

Browse files
authored
Merge pull request #814 from utopia-php/schemaless-datetime
Normalize extended ISO 8601 datetime strings in query values for Mong…
2 parents adfdf20 + 1197151 commit a4398c1

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

src/Database/Adapter/Mongo.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2622,6 +2622,22 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr
26222622
*/
26232623
protected function buildFilter(Query $query): array
26242624
{
2625+
// Normalize extended ISO 8601 datetime strings in query values to UTCDateTime
2626+
// so they can be correctly compared against datetime fields stored in MongoDB.
2627+
if (!$this->getSupportForAttributes() || \in_array($query->getAttribute(), ['$createdAt', '$updatedAt'], true)) {
2628+
$values = $query->getValues();
2629+
foreach ($values as $k => $value) {
2630+
if (is_string($value) && $this->isExtendedISODatetime($value)) {
2631+
try {
2632+
$values[$k] = $this->toMongoDatetime($value);
2633+
} catch (\Throwable $th) {
2634+
// Leave value as-is if it cannot be parsed as a datetime
2635+
}
2636+
}
2637+
}
2638+
$query->setValues($values);
2639+
}
2640+
26252641
if ($query->getAttribute() === '$id') {
26262642
$query->setAttribute('_uid');
26272643
} elseif ($query->getAttribute() === '$sequence') {

tests/e2e/Adapter/Scopes/SchemalessTests.php

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,4 +3242,266 @@ 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+
}
3373+
3374+
public function testSchemalessCreatedAndUpdatedAtQuery(): void
3375+
{
3376+
/** @var Database $database */
3377+
$database = static::getDatabase();
3378+
3379+
if ($database->getAdapter()->getSupportForAttributes()) {
3380+
$this->expectNotToPerformAssertions();
3381+
return;
3382+
}
3383+
3384+
// Create a simple schemaless collection and one document.
3385+
$database->createCollection('schemaless_time', permissions: [
3386+
Permission::read(Role::any()),
3387+
Permission::create(Role::any()),
3388+
Permission::update(Role::any()),
3389+
Permission::delete(Role::any()),
3390+
]);
3391+
3392+
$database->createDocument('schemaless_time', new Document([
3393+
'$id' => ID::unique(),
3394+
'$permissions' => [Permission::read(Role::any())],
3395+
'name' => 'Schemaless Movie',
3396+
]));
3397+
3398+
$futureDate = '2050-01-01T00:00:00.000Z';
3399+
$pastDate = '1900-01-01T00:00:00.000Z';
3400+
$recentPastDate = '2020-01-01T00:00:00.000Z';
3401+
$nearFutureDate = '2025-01-01T00:00:00.000Z';
3402+
3403+
// --- createdBefore ---
3404+
$documents = $database->find('schemaless_time', [
3405+
Query::createdBefore($futureDate),
3406+
Query::limit(1),
3407+
]);
3408+
$this->assertGreaterThan(0, count($documents));
3409+
3410+
$documents = $database->find('schemaless_time', [
3411+
Query::createdBefore($pastDate),
3412+
Query::limit(1),
3413+
]);
3414+
$this->assertEquals(0, count($documents));
3415+
3416+
// --- createdAfter ---
3417+
$documents = $database->find('schemaless_time', [
3418+
Query::createdAfter($pastDate),
3419+
Query::limit(1),
3420+
]);
3421+
$this->assertGreaterThan(0, count($documents));
3422+
3423+
$documents = $database->find('schemaless_time', [
3424+
Query::createdAfter($futureDate),
3425+
Query::limit(1),
3426+
]);
3427+
$this->assertEquals(0, count($documents));
3428+
3429+
// --- updatedBefore ---
3430+
$documents = $database->find('schemaless_time', [
3431+
Query::updatedBefore($futureDate),
3432+
Query::limit(1),
3433+
]);
3434+
$this->assertGreaterThan(0, count($documents));
3435+
3436+
$documents = $database->find('schemaless_time', [
3437+
Query::updatedBefore($pastDate),
3438+
Query::limit(1),
3439+
]);
3440+
$this->assertEquals(0, count($documents));
3441+
3442+
// --- updatedAfter ---
3443+
$documents = $database->find('schemaless_time', [
3444+
Query::updatedAfter($pastDate),
3445+
Query::limit(1),
3446+
]);
3447+
$this->assertGreaterThan(0, count($documents));
3448+
3449+
$documents = $database->find('schemaless_time', [
3450+
Query::updatedAfter($futureDate),
3451+
Query::limit(1),
3452+
]);
3453+
$this->assertEquals(0, count($documents));
3454+
3455+
// --- createdBetween ---
3456+
$documents = $database->find('schemaless_time', [
3457+
Query::createdBetween($pastDate, $futureDate),
3458+
Query::limit(25),
3459+
]);
3460+
$this->assertGreaterThan(0, count($documents));
3461+
3462+
$documents = $database->find('schemaless_time', [
3463+
Query::createdBetween($pastDate, $pastDate),
3464+
Query::limit(25),
3465+
]);
3466+
$this->assertEquals(0, count($documents));
3467+
3468+
$documents = $database->find('schemaless_time', [
3469+
Query::createdBetween($recentPastDate, $nearFutureDate),
3470+
Query::limit(25),
3471+
]);
3472+
$count = count($documents);
3473+
3474+
$documents = $database->find('schemaless_time', [
3475+
Query::createdBetween($pastDate, $nearFutureDate),
3476+
Query::limit(25),
3477+
]);
3478+
$this->assertGreaterThanOrEqual($count, count($documents));
3479+
3480+
// --- updatedBetween ---
3481+
$documents = $database->find('schemaless_time', [
3482+
Query::updatedBetween($pastDate, $futureDate),
3483+
Query::limit(25),
3484+
]);
3485+
$this->assertGreaterThan(0, count($documents));
3486+
3487+
$documents = $database->find('schemaless_time', [
3488+
Query::updatedBetween($pastDate, $pastDate),
3489+
Query::limit(25),
3490+
]);
3491+
$this->assertEquals(0, count($documents));
3492+
3493+
$documents = $database->find('schemaless_time', [
3494+
Query::updatedBetween($recentPastDate, $nearFutureDate),
3495+
Query::limit(25),
3496+
]);
3497+
$count = count($documents);
3498+
3499+
$documents = $database->find('schemaless_time', [
3500+
Query::updatedBetween($pastDate, $nearFutureDate),
3501+
Query::limit(25),
3502+
]);
3503+
$this->assertGreaterThanOrEqual($count, count($documents));
3504+
3505+
$database->deleteCollection('schemaless_time');
3506+
}
32453507
}

0 commit comments

Comments
 (0)