From 3d269efd3bf094355b2de686c3d84c69d6aea8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 13 Jul 2023 11:56:35 +0200 Subject: [PATCH 1/6] PHPORM-49 Implement whereNot by encapsulating into $not --- src/Query/Builder.php | 13 +++ tests/Query/BuilderTest.php | 166 ++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index b6924bb..54669d8 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -1019,6 +1019,15 @@ protected function compileWheres(): array } } + $not = false; + if ($where['boolean'] === 'and not') { + $where['boolean'] = 'and'; + $not = true; + } elseif ($where['boolean'] === 'or not') { + $where['boolean'] = 'or'; + $not = true; + } + // The next item in a "chain" of wheres devices the boolean of the // first item. So if we see that there are multiple wheres, we will // use the operator of the next where. @@ -1030,6 +1039,10 @@ protected function compileWheres(): array $method = "compileWhere{$where['type']}"; $result = $this->{$method}($where); + if ($not) { + $result = ['$not' => $result]; + } + // Wrap the where with an $or operator. if ($where['boolean'] == 'or') { $result = ['$or' => [$result]]; diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index f600fa7..0c56642 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -50,6 +50,17 @@ public static function provideQueryBuilderToMql(): iterable fn (Builder $builder) => $builder->where('foo', 'bar'), ]; + yield 'where with single array of conditions' => [ + ['find' => [ + ['$and' => [ + ['foo' => 1], + ['bar' => 2], + ]], + [], // options + ]], + fn (Builder $builder) => $builder->where(['foo' => 1, 'bar' => 2]), + ]; + yield 'find > date' => [ ['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]], fn (Builder $builder) => $builder->where('foo', '>', $date), @@ -65,6 +76,161 @@ public static function provideQueryBuilderToMql(): iterable fn (Builder $builder) => $builder->limit(10)->offset(5)->select('foo', 'bar'), ]; + /** @see DatabaseQueryBuilderTest::testBasicWhereNot() */ + yield 'whereNot' => [ + ['find' => [ + ['$and' => [ + ['$not' => ['name' => 'foo']], + ['$not' => ['name' => ['$ne' => 'bar']]], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot('name', 'foo') + ->whereNot('name', '<>', 'bar'), + ]; + + /** @see DatabaseQueryBuilderTest::testBasicOrWheres() */ + yield 'where orWhere' => [ + ['find' => [ + ['$or' => [ + ['id' => 1], + ['email' => 'foo'], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->where('id', '=', 1) + ->orWhere('email', '=', 'foo'), + ]; + + /** @see DatabaseQueryBuilderTest::testBasicOrWhereNot() */ + yield 'orWhereNot' => [ + ['find' => [ + ['$or' => [ + ['$not' => ['name' => 'foo']], + ['$not' => ['name' => ['$ne' => 'bar']]], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->orWhereNot('name', 'foo') + ->orWhereNot('name', '<>', 'bar'), + ]; + + /** @see DatabaseQueryBuilderTest::testWhereNot() */ + yield 'whereNot callable' => [ + ['find' => [ + ['$not' => ['name' => 'foo']], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot(fn (Builder $q) => $q->where('name', 'foo')), + ]; + + yield 'where and whereNot' => [ + ['find' => [ + ['$and' => [ + ['name' => 'bar'], + ['$not' => ['email' => 'foo']], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->where('name', '=', 'bar') + ->whereNot(function (Builder $q) { + $q->where('email', '=', 'foo'); + }), + ]; + + yield 'nested whereNot' => [ + ['find' => [ + ['$not' => [ + '$and' => [ + ['name' => 'foo'], + ['$not' => ['email' => ['$ne' => 'bar']]], + ], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot(function (Builder $q) { + $q->where('name', '=', 'foo') + ->whereNot('email', '<>', 'bar'); + }), + ]; + + yield 'orWhere orWhereNot' => [ + ['find' => [ + ['$or' => [ + ['name' => 'bar'], + ['$not' => ['email' => 'foo']], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->orWhere('name', '=', 'bar') + ->orWhereNot(function (Builder $q) { + $q->where('email', '=', 'foo'); + }), + ]; + + yield 'where or whereNot' => [ + ['find' => [ + [ + '$and' => [['name' => 'bar']], + '$or' => [['$not' => ['email' => 'foo']]], + ], + [], // options + ]], + fn (Builder $builder) => $builder + ->where('name', '=', 'bar') + ->orWhereNot('email', '=', 'foo'), + ]; + + /** @see DatabaseQueryBuilderTest::testWhereNotWithArrayConditions() */ + yield 'whereNot with arrays of single condition' => [ + ['find' => [ + ['$not' => [ + '$and' => [ + ['foo' => 1], + ['bar' => 2], + ], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot([['foo', 1], ['bar', 2]]), + ]; + + yield 'whereNot with single array of conditions' => [ + ['find' => [ + ['$not' => [ + '$and' => [ + ['foo' => 1], + ['bar' => 2], + ], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot(['foo' => 1, 'bar' => 2]), + ]; + + yield 'whereNot with arrays of single condition with operator' => [ + ['find' => [ + ['$not' => [ + '$and' => [ + ['foo' => 1], + ['bar' => ['$lt' => 2]], + ], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot([['foo', 1], ['bar', '<', 2]]), + ]; + /** @see DatabaseQueryBuilderTest::testOrderBys() */ yield 'orderBy multiple columns' => [ ['find' => [[], ['sort' => ['email' => 1, 'age' => -1]]]], From 70022abcbc1326ebda558e68735e6d6650c5030c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 13 Jul 2023 13:54:25 +0200 Subject: [PATCH 2/6] Fix nested "or not" --- src/Query/Builder.php | 20 +++++++------------- tests/Query/BuilderTest.php | 32 ++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 54669d8..132ae0b 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -1019,32 +1019,26 @@ protected function compileWheres(): array } } - $not = false; - if ($where['boolean'] === 'and not') { - $where['boolean'] = 'and'; - $not = true; - } elseif ($where['boolean'] === 'or not') { - $where['boolean'] = 'or'; - $not = true; - } - // The next item in a "chain" of wheres devices the boolean of the // first item. So if we see that there are multiple wheres, we will // use the operator of the next where. - if ($i == 0 && count($wheres) > 1 && $where['boolean'] == 'and') { - $where['boolean'] = $wheres[$i + 1]['boolean']; + if ($i == 0 && count($wheres) > 1 + && str_starts_with($where['boolean'], 'and') + && str_starts_with($wheres[$i + 1]['boolean'], 'or') + ) { + $where['boolean'] = 'or'.(str_ends_with($where['boolean'], 'not') ? ' not' : ''); } // We use different methods to compile different wheres. $method = "compileWhere{$where['type']}"; $result = $this->{$method}($where); - if ($not) { + if (str_ends_with($where['boolean'], 'not')) { $result = ['$not' => $result]; } // Wrap the where with an $or operator. - if ($where['boolean'] == 'or') { + if (str_starts_with($where['boolean'], 'or')) { $result = ['$or' => [$result]]; } diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 0c56642..1612450 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -61,6 +61,30 @@ public static function provideQueryBuilderToMql(): iterable fn (Builder $builder) => $builder->where(['foo' => 1, 'bar' => 2]), ]; + yield 'nested orWhere and where' => [ + ['find' => [ + ['$and' => [ + ['$or' => [ + ['$and' => [ + ['foo' => 1], + ['$not' => ['bar' => 2]], + ]], + ]], + ['baz' => 3], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->where( + fn (Builder $q) => $q->orWhere( + fn (Builder $q) => $q + ->where('foo', 1) + ->whereNot('bar', 2) + ) + ) + ->where('baz', 3), + ]; + yield 'find > date' => [ ['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]], fn (Builder $builder) => $builder->where('foo', '>', $date), @@ -177,10 +201,10 @@ public static function provideQueryBuilderToMql(): iterable yield 'where or whereNot' => [ ['find' => [ - [ - '$and' => [['name' => 'bar']], - '$or' => [['$not' => ['email' => 'foo']]], - ], + ['$or' => [ + ['name' => 'bar'], + ['$not' => ['email' => 'foo']], + ]], [], // options ]], fn (Builder $builder) => $builder From e6cba45ef2d654a8be181660aa6541108907b56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 17 Jul 2023 13:50:41 +0200 Subject: [PATCH 3/6] Improve comment --- src/Query/Builder.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 132ae0b..4db2b5a 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -1019,9 +1019,9 @@ protected function compileWheres(): array } } - // The next item in a "chain" of wheres devices the boolean of the - // first item. So if we see that there are multiple wheres, we will - // use the operator of the next where. + // In a sequence of "where" clauses, the logical operator of the + // first "where" is determined by the 2nd "where". + // $where['boolean'] = "and", "or", "and not" or "or not" if ($i == 0 && count($wheres) > 1 && str_starts_with($where['boolean'], 'and') && str_starts_with($wheres[$i + 1]['boolean'], 'or') From df20bb6b36f5300013219f9128904d3b5aaab343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 19 Jul 2023 10:42:57 +0200 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Jeremy Mikola --- tests/Query/BuilderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 1612450..3fa617f 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -152,7 +152,7 @@ public static function provideQueryBuilderToMql(): iterable ->whereNot(fn (Builder $q) => $q->where('name', 'foo')), ]; - yield 'where and whereNot' => [ + yield 'where whereNot' => [ ['find' => [ ['$and' => [ ['name' => 'bar'], @@ -167,7 +167,7 @@ public static function provideQueryBuilderToMql(): iterable }), ]; - yield 'nested whereNot' => [ + yield 'whereNot (nested)' => [ ['find' => [ ['$not' => [ '$and' => [ @@ -199,7 +199,7 @@ public static function provideQueryBuilderToMql(): iterable }), ]; - yield 'where or whereNot' => [ + yield 'where orWhereNot' => [ ['find' => [ ['$or' => [ ['name' => 'bar'], From f7327312391cad3f9014a5f812952379d8cf7836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 19 Jul 2023 11:14:24 +0200 Subject: [PATCH 5/6] Review --- tests/Query/BuilderTest.php | 44 +++++++++++++++---------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 3fa617f..8c8a00c 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -61,30 +61,6 @@ public static function provideQueryBuilderToMql(): iterable fn (Builder $builder) => $builder->where(['foo' => 1, 'bar' => 2]), ]; - yield 'nested orWhere and where' => [ - ['find' => [ - ['$and' => [ - ['$or' => [ - ['$and' => [ - ['foo' => 1], - ['$not' => ['bar' => 2]], - ]], - ]], - ['baz' => 3], - ]], - [], // options - ]], - fn (Builder $builder) => $builder - ->where( - fn (Builder $q) => $q->orWhere( - fn (Builder $q) => $q - ->where('foo', 1) - ->whereNot('bar', 2) - ) - ) - ->where('baz', 3), - ]; - yield 'find > date' => [ ['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]], fn (Builder $builder) => $builder->where('foo', '>', $date), @@ -101,7 +77,7 @@ public static function provideQueryBuilderToMql(): iterable ]; /** @see DatabaseQueryBuilderTest::testBasicWhereNot() */ - yield 'whereNot' => [ + yield 'whereNot (multiple)' => [ ['find' => [ ['$and' => [ ['$not' => ['name' => 'foo']], @@ -142,6 +118,19 @@ public static function provideQueryBuilderToMql(): iterable ->orWhereNot('name', '<>', 'bar'), ]; + yield 'whereNot orWhere' => [ + ['find' => [ + ['$or' => [ + ['$not' => ['name' => 'foo']], + ['name' => ['$ne' => 'bar']], + ]], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot('name', 'foo') + ->orWhere('name', '<>', 'bar'), + ]; + /** @see DatabaseQueryBuilderTest::testWhereNot() */ yield 'whereNot callable' => [ ['find' => [ @@ -252,7 +241,10 @@ public static function provideQueryBuilderToMql(): iterable [], // options ]], fn (Builder $builder) => $builder - ->whereNot([['foo', 1], ['bar', '<', 2]]), + ->whereNot([ + ['foo', 1], + ['bar', '<', 2], + ]), ]; /** @see DatabaseQueryBuilderTest::testOrderBys() */ From a188374c9db1b7191e50469d14e842dfc2764616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 19 Jul 2023 12:07:00 +0200 Subject: [PATCH 6/6] Update docs --- CHANGELOG.md | 10 +++++++++- README.md | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1018c1..04b8238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,17 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Add classes to cast ObjectId and UUID instances [#1](https://github.com/GromNaN/laravel-mongodb-private/pull/1) by [@alcaeus](https://github.com/alcaeus). +- Add `Query\Builder::toMql()` to simplify comprehensive query tests [#6](https://github.com/GromNaN/laravel-mongodb-private/pull/6) by [@GromNaN](https://github.com/GromNaN). +- Fix `Query\Builder::whereNot` to use MongoDB [`$not`](https://www.mongodb.com/docs/manual/reference/operator/query/not/) operator [#13](https://github.com/GromNaN/laravel-mongodb-private/pull/13) by [@GromNaN](https://github.com/GromNaN). +- Fix `Query\Builder::whereBetween` to accept `Carbon\Period` object [#10](https://github.com/GromNaN/laravel-mongodb-private/pull/10) by [@GromNaN](https://github.com/GromNaN). +- Throw an exception for unsupported `Query\Builder` methods [#9](https://github.com/GromNaN/laravel-mongodb-private/pull/9) by [@GromNaN](https://github.com/GromNaN). +- Throw an exception when `Query\Builder::orderBy()` is used with invalid direction [#7](https://github.com/GromNaN/laravel-mongodb-private/pull/7) by [@GromNaN](https://github.com/GromNaN). +- Throw an exception when `Query\Builder::push()` is used incorrectly [#5](https://github.com/GromNaN/laravel-mongodb-private/pull/5) by [@GromNaN](https://github.com/GromNaN). + ## [3.9.2] - 2022-09-01 -### Addded +### Added - Add single word name mutators [#2438](https://github.com/jenssegers/laravel-mongodb/pull/2438) by [@RosemaryOrchard](https://github.com/RosemaryOrchard) & [@mrneatly](https://github.com/mrneatly). ### Fixed diff --git a/README.md b/README.md index 6a67525..71e7768 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,12 @@ $users = ->get(); ``` +**NOT statements** + +```php +$users = User::whereNot('age', '>', 18)->get(); +``` + **whereIn** ```php