diff --git a/CHANGELOG.md b/CHANGELOG.md index b1018c1cb..04b823816 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 6a6752575..71e7768e5 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,12 @@ $users = ->get(); ``` +**NOT statements** + +```php +$users = User::whereNot('age', '>', 18)->get(); +``` + **whereIn** ```php diff --git a/src/Query/Builder.php b/src/Query/Builder.php index b6924bb47..4db2b5a91 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -1019,19 +1019,26 @@ 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. - if ($i == 0 && count($wheres) > 1 && $where['boolean'] == 'and') { - $where['boolean'] = $wheres[$i + 1]['boolean']; + // 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') + ) { + $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 (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 f600fa73a..8c8a00c50 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,177 @@ public static function provideQueryBuilderToMql(): iterable fn (Builder $builder) => $builder->limit(10)->offset(5)->select('foo', 'bar'), ]; + /** @see DatabaseQueryBuilderTest::testBasicWhereNot() */ + yield 'whereNot (multiple)' => [ + ['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'), + ]; + + 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' => [ + ['$not' => ['name' => 'foo']], + [], // options + ]], + fn (Builder $builder) => $builder + ->whereNot(fn (Builder $q) => $q->where('name', 'foo')), + ]; + + yield 'where 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 'whereNot (nested)' => [ + ['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 orWhereNot' => [ + ['find' => [ + ['$or' => [ + ['name' => 'bar'], + ['$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]]]],