Builder.php 27.9 KB
Newer Older
Jens Segers's avatar
Jens Segers committed
1
<?php namespace Jenssegers\Mongodb\Query;
Jens Segers's avatar
Jens Segers committed
2

Jens Segers's avatar
Jens Segers committed
3
use Closure;
4
use DateTime;
5
use Illuminate\Database\Query\Builder as BaseBuilder;
6
use Illuminate\Database\Query\Expression;
7
use Illuminate\Support\Arr;
Jens Segers's avatar
Jens Segers committed
8
use Illuminate\Support\Collection;
Jens Segers's avatar
Jens Segers committed
9
use Jenssegers\Mongodb\Connection;
Jens Segers's avatar
Jens Segers committed
10 11 12
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\Regex;
use MongoDB\BSON\UTCDateTime;
Jens Segers's avatar
Jens Segers committed
13

Jens Segers's avatar
Jens Segers committed
14 15
class Builder extends BaseBuilder
{
Jens Segers's avatar
Jens Segers committed
16
    /**
Jens Segers's avatar
Jens Segers committed
17
     * The database collection.
Jens Segers's avatar
Jens Segers committed
18 19 20
     *
     * @var MongoCollection
     */
Jens Segers's avatar
Jens Segers committed
21
    protected $collection;
Jens Segers's avatar
Jens Segers committed
22

23 24 25 26 27 28 29
    /**
     * The column projections.
     *
     * @var array
     */
    public $projections;

30 31 32 33 34 35
    /**
     * The cursor timeout value.
     *
     * @var int
     */
    public $timeout;
Jens Segers's avatar
Jens Segers committed
36

Roy de Vos Burchart's avatar
Roy de Vos Burchart committed
37 38 39 40 41 42
    /**
     * The cursor hint value.
     *
     * @var int
     */
    public $hint;
43

44 45 46 47 48 49 50
    /**
     * Indicate if we are executing a pagination query.
     *
     * @var bool
     */
    public $paginating = false;

Jens Segers's avatar
Jens Segers committed
51
    /**
52 53 54 55
     * All of the available clause operators.
     *
     * @var array
     */
56
    protected $operators = [
57 58 59
        '=', '<', '>', '<=', '>=', '<>', '!=',
        'like', 'not like', 'between', 'ilike',
        '&', '|', '^', '<<', '>>',
60 61 62 63
        'rlike', 'regexp', 'not regexp',
        'exists', 'type', 'mod', 'where', 'all', 'size', 'regex', 'text', 'slice', 'elemmatch',
        'geowithin', 'geointersects', 'near', 'nearsphere', 'geometry',
        'maxdistance', 'center', 'centersphere', 'box', 'polygon', 'uniquedocs',
64
    ];
65 66 67

    /**
     * Operator conversion.
Jens Segers's avatar
Jens Segers committed
68 69 70
     *
     * @var array
     */
71
    protected $conversion = [
Jens Segers's avatar
Jens Segers committed
72
        '='  => '=',
Jens's avatar
Jens committed
73
        '!=' => '$ne',
74
        '<>' => '$ne',
Jens Segers's avatar
Jens Segers committed
75
        '<'  => '$lt',
Jens's avatar
Jens committed
76
        '<=' => '$lte',
Jens Segers's avatar
Jens Segers committed
77
        '>'  => '$gt',
Jens's avatar
Jens committed
78
        '>=' => '$gte',
79
    ];
Jens Segers's avatar
Jens Segers committed
80 81

