SQLSelect::addGroupBy()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Queries;
4
5
use SilverStripe\Core\Injector\Injector;
6
use SilverStripe\Dev\Deprecation;
7
use SilverStripe\ORM\DB;
8
use InvalidArgumentException;
9
10
/**
11
 * Object representing a SQL SELECT query.
12
 * The various parts of the SQL query can be manipulated individually.
13
 */
14
class SQLSelect extends SQLConditionalExpression
15
{
16
17
    /**
18
     * An array of SELECT fields, keyed by an optional alias.
19
     *
20
     * @var array
21
     */
22
    protected $select = array();
23
24
    /**
25
     * An array of GROUP BY clauses.
26
     *
27
     * @var array
28
     */
29
    protected $groupby = array();
30
31
    /**
32
     * An array of having clauses.
33
     * Each item in this array will be in the form of a single-length array
34
     * in the format array('predicate' => array($parameters))
35
     *
36
     * @var array
37
     */
38
    protected $having = array();
39
40
    /**
41
     * If this is true DISTINCT will be added to the SQL.
42
     *
43
     * @var bool
44
     */
45
    protected $distinct = false;
46
47
    /**
48
     * An array of ORDER BY clauses, functions. Stores as an associative
49
     * array of column / function to direction.
50
     *
51
     * May be used on SELECT or single table DELETE queries in some adapters
52
     *
53
     * @var array
54
     */
55
    protected $orderby = array();
56
57
    /**
58
     * An array containing limit and offset keys for LIMIT clause.
59
     *
60
     * May be used on SELECT or single table DELETE queries in some adapters
61
     *
62
     * @var array
63
     */
64
    protected $limit = array();
65
66
    /**
67
     * Construct a new SQLSelect.
68
     *
69
     * @param array|string $select An array of SELECT fields.
70
     * @param array|string $from An array of FROM clauses. The first one should be just the table name.
71
     * Each should be ANSI quoted.
72
     * @param array $where An array of WHERE clauses.
73
     * @param array $orderby An array ORDER BY clause.
74
     * @param array $groupby An array of GROUP BY clauses.
75
     * @param array $having An array of HAVING clauses.
76
     * @param array|string $limit A LIMIT clause or array with limit and offset keys
77
     * @return static
78
     */
79
    public static function create(
80
        $select = "*",
81
        $from = array(),
82
        $where = array(),
83
        $orderby = array(),
84
        $groupby = array(),
85
        $having = array(),
86
        $limit = array()
87
    ) {
88
        return Injector::inst()->createWithArgs(__CLASS__, func_get_args());
89
    }
90
91
    /**
92
     * Construct a new SQLSelect.
93
     *
94
     * @param array|string $select An array of SELECT fields.
95
     * @param array|string $from An array of FROM clauses. The first one should be just the table name.
96
     * Each should be ANSI quoted.
97
     * @param array $where An array of WHERE clauses.
98
     * @param array $orderby An array ORDER BY clause.
99
     * @param array $groupby An array of GROUP BY clauses.
100
     * @param array $having An array of HAVING clauses.
101
     * @param array|string $limit A LIMIT clause or array with limit and offset keys
102
     */
103
    public function __construct(
104
        $select = "*",
105
        $from = array(),
106
        $where = array(),
107
        $orderby = array(),
108
        $groupby = array(),
109
        $having = array(),
110
        $limit = array()
111
    ) {
112
113
        parent::__construct($from, $where);
114
115
        $this->setSelect($select);
116
        $this->setOrderBy($orderby);
117
        $this->setGroupBy($groupby);
118
        $this->setHaving($having);
119
        $this->setLimit($limit);
120
    }
121
122
    /**
123
     * Set the list of columns to be selected by the query.
124
     *
125
     * <code>
126
     *  // pass fields to select as single parameter array
127
     *  $query->setSelect(array('"Col1"', '"Col2"'))->setFrom('"MyTable"');
128
     *
129
     *  // pass fields to select as multiple parameters
130
     *  $query->setSelect('"Col1"', '"Col2"')->setFrom('"MyTable"');
131
     *
132
     *  // Set a list of selected fields as aliases
133
     *  $query->setSelect(array('Name' => '"Col1"', 'Details' => '"Col2"')->setFrom('"MyTable"');
134
     * </code>
135
     *
136
     * @param string|array $fields Field names should be ANSI SQL quoted. Array keys should be unquoted.
137
     * @return $this Self reference
138
     */
139
    public function setSelect($fields)
140
    {
141
        $this->select = array();
142
        if (func_num_args() > 1) {
143
            $fields = func_get_args();
144
        } elseif (!is_array($fields)) {
145
            $fields = array($fields);
146
        }
147
        return $this->addSelect($fields);
148
    }
149
150
    /**
151
     * Add to the list of columns to be selected by the query.
152
     *
153
     * @see setSelect for example usage
154
     *
155
     * @param string|array $fields Field names should be ANSI SQL quoted. Array keys should be unquoted.
156
     * @return $this Self reference
157
     */
158
    public function addSelect($fields)
159
    {
160
        if (func_num_args() > 1) {
161
            $fields = func_get_args();
162
        } elseif (!is_array($fields)) {
163
            $fields = array($fields);
164
        }
165
        foreach ($fields as $idx => $field) {
166
            $this->selectField($field, is_numeric($idx) ? null : $idx);
167
        }
168
169
        return $this;
170
    }
171
172
    /**
173
     * Select an additional field.
174
     *
175
     * @param string $field The field to select (ansi quoted SQL identifier or statement)
176
     * @param string|null $alias The alias of that field (unquoted SQL identifier).
177
     * Defaults to the unquoted column name of the $field parameter.
178
     * @return $this Self reference
179
     */
180
    public function selectField($field, $alias = null)
181
    {
182
        if (!$alias) {
183
            if (preg_match('/"([^"]+)"$/', $field, $matches)) {
184
                $alias = $matches[1];
185
            } else {
186
                $alias = $field;
187
            }
188
        }
189
        $this->select[$alias] = $field;
190
        return $this;
191
    }
192
193
    /**
194
     * Return the SQL expression for the given field alias.
195
     * Returns null if the given alias doesn't exist.
196
     * See {@link selectField()} for details on alias generation.
197
     *
198
     * @param string $field
199
     * @return string
200
     */
201
    public function expressionForField($field)
202
    {
203
        return isset($this->select[$field]) ? $this->select[$field] : null;
204
    }
205
206
    /**
207
     * Set distinct property.
208
     *
209
     * @param bool $value
210
     * @return $this Self reference
211
     */
212
    public function setDistinct($value)
213
    {
214
        $this->distinct = $value;
215
        return $this;
216
    }
217
218
    /**
219
     * Get the distinct property.
220
     *
221
     * @return bool
222
     */
223
    public function getDistinct()
224
    {
225
        return $this->distinct;
226
    }
227
228
    /**
229
     * Get the limit property.
230
     * @return array
231
     */
232
    public function getLimit()
233
    {
234
        return $this->limit;
235
    }
236
237
    /**
238
     * Pass LIMIT clause either as SQL snippet or in array format.
239
     * Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
240
     *
241
     * @param int|string|array|null $limit If passed as a string or array, assumes SQL escaped data.
242
     * Only applies for positive values.
243
     * @param int $offset
244
     * @throws InvalidArgumentException
245
     * @return $this Self reference
246
     */
247
    public function setLimit($limit, $offset = 0)
248
    {
249
        if ((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) {
250
            throw new InvalidArgumentException("SQLSelect::setLimit() only takes positive values");
251
        }
252
253
        if ($limit === 0) {
254
            Deprecation::notice(
255
                '4.3',
256
                "setLimit(0) is deprecated in SS4. To clear limit, call setLimit(null). " .
257
                    "In SS5 a limit of 0 will instead return no records."
258
            );
259
        }
260
261
        if (is_numeric($limit) && ($limit || $offset)) {
262
            $this->limit = array(
263
                'start' => (int)$offset,
264
                'limit' => (int)$limit,
265
            );
266
        } elseif ($limit && is_string($limit)) {
267
            if (strpos($limit, ',') !== false) {
268
                list($start, $innerLimit) = explode(',', $limit, 2);
269
            } else {
270
                list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
271
            }
272
273
            $this->limit = array(
274
                'start' => (int)$start,
275
                'limit' => (int)$innerLimit,
276
            );
277
        } elseif ($limit === null && $offset) {
278
            $this->limit = array(
279
                'start' => (int)$offset,
280
                'limit' => $limit
281
            );
282
        } else {
283
            $this->limit = $limit;
0 ignored issues
show
Documentation Bug introduced by
It seems like $limit can also be of type integer or string. However, the property $limit is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
284
        }
285
286
        return $this;
287
    }
288
289
    /**
290
     * Set ORDER BY clause either as SQL snippet or in array format.
291
     *
292
     * @example $sql->setOrderBy("Column");
293
     * @example $sql->setOrderBy("Column DESC");
294
     * @example $sql->setOrderBy("Column DESC, ColumnTwo ASC");
295
     * @example $sql->setOrderBy("Column", "DESC");
296
     * @example $sql->setOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
297
     *
298
     * @param string|array $clauses Clauses to add (escaped SQL statement)
299
     * @param string $direction Sort direction, ASC or DESC
300
     *
301
     * @return $this Self reference
302
     */
303
    public function setOrderBy($clauses = null, $direction = null)
304
    {
305
        $this->orderby = array();
306
        return $this->addOrderBy($clauses, $direction);
307
    }
308
309
    /**
310
     * Add ORDER BY clause either as SQL snippet or in array format.
311
     *
312
     * @example $sql->addOrderBy("Column");
313
     * @example $sql->addOrderBy("Column DESC");
314
     * @example $sql->addOrderBy("Column DESC, ColumnTwo ASC");
315
     * @example $sql->addOrderBy("Column", "DESC");
316
     * @example $sql->addOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
317
     *
318
     * @param string|array $clauses Clauses to add (escaped SQL statements)
319
     * @param string $direction Sort direction, ASC or DESC
320
     * @return $this Self reference
321
     */
322
    public function addOrderBy($clauses = null, $direction = null)
323
    {
324
        if (empty($clauses)) {
325
            return $this;
326
        }
327
328
        if (is_string($clauses)) {
329
            if (strpos($clauses, "(") !== false) {
330
                $sort = preg_split("/,(?![^()]*+\\))/", $clauses);
331
            } else {
332
                $sort = explode(",", $clauses);
333
            }
334
335
            $clauses = array();
336
337
            foreach ($sort as $clause) {
338
                list($column, $direction) = $this->getDirectionFromString($clause, $direction);
339
                $clauses[$column] = $direction;
340
            }
341
        }
342
343
        if (is_array($clauses)) {
344
            foreach ($clauses as $key => $value) {
345
                if (!is_numeric($key)) {
346
                    $column = trim($key);
347
                    $columnDir = strtoupper(trim($value));
348
                } else {
349
                    list($column, $columnDir) = $this->getDirectionFromString($value);
350
                }
351
352
                $this->orderby[$column] = $columnDir;
353
            }
354
        } else {
355
            user_error('SQLSelect::orderby() incorrect format for $orderby', E_USER_WARNING);
356
        }
357
358
        // If sort contains a public function call, let's move the sort clause into a
359
        // separate selected field.
360
        //
361
        // Some versions of MySQL choke if you have a group public function referenced
362
        // directly in the ORDER BY
363
        if ($this->orderby) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orderby of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
364
            $i = 0;
365
            $orderby = array();
366
            foreach ($this->orderby as $clause => $dir) {
367
                // public function calls and multi-word columns like "CASE WHEN ..."
368
                if (strpos($clause, '(') !== false || strpos($clause, " ") !== false) {
369
                    // Move the clause to the select fragment, substituting a placeholder column in the sort fragment.
370
                    $clause = trim($clause);
371
                    do {
372
                        $column = "_SortColumn{$i}";
373
                        ++$i;
374
                    } while (array_key_exists('"' . $column . '"', $this->orderby));
375
                    $this->selectField($clause, $column);
376
                    $clause = '"' . $column . '"';
377
                }
378
                $orderby[$clause] = $dir;
379
            }
380
            $this->orderby = $orderby;
381
        }
382
383
        return $this;
384
    }
385
386
    /**
387
     * Extract the direction part of a single-column order by clause.
388
     *
389
     * @param string $value
390
     * @param string $defaultDirection
391
     * @return array A two element array: array($column, $direction)
392
     */
393
    private function getDirectionFromString($value, $defaultDirection = null)
394
    {
395
        if (preg_match('/^(.*)(asc|desc)$/i', $value, $matches)) {
396
            $column = trim($matches[1]);
397
            $direction = strtoupper($matches[2]);
398
        } else {
399
            $column = $value;
400
            $direction = $defaultDirection ? $defaultDirection : "ASC";
401
        }
402
        return array($column, $direction);
403
    }
404
405
    /**
406
     * Returns the current order by as array if not already. To handle legacy
407
     * statements which are stored as strings. Without clauses and directions,
408
     * convert the orderby clause to something readable.
409
     *
410
     * @return array
411
     */
412
    public function getOrderBy()
413
    {
414
        $orderby = $this->orderby;
415
        if (!$orderby) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderby of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
416
            $orderby = array();
417
        }
418
419
        if (!is_array($orderby)) {
0 ignored issues
show
introduced by
The condition is_array($orderby) is always true.
Loading history...
420
            // spilt by any commas not within brackets
421
            $orderby = preg_split('/,(?![^()]*+\\))/', $orderby);
422
        }
423
424
        foreach ($orderby as $k => $v) {
425
            if (strpos($v, ' ') !== false) {
426
                unset($orderby[$k]);
427
428
                $rule = explode(' ', trim($v));
429
                $clause = $rule[0];
430
                $dir = (isset($rule[1])) ? $rule[1] : 'ASC';
431
432
                $orderby[$clause] = $dir;
433
            }
434
        }
435
436
        return $orderby;
437
    }
