diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c10feb8..fc10adb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Fix validation of unique values when the validated value is found as part of an existing value. [#21](https://github.com/GromNaN/laravel-mongodb/pull/21) by [@GromNaN](https://github.com/GromNaN). - Support `%` and `_` in `like` expression [#17](https://github.com/GromNaN/laravel-mongodb/pull/17) by [@GromNaN](https://github.com/GromNaN). - Change signature of `Query\Builder::__constructor` to match the parent class [#26](https://github.com/GromNaN/laravel-mongodb-private/pull/26) by [@GromNaN](https://github.com/GromNaN). +- Fix Query on `whereDate`, `whereDay`, `whereMonth`, `whereYear`, `whereTime` to use MongoDB operators [#2570](https://github.com/jenssegers/laravel-mongodb/pull/2376) by [@Davpyu](https://github.com/Davpyu) and [@GromNaN](https://github.com/GromNaN). ## [3.9.2] - 2022-09-01 diff --git a/src/Query/Builder.php b/src/Query/Builder.php index a65edd24a..682c70c19 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -8,6 +8,7 @@ use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; use Jenssegers\Mongodb\Connection; @@ -115,6 +116,7 @@ class Builder extends BaseBuilder * @var array */ protected $conversion = [ + '=' => 'eq', '!=' => 'ne', '<>' => 'ne', '<' => 'lt', @@ -1075,7 +1077,7 @@ protected function compileWhereBasic(array $where): array $operator = $operator === 'regex' ? '=' : 'not'; } - if (! isset($operator) || $operator == '=') { + if (! isset($operator) || $operator === '=' || $operator === 'eq') { $query = [$column => $value]; } else { $query = [$column => ['$'.$operator => $value]]; @@ -1180,12 +1182,35 @@ protected function compileWhereBetween(array $where): array */ protected function compileWhereDate(array $where): array { - extract($where); - - $where['operator'] = $operator; - $where['value'] = $value; + $startOfDay = new UTCDateTime(Carbon::parse($where['value'])->startOfDay()); + $endOfDay = new UTCDateTime(Carbon::parse($where['value'])->endOfDay()); - return $this->compileWhereBasic($where); + return match($where['operator']) { + 'eq', '=' => [ + $where['column'] => [ + '$gte' => $startOfDay, + '$lte' => $endOfDay, + ], + ], + 'ne' => [ + $where['column'] => [ + '$not' => [ + '$gte' => $startOfDay, + '$lte' => $endOfDay, + ], + ], + ], + 'lt', 'gte' => [ + $where['column'] => [ + '$'.$where['operator'] => $startOfDay, + ], + ], + 'gt', 'lte' => [ + $where['column'] => [ + '$'.$where['operator'] => $endOfDay, + ], + ], + }; } /** @@ -1194,12 +1219,16 @@ protected function compileWhereDate(array $where): array */ protected function compileWhereMonth(array $where): array { - extract($where); - - $where['operator'] = $operator; - $where['value'] = $value; - - return $this->compileWhereBasic($where); + return [ + '$expr' => [ + '$'.$where['operator'] => [ + [ + '$month' => '$'.$where['column'], + ], + (int) $where['value'], + ], + ], + ]; } /** @@ -1208,12 +1237,16 @@ protected function compileWhereMonth(array $where): array */ protected function compileWhereDay(array $where): array { - extract($where); - - $where['operator'] = $operator; - $where['value'] = $value; - - return $this->compileWhereBasic($where); + return [ + '$expr' => [ + '$'.$where['operator'] => [ + [ + '$dayOfMonth' => '$'.$where['column'], + ], + (int) $where['value'], + ], + ], + ]; } /** @@ -1222,12 +1255,16 @@ protected function compileWhereDay(array $where): array */ protected function compileWhereYear(array $where): array { - extract($where); - - $where['operator'] = $operator; - $where['value'] = $value; - - return $this->compileWhereBasic($where); + return [ + '$expr' => [ + '$'.$where['operator'] => [ + [ + '$year' => '$'.$where['column'], + ], + (int) $where['value'], + ], + ], + ]; } /** @@ -1236,12 +1273,26 @@ protected function compileWhereYear(array $where): array */ protected function compileWhereTime(array $where): array { - extract($where); + if (! is_string($where['value']) || ! preg_match('/^[0-2][0-9](:[0-6][0-9](:[0-6][0-9])?)?$/', $where['value'], $matches)) { + throw new \InvalidArgumentException(sprintf('Invalid time format, expected HH:MM:SS, HH:MM or HH, got "%s"', is_string($where['value']) ? $where['value'] : get_debug_type($where['value']))); + } - $where['operator'] = $operator; - $where['value'] = $value; + $format = match (count($matches)) { + 1 => '%H', + 2 => '%H:%M', + 3 => '%H:%M:%S', + }; - return $this->compileWhereBasic($where); + return [ + '$expr' => [ + '$'.$where['operator'] => [ + [ + '$dateToString' => ['date' => '$'.$where['column'], 'format' => $format], + ], + $where['value'], + ], + ], + ]; } /** diff --git a/tests/Models/Birthday.php b/tests/Models/Birthday.php index 2afca41e0..712d18d3f 100644 --- a/tests/Models/Birthday.php +++ b/tests/Models/Birthday.php @@ -11,14 +11,15 @@ * * @property string $name * @property string $birthday - * @property string $day - * @property string $month - * @property string $year * @property string $time */ class Birthday extends Eloquent { protected $connection = 'mongodb'; protected $collection = 'birthday'; - protected $fillable = ['name', 'birthday', 'day', 'month', 'year', 'time']; + protected $fillable = ['name', 'birthday']; + + protected $casts = [ + 'birthday' => 'datetime', + ]; } diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 60c05e23f..8e76840af 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -646,6 +646,170 @@ function (Builder $builder) { fn (Builder $builder) => $builder->where('name', 'not regex', '/^acme$/si'), ]; + yield 'where date' => [ + ['find' => [['created_at' => [ + '$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')), + '$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '2018-09-30'), + ]; + + yield 'where date DateTimeImmutable' => [ + ['find' => [['created_at' => [ + '$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')), + '$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +02:00')), + ]; + + yield 'where date !=' => [ + ['find' => [['created_at' => [ + '$not' => [ + '$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')), + '$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')), + ], + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '!=', '2018-09-30'), + ]; + + yield 'where date <' => [ + ['find' => [['created_at' => [ + '$lt' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '<', '2018-09-30'), + ]; + + yield 'where date >=' => [ + ['find' => [['created_at' => [ + '$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '>=', '2018-09-30'), + ]; + + yield 'where date >' => [ + ['find' => [['created_at' => [ + '$gt' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '>', '2018-09-30'), + ]; + + yield 'where date <=' => [ + ['find' => [['created_at' => [ + '$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')), + ]], []]], + fn (Builder $builder) => $builder->whereDate('created_at', '<=', '2018-09-30'), + ]; + + yield 'where day' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$dayOfMonth' => '$created_at'], + 5, + ], + ]], []]], + fn (Builder $builder) => $builder->whereDay('created_at', 5), + ]; + + yield 'where day > string' => [ + ['find' => [['$expr' => [ + '$gt' => [ + ['$dayOfMonth' => '$created_at'], + 5, + ], + ]], []]], + fn (Builder $builder) => $builder->whereDay('created_at', '>', '05'), + ]; + + yield 'where month' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$month' => '$created_at'], + 10, + ], + ]], []]], + fn (Builder $builder) => $builder->whereMonth('created_at', 10), + ]; + + yield 'where month > string' => [ + ['find' => [['$expr' => [ + '$gt' => [ + ['$month' => '$created_at'], + 5, + ], + ]], []]], + fn (Builder $builder) => $builder->whereMonth('created_at', '>', '05'), + ]; + + yield 'where year' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$year' => '$created_at'], + 2023, + ], + ]], []]], + fn (Builder $builder) => $builder->whereYear('created_at', 2023), + ]; + + yield 'where year > string' => [ + ['find' => [['$expr' => [ + '$gt' => [ + ['$year' => '$created_at'], + 2023, + ], + ]], []]], + fn (Builder $builder) => $builder->whereYear('created_at', '>', '2023'), + ]; + + yield 'where time HH:MM:SS' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']], + '10:11:12', + ], + ]], []]], + fn (Builder $builder) => $builder->whereTime('created_at', '10:11:12'), + ]; + + yield 'where time HH:MM' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M']], + '10:11', + ], + ]], []]], + fn (Builder $builder) => $builder->whereTime('created_at', '10:11'), + ]; + + yield 'where time HH' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$dateToString' => ['date' => '$created_at', 'format' => '%H']], + '10', + ], + ]], []]], + fn (Builder $builder) => $builder->whereTime('created_at', '10'), + ]; + + yield 'where time DateTime' => [ + ['find' => [['$expr' => [ + '$eq' => [ + ['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']], + '10:11:12', + ], + ]], []]], + fn (Builder $builder) => $builder->whereTime('created_at', new \DateTimeImmutable('2023-08-22 10:11:12')), + ]; + + yield 'where time >' => [ + ['find' => [['$expr' => [ + '$gt' => [ + ['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']], + '10:11:12', + ], + ]], []]], + fn (Builder $builder) => $builder->whereTime('created_at', '>', '10:11:12'), + ]; + /** @see DatabaseQueryBuilderTest::testBasicSelectDistinct */ yield 'distinct' => [ ['distinct' => ['foo', [], []]], @@ -774,6 +938,24 @@ public static function provideExceptions(): iterable 'Missing expected ending delimiter "/" in regular expression "/foo#bar"', fn (Builder $builder) => $builder->where('name', 'regex', '/foo#bar'), ]; + + yield 'whereTime with invalid time' => [ + \InvalidArgumentException::class, + 'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "10:11:12:13"', + fn (Builder $builder) => $builder->whereTime('created_at', '10:11:12:13'), + ]; + + yield 'whereTime out of range' => [ + \InvalidArgumentException::class, + 'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "23:70"', + fn (Builder $builder) => $builder->whereTime('created_at', '23:70'), + ]; + + yield 'whereTime invalid type' => [ + \InvalidArgumentException::class, + 'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "stdClass"', + fn (Builder $builder) => $builder->whereTime('created_at', new \stdClass()), + ]; } /** @dataProvider getEloquentMethodsNotSupported */ diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 754f204dc..8737a7d68 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -4,6 +4,7 @@ namespace Jenssegers\Mongodb\Tests; +use DateTimeImmutable; use Jenssegers\Mongodb\Tests\Models\Birthday; use Jenssegers\Mongodb\Tests\Models\Scoped; use Jenssegers\Mongodb\Tests\Models\User; @@ -24,12 +25,13 @@ public function setUp(): void User::create(['name' => 'Tommy Toe', 'age' => 33, 'title' => 'user']); User::create(['name' => 'Yvonne Yoe', 'age' => 35, 'title' => 'admin']); User::create(['name' => 'Error', 'age' => null, 'title' => null]); - Birthday::create(['name' => 'Mark Moe', 'birthday' => '2020-04-10', 'day' => '10', 'month' => '04', 'year' => '2020', 'time' => '10:53:11']); - Birthday::create(['name' => 'Jane Doe', 'birthday' => '2021-05-12', 'day' => '12', 'month' => '05', 'year' => '2021', 'time' => '10:53:12']); - Birthday::create(['name' => 'Harry Hoe', 'birthday' => '2021-05-11', 'day' => '11', 'month' => '05', 'year' => '2021', 'time' => '10:53:13']); - Birthday::create(['name' => 'Robert Doe', 'birthday' => '2021-05-12', 'day' => '12', 'month' => '05', 'year' => '2021', 'time' => '10:53:14']); - Birthday::create(['name' => 'Mark Moe', 'birthday' => '2021-05-12', 'day' => '12', 'month' => '05', 'year' => '2021', 'time' => '10:53:15']); - Birthday::create(['name' => 'Mark Moe', 'birthday' => '2022-05-12', 'day' => '12', 'month' => '05', 'year' => '2022', 'time' => '10:53:16']); + Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2020-04-10 10:53:11')]); + Birthday::create(['name' => 'Jane Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:12')]); + Birthday::create(['name' => 'Harry Hoe', 'birthday' => new DateTimeImmutable('2021-05-11 10:53:13')]); + Birthday::create(['name' => 'Robert Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:14')]); + Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:15')]); + Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2022-05-12 10:53:16')]); + Birthday::create(['name' => 'Boo']); } public function tearDown(): void @@ -204,45 +206,84 @@ public function testWhereDate(): void $birthdayCount = Birthday::whereDate('birthday', '2021-05-11')->get(); $this->assertCount(1, $birthdayCount); + + $birthdayCount = Birthday::whereDate('birthday', '>', '2021-05-11')->get(); + $this->assertCount(4, $birthdayCount); + + $birthdayCount = Birthday::whereDate('birthday', '>=', '2021-05-11')->get(); + $this->assertCount(5, $birthdayCount); + + $birthdayCount = Birthday::whereDate('birthday', '<', '2021-05-11')->get(); + $this->assertCount(1, $birthdayCount); + + $birthdayCount = Birthday::whereDate('birthday', '<=', '2021-05-11')->get(); + $this->assertCount(2, $birthdayCount); + + $birthdayCount = Birthday::whereDate('birthday', '<>', '2021-05-11')->get(); + $this->assertCount(6, $birthdayCount); } public function testWhereDay(): void { - $day = Birthday::whereDay('day', '12')->get(); + $day = Birthday::whereDay('birthday', '12')->get(); $this->assertCount(4, $day); - $day = Birthday::whereDay('day', '11')->get(); + $day = Birthday::whereDay('birthday', '11')->get(); $this->assertCount(1, $day); } public function testWhereMonth(): void { - $month = Birthday::whereMonth('month', '04')->get(); + $month = Birthday::whereMonth('birthday', '04')->get(); $this->assertCount(1, $month); - $month = Birthday::whereMonth('month', '05')->get(); + $month = Birthday::whereMonth('birthday', '05')->get(); + $this->assertCount(5, $month); + + $month = Birthday::whereMonth('birthday', '>=', '5')->get(); $this->assertCount(5, $month); + + $month = Birthday::whereMonth('birthday', '<', '10')->get(); + $this->assertCount(7, $month); + + $month = Birthday::whereMonth('birthday', '<>', '5')->get(); + $this->assertCount(2, $month); } public function testWhereYear(): void { - $year = Birthday::whereYear('year', '2021')->get(); + $year = Birthday::whereYear('birthday', '2021')->get(); $this->assertCount(4, $year); - $year = Birthday::whereYear('year', '2022')->get(); + $year = Birthday::whereYear('birthday', '2022')->get(); $this->assertCount(1, $year); - $year = Birthday::whereYear('year', '<', '2021')->get(); - $this->assertCount(1, $year); + $year = Birthday::whereYear('birthday', '<', '2021')->get(); + $this->assertCount(2, $year); + + $year = Birthday::whereYear('birthday', '<>', '2021')->get(); + $this->assertCount(3, $year); } public function testWhereTime(): void { - $time = Birthday::whereTime('time', '10:53:11')->get(); + $time = Birthday::whereTime('birthday', '10:53:11')->get(); $this->assertCount(1, $time); - $time = Birthday::whereTime('time', '>=', '10:53:14')->get(); + $time = Birthday::whereTime('birthday', '10:53')->get(); + $this->assertCount(6, $time); + + $time = Birthday::whereTime('birthday', '10')->get(); + $this->assertCount(6, $time); + + $time = Birthday::whereTime('birthday', '>=', '10:53:14')->get(); $this->assertCount(3, $time); + + $time = Birthday::whereTime('birthday', '!=', '10:53:14')->get(); + $this->assertCount(6, $time); + + $time = Birthday::whereTime('birthday', '<', '10:53:12')->get(); + $this->assertCount(2, $time); } public function testOrder(): void