Skip to content
This repository was archived by the owner on Aug 22, 2023. It is now read-only.

PHPORM-60 Fix Query on whereDate, whereDay, whereMonth, whereYear #28

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
- Remove `Query\Builder::whereAll($column, $values)`. Use `Query\Builder::where($column, 'all', $values)` instead. [#16](https://github.com/GromNaN/laravel-mongodb-private/pull/16) by [@GromNaN](https://github.com/GromNaN).
- Fix validation of unique values when the validated value is found as part of an existing value. [#21](https://github.com/GromNaN/laravel-mongodb-private/pull/21) by [@GromNaN](https://github.com/GromNaN).
- Support `%` and `_` in `like` expression [#17](https://github.com/GromNaN/laravel-mongodb-private/pull/17) by [@GromNaN](https://github.com/GromNaN).
- Fix Query on `whereDate`, `whereDay`, `whereMonth`, `whereYear` to use MongoDB operators [#2376](https://github.com/jenssegers/laravel-mongodb/pull/2376) by [@Davpyu](https://github.com/Davpyu)

## [3.9.2] - 2022-09-01

Expand Down
83 changes: 70 additions & 13 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 Illuminate\Support\Str;
Expand Down Expand Up @@ -116,6 +117,7 @@ class Builder extends BaseBuilder
* @var array
*/
protected $conversion = [
'=' => 'eq',
'!=' => 'ne',
'<>' => 'ne',
'<' => 'lt',
Expand Down Expand Up @@ -1084,7 +1086,7 @@ protected function compileWhereBasic(array $where): array
$operator = $operator === 'regex' ? '=' : 'not';
}

if (! isset($operator) || $operator == '=') {
if (! isset($operator) || $operator === '=' || $operator === 'eq') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could result in a subtle behavioral change when using MongoDB\BSON\Regex objects. Quoting $eq docs:

Specifying the $eq
operator is equivalent to using the form { field: } except when the is a regular expression. See below for examples.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ field: { eq: Regex } } is not supported ATM as it will always end to { field: Regex } for matching the regex. I don't think storing Regex in database is common enough to fix it know. PHPORM-74

$query = [$column => $value];
} else {
$query = [$column => ['$'.$operator => $value]];
Expand Down Expand Up @@ -1191,10 +1193,41 @@ protected function compileWhereDate(array $where): array
{
extract($where);

$where['operator'] = $operator;
$where['value'] = $value;
$startOfDay = new UTCDateTime(Carbon::parse($value)->startOfDay());
$endOfDay = new UTCDateTime(Carbon::parse($value)->endOfDay());

return $this->compileWhereBasic($where);
return match($operator) {
'eq', '=' => [
$column => [
'$gte' => $startOfDay,
'$lte' => $endOfDay,
],
],
'ne' => [
'$or' => [
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this alternative, but got segfaults.

'ne' => ['$not' => [
    $column => [
        '$gte' => $startOfDay,
        '$lte' => $endOfDay,
    ],
]],

Copy link
Collaborator

@jmikola jmikola Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got segfaults

Segfaults in mongod (executing the query) or PHP (executing this match construct)?

I'm trying to think about how this would trigger a segfault in PHP. The only thing I can think of is that $column is referenced at multiple levels of the same return value. If so, that seems like something you might be able to isolate in a PHPT test. I'd be happy to dive into it further if so.

If this is actually a mongod segfault, I assume it'd be reproducible in the shell and would warrant a new SERVER ticket.

[
$column => [
'$lt' => $startOfDay,
],
],
[
$column => [
'$gt' => $endOfDay,
],
],
],
],
'lt', 'gte' => [
$column => [
'$'.$operator => $startOfDay,
],
],
'gt', 'lte' => [
$column => [
'$'.$operator => $endOfDay,
],
],
};
}

/**
Expand All @@ -1205,10 +1238,18 @@ protected function compileWhereMonth(array $where): array
{
extract($where);

$where['operator'] = $operator;
$where['value'] = $value;
$value = (int) ltrim($value, '0');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ltrim() necessary to avoid interpreting a leading zero as octal notation? I would only expect that to apply to integer literals.

In my local testing with a PHP 8.2 REPL, the integer cast seems to ignore leading zeroes.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, I blindly adopted the PR code without even testing it. Fixed.


return $this->compileWhereBasic($where);
return [
'$expr' => [
'$'.$operator => [
[
'$month' => '$'.$column,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume $column is created by extract($where). Is there any enforcement on that? It seems hard to track given we have no typing/shape information for the $where array.

Is it possible for $column to be a field path (e.g. foo.bar), or would it only be a top-level field name?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing all this extract and replacing with array access would be more verbose but a lot more explicit.

],
$value,
],
],
];
}

/**
Expand All @@ -1219,10 +1260,18 @@ protected function compileWhereDay(array $where): array
{
extract($where);

$where['operator'] = $operator;
$where['value'] = $value;
$value = (int) ltrim($value, '0');

return $this->compileWhereBasic($where);
return [
'$expr' => [
'$'.$operator => [
[
'$dayOfMonth' => '$'.$column,
],
$value,
],
],
];
}

/**
Expand All @@ -1233,10 +1282,18 @@ protected function compileWhereYear(array $where): array
{
extract($where);

$where['operator'] = $operator;
$where['value'] = $value;
$value = (int) $value;

return $this->compileWhereBasic($where);
return [
'$expr' => [
'$'.$operator => [
[
'$year' => '$'.$column,
],
$value,
],
],
];
}

/**
Expand Down
9 changes: 5 additions & 4 deletions tests/Models/Birthday.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'time'];

protected $casts = [
'birthday' => 'datetime',
];
}
112 changes: 112 additions & 0 deletions tests/Query/BuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,118 @@ 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' => [['$or' => [
['created_at' => ['$lt' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00'))]],
['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' => [
'$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'),
];

/** @see DatabaseQueryBuilderTest::testBasicSelectDistinct */
yield 'distinct' => [
['distinct' => ['foo', [], []]],
Expand Down
57 changes: 43 additions & 14 deletions tests/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'), 'time' => '10:53:11']);
Birthday::create(['name' => 'Jane Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:12'), 'time' => '10:53:12']);
Birthday::create(['name' => 'Harry Hoe', 'birthday' => new DateTimeImmutable('2021-05-11 10:53:13'), 'time' => '10:53:13']);
Birthday::create(['name' => 'Robert Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:14'), 'time' => '10:53:14']);
Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:15'), 'time' => '10:53:15']);
Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2022-05-12 10:53:16'), 'time' => '10:53:16']);
Birthday::create(['name' => 'Boo']);
Copy link
Owner Author

@GromNaN GromNaN Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a document without birthday field to test the behavior.

}

public function tearDown(): void
Expand Down Expand Up @@ -204,36 +206,63 @@ 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(5, $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
Expand Down