Skip to content

Commit bbfd062

Browse files
committed
Restore find query
1 parent 2aa92f9 commit bbfd062

File tree

5 files changed

+739
-242
lines changed

5 files changed

+739
-242
lines changed

src/Query/Builder.php

Lines changed: 138 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use MongoDB\BSON\Regex;
2323
use MongoDB\BSON\UTCDateTime;
2424
use MongoDB\Builder\Accumulator;
25-
use MongoDB\Builder\BuilderEncoder;
2625
use MongoDB\Builder\Expression\FieldPath;
2726
use MongoDB\Builder\Variable;
2827
use MongoDB\Driver\Cursor;
@@ -87,7 +86,7 @@ class Builder extends BaseBuilder
8786
*
8887
* @var array
8988
*/
90-
public $projections;
89+
public $projections = [];
9190

9291
/**
9392
* The maximum amount of seconds to allow the query to run.
@@ -283,23 +282,73 @@ public function dump(mixed ...$args)
283282
return $this;
284283
}
285284

286-
protected function getPipelineBuilder(): PipelineBuilder
285+
private function getPipelineBuilder(): PipelineBuilder
287286
{
288-
$columns = $this->columns ?? [];
289-
290287
$pipelineBuilder = new PipelineBuilder([], $this->collection, $this->options);
291288

289+
$wheres = $this->compileWheres();
290+
291+
if (count($wheres)) {
292+
$pipelineBuilder->match(...$wheres);
293+
}
294+
295+
// Distinct query
296+
if ($this->distinct) {
297+
// Return distinct results directly
298+
$column = $columns[0] ?? '_id';
299+
300+
$pipelineBuilder->group(
301+
_id: \MongoDB\Builder\Expression::fieldPath($column),
302+
_document: Accumulator::first(Variable::root()),
303+
);
304+
$pipelineBuilder->replaceRoot(
305+
newRoot: new FieldPath('_document'),
306+
);
307+
}
308+
309+
if ($this->orders) {
310+
$pipelineBuilder->sort(...$this->orders);
311+
}
312+
313+
if ($this->offset) {
314+
$pipelineBuilder->skip($this->offset);
315+
}
316+
317+
if ($this->limit) {
318+
$pipelineBuilder->limit($this->limit);
319+
}
320+
321+
// Normal query
322+
// Add custom projections.
323+
if ($this->projections || $this->columns) {
324+
$columns = in_array('*', $this->columns) ? [] : $this->columns;
325+
$projection = array_fill_keys($columns, true) + $this->projections;
326+
if ($projection) {
327+
$pipelineBuilder->project(...$projection);
328+
}
329+
}
330+
331+
return $pipelineBuilder;
332+
}
333+
334+
/**
335+
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
336+
*
337+
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
338+
*
339+
* @return array<string, mixed[]>
340+
*/
341+
public function toMql(): array
342+
{
343+
$columns = $this->columns ?? [];
344+
292345
// Drop all columns if * is present, MongoDB does not work this way.
293346
if (in_array('*', $columns)) {
294347
$columns = [];
295348
}
296349

297350
$wheres = $this->compileWheres();
298351

299-
if (count($wheres)) {
300-
$pipelineBuilder->match(...$wheres);
301-
}
302-
303352
// Use MongoDB's aggregation framework when using grouping or aggregation functions.
304353
if ($this->groups || $this->aggregate) {
305354
$group = [];
@@ -359,59 +408,60 @@ protected function getPipelineBuilder(): PipelineBuilder
359408
$group['_id'] = null;
360409
}
361410

411+
// Build the aggregation pipeline.
412+
$pipeline = [];
413+
if ($wheres) {
414+
$pipeline[] = ['$match' => $wheres];
415+
}
416+
362417
// apply unwinds for subdocument array aggregation
363418
foreach ($unwinds as $unwind) {
364-
$pipelineBuilder->unwind($unwind);
419+
$pipeline[] = ['$unwind' => '$' . $unwind];
365420
}
366421

367422
if ($group) {
368-
$pipelineBuilder->group(...$group);
423+
$pipeline[] = ['$group' => $group];
369424
}
370425

371426
// Apply order and limit
372427
if ($this->orders) {
373-
$pipelineBuilder->sort($this->orders);
428+
$pipeline[] = ['$sort' => $this->orders];
374429
}
375430

376431
if ($this->offset) {
377-
$pipelineBuilder->skip($this->offset);
432+
$pipeline[] = ['$skip' => $this->offset];
378433
}
379434

380435
if ($this->limit) {
381-
$pipelineBuilder->limit($this->limit);
436+
$pipeline[] = ['$limit' => $this->limit];
382437
}
383438

384439
if ($this->projections) {
385-
$pipelineBuilder->project(...$this->projections);
440+
$pipeline[] = ['$project' => $this->projections];
441+
}
442+
443+
$options = [
444+
'typeMap' => ['root' => 'array', 'document' => 'array'],
445+
];
446+
447+
// Add custom query options
448+
if (count($this->options)) {
449+
$options = array_merge($options, $this->options);
386450
}
387451

388-
return $pipelineBuilder;
452+
$options = $this->inheritConnectionOptions($options);
453+
454+
return ['aggregate' => [$pipeline, $options]];
389455
}
390456