    /**
Jens Segers's avatar
Jens Segers committed
82 83 84 85
     * Create a new query builder instance.
     *
     * @param Connection $connection
     */
86
    public function __construct(Connection $connection, Processor $processor)
Jens Segers's avatar
Jens Segers committed
87
    {
88
        $this->grammar = new Grammar;
Jens Segers's avatar
Jens Segers committed
89
        $this->connection = $connection;
90
        $this->processor = $processor;
Jens Segers's avatar
Jens Segers committed
91 92
    }

93 94 95 96 97 98 99 100 101 102 103 104 105
    /**
     * Set the projections.
     *
     * @param  array  $columns
     * @return $this
     */
    public function project($columns)
    {
        $this->projections = is_array($columns) ? $columns : func_get_args();

        return $this;
    }

106 107 108 109 110 111 112 113 114 115 116 117
    /**
     * Set the cursor timeout in seconds.
     *
     * @param  int $seconds
     * @return $this
     */
    public function timeout($seconds)
    {
        $this->timeout = $seconds;

        return $this;
    }
Jens Segers's avatar
Jens Segers committed
118

Roy de Vos Burchart's avatar
Roy de Vos Burchart committed
119 120 121 122 123 124 125 126 127 128 129 130
    /**
     * Set the cursor hint.
     *
     * @param  mixed $index
     * @return $this
     */
    public function hint($index)
    {
        $this->hint = $index;

        return $this;
    }
131

Jens Segers's avatar
Jens Segers committed
132 133 134
    /**
     * Execute a query for a single record by ID.
     *
135
     * @param  mixed  $id
Jens Segers's avatar
Jens Segers committed
136 137 138
     * @param  array  $columns
     * @return mixed
     */
139
    public function find($id, $columns = [])
Jens Segers's avatar
Jens Segers committed
140
    {
141
        return $this->where('_id', '=', $this->convertKey($id))->first($columns);
Jens Segers's avatar
Jens Segers committed
142 143
    }

Jens Segers's avatar
Jens Segers committed
144 145 146 147 148 149
    /**
     * Execute the query as a "select" statement.
     *
     * @param  array  $columns
     * @return array|static[]
     */
150
    public function get($columns = [])
Jens Segers's avatar
Jens Segers committed
151
    {
152
        return $this->getFresh($columns);
Jens Segers's avatar
Jens Segers committed
153 154
    }

Jens Segers's avatar
Jens Segers committed
155
    /**
Jens Segers's avatar
Jens Segers committed
156
     * Execute the query as a fresh "select" statement.
Jens Segers's avatar
Jens Segers committed
157
     *
Jens Segers's avatar
Jens Segers committed
158
     * @param  array  $columns
Jens Segers's avatar
Jens Segers committed
159
     * @return array|static[]
Jens Segers's avatar
Jens Segers committed
160
     */
161
    public function getFresh($columns = [])
Jens Segers's avatar
Jens Segers committed
162
    {
Jens Segers's avatar
Jens Segers committed
163 164 165
        // If no columns have been specified for the select statement, we will set them
        // here to either the passed columns, or the standard default of retrieving
        // all of the columns on the table using the "wildcard" column character.
Jens Segers's avatar
Jens Segers committed
166 167 168
        if (is_null($this->columns)) {
            $this->columns = $columns;
        }
Jens Segers's avatar
Jens Segers committed
169

170
        // Drop all columns if * is present, MongoDB does not work this way.
Jens Segers's avatar
Jens Segers committed
171 172 173
        if (in_array('*', $this->columns)) {
            $this->columns = [];
        }
Jens Segers's avatar
Jens Segers committed
174

Jens Segers's avatar
Jens Segers committed
175 176
        // Compile wheres
        $wheres = $this->compileWheres();
Jens Segers's avatar
Jens Segers committed
177

178
        // Use MongoDB's aggregation framework when using grouping or aggregation functions.
Jens Segers's avatar
Jens Segers committed
179
        if ($this->groups or $this->aggregate or $this->paginating) {
180
            $group = [];
Jens Segers's avatar
Jens Segers committed
181

182
            // Add grouping columns to the $group part of the aggregation pipeline.
Jens Segers's avatar
Jens Segers committed
183 184
            if ($this->groups) {
                foreach ($this->groups as $column) {
185
                    $group['_id'][$column] = '$' . $column;
186

187 188
                    // When grouping, also add the $last operator to each grouped field,
                    // this mimics MySQL's behaviour a bit.
189
                    $group[$column] = ['$last' => '$' . $column];
190
                }
191 192

                // Do the same for other columns that are selected.
Jens Segers's avatar
Jens Segers committed
193
                foreach ($this->columns as $column) {
194 195
                    $key = str_replace('.', '_', $column);

196
                    $group[$key] = ['$last' => '$' . $column];
197
                }
198
            }
Jens Segers's avatar
Jens Segers committed
199

200 201
            // Add aggregation functions to the $group part of the aggregation pipeline,
            // these may override previous aggregations.
Jens Segers's avatar
Jens Segers committed
202
            if ($this->aggregate) {
Jens Segers's avatar
Jens Segers committed
203 204
                $function = $this->aggregate['function'];

Jens Segers's avatar
Jens Segers committed
205
                foreach ($this->aggregate['columns'] as $column) {
206
                    // Translate count into sum.
Jens Segers's avatar
Jens Segers committed
207
                    if ($function == 'count') {
208
                        $group['aggregate'] = ['$sum' => 1];
Jens Segers's avatar
Jens Segers committed
209
                    }
210
                    // Pass other functions directly.
Jens Segers's avatar
Jens Segers committed
211
                    else {
212
                        $group['aggregate'] = ['$' . $function => '$' . $column];
Jens Segers's avatar
Jens Segers committed
213
                    }
Jens Segers's avatar
Jens Segers committed
214 215 216
                }
            }

217 218
            // When using pagination, we limit the number of returned columns
            // by adding a projection.
Jens Segers's avatar
Jens Segers committed
219 220
            if ($this->paginating) {
                foreach ($this->columns as $column) {
221
                    $this->projections[$column] = 1;
222 223 224
                }
            }

225
            // The _id field is mandatory when using grouping.
Jens Segers's avatar
Jens Segers committed
226
            if ($group and empty($group['_id'])) {
227 228 229
                $group['_id'] = null;
            }

230
            // Build the aggregation pipeline.
231
            $pipeline = [];
Jens Segers's avatar
Jens Segers committed
232 233 234 235 236 237
            if ($wheres) {
                $pipeline[] = ['$match' => $wheres];
            }
            if ($group) {
                $pipeline[] = ['$group' => $group];
            }
Jens Segers's avatar
Jens Segers committed
238 239

            // Apply order and limit
Jens Segers's avatar
Jens Segers committed
240 241 242 243 244 245 246 247 248 249 250 251
            if ($this->orders) {
                $pipeline[] = ['$sort' => $this->orders];
            }
            if ($this->offset) {
                $pipeline[] = ['$skip' => $this->offset];
            }
            if ($this->limit) {
                $pipeline[] = ['$limit' => $this->limit];
            }
            if ($this->projections) {
                $pipeline[] = ['$project' => $this->projections];
            }
Jens Segers's avatar
Jens Segers committed
252

Jens Segers's avatar
Jens Segers committed
253 254 255 256
            $options = [
                'typeMap' => ['root' => 'array', 'document' => 'array'],
            ];

257
            // Execute aggregation
Jens Segers's avatar
Jens Segers committed
258
            $results = iterator_to_array($this->collection->aggregate($pipeline, $options));
Jens Segers's avatar
Jens Segers committed
259 260

            // Return results
261
            return $results;
Jens Segers's avatar
Jens Segers committed
262
        }
263 264

        // Distinct query
Jens Segers's avatar
Jens Segers committed
265
        elseif ($this->distinct) {
266 267
            // Return distinct results directly
            $column = isset($this->columns[0]) ? $this->columns[0] : '_id';
268 269

            // Execute distinct
Jens Segers's avatar
Jens Segers committed
270
            if ($wheres) {
271
                $result = $this->collection->distinct($column, $wheres);
Jens Segers's avatar
Jens Segers committed
272
            } else {
273 274
                $result = $this->collection->distinct($column);
            }
275 276

            return $result;
277
        }
278

279
        // Normal query
Jens Segers's avatar
Jens Segers committed
280
        else {
281
            $columns = [];
282 283

            // Convert select columns to simple projections.
Jens Segers's avatar
Jens Segers committed
284
            foreach ($this->columns as $column) {
Jens Segers's avatar
Jens Segers committed
285 286 287
                $columns[$column] = true;
            }

Dimasdanz's avatar
Dimasdanz committed
288
            // Add custom projections.
Jens Segers's avatar
Jens Segers committed
289
            if ($this->projections) {
Dimasdanz's avatar
Dimasdanz committed
290 291
                $columns = array_merge($columns, $this->projections);
            }
292
            $options = [];
Jens Segers's avatar
Jens Segers committed
293

294
            // Apply order, offset, limit and projection
Jens Segers's avatar
Jens Segers committed
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
            if ($this->timeout) {
                $options['maxTimeMS'] = $this->timeout;
            }
            if ($this->orders) {
                $options['sort'] = $this->orders;
            }
            if ($this->offset) {
                $options['skip'] = $this->offset;
            }
            if ($this->limit) {
                $options['limit'] = $this->limit;
            }
            if ($columns) {
                $options['projection'] = $columns;
            }
Jens Segers's avatar
Jens Segers committed
310 311 312 313
            // if ($this->hint)    $cursor->hint($this->hint);

            // Fix for legacy support, converts the results to arrays instead of objects.
            $options['typeMap'] = ['root' => 'array', 'document' => 'array'];
Jens Segers's avatar
Jens Segers committed
314

315 316 317
            // Execute query and get MongoCursor
            $cursor = $this->collection->find($wheres, $options);

318
            // Return results as an array with numeric keys
319
            return iterator_to_array($cursor, false);
Jens Segers's avatar
Jens Segers committed
320
        }
Jens Segers's avatar
Jens Segers committed
321
    }
Jens Segers's avatar
Jens Segers committed
322

Jens Segers's avatar
Jens Segers committed
323
    /**
324
     * Generate the unique cache key for the current query.
Jens Segers's avatar
Jens Segers committed
325 326 327 328 329
     *
     * @return string
     */
    public function generateCacheKey()
    {
330
        $key = [
331 332
            'connection' => $this->collection->getDatabaseName(),
            'collection' => $this->collection->getCollectionName(),
Jens Segers's avatar
Jens Segers committed
333 334 335 336 337
            'wheres'     => $this->wheres,
            'columns'    => $this->columns,
            'groups'     => $this->groups,
            'orders'     => $this->orders,
            'offset'     => $this->offset,
338
            'limit'      => $this->limit,
Jens Segers's avatar
Jens Segers committed
339
            'aggregate'  => $this->aggregate,
340
        ];
Jens Segers's avatar
Jens Segers committed
341 342 343 344

        return md5(serialize(array_values($key)));
    }

Jens Segers's avatar
Jens Segers committed
345 346 347 348 349 350 351
    /**
     * Execute an aggregate function on the database.
     *
     * @param  string  $function
     * @param  array   $columns
     * @return mixed
     */
352
    public function aggregate($function, $columns = [])
Jens Segers's avatar
Jens Segers committed
353 354 355 356 357
    {
        $this->aggregate = compact('function', 'columns');

        $results = $this->get($columns);

Jens Segers's avatar
Jens Segers committed
358 359 360
        // Once we have executed the query, we will reset the aggregate property so
        // that more select queries can be executed against the database without
        // the aggregate value getting in the way when the grammar builds it.
Jens Segers's avatar
Jens Segers committed
361 362
        $this->columns = null;
        $this->aggregate = null;
Jens Segers's avatar
Jens Segers committed
363

Jens Segers's avatar
Jens Segers committed
364
        if (isset($results[0])) {
365 366 367
            $result = (array) $results[0];

            return $result['aggregate'];
Jens Segers's avatar
Jens Segers committed
368 369 370
        }
    }

371 372 373 374 375 376 377 378 379 380
    /**
     * Determine if any rows exist for the current query.
     *
     * @return bool
     */
    public function exists()
    {
        return ! is_null($this->first());
    }

Jens Segers's avatar
Jens Segers committed
381 382 383
    /**
     * Force the query to only return distinct results.
     *
Jens's avatar
Jens committed
384
     * @return Builder
Jens Segers's avatar
Jens Segers committed
385 386 387 388 389
     */
    public function distinct($column = false)
    {
        $this->distinct = true;

Jens Segers's avatar
Jens Segers committed
390
        if ($column) {
391
            $this->columns = [$column];
Jens Segers's avatar
Jens Segers committed
392 393 394 395 396
        }

        return $this;
    }

Jens Segers's avatar
Jens Segers committed
397
    /**
Jens Segers's avatar
Jens Segers committed
398
     * Add an "order by" clause to the query.
Jens Segers's avatar
Jens Segers committed
399
     *
Jens Segers's avatar
Jens Segers committed
400 401
     * @param  string  $column
     * @param  string  $direction
Jens Segers's avatar
Jens Segers committed
402
     * @return Builder
Jens Segers's avatar
Jens Segers committed
403
     */
404
    public function orderBy($column, $direction = 'asc')
Jens Segers's avatar
Jens Segers committed
405
    {
Jens Segers's avatar
Jens Segers committed
406
        if (is_string($direction)) {
407 408
            $direction = (strtolower($direction) == 'asc' ? 1 : -1);
        }
409

Jens Segers's avatar
Jens Segers committed
410
        if ($column == 'natural') {
411
            $this->orders['$natural'] = $direction;
Jens Segers's avatar
Jens Segers committed
412
        } else {
413
            $this->orders[$column] = $direction;
414
        }
Jens Segers's avatar
Jens Segers committed
415

Jens Segers's avatar
Jens Segers committed
416
        return $this;
Jens Segers's avatar
Jens Segers committed
417 418
    }

Jens Segers's avatar
Jens Segers committed
419 420 421 422 423 424
    /**
     * Add a where between statement to the query.
     *
     * @param  string  $column
     * @param  array   $values
     * @param  string  $boolean
425 426
     * @param  bool  $not
     * @return Builder
Jens Segers's avatar
Jens Segers committed
427
     */
428
    public function whereBetween($column, array $values, $boolean = 'and', $not = false)
Jens Segers's avatar
Jens Segers committed
429 430 431
    {
        $type = 'between';

432
        $this->wheres[] = compact('column', 'type', 'boolean', 'values', 'not');
Jens Segers's avatar
Jens Segers committed
433 434 435 436

        return $this;
    }

437 438 439 440 441 442 443 444 445 446 447 448 449 450
    /**
     * Set the limit and offset for a given page.
     *
     * @param  int  $page
     * @param  int  $perPage
     * @return \Illuminate\Database\Query\Builder|static
     */
    public function forPage($page, $perPage = 15)
    {
        $this->paginating = true;

        return $this->skip(($page - 1) * $perPage)->take($perPage);
    }

Jens Segers's avatar
Jens Segers committed
451
    /**
Jens Segers's avatar
Jens Segers committed
452
     * Insert a new record into the database.
Jens Segers's avatar
Jens Segers committed
453
     *
Jens Segers's avatar
Jens Segers committed
454 455
     * @param  array  $values
     * @return bool
Jens Segers's avatar
Jens Segers committed
456
     */
Jens Segers's avatar
Jens Segers committed
457
    public function insert(array $values)
Jens Segers's avatar
Jens Segers committed
458
    {
459 460 461
        // Since every insert gets treated like a batch insert, we will have to detect
        // if the user is inserting a single document or an array of documents.
        $batch = true;
462

Jens Segers's avatar
Jens Segers committed
463
        foreach ($values as $value) {
464 465
            // As soon as we find a value that is not an array we assume the user is
            // inserting a single document.
Jens Segers's avatar
Jens Segers committed
466 467 468
            if (! is_array($value)) {
                $batch = false;
                break;
469
            }
470
        }
Jens Segers's avatar
Jens Segers committed
471

Jens Segers's avatar
Jens Segers committed
472 473 474
        if (! $batch) {
            $values = [$values];
        }
475

476
        // Batch insert
Jens Segers's avatar
Jens Segers committed
477
        $result = $this->collection->insertMany($values);
478

479
        return (1 == (int) $result->isAcknowledged());
Jens Segers's avatar
Jens Segers committed
480 481
    }

Jens Segers's avatar
Jens Segers committed
482 483 484 485 486 487 488 489 490
    /**
     * Insert a new record and get the value of the primary key.
     *
     * @param  array   $values
     * @param  string  $sequence
     * @return int
     */
    public function insertGetId(array $values, $sequence = null)
    {
Jens Segers's avatar
Jens Segers committed
491 492
        $result = $this->collection->insertOne($values);

Jens Segers's avatar
Jens Segers committed
493 494
        if (1 == (int) $result->isAcknowledged()) {
            if (is_null($sequence)) {
Jens Segers's avatar
Jens Segers committed
495 496 497
                $sequence = '_id';
            }

498
            // Return id
Jens Segers's avatar
Jens Segers committed
499
            return $sequence == '_id' ? $result->getInsertedId() : $values[$sequence];
Jens's avatar
Jens committed
500
        }
Jens Segers's avatar
Jens Segers committed
501 502
    }

Jens Segers's avatar
Jens Segers committed
503
    /**
Jens Segers's avatar
Jens Segers committed
504
     * Update a record in the database.
Jens Segers's avatar
Jens Segers committed
505
     *
Jens Segers's avatar
Jens Segers committed
506
     * @param  array  $values
Jens Segers's avatar
Jens Segers committed
507
     * @param  array  $options
Jens Segers's avatar
Jens Segers committed
508 509
     * @return int
     */
510
    public function update(array $values, array $options = [])
Jens Segers's avatar
Jens Segers committed
511
    {
Jens Segers's avatar
Jens Segers committed
512
        // Use $set as default operator.
Jens Segers's avatar
Jens Segers committed
513
        if (! starts_with(key($values), '$')) {
514
            $values = ['$set' => $values];
Jens Segers's avatar
Jens Segers committed
515 516 517
        }

        return $this->performUpdate($values, $options);
Jens Segers's avatar
Jens Segers committed
518 519
    }

520 521 522 523 524 525 526 527
    /**
     * Increment a column's value by a given amount.
     *
     * @param  string  $column
     * @param  int     $amount
     * @param  array   $extra
     * @return int
     */
528
    public function increment($column, $amount = 1, array $extra = [], array $options = [])
529
    {
530
        $query = ['$inc' => [$column => $amount]];
531

Jens Segers's avatar
Jens Segers committed
532
        if (! empty($extra)) {
533
            $query['$set'] = $extra;
534
        }
535

536
        // Protect
Jens Segers's avatar
Jens Segers committed
537
        $this->where(function ($query) use ($column) {
538
            $query->where($column, 'exists', false);
539

540 541 542
            $query->orWhereNotNull($column);
        });

543
        return $this->performUpdate($query, $options);
544 545 546 547 548 549 550 551 552 553
    }

