From 343eb2c8a379217a81baf4d7f86df53bcf55678b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 16 Feb 2026 16:32:06 +0530 Subject: [PATCH 1/5] Normalize extended ISO 8601 datetime strings in query values for MongoDB compatibility --- src/Database/Adapter/Mongo.php | 14 ++ tests/e2e/Adapter/Scopes/SchemalessTests.php | 128 +++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index c8f45b7e8..4e3f62cfe 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2473,6 +2473,20 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr */ protected function buildFilter(Query $query): array { + // Normalize extended ISO 8601 datetime strings in query values to UTCDateTime + // so they can be correctly compared against datetime fields stored in MongoDB. + $values = $query->getValues(); + foreach ($values as $k => $value) { + if (is_string($value) && $this->isExtendedISODatetime($value)) { + try { + $values[$k] = $this->toMongoDatetime($value); + } catch (\Throwable $th) { + // Leave value as-is if it cannot be parsed as a datetime + } + } + } + $query->setValues($values); + if ($query->getAttribute() === '$id') { $query->setAttribute('_uid'); } elseif ($query->getAttribute() === '$sequence') { diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 1a96359c4..bf836398a 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -3242,4 +3242,132 @@ public function testSchemalessMongoDotNotationIndexes(): void $database->deleteCollection($col); } + + public function testQueryWithDatetime(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $col = uniqid('sl_query_datetime'); + $database->createCollection($col); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + // Documents with datetime field (ISO 8601) for query tests + // Dates: Jan 15 2024, Feb 20 2024, Mar 25 2024, Jun 15 2024, Dec 31 2024 + $docs = [ + new Document([ + '$id' => 'dt1', + '$permissions' => $permissions, + 'name' => 'January', + 'datetime' => '2024-01-15T10:30:00.000+00:00' + ]), + new Document([ + '$id' => 'dt2', + '$permissions' => $permissions, + 'name' => 'February', + 'datetime' => '2024-02-20T14:45:30.123Z' + ]), + new Document([ + '$id' => 'dt3', + '$permissions' => $permissions, + 'name' => 'March', + // Use a valid extended ISO 8601 datetime that will be normalized + // to MongoDB UTCDateTime for comparison queries. + 'datetime' => '2024-03-25T08:15:45.000+00:00' + ]), + new Document([ + '$id' => 'dt4', + '$permissions' => $permissions, + 'name' => 'June', + 'datetime' => '2024-06-15T12:00:00.000Z' + ]), + new Document([ + '$id' => 'dt5', + '$permissions' => $permissions, + 'name' => 'December', + 'datetime' => '2024-12-31T23:59:59.999+00:00' + ]), + ]; + + $createdCount = $database->createDocuments($col, $docs); + $this->assertEquals(5, $createdCount); + + // Query: equal - find document with exact datetime (Jan 15 2024) + $equalResults = $database->find($col, [ + Query::equal('datetime', ['2024-01-15T10:30:00.000+00:00']) + ]); + $this->assertCount(1, $equalResults); + $this->assertEquals('dt1', $equalResults[0]->getId()); + $this->assertEquals('January', $equalResults[0]->getAttribute('name')); + + // Query: greaterThan - datetimes after 2024-03-01 (dt3, dt4, dt5) + $greaterResults = $database->find($col, [ + Query::greaterThan('datetime', '2024-03-01T00:00:00.000Z') + ]); + $this->assertCount(3, $greaterResults); + $greaterIds = array_map(fn ($d) => $d->getId(), $greaterResults); + $this->assertContains('dt3', $greaterIds); + $this->assertContains('dt4', $greaterIds); + $this->assertContains('dt5', $greaterIds); + + // Query: lessThan - datetimes before 2024-03-01 (dt1, dt2) + $lessResults = $database->find($col, [ + Query::lessThan('datetime', '2024-03-01T00:00:00.000Z') + ]); + $this->assertCount(2, $lessResults); + $lessIds = array_map(fn ($d) => $d->getId(), $lessResults); + $this->assertContains('dt1', $lessIds); + $this->assertContains('dt2', $lessIds); + + // Query: greaterThanEqual - datetimes on or after 2024-02-20 (dt2, dt3, dt4, dt5) + $gteResults = $database->find($col, [ + Query::greaterThanEqual('datetime', '2024-02-20T14:45:30.123Z') + ]); + $this->assertCount(4, $gteResults); + $gteIds = array_map(fn ($d) => $d->getId(), $gteResults); + $this->assertContains('dt2', $gteIds); + $this->assertContains('dt3', $gteIds); + $this->assertContains('dt4', $gteIds); + $this->assertContains('dt5', $gteIds); + + // Query: lessThanEqual - datetimes on or before 2024-06-15 (dt1, dt2, dt3, dt4) + $lteResults = $database->find($col, [ + Query::lessThanEqual('datetime', '2024-06-15T12:00:00.000Z') + ]); + $this->assertCount(4, $lteResults); + $lteIds = array_map(fn ($d) => $d->getId(), $lteResults); + $this->assertContains('dt1', $lteIds); + $this->assertContains('dt2', $lteIds); + $this->assertContains('dt3', $lteIds); + $this->assertContains('dt4', $lteIds); + + // Query: between - datetimes in range [2024-02-01, 2024-07-01) (dt2, dt3, dt4) + $betweenResults = $database->find($col, [ + Query::between('datetime', '2024-02-01T00:00:00.000Z', '2024-07-01T00:00:00.000Z') + ]); + $this->assertCount(3, $betweenResults); + $betweenIds = array_map(fn ($d) => $d->getId(), $betweenResults); + $this->assertContains('dt2', $betweenIds); + $this->assertContains('dt3', $betweenIds); + $this->assertContains('dt4', $betweenIds); + + // Query: equal with no match + $noneResults = $database->find($col, [ + Query::equal('datetime', ['2020-01-01T00:00:00.000Z']) + ]); + $this->assertCount(0, $noneResults); + + // $database->deleteCollection($col); + } } From a7b932060577ec2d332cefa4971721ffca68f4b7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 16 Feb 2026 16:51:22 +0530 Subject: [PATCH 2/5] added delete collection --- tests/e2e/Adapter/Scopes/SchemalessTests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index bf836398a..5c94ed95b 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -3368,6 +3368,6 @@ public function testQueryWithDatetime(): void ]); $this->assertCount(0, $noneResults); - // $database->deleteCollection($col); + $database->deleteCollection($col); } } From fb401cd17f3b62abd3b3700b9b6fc64603a82bd7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 16 Feb 2026 17:13:46 +0530 Subject: [PATCH 3/5] added query support check --- src/Database/Adapter/Mongo.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 4e3f62cfe..383628126 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2475,17 +2475,19 @@ protected function buildFilter(Query $query): array { // Normalize extended ISO 8601 datetime strings in query values to UTCDateTime // so they can be correctly compared against datetime fields stored in MongoDB. - $values = $query->getValues(); - foreach ($values as $k => $value) { - if (is_string($value) && $this->isExtendedISODatetime($value)) { - try { - $values[$k] = $this->toMongoDatetime($value); - } catch (\Throwable $th) { - // Leave value as-is if it cannot be parsed as a datetime + if (!$this->getSupportForAttributes()) { + $values = $query->getValues(); + foreach ($values as $k => $value) { + if (is_string($value) && $this->isExtendedISODatetime($value)) { + try { + $values[$k] = $this->toMongoDatetime($value); + } catch (\Throwable $th) { + // Leave value as-is if it cannot be parsed as a datetime + } } } + $query->setValues($values); } - $query->setValues($values); if ($query->getAttribute() === '$id') { $query->setAttribute('_uid'); From c040345dad270ec478110f1b6f303218daf9e1cf Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 16 Feb 2026 17:39:56 +0530 Subject: [PATCH 4/5] updated tests --- src/Database/Adapter/Mongo.php | 2 +- tests/e2e/Adapter/Scopes/SchemalessTests.php | 134 +++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 383628126..733053a36 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2475,7 +2475,7 @@ protected function buildFilter(Query $query): array { // Normalize extended ISO 8601 datetime strings in query values to UTCDateTime // so they can be correctly compared against datetime fields stored in MongoDB. - if (!$this->getSupportForAttributes()) { + if (!$this->getSupportForAttributes() || \in_array($query->getAttribute(), ['$createdAt', '$updatedAt'], true)) { $values = $query->getValues(); foreach ($values as $k => $value) { if (is_string($value) && $this->isExtendedISODatetime($value)) { diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 5c94ed95b..551d37a22 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -3370,4 +3370,138 @@ public function testQueryWithDatetime(): void $database->deleteCollection($col); } + + public function testSchemalessCreatedAndUpdatedAtQuery(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Create a simple schemaless collection and one document. + $database->createCollection('movies', permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ]); + + $database->createDocument('movies', new Document([ + '$id' => ID::unique(), + '$permissions' => [Permission::read(Role::any())], + 'name' => 'Schemaless Movie', + ])); + + $futureDate = '2050-01-01T00:00:00.000Z'; + $pastDate = '1900-01-01T00:00:00.000Z'; + $recentPastDate = '2020-01-01T00:00:00.000Z'; + $nearFutureDate = '2025-01-01T00:00:00.000Z'; + + // --- createdBefore --- + $documents = $database->find('movies', [ + Query::createdBefore($futureDate), + Query::limit(1), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::createdBefore($pastDate), + Query::limit(1), + ]); + $this->assertEquals(0, count($documents)); + + // --- createdAfter --- + $documents = $database->find('movies', [ + Query::createdAfter($pastDate), + Query::limit(1), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::createdAfter($futureDate), + Query::limit(1), + ]); + $this->assertEquals(0, count($documents)); + + // --- updatedBefore --- + $documents = $database->find('movies', [ + Query::updatedBefore($futureDate), + Query::limit(1), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::updatedBefore($pastDate), + Query::limit(1), + ]); + $this->assertEquals(0, count($documents)); + + // --- updatedAfter --- + $documents = $database->find('movies', [ + Query::updatedAfter($pastDate), + Query::limit(1), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::updatedAfter($futureDate), + Query::limit(1), + ]); + $this->assertEquals(0, count($documents)); + + // --- createdBetween --- + $documents = $database->find('movies', [ + Query::createdBetween($pastDate, $futureDate), + Query::limit(25), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::createdBetween($pastDate, $pastDate), + Query::limit(25), + ]); + $this->assertEquals(0, count($documents)); + + $documents = $database->find('movies', [ + Query::createdBetween($recentPastDate, $nearFutureDate), + Query::limit(25), + ]); + $count = count($documents); + + $documents = $database->find('movies', [ + Query::createdBetween($pastDate, $nearFutureDate), + Query::limit(25), + ]); + $this->assertGreaterThanOrEqual($count, count($documents)); + + // --- updatedBetween --- + $documents = $database->find('movies', [ + Query::updatedBetween($pastDate, $futureDate), + Query::limit(25), + ]); + $this->assertGreaterThan(0, count($documents)); + + $documents = $database->find('movies', [ + Query::updatedBetween($pastDate, $pastDate), + Query::limit(25), + ]); + $this->assertEquals(0, count($documents)); + + $documents = $database->find('movies', [ + Query::updatedBetween($recentPastDate, $nearFutureDate), + Query::limit(25), + ]); + $count = count($documents); + + $documents = $database->find('movies', [ + Query::updatedBetween($pastDate, $nearFutureDate), + Query::limit(25), + ]); + $this->assertGreaterThanOrEqual($count, count($documents)); + + $database->deleteCollection('movies'); + } } From 11971515acab75921a2205350d79e63a408bbf3b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 16 Feb 2026 17:45:32 +0530 Subject: [PATCH 5/5] updated collection name --- tests/e2e/Adapter/Scopes/SchemalessTests.php | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 551d37a22..9f8d150bf 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -3382,14 +3382,14 @@ public function testSchemalessCreatedAndUpdatedAtQuery(): void } // Create a simple schemaless collection and one document. - $database->createCollection('movies', permissions: [ + $database->createCollection('schemaless_time', permissions: [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ]); - $database->createDocument('movies', new Document([ + $database->createDocument('schemaless_time', new Document([ '$id' => ID::unique(), '$permissions' => [Permission::read(Role::any())], 'name' => 'Schemaless Movie', @@ -3401,107 +3401,107 @@ public function testSchemalessCreatedAndUpdatedAtQuery(): void $nearFutureDate = '2025-01-01T00:00:00.000Z'; // --- createdBefore --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBefore($futureDate), Query::limit(1), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBefore($pastDate), Query::limit(1), ]); $this->assertEquals(0, count($documents)); // --- createdAfter --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdAfter($pastDate), Query::limit(1), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdAfter($futureDate), Query::limit(1), ]); $this->assertEquals(0, count($documents)); // --- updatedBefore --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBefore($futureDate), Query::limit(1), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBefore($pastDate), Query::limit(1), ]); $this->assertEquals(0, count($documents)); // --- updatedAfter --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedAfter($pastDate), Query::limit(1), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedAfter($futureDate), Query::limit(1), ]); $this->assertEquals(0, count($documents)); // --- createdBetween --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBetween($pastDate, $futureDate), Query::limit(25), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBetween($pastDate, $pastDate), Query::limit(25), ]); $this->assertEquals(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBetween($recentPastDate, $nearFutureDate), Query::limit(25), ]); $count = count($documents); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::createdBetween($pastDate, $nearFutureDate), Query::limit(25), ]); $this->assertGreaterThanOrEqual($count, count($documents)); // --- updatedBetween --- - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBetween($pastDate, $futureDate), Query::limit(25), ]); $this->assertGreaterThan(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBetween($pastDate, $pastDate), Query::limit(25), ]); $this->assertEquals(0, count($documents)); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBetween($recentPastDate, $nearFutureDate), Query::limit(25), ]); $count = count($documents); - $documents = $database->find('movies', [ + $documents = $database->find('schemaless_time', [ Query::updatedBetween($pastDate, $nearFutureDate), Query::limit(25), ]); $this->assertGreaterThanOrEqual($count, count($documents)); - $database->deleteCollection('movies'); + $database->deleteCollection('schemaless_time'); } }