438
439
    /**
440
     * Reverses the order by clause by replacing ASC or DESC references in the
441
     * current order by with it's corollary.
442
     *
443
     * @return $this Self reference
444
     */
445
    public function reverseOrderBy()
446
    {
447
        $order = $this->getOrderBy();
448
        $this->orderby = array();
449
450
        foreach ($order as $clause => $dir) {
451
            $dir = (strtoupper($dir) == 'DESC') ? 'ASC' : 'DESC';
452
            $this->addOrderBy($clause, $dir);
453
        }
454
455
        return $this;
456
    }
457
458
    /**
459
     * Set a GROUP BY clause.
460
     *
461
     * @param string|array $groupby Escaped SQL statement
462
     * @return $this Self reference
463
     */
464
    public function setGroupBy($groupby)
465
    {
466
        $this->groupby = array();
467
        return $this->addGroupBy($groupby);
468
    }
469
470
    /**
471
     * Add a GROUP BY clause.
472
     *
473
     * @param string|array $groupby Escaped SQL statement
474
     * @return $this Self reference
475
     */
476
    public function addGroupBy($groupby)
477
    {
478
        if (is_array($groupby)) {
479
            $this->groupby = array_merge($this->groupby, $groupby);
480
        } elseif (!empty($groupby)) {
481
            $this->groupby[] = $groupby;
482
        }
483
484
        return $this;
485
    }