    /**
     * Decrement a column's value by a given amount.
     *
     * @param  string  $column
     * @param  int     $amount
     * @param  array   $extra
     * @return int
     */
554
    public function decrement($column, $amount = 1, array $extra = [], array $options = [])
555
    {
556
        return $this->increment($column, -1 * $amount, $extra, $options);
557 558
    }

559
    /**
560
     * Get an array with the values of a given column.
561 562
     *
     * @param  string  $column
563 564
     * @param  string|null  $key
     * @return array
565
     */
566
    public function pluck($column, $key = null)
567
    {
568 569 570 571 572 573 574 575 576 577
        $results = $this->get(is_null($key) ? [$column] : [$column, $key]);

        // If the columns are qualified with a table or have an alias, we cannot use
        // those directly in the "pluck" operations since the results from the DB
        // are only keyed by the column itself. We'll strip the table out here.
        return Arr::pluck(
            $results,
            $column,
            $key
        );
578 579
    }

Jens Segers's avatar
Jens Segers committed
580
    /**
Jens Segers's avatar
Jens Segers committed
581
     * Delete a record from the database.
Jens Segers's avatar
Jens Segers committed
582
     *
Jens Segers's avatar
Jens Segers committed
583
     * @param  mixed  $id
Jens Segers's avatar
Jens Segers committed
584 585
     * @return int
     */
Jens Segers's avatar
Jens Segers committed
586
    public function delete($id = null)
Jens Segers's avatar
Jens Segers committed
587
    {
588
        $wheres = $this->compileWheres();
589
        $result = $this->collection->DeleteMany($wheres);
Jens Segers's avatar
Jens Segers committed
590
        if (1 == (int) $result->isAcknowledged()) {
591
            return $result->getDeletedCount();
Jens Segers's avatar
Jens Segers committed
592 593 594 595 596
        }

        return 0;
    }

Jens Segers's avatar
Jens Segers committed
597 598 599 600 601 602 603 604
    /**
     * Set the collection which the query is targeting.
     *
     * @param  string  $collection
     * @return Builder
     */
    public function from($collection)
    {
Jens Segers's avatar
Jens Segers committed
605
        if ($collection) {
Jens's avatar
Jens committed
606
            $this->collection = $this->connection->getCollection($collection);
Jens Segers's avatar
Jens Segers committed
607
        }
Jens Segers's avatar
Jens Segers committed
608

609
        return parent::from($collection);
Jens Segers's avatar
Jens Segers committed
610 611
    }

Jens Segers's avatar
Jens Segers committed
612 613 614 615 616
    /**
     * Run a truncate statement on the table.
     */
    public function truncate()
    {
617
        $result = $this->collection->drop();
Jens Segers's avatar
Jens Segers committed
618

619
        return (1 == (int) $result->ok);
Jens Segers's avatar
Jens Segers committed
620 621
    }

Jens Segers's avatar
Jens Segers committed
622 623 624 625 626 627 628 629 630
    /**
     * Get an array with the values of a given column.
     *
     * @param  string  $column
     * @param  string  $key
     * @return array
     */
    public function lists($column, $key = null)
    {
Jens Segers's avatar
Jens Segers committed
631
        if ($key == '_id') {
Jens Segers's avatar
Jens Segers committed
632 633
            $results = new Collection($this->get([$column, $key]));

Jens Segers's avatar
Jens Segers committed
634
            // Convert ObjectID's to strings so that lists can do its work.
Jens Segers's avatar
Jens Segers committed
635
            $results = $results->map(function ($item) {
Jens Segers's avatar
Jens Segers committed
636 637 638 639 640
                $item['_id'] = (string) $item['_id'];

                return $item;
            });

641
            return $results->lists($column, $key)->all();
Jens Segers's avatar
Jens Segers committed
642 643 644 645 646
        }

        return parent::lists($column, $key);
    }

647 648 649 650 651 652
    /**
     * Create a raw database expression.
     *
     * @param  closure  $expression
     * @return mixed
     */
653
    public function raw($expression = null)
654
    {
655
        // Execute the closure on the mongodb collection
Jens Segers's avatar
Jens Segers committed
656
        if ($expression instanceof Closure) {
657 658
            return call_user_func($expression, $this->collection);
        }
659

660
        // Create an expression for the given value
Jens Segers's avatar
Jens Segers committed
661
        elseif (! is_null($expression)) {
662 663 664 665
            return new Expression($expression);
        }

        // Quick access to the mongodb collection
666
        return $this->collection;
667 668
    }

669
    /**
Jens Segers's avatar
Jens Segers committed
670
     * Append one or more values to an array.
671
     *
Jens Segers's avatar
Jens Segers committed
672
     * @param  mixed   $column
673 674 675
     * @param  mixed   $value
     * @return int
     */
676
    public function push($column, $value = null, $unique = false)
677
    {
678 679 680
        // Use the addToSet operator in case we only want unique items.
        $operator = $unique ? '$addToSet' : '$push';

Jens Segers's avatar
Jens Segers committed
681
        // Check if we are pushing multiple values.
682
        $batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1));
Jens Segers's avatar
Jens Segers committed
683

Jens Segers's avatar
Jens Segers committed
684
        if (is_array($column)) {
685
            $query = [$operator => $column];
Jens Segers's avatar
Jens Segers committed
686
        } elseif ($batch) {
687
            $query = [$operator => [$column => ['$each' => $value]]];
Jens Segers's avatar
Jens Segers committed
688
        } else {
689
            $query = [$operator => [$column => $value]];
690 691 692 693 694
        }

        return $this->performUpdate($query);
    }

Jens Segers's avatar
Jens Segers committed
695 696 697 698 699 700 701 702 703
    /**
     * Remove one or more values from an array.
     *
     * @param  mixed   $column
     * @param  mixed   $value
     * @return int
     */
    public function pull($column, $value = null)
    {
Jens Segers's avatar
Jens Segers committed
704
        // Check if we passed an associative array.
705
        $batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1));
Jens Segers's avatar
Jens Segers committed
706

707
        // If we are pulling multiple values, we need to use $pullAll.
708
        $operator = $batch ? '$pullAll' : '$pull';
709

Jens Segers's avatar
Jens Segers committed
710
        if (is_array($column)) {
711
            $query = [$operator => $column];
Jens Segers's avatar
Jens Segers committed
712
        } else {
713
            $query = [$operator => [$column => $value]];
Jens Segers's avatar
Jens Segers committed
714 715 716 717 718
        }

        return $this->performUpdate($query);
    }