391457
// Distinct query
392458
if ($this->distinct) {
393459
// Return distinct results directly
394460
$column = $columns[0] ?? '_id';
395461

396-
$pipelineBuilder->group(
397-
_id: \MongoDB\Builder\Expression::fieldPath($column),
398-
_document: Accumulator::first(Variable::root()),
399-
);
400-
$pipelineBuilder->replaceRoot(
401-
newRoot: new FieldPath('_document'),
402-
);
403-
}
404-
405-
if ($this->orders) {
406-
$pipelineBuilder->sort(...$this->orders);
407-
}
408-
409-
if ($this->offset) {
410-
$pipelineBuilder->skip($this->offset);
411-
}
462+
$options = $this->inheritConnectionOptions();
412463

413-
if ($this->limit) {
414-
$pipelineBuilder->limit($this->limit);
464+
return ['distinct' => [$column, $wheres, $options]];
415465
}
416466

417467
// Normal query
@@ -423,39 +473,44 @@ protected function getPipelineBuilder(): PipelineBuilder
423473
$projection = array_merge($projection, $this->projections);
424474
}
425475

426-
if ($projection) {
427-
$pipelineBuilder->project(...$projection);
428-
}
476+
$options = [];
429477

430-
return $pipelineBuilder;
431-
}
478+
// Apply order, offset, limit and projection
479+
if ($this->timeout) {
480+
$options['maxTimeMS'] = $this->timeout * 1000;
481+
}
432482