486
487
    /**
488
     * Set a HAVING clause.
489
     *
490
     * @see SQLSelect::addWhere() for syntax examples
491
     *
492
     * @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries
493
     * @param mixed $having,... Unlimited additional predicates
494
     * @return $this Self reference
495
     */
496
    public function setHaving($having)
497
    {
498
        $having = func_num_args() > 1 ? func_get_args() : $having;
499
        $this->having = array();
500
        return $this->addHaving($having);
501
    }
502
503
    /**
504
     * Add a HAVING clause
505
     *
506
     * @see SQLSelect::addWhere() for syntax examples
507
     *
508
     * @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries
509
     * @param mixed $having,... Unlimited additional predicates
510
     * @return $this Self reference
511
     */
512
    public function addHaving($having)
513
    {
514
        $having = $this->normalisePredicates(func_get_args());
515
516
        // If the function is called with an array of items
517
        $this->having = array_merge($this->having, $having);
518
519
        return $this;
520
    }
521
522
    /**
523
     * Return a list of HAVING clauses used internally.
524
     * @return array
525
     */
526
    public function getHaving()
527
    {
528
        return $this->having;
529
    }
530
531
    /**
532
     * Return a list of HAVING clauses used internally.
533
     *
534
     * @param array $parameters Out variable for parameters required for this query
535
     * @return array
536
     */