Jens Segers's avatar
Jens Segers committed
719 720 721 722 723 724
    /**
     * Remove one or more fields.
     *
     * @param  mixed $columns
     * @return int
     */
725
    public function drop($columns)
Jens Segers's avatar
Jens Segers committed
726
    {
Jens Segers's avatar
Jens Segers committed
727 728 729
        if (! is_array($columns)) {
            $columns = [$columns];
        }
Jens Segers's avatar
Jens Segers committed
730

731
        $fields = [];
Jens Segers's avatar
Jens Segers committed
732

Jens Segers's avatar
Jens Segers committed
733
        foreach ($columns as $column) {
Jens Segers's avatar
Jens Segers committed
734 735 736
            $fields[$column] = 1;
        }

737
        $query = ['$unset' => $fields];
Jens Segers's avatar
Jens Segers committed
738 739 740 741

        return $this->performUpdate($query);
    }

Jens Segers's avatar
Jens Segers committed
742 743 744 745 746 747 748
    /**
     * Get a new instance of the query builder.
     *
     * @return Builder
     */
    public function newQuery()
    {
749
        return new Builder($this->connection, $this->processor);
Jens Segers's avatar
Jens Segers committed
750 751
    }

752
    /**
Jens Segers's avatar
Jens Segers committed
753
     * Perform an update query.
754 755
     *
     * @param  array  $query
Jens Segers's avatar
Jens Segers committed
756
     * @param  array  $options
757 758
     * @return int
     */
759
    protected function performUpdate($query, array $options = [])
760
    {
Jens Segers's avatar
Jens Segers committed
761
        // Update multiple items by default.
Jens Segers's avatar
Jens Segers committed
762
        if (! array_key_exists('multiple', $options)) {
Jens Segers's avatar
Jens Segers committed
763 764
            $options['multiple'] = true;
        }
765

766
        $wheres = $this->compileWheres();
767
        $result = $this->collection->UpdateMany($wheres, $query, $options);
Jens Segers's avatar
Jens Segers committed
768
        if (1 == (int) $result->isAcknowledged()) {
769
            return $result->getModifiedCount() ? $result->getModifiedCount() : $result->getUpsertedCount();
770 771 772 773 774
        }

        return 0;
    }

775
    /**
Jens Segers's avatar
Jens Segers committed
776
     * Convert a key to ObjectID if needed.
777
     *
778 779 780
     * @param  mixed $id
     * @return mixed
     */
781
    public function convertKey($id)
782
    {
Jens Segers's avatar
Jens Segers committed
783
        if (is_string($id) and strlen($id) === 24 and ctype_xdigit($id)) {
Jens Segers's avatar
Jens Segers committed
784 785 786 787
            return new ObjectID($id);
        }

        return $id;
788 789
    }

790 791 792 793 794 795 796 797 798 799 800 801 802
    /**
     * Add a basic where clause to the query.
     *
     * @param  string  $column
     * @param  string  $operator
     * @param  mixed   $value
     * @param  string  $boolean
     * @return \Illuminate\Database\Query\Builder|static
     *
     * @throws \InvalidArgumentException
     */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
duxet's avatar
duxet committed
803
        $params = func_get_args();
804

805
        // Remove the leading $ from operators.
Jens Segers's avatar
Jens Segers committed
806
        if (func_num_args() == 3) {
duxet's avatar
duxet committed
807
            $operator = &$params[1];
808

Jens Segers's avatar
Jens Segers committed
809
            if (starts_with($operator, '$')) {
810 811 812 813
                $operator = substr($operator, 1);
            }
        }

duxet's avatar
duxet committed
814
        return call_user_func_array('parent::where', $params);
815 816
    }

Jens Segers's avatar
Jens Segers committed
817
    /**
Jens Segers's avatar
Jens Segers committed
818 819 820 821
     * Compile the where array.
     *
     * @return array
     */
Jens Segers's avatar
Jens Segers committed
822
    protected function compileWheres()
Jens Segers's avatar
Jens Segers committed
823
    {
Jens Segers's avatar
Jens Segers committed
824
        // The wheres to compile.
825
        $wheres = $this->wheres ?: [];
Jens Segers's avatar
Jens Segers committed
826

827
        // We will add all compiled wheres to this array.
828
        $compiled = [];
Jens Segers's avatar
Jens Segers committed
829

Jens Segers's avatar
Jens Segers committed
830
        foreach ($wheres as $i => &$where) {
831
            // Make sure the operator is in lowercase.
Jens Segers's avatar
Jens Segers committed
832
            if (isset($where['operator'])) {
833
                $where['operator'] = strtolower($where['operator']);
Jens Segers's avatar
Jens Segers committed
834

835
                // Operator conversions
836
                $convert = [
Jens Segers's avatar
Jens Segers committed
837 838
                    'regexp'        => 'regex',
                    'elemmatch'     => 'elemMatch',
839
                    'geointersects' => 'geoIntersects',
Jens Segers's avatar
Jens Segers committed
840 841 842 843 844
                    'geowithin'     => 'geoWithin',
                    'nearsphere'    => 'nearSphere',
                    'maxdistance'   => 'maxDistance',
                    'centersphere'  => 'centerSphere',
                    'uniquedocs'    => 'uniqueDocs',
845
                ];
846

Jens Segers's avatar
Jens Segers committed
847
                if (array_key_exists($where['operator'], $convert)) {
848
                    $where['operator'] = $convert[$where['operator']];
Jens Segers's avatar
Jens Segers committed
849
                }
850 851
            }

852
            // Convert id's.
Jens Segers's avatar
Jens Segers committed
853
            if (isset($where['column']) and ($where['column'] == '_id' or ends_with($where['column'], '._id'))) {
854
                // Multiple values.
Jens Segers's avatar
Jens Segers committed
855 856
                if (isset($where['values'])) {
                    foreach ($where['values'] as &$value) {
857
                        $value = $this->convertKey($value);
Jens Segers's avatar
Jens Segers committed
858 859
                    }
                }
860 861

                // Single value.
Jens Segers's avatar
Jens Segers committed
862
                elseif (isset($where['value'])) {
863
                    $where['value'] = $this->convertKey($where['value']);
Jens Segers's avatar
Jens Segers committed
864
                }
Jens's avatar
Jens committed
865 866
            }

Jens Segers's avatar
Jens Segers committed
867
            // Convert DateTime values to UTCDateTime.
Jens Segers's avatar
Jens Segers committed
868
            if (isset($where['value']) and $where['value'] instanceof DateTime) {
869
                $where['value'] = new UTCDateTime($where['value']->getTimestamp() * 1000);
870 871
            }

872 873 874
            // 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.
Jens Segers's avatar
Jens Segers committed
875
            if ($i == 0 and count($wheres) > 1 and $where['boolean'] == 'and') {
Jens Segers's avatar
Jens Segers committed
876
                $where['boolean'] = $wheres[$i + 1]['boolean'];
Jens's avatar
Jens committed
877 878
            }

879
            // We use different methods to compile different wheres.
Jens Segers's avatar
Jens Segers committed
880
            $method = "compileWhere{$where['type']}";
881
            $result = $this->{$method}($where);
Jens Segers's avatar
Jens Segers committed
882

883
            // Wrap the where with an $or operator.
Jens Segers's avatar
Jens Segers committed
884
            if ($where['boolean'] == 'or') {
885
                $result = ['$or' => [$result]];
886 887 888 889
            }

            // If there are multiple wheres, we will wrap it with $and. This is needed
            // to make nested wheres work.
Jens Segers's avatar
Jens Segers committed
890
            elseif (count($wheres) > 1) {
891
                $result = ['$and' => [$result]];
Jens Segers's avatar
Jens Segers committed
892 893
            }

894 895
            // Merge the compiled where with the others.
            $compiled = array_merge_recursive($compiled, $result);
Jens Segers's avatar
Jens Segers committed
896 897
        }

898
        return $compiled;
Jens Segers's avatar
Jens Segers committed
899 900
    }

Jens Segers's avatar
Jens Segers committed
901
    protected function compileWhereBasic($where)