433-
/**
434-
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
435-
*
436-
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
437-
*
438-
* @return array<string, mixed[]>
439-
*/
440-
public function toMql(): array
441-
{
442-
$encoder = new BuilderEncoder();
443-
$pipeline = $encoder->encode($this->getPipelineBuilder()->getPipeline());
483+
if ($this->orders) {
484+
$options['sort'] = $this->orders;
485+
}
444486

445-
$options = ['typeMap' => ['root' => 'array', 'document' => 'array']];
487+
if ($this->offset) {
488+
$options['skip'] = $this->offset;
489+
}
446490

447-
if ($this->timeout) {
448-
$options['maxTimeMS'] = $this->timeout * 1000;
491+
if ($this->limit) {
492+
$options['limit'] = $this->limit;
449493
}
450494

451495
if ($this->hint) {
452496
$options['hint'] = $this->hint;
453497
}
454498

455-
$options = array_merge($options, $this->options);
499+
if ($projection) {
500+
$options['projection'] = $projection;
501+
}
502+
503+
// Fix for legacy support, converts the results to arrays instead of objects.
504+
$options['typeMap'] = ['root' => 'array', 'document' => 'array'];
505+
506+
// Add custom query options
507+
if (count($this->options)) {
508+
$options = array_merge($options, $this->options);
509+
}
510+
456511
$options = $this->inheritConnectionOptions($options);
457512

458-
return ['aggregate' => [$pipeline, $options]];
513+
return ['find' => [$wheres, $options]];
459514
}
460515

461516
/**
@@ -538,37 +593,32 @@ public function generateCacheKey()
538593
/** @return ($function === null ? PipelineBuilder : self) */
539594
public function aggregate($function = null, $columns = [])
540595
{
541-
$builder = $this->getPipelineBuilder();
542-
543596
if ($function === null) {
544-
return $builder;
545-
}
546-
547-
match ($function) {
548-
'count' => $builder->group(
549-
_id: null,
550-
aggregate: Accumulator::sum(1),
551-
),
552-
'sum' => $builder->group(
553-
_id: null,
554-
aggregate: Accumulator::sum(\MongoDB\Builder\Expression::fieldPath($columns[0])),
555-
),
556-
'avg' => $builder->group(
557-
_id: null,
558-
aggregate: Accumulator::avg(\MongoDB\Builder\Expression::fieldPath($columns[0])),
559-
),
560-
'min' => $builder->group(
561-
_id: null,
562-
aggregate: Accumulator::min(\MongoDB\Builder\Expression::fieldPath($columns[0])),
563-
),
564-
'max' => $builder->group(
565-
_id: null,
566-
aggregate: Accumulator::max(\MongoDB\Builder\Expression::fieldPath($columns[0])),
567-
),
568-
default => throw new InvalidArgumentException('Unknown aggregate function: ' . $function),
569-
};
597+
return $this->getPipelineBuilder();
598+
}
599+
600+
$this->aggregate = [
601+
'function' => $function,
602+
'columns' => $columns,
603+
];
604+
605+
$previousColumns = $this->columns;
570606

571-
$results = $builder->get();
607+
// We will also back up the select bindings since the select clause will be
608+
// removed when performing the aggregate function. Once the query is run
609+
// we will add the bindings back onto this query so they can get used.
610+
$previousSelectBindings = $this->bindings['select'];
611+
612+
$this->bindings['select'] = [];
613+
614+
$results = $this->get($columns);
615+
616+
// Once we have executed the query, we will reset the aggregate property so
617+
// that more select queries can be executed against the database without
618+
// the aggregate value getting in the way when the grammar builds it.
619+
$this->aggregate = null;
620+
$this->columns = $previousColumns;
621+
$this->bindings['select'] = $previousSelectBindings;
572622

573623
if (isset($results[0])) {
574624
$result = (array) $results[0];
@@ -577,19 +627,6 @@ public function aggregate($function = null, $columns = [])
577627
}
578628
}
579629

580-
public function count($columns = '*'): int
581-
{
582-
if ($columns !== '*') {
583-
// @todo trigger warning, $columns is ignored
584-
}
585-
586-
return $this
587-
->aggregate()
588-
->count('aggregate')
589-
->get()
590-
->value('aggregate', 0);
591-
}
592-
593630
/** @inheritdoc */
594631
public function exists()
595632
{
@@ -963,14 +1000,14 @@ public function runPaginationCountQuery($columns = ['*'])
9631000
if ($this->groups || $this->havings) {
9641001
$without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
9651002

966-
$pipelienBuilder = $this->cloneWithout($without)
1003+
$mql = $this->cloneWithout($without)
9671004
->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
968-
->getPipelineBuilder();
1005+
->toMql();
9691006

9701007
// Adds the $count stage to the pipeline
971-
$pipelienBuilder->count('aggregate');
1008+
$mql['aggregate'][0][] = ['$count' => 'aggregate'];
9721009

973-
return $pipelienBuilder->get();
1010+
return $this->collection->aggregate($mql['aggregate'][0], $mql['aggregate'][1])->toArray();
9741011
}
9751012

9761013
return parent::runPaginationCountQuery($columns);
@@ -1173,11 +1210,6 @@ protected function compileWheres(): array
11731210
return $compiled;
11741211
}
11751212

1176-
/**
1177-
* @param array $where
1178-
*
1179-
* @return array
1180-
*/
11811213
protected function compileWhereBasic(array $where): array
11821214
{
11831215
$column = $where['column'];

tests/ModelTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,6 @@ public function testNumericFieldName(): void
10361036
$user->{2} = ['3' => 'two.three'];
10371037
$user->save();
10381038

1039-
// Test failure: 1 is transformed into 0 by array unpacking and variadic arguments
10401039
$found = User::where(1, 'one')->first();
10411040
$this->assertInstanceOf(User::class, $found);
10421041
$this->assertEquals('one', $found[1]);

0 commit comments

Comments
 (0)