537
    public function getHavingParameterised(&$parameters)
538
    {
539
        $this->splitQueryParameters($this->having, $conditions, $parameters);
540
        return $conditions;
541
    }
542
543
    /**
544
     * Return a list of GROUP BY clauses used internally.
545
     *
546
     * @return array
547
     */
548
    public function getGroupBy()
549
    {
550
        return $this->groupby;
551
    }
552
553
    /**
554
     * Return an itemised select list as a map, where keys are the aliases, and values are the column sources.
555
     * Aliases will always be provided (if the alias is implicit, the alias value will be inferred), and won't be
556
     * quoted.
557
     * E.g., 'Title' => '"SiteTree"."Title"'.
558
     *
559
     * @return array
560
     */
561
    public function getSelect()
562
    {
563
        return $this->select;
564
    }
565
566
    /// VARIOUS TRANSFORMATIONS BELOW
567
568
    /**
569
     * Return the number of rows in this query if the limit were removed.  Useful in paged data sets.
570
     *
571
     * @param string $column
572
     * @return int
573
     */
574
    public function unlimitedRowCount($column = null)
575
    {
576
        // we can't clear the select if we're relying on its output by a HAVING clause
577
        if (count($this->having)) {
578
            $records = $this->execute();
579
            return $records->numRecords();
580
        }
581
582
        $clone = clone $this;
583
        $clone->limit = null;
584
        $clone->orderby = null;
585
586
        // Choose a default column
587
        if ($column == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $column of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
588
            if ($this->groupby) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->groupby of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
589
                // @todo Test case required here
590
                $countQuery = new SQLSelect();
591
                $countQuery->setSelect("count(*)");
592
                $countQuery->setFrom(array('(' . $clone->sql($innerParameters) . ') all_distinct'));
593
                $sql = $countQuery->sql($parameters); // $parameters should be empty
594
                $result = DB::prepared_query($sql, $innerParameters);
595
                return (int)$result->value();
596
            } else {
597
                $clone->setSelect(array("count(*)"));
598
            }
599
        } else {
600
            $clone->setSelect(array("count($column)"));
601
        }
602
603
        $clone->setGroupBy(array());
604
        return (int)$clone->execute()->value();
605
    }
606
607
    /**
608
     * Returns true if this query can be sorted by the given field.
609
     *
610
     * @param string $fieldName
611
     * @return bool
612
     */
613
    public function canSortBy($fieldName)
614
    {
615
        $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
616
617
        return isset($this->select[$fieldName]);
618
    }
619
620
621
    /**
622
     * Return the number of rows in this query, respecting limit and offset.
623
     *
624
     * @param string $column Quoted, escaped column name
625
     * @return int
626
     */
627
    public function count($column = null)
628
    {
629
        // we can't clear the select if we're relying on its output by a HAVING clause
630
        if (!empty($this->having)) {
631
            $records = $this->execute();
632
            return $records->numRecords();
633
        } elseif ($column == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $column of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
634
            // Choose a default column
635
            if ($this->groupby) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->groupby of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
636
                $column = 'DISTINCT ' . implode(", ", $this->groupby);
637
            } else {
638
                $column = '*';
639
            }
640
        }
641
642
        $clone = clone $this;
643
        $clone->select = array('Count' => "count($column)");
644
        $clone->limit = null;
645
        $clone->orderby = null;
646
        $clone->groupby = null;
647
648
        $count = (int)$clone->execute()->value();
649
        // If there's a limit set, then that limit is going to heavily affect the count
650
        if ($this->limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->limit of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
651
            if ($this->limit['limit'] !== null && $count >= ($this->limit['start'] + $this->limit['limit'])) {
652
                return $this->limit['limit'];
653
            } else {
654
                return max(0, $count - $this->limit['start']);
655
            }
656
657
        // Otherwise, the count is going to be the output of the SQL query
658
        } else {
659
            return $count;
660
        }
661
    }
662
663
    /**
664
     * Return a new SQLSelect that calls the given aggregate functions on this data.
665
     *
666
     * @param string $column An aggregate expression, such as 'MAX("Balance")', or a set of them
667
     * (as an escaped SQL statement)
668
     * @param string $alias An optional alias for the aggregate column.
669
     * @return SQLSelect A clone of this object with the given aggregate function
670
     */
671
    public function aggregate($column, $alias = null)
672
    {
673
674
        $clone = clone $this;
675
676
        // don't set an ORDER BY clause if no limit has been set. It doesn't make
677
        // sense to add an ORDER BY if there is no limit, and it will break
678
        // queries to databases like MSSQL if you do so. Note that the reason
679
        // this came up is because DataQuery::initialiseQuery() introduces
680
        // a default sort.
681
        if ($this->limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->limit of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
682
            $clone->setLimit($this->limit);
683
            $clone->setOrderBy($this->orderby);
684
        } else {
685
            $clone->setOrderBy(array());
686
        }
687
688
        $clone->setGroupBy($this->groupby);
689
        if ($alias) {
690
            $clone->setSelect(array());
691
            $clone->selectField($column, $alias);
692
        } else {
693
            $clone->setSelect($column);
694
        }
695
696
        return $clone;
697
    }
698
699
    /**
700
     * Returns a query that returns only the first row of this query
701
     *
702
     * @return SQLSelect A clone of this object with the first row only
703
     */
704
    public function firstRow()
705
    {
706
        $query = clone $this;
707
        $offset = $this->limit ? $this->limit['start'] : 0;
708
        $query->setLimit(1, $offset);
709
        return $query;
710
    }
711
712
    /**
713
     * Returns a query that returns only the last row of this query
714
     *
715
     * @return SQLSelect A clone of this object with the last row only
716
     */
717
    public function lastRow()
718
    {
719
        $query = clone $this;
720
        $offset = $this->limit ? $this->limit['start'] : 0;
721
722
        // Limit index to start in case of empty results
723
        $index = max($this->count() + $offset - 1, 0);
724
        $query->setLimit(1, $index);
725
        return $query;
726
    }
727
}
728