Jens Segers's avatar
Jens Segers committed
902 903 904
    {
        extract($where);

Jens Segers's avatar
Jens Segers committed
905
        // Replace like with a Regex instance.
Jens Segers's avatar
Jens Segers committed
906
        if ($operator == 'like') {
Jens Segers's avatar
Jens Segers committed
907 908 909
            $operator = '=';
            $regex = str_replace('%', '', $value);

910
            // Convert like to regular expression.
Jens Segers's avatar
Jens Segers committed
911 912 913 914 915 916
            if (! starts_with($value, '%')) {
                $regex = '^' . $regex;
            }
            if (! ends_with($value, '%')) {
                $regex = $regex . '$';
            }
Jens Segers's avatar
Jens Segers committed
917 918

            $value = new Regex($regex, 'i');
Jens Segers's avatar
Jens Segers committed
919 920
        }

921
        // Manipulate regexp operations.
Jens Segers's avatar
Jens Segers committed
922
        elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) {
Jens Segers's avatar
Jens Segers committed
923
            // Automatically convert regular expression strings to Regex objects.
Jens Segers's avatar
Jens Segers committed
924
            if (! $value instanceof Regex) {
Jens Segers's avatar
Jens Segers committed
925 926 927 928
                $e = explode('/', $value);
                $flag = end($e);
                $regstr = substr($value, 1, -(strlen($flag) + 1));
                $value = new Regex($regstr, $flag);
929 930 931
            }

            // For inverse regexp operations, we can just use the $not operator
Jens Segers's avatar
Jens Segers committed
932
            // and pass it a Regex instence.
Jens Segers's avatar
Jens Segers committed
933
            if (starts_with($operator, 'not')) {
934 935 936 937
                $operator = 'not';
            }
        }

Jens Segers's avatar
Jens Segers committed
938
        if (! isset($operator) or $operator == '=') {
939
            $query = [$column => $value];
Jens Segers's avatar
Jens Segers committed
940
        } elseif (array_key_exists($operator, $this->conversion)) {
941
            $query = [$column => [$this->conversion[$operator] => $value]];
Jens Segers's avatar
Jens Segers committed
942
        } else {
943
            $query = [$column => ['$' . $operator => $value]];
944
        }
Jens Segers's avatar
Jens Segers committed
945

Jens's avatar
Jens committed
946
        return $query;
Jens Segers's avatar
Jens Segers committed
947 948
    }

Jens Segers's avatar
Jens Segers committed
949
    protected function compileWhereNested($where)
950 951 952
    {
        extract($where);

Jens Segers's avatar
Jens Segers committed
953
        return $query->compileWheres();
954 955
    }

Jens Segers's avatar
Jens Segers committed
956
    protected function compileWhereIn($where)
Jens Segers's avatar
Jens Segers committed
957
    {
Jens Segers's avatar
Jens Segers committed
958
        extract($where);
Jens's avatar
Jens committed
959

960
        return [$column => ['$in' => array_values($values)]];
Jens Segers's avatar
Jens Segers committed
961
    }
Jens Segers's avatar
Jens Segers committed
962

Jens Segers's avatar
Jens Segers committed
963
    protected function compileWhereNotIn($where)
964 965 966
    {
        extract($where);

967
        return [$column => ['$nin' => array_values($values)]];
968 969
    }

Jens Segers's avatar
Jens Segers committed
970
    protected function compileWhereNull($where)
Jens's avatar
Jens committed
971
    {
972 973
        $where['operator'] = '=';
        $where['value'] = null;
Jens's avatar
Jens committed
974

975
        return $this->compileWhereBasic($where);
Jens's avatar
Jens committed
976 977
    }

Jens Segers's avatar
Jens Segers committed
978
    protected function compileWhereNotNull($where)
Jens's avatar
Jens committed
979
    {
980 981
        $where['operator'] = '!=';
        $where['value'] = null;
Jens's avatar
Jens committed
982

983
        return $this->compileWhereBasic($where);
Jens's avatar
Jens committed
984 985
    }

Jens Segers's avatar
Jens Segers committed
986
    protected function compileWhereBetween($where)
Jens Segers's avatar
Jens Segers committed
987 988 989
    {
        extract($where);

Jens Segers's avatar
Jens Segers committed
990
        if ($not) {
991 992 993 994
            return [
                '$or' => [
                    [
                        $column => [
Jens Segers's avatar
Jens Segers committed
995
                            '$lte' => $values[0],
996 997 998 999
                        ],
                    ],
                    [
                        $column => [
Jens Segers's avatar
Jens Segers committed
1000
                            '$gte' => $values[1],
1001 1002 1003 1004
                        ],
                    ],
                ],
            ];
Jens Segers's avatar
Jens Segers committed
1005
        } else {
1006 1007
            return [
                $column => [
1008
                    '$gte' => $values[0],
Jens Segers's avatar
Jens Segers committed
1009
                    '$lte' => $values[1],
1010 1011
                ],
            ];
1012
        }
Jens Segers's avatar
Jens Segers committed
1013 1014
    }

Jens Segers's avatar
Jens Segers committed
1015
    protected function compileWhereRaw($where)
Jens Segers's avatar
Jens Segers committed
1016 1017 1018 1019
    {
        return $where['sql'];
    }

Jens Segers's avatar
Jens Segers committed
1020 1021 1022 1023 1024 1025 1026 1027 1028
    /**
     * Handle dynamic method calls into the method.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
Jens Segers's avatar
Jens Segers committed
1029
        if ($method == 'unset') {
1030
            return call_user_func_array([$this, 'drop'], $parameters);
Jens Segers's avatar
Jens Segers committed
1031 1032 1033 1034
        }

        return parent::__call($method, $parameters);
    }
1035
}