Passed
Push — master ( 4281e6...ce1733 )
by Alexander
03:46
created

Query::join()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 4
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Closure;
8
use Iterator;
9
use Throwable;
10
use Yiisoft\Db\Command\Command;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Expression\ExpressionInterface;
18
use Yiisoft\Arrays\ArrayHelper;
19
use Yiisoft\Cache\Dependency\Dependency;
20
21
use function array_merge;
22
use function call_user_func;
23
use function count;
24
use function gettype;
25
use function is_array;
26
use function is_int;
27
use function is_string;
28
use function key;
29
use function preg_match;
30
use function preg_split;
31
use function reset;
32
use function str_replace;
33
use function strcasecmp;
34
use function strlen;
35
use function strpos;
36
use function substr;
37
use function trim;
38
39
/**
40
 * Query represents a SELECT SQL statement in a way that is independent of DBMS.
41
 *
42
 * Query provides a set of methods to facilitate the specification of different clauses in a SELECT statement. These
43
 * methods can be chained together.
44
 *
45
 * By calling {@see createCommand()}, we can get a {@see Command} instance which can be further used to perform/execute
46
 * the DB query against a database.
47
 *
48
 * For example,
49
 *
50
 * ```php
51
 * $query = new Query;
52
 * // compose the query
53
 * $query->select('id, name')
54
 *     ->from('user')
55
 *     ->limit(10);
56
 * // build and execute the query
57
 * $rows = $query->all();
58
 * // alternatively, you can create DB command and execute it
59
 * $command = $query->createCommand();
60
 * // $command->sql returns the actual SQL
61
 * $rows = $command->queryAll();
62
 * ```
63
 *
64
 * Query internally uses the {@see QueryBuilder} class to generate the SQL statement.
65
 *
66
 * A more detailed usage guide on how to work with Query can be found in the
67
 * [guide article on Query Builder](guide:db-query-builder).
68
 *
69
 * @property string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only.
70
 */
71
class Query implements QueryInterface, ExpressionInterface
72
{
73
    use QueryTrait;
74
75
    protected array $select = [];
76
    protected ?string $selectOption = null;
77
    protected ?bool $distinct = null;
78
    protected $from;
79
    protected array $groupBy = [];
80
    protected array $join = [];
81
    protected $having;
82
    protected array $union = [];
83
    protected array $withQueries = [];
84
    protected array $params = [];
85
    private ?Dependency $queryCacheDependency = null;
86
    private ConnectionInterface $db;
87
    /** @var $queryCacheDuration bool|int|null */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $queryCacheDuration at position 0 could not be parsed: Unknown type name '$queryCacheDuration' at position 0 in $queryCacheDuration.
Loading history...
88
    private $queryCacheDuration;
89
90 1465
    public function __construct(ConnectionInterface $db)
91
    {
92 1465
        $this->db = $db;
93 1465
    }
94
95
    /**
96
     * Creates a DB command that can be used to execute this query.
97
     *
98
     * If this parameter is not given, the `db` application component will be used.
99
     *
100
     * @throws Exception|InvalidConfigException
101
     *
102
     * @return Command the created DB command instance.
103
     */
104 100
    public function createCommand(): Command
105
    {
106 100
        [$sql, $params] = $this->db->getQueryBuilder()->build($this);
0 ignored issues
show
Bug introduced by
The method getQueryBuilder() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
        [$sql, $params] = $this->db->/** @scrutinizer ignore-call */ getQueryBuilder()->build($this);
Loading history...
107
108 100
        $command = $this->db->createCommand($sql, $params);
109
110 100
        $this->setCommandCache($command);
111
112 100
        return $command;
113
    }
114
115
    /**
116
     * Prepares for building SQL.
117
     *
118
     * This method is called by {@see QueryBuilder} when it starts to build SQL from a query object. You may override
119
     * this method to do some final preparation work when converting a query into a SQL statement.
120
     *
121
     * @param QueryBuilder $builder
122
     *
123
     * @return $this a prepared query instance which will be used by {@see QueryBuilder} to build the SQL.
124
     */
125 740
    public function prepare(QueryBuilder $builder): self
0 ignored issues
show
Unused Code introduced by
The parameter $builder is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

125
    public function prepare(/** @scrutinizer ignore-unused */ QueryBuilder $builder): self

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
126
    {
127 740
        return $this;
128
    }
129
130
    /**
131
     * Starts a batch query.
132
     *
133
     * A batch query supports fetching data in batches, which can keep the memory usage under a limit.
134
     *
135
     * This method will return a {@see BatchQueryResult} object which implements the {@see Iterator} interface and can
136
     * be traversed to retrieve the data in batches.
137
     *
138
     * For example,
139
     *
140
     * ```php
141
     * $query = (new Query)->from('user');
142
     * foreach ($query->batch() as $rows) {
143
     *     // $rows is an array of 100 or fewer rows from user table
144
     * }
145
     * ```
146
     *
147
     * @param int $batchSize the number of records to be fetched in each batch.
148
     *
149
     * @return BatchQueryResult the batch query result. It implements the {@see Iterator} interface and can be
150
     * traversed to retrieve the data in batches.
151
     */
152 24
    public function batch(int $batchSize = 100): BatchQueryResult
153
    {
154 24
        return (new BatchQueryResult())
155 24
            ->query($this)
156 24
            ->batchSize($batchSize)
157 24
            ->db($this->db)
158 24
            ->each(false);
159
    }
160
161
    /**
162
     * Starts a batch query and retrieves data row by row.
163
     *
164
     * This method is similar to {@see batch()} except that in each iteration of the result, only one row of data is
165
     * returned. For example,
166
     *
167
     * ```php
168
     * $query = (new Query)->from('user');
169
     * foreach ($query->each() as $row) {
170
     * }
171
     * ```
172
     *
173
     * @param int $batchSize the number of records to be fetched in each batch.
174
     *
175
     * @return BatchQueryResult the batch query result. It implements the {@see Iterator} interface and can be
176
     * traversed to retrieve the data in batches.
177
     */
178 8
    public function each(int $batchSize = 100): BatchQueryResult
179
    {
180 8
        return (new BatchQueryResult())
181 8
            ->query($this)
182 8
            ->batchSize($batchSize)
183 8
            ->db($this->db)
184 8
            ->each(true);
185
    }
186
187
    /**
188
     * Executes the query and returns all results as an array.
189
     *
190
     * If this parameter is not given, the `db` application component will be used.
191
     *
192
     * @throws Exception|InvalidConfigException|Throwable
193
     *
194
     * @return array the query results. If the query results in nothing, an empty array will be returned.
195
     */
196 238
    public function all(): array
197
    {
198 238
        if ($this->emulateExecution) {
199 12
            return [];
200
        }
201
202 230
        $rows = $this->createCommand()->queryAll();
203
204 230
        return $this->populate($rows);
205
    }
206
207
    /**
208
     * Converts the raw query results into the format as specified by this query.
209
     *
210
     * This method is internally used to convert the data fetched from database into the format as required by this
211
     * query.
212
     *
213
     * @param array $rows the raw query result from database.
214
     *
215
     * @return array the converted query result.
216
     */
217 359
    public function populate(array $rows): array
218
    {
219 359
        if ($this->indexBy === null) {
220 343
            return $rows;
221
        }
222
223 44
        $result = [];
224
225 44
        foreach ($rows as $row) {
226 44
            $result[ArrayHelper::getValueByPath($row, $this->indexBy)] = $row;
227
        }
228
229 44
        return $result;
230
    }
231
232
    /**
233
     * Executes the query and returns a single row of result.
234
     *
235
     * If this parameter is not given, the `db` application component will be used.
236
     *
237
     * @throws Exception|InvalidConfigException|Throwable
238
     *
239
     * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query
240
     * results in nothing.
241
     */
242 301
    public function one()
243
    {
244 301
        if ($this->emulateExecution) {
245 8
            return false;
246
        }
247
248 293
        return $this->createCommand()->queryOne();
249
    }
250
251
    /**
252
     * Returns the query result as a scalar value.
253
     *
254
     * The value returned will be the first column in the first row of the query results.
255
     *
256
     * @throws Exception|InvalidConfigException|Throwable
257
     *
258
     * @return string|null|false the value of the first column in the first row of the query result. False is returned
259
     * if the query result is empty.
260
     */
261 20
    public function scalar()
262
    {
263 20
        if ($this->emulateExecution) {
264 8
            return null;
265
        }
266
267 12
        return $this->createCommand()->queryScalar();
268
    }
269
270
    /**
271
     * Executes the query and returns the first column of the result.
272
     *
273
     * If this parameter is not given, the `db` application component will be used.
274
     *
275
     * @throws Exception|InvalidConfigException|Throwable
276
     *
277
     * @return array the first column of the query result. An empty array is returned if the query results in nothing.
278
     */
279 20
    public function column(): array
280
    {
281 20
        if ($this->emulateExecution) {
282 8
            return [];
283
        }
284
285 12
        if ($this->indexBy === null) {
286 12
            return $this->createCommand()->queryColumn();
287
        }
288
289 4
        if (is_string($this->indexBy) && is_array($this->select) && count($this->select) === 1) {
290 4
            if (strpos($this->indexBy, '.') === false && count($tables = $this->getTablesUsedInFrom()) > 0) {
291 4
                $this->select[] = key($tables) . '.' . $this->indexBy;
292
            } else {
293
                $this->select[] = $this->indexBy;
294
            }
295
        }
296
297 4
        $rows = $this->createCommand()->queryAll();
298 4
        $results = [];
299 4
        foreach ($rows as $row) {
300 4
            $value = reset($row);
301
302 4
            if ($this->indexBy instanceof Closure) {
303 4
                $results[call_user_func($this->indexBy, $row)] = $value;
304
            } else {
305 4
                $results[$row[$this->indexBy]] = $value;
306
            }
307
        }
308
309 4
        return $results;
310
    }
311
312
    /**
313
     * Returns the number of records.
314
     *
315
     * @param string $q the COUNT expression. Defaults to '*'.
316
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
317
     *
318
     * @throws Exception|InvalidConfigException|Throwable
319
     *
320
     * @return mixed number of records. The result may be a string depending on the underlying database engine and to
321
     * support integer values higher than a 32bit PHP integer can handle.
322
     */
323 78
    public function count(string $q = '*')
324
    {
325 78
        if ($this->emulateExecution) {
326 8
            return 0;
327
        }
328
329 78
        return $this->queryScalar("COUNT($q)");
330
    }
331
332
    /**
333
     * Returns the sum of the specified column values.
334
     *
335
     * @param string $q the column name or expression.
336
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
337
     *
338
     * @throws Exception|InvalidConfigException|Throwable
339
     *
340
     * @return mixed the sum of the specified column values.
341
     */
342 12
    public function sum(string $q)
343
    {
344 12
        if ($this->emulateExecution) {
345 8
            return 0;
346
        }
347
348 4
        return $this->queryScalar("SUM($q)");
349
    }
350
351
    /**
352
     * Returns the average of the specified column values.
353
     *
354
     * @param string $q the column name or expression.
355
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
356
     *
357
     * @throws Throwable
358
     *
359
     * @return mixed the average of the specified column values.
360
     */
361 12
    public function average(string $q)
362
    {
363 12
        if ($this->emulateExecution) {
364 8
            return 0;
365
        }
366
367 4
        return $this->queryScalar("AVG($q)");
368
    }
369
370
    /**
371
     * Returns the minimum of the specified column values.
372
     *
373
     * @param string $q the column name or expression.
374
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
375
     *
376
     * @throws Exception|InvalidConfigException
377
     *
378
     * @return mixed the minimum of the specified column values.
379
     */
380 12
    public function min(string $q)
381
    {
382 12
        return $this->queryScalar("MIN($q)");
383
    }
384
385
    /**
386
     * Returns the maximum of the specified column values.
387
     *
388
     * @param string $q the column name or expression.
389
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
390
     *
391
     * @throws Exception|InvalidConfigException
392
     *
393
     * @return mixed the maximum of the specified column values.
394
     */
395 12
    public function max(string $q)
396
    {
397 12
        return $this->queryScalar("MAX($q)");
398
    }
399
400
    /**
401
     * Returns a value indicating whether the query result contains any row of data.
402
     *
403
     * @throws Exception|InvalidConfigException|Throwable
404
     *
405
     * @return bool whether the query result contains any row of data.
406
     */
407 20
    public function exists(): bool
408
    {
409 20
        if ($this->emulateExecution) {
410 8
            return false;
411
        }
412
413 12
        $command = $this->createCommand();
414 12
        $params = $command->getParams();
415 12
        $command->setSql($command->getDb()->getQueryBuilder()->selectExists($command->getSql()));
416 12
        $command->bindValues($params);
417
418 12
        return (bool) $command->queryScalar();
419
    }
420
421
    /**
422
     * Queries a scalar value by setting {@see select} first.
423
     *
424
     * Restores the value of select to make this query reusable.
425
     *
426
     * @param string|ExpressionInterface $selectExpression
427
     *
428
     * @throws Exception|InvalidConfigException|Throwable
429
     *
430
     * @return bool|string
431
     */
432 78
    protected function queryScalar($selectExpression)
433
    {
434 78
        if ($this->emulateExecution) {
435 8
            return null;
436
        }
437
438
        if (
439 78
            !$this->distinct
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->distinct of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
440 78
            && empty($this->groupBy)
441 78
            && empty($this->having)
442 78
            && empty($this->union)
443 78
            && empty($this->with)
444
        ) {
445 78
            $select = $this->select;
446 78
            $order = $this->orderBy;
447 78
            $limit = $this->limit;
448 78
            $offset = $this->offset;
449
450 78
            $this->select = [$selectExpression];
451 78
            $this->orderBy = [];
452 78
            $this->limit = null;
453 78
            $this->offset = null;
454
455
            try {
456 78
                $command = $this->createCommand();
457
            } catch (\Exception $e) {
458
                /** throw it later */
459
            } catch (Throwable $e) {
460
                /** throw it later */
461
            }
462
463 78
            $this->select = $select;
464 78
            $this->orderBy = $order;
465 78
            $this->limit = $limit;
466 78
            $this->offset = $offset;
467
468 78
            if (isset($e)) {
469
                throw $e;
470
            }
471
472 78
            return $command->queryScalar();
473
        }
474
475 8
        $command = static::createInstance($this->db)
476 8
            ->select([$selectExpression])
477 8
            ->from(['c' => $this])
478 8
            ->createCommand();
479
480 8
        $this->setCommandCache($command);
481
482 8
        return $command->queryScalar();
483
    }
484
485
    /**
486
     * Returns table names used in {@see from} indexed by aliases.
487
     *
488
     * Both aliases and names are enclosed into {{ and }}.
489
     *
490
     * @throws InvalidArgumentException
491
     * @throws InvalidConfigException
492
     *
493
     * @return array table names indexed by aliases
494
     */
495 112
    public function getTablesUsedInFrom(): array
496
    {
497 112
        if (empty($this->from)) {
498
            return [];
499
        }
500
501 112
        if (is_array($this->from)) {
502 108
            $tableNames = $this->from;
503 4
        } elseif (is_string($this->from)) {
504
            $tableNames = preg_split('/\s*,\s*/', trim($this->from), -1, PREG_SPLIT_NO_EMPTY);
505 4
        } elseif ($this->from instanceof Expression) {
506
            $tableNames = [$this->from];
507
        } else {
508 4
            throw new InvalidConfigException(gettype($this->from) . ' in $from is not supported.');
509
        }
510
511 108
        return $this->cleanUpTableNames($tableNames);
0 ignored issues
show
Bug introduced by
It seems like $tableNames can also be of type false; however, parameter $tableNames of Yiisoft\Db\Query\Query::cleanUpTableNames() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

511
        return $this->cleanUpTableNames(/** @scrutinizer ignore-type */ $tableNames);
Loading history...
512
    }
513
514
    /**
515
     * Clean up table names and aliases.
516
     *
517
     * Both aliases and names are enclosed into {{ and }}.
518
     *
519
     * @param array $tableNames non-empty array
520
     *
521
     * @throws InvalidArgumentException
522
     *
523
     * @return string[] table names indexed by aliases
524
     */
525 204
    protected function cleanUpTableNames(array $tableNames): array
526
    {
527 204
        $cleanedUpTableNames = [];
528 204
        foreach ($tableNames as $alias => $tableName) {
529 204
            if (is_string($tableName) && !is_string($alias)) {
530
                $pattern = <<<PATTERN
531 140
~
532
^
533
\s*
534
(
535
(?:['"`\[]|{{)
536
.*?
537
(?:['"`\]]|}})
538
|
539
\(.*?\)
540
|
541
.*?
542
)
543
(?:
544
(?:
545
    \s+
546
    (?:as)?
547
    \s*
548
)
549
(
550
   (?:['"`\[]|{{)
551
    .*?
552
    (?:['"`\]]|}})
553
    |
554
    .*?
555
)
556
)?
557
\s*
558
$
559
~iux
560
PATTERN;
561 140
                if (preg_match($pattern, $tableName, $matches)) {
562 140
                    if (isset($matches[2])) {
563 12
                        [, $tableName, $alias] = $matches;
564
                    } else {
565 136
                        $tableName = $alias = $matches[1];
566
                    }
567
                }
568
            }
569
570 204
            if ($tableName instanceof Expression) {
571 8
                if (!is_string($alias)) {
572 4
                    throw new InvalidArgumentException(
573 4
                        'To use Expression in from() method, pass it in array format with alias.'
574
                    );
575
                }
576 4
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
577 196
            } elseif ($tableName instanceof self) {
578 4
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
579
            } else {
580 192
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $this->ensureNameQuoted($tableName);
581
            }
582
        }
583
584 200
        return $cleanedUpTableNames;
585
    }
586
587
    /**
588
     * Ensures name is wrapped with {{ and }}.
589
     *
590
     * @param string $name
591
     *
592
     * @return string
593
     */
594 200
    private function ensureNameQuoted(string $name): string
595
    {
596 200
        $name = str_replace(["'", '"', '`', '[', ']'], '', $name);
597 200
        if ($name && !preg_match('/^{{.*}}$/', $name)) {
598 196
            return '{{' . $name . '}}';
599
        }
600
601 16
        return $name;
602
    }
603
604
    /**
605
     * Sets the SELECT part of the query.
606
     *
607
     * @param string|array|ExpressionInterface $columns the columns to be selected.
608
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
609
     * Columns can be prefixed with table names (e.g. "user.id") and/or contain column aliases
610
     * (e.g. "user.id AS user_id").
611
     *
612
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
613
     * column contains a DB expression). A DB expression may also be passed in form of an {@see ExpressionInterface}
614
     * object.
615
     *
616
     * Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should use an array
617
     * to specify the columns. Otherwise, the expression may be incorrectly split into several parts.
618
     *
619
     * When the columns are specified as an array, you may also use array keys as the column aliases (if a column does
620
     * not need alias, do not use a string key).
621
     * @param string|null $option additional option that should be appended to the 'SELECT' keyword. For example,
622
     * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used.
623
     *
624
     * @return $this the query object itself.
625
     */
626 565
    public function select($columns, ?string $option = null): self
627
    {
628 565
        $this->select = $this->normalizeSelect($columns);
629 565
        $this->selectOption = $option;
630
631 565
        return $this;
632
    }
633
634
    /**
635
     * Add more columns to the SELECT part of the query.
636
     *
637
     * Note, that if {@see select} has not been specified before, you should include `*` explicitly if you want to
638
     * select all remaining columns too:
639
     *
640
     * ```php
641
     * $query->addSelect(["*", "CONCAT(first_name, ' ', last_name) AS full_name"])->one();
642
     * ```
643
     *
644
     * @param string|array|ExpressionInterface $columns the columns to add to the select. See {@see select()} for more
645
     * details about the format of this parameter.
646
     *
647
     * @return $this the query object itself.
648
     *
649
     * {@see select()}
650
     */
651 12
    public function addSelect($columns): self
652
    {
653 12
        if ($this->select === null) {
654
            return $this->select($columns);
655
        }
656
657 12
        if (!is_array($this->select)) {
0 ignored issues
show
introduced by
The condition is_array($this->select) is always true.
Loading history...
658
            $this->select = $this->normalizeSelect($this->select);
659
        }
660
661 12
        $this->select = array_merge($this->select, $this->normalizeSelect($columns));
662
663 12
        return $this;
664
    }
665
666
    /**
667
     * Normalizes the SELECT columns passed to {@see select()} or {@see addSelect()}.
668
     *
669
     * @param string|array|ExpressionInterface $columns
670
     *
671
     * @return array
672
     */
673 565
    protected function normalizeSelect($columns): array
674
    {
675 565
        if ($columns instanceof ExpressionInterface) {
676 4
            $columns = [$columns];
677 565
        } elseif (!is_array($columns)) {
678 91
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
679
        }
680
681 565
        $select = [];
682 565
        foreach ($columns as $columnAlias => $columnDefinition) {
683 250
            if (is_string($columnAlias)) {
684
                /** Already in the normalized format, good for them */
685 116
                $select[$columnAlias] = $columnDefinition;
686 116
                continue;
687
            }
688 246
            if (is_string($columnDefinition)) {
689
                if (
690 242
                    preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $columnDefinition, $matches) &&
691 242
                    !preg_match('/^\d+$/', $matches[2]) &&
692 242
                    strpos($matches[2], '.') === false
693
                ) {
694
                    /** Using "columnName as alias" or "columnName alias" syntax */
695 20
                    $select[$matches[2]] = $matches[1];
696 20
                    continue;
697
                }
698 242
                if (strpos($columnDefinition, '(') === false) {
699
                    /** Normal column name, just alias it to itself to ensure it's not selected twice */
700 197
                    $select[$columnDefinition] = $columnDefinition;
701 197
                    continue;
702
                }
703
            }
704
            /** Either a string calling a function, DB expression, or sub-query */
705 85
            $select[] = $columnDefinition;
706
        }
707
708 565
        return $select;
709
    }
710
711
    /**
712
     * Sets the value indicating whether to SELECT DISTINCT or not.
713
     *
714
     * @param bool $value whether to SELECT DISTINCT or not.
715
     *
716
     * @return $this the query object itself
717
     */
718 436
    public function distinct(?bool $value = true): self
719
    {
720 436
        $this->distinct = $value;
721
722 436
        return $this;
723
    }
724
725
    /**
726
     * Sets the FROM part of the query.
727
     *
728
     * @param string|array|ExpressionInterface $tables the table(s) to be selected from. This can be either a string
729
     * (e.g. `'user'`) or an array (e.g. `['user', 'profile']`) specifying one or several table names.
730
     *
731
     * Table names can contain schema prefixes (e.g. `'public.user'`) and/or table aliases (e.g. `'user u'`).
732
     *
733
     * The method will automatically quote the table names unless it contains some parenthesis (which means the table is
734
     * given as a sub-query or DB expression).
735
     *
736
     * When the tables are specified as an array, you may also use the array keys as the table aliases (if a table does
737
     * not need alias, do not use a string key).
738
     *
739
     * Use a Query object to represent a sub-query. In this case, the corresponding array key will be used as the alias
740
     * for the sub-query.
741
     *
742
     * To specify the `FROM` part in plain SQL, you may pass an instance of {@see ExpressionInterface}.
743
     *
744
     * Here are some examples:
745
     *
746
     * ```php
747
     * // SELECT * FROM  `user` `u`, `profile`;
748
     * $query = (new \Yiisoft\Db\Query\Query)->from(['u' => 'user', 'profile']);
749
     *
750
     * // SELECT * FROM (SELECT * FROM `user` WHERE `active` = 1) `activeusers`;
751
     * $subquery = (new \Yiisoft\Db\Query\Query)->from('user')->where(['active' => true])
752
     * $query = (new \Yiisoft\Db\Query\Query)->from(['activeusers' => $subquery]);
753
     *
754
     * // subquery can also be a string with plain SQL wrapped in parenthesis
755
     * // SELECT * FROM (SELECT * FROM `user` WHERE `active` = 1) `activeusers`;
756
     * $subquery = "(SELECT * FROM `user` WHERE `active` = 1)";
757
     * $query = (new \Yiisoft\Db\Query\Query)->from(['activeusers' => $subquery]);
758
     * ```
759
     *
760
     * @return $this the query object itself
761
     */
762 708
    public function from($tables): self
763
    {
764 708
        if ($tables instanceof ExpressionInterface) {
765 13
            $tables = [$tables];
766
        }
767 708
        if (is_string($tables)) {
768 202
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
769
        }
770 708
        $this->from = $tables;
771
772 708
        return $this;
773
    }
774
775
    /**
776
     * Sets the WHERE part of the query.
777
     *
778
     * The method requires a `$condition` parameter, and optionally a `$params` parameter specifying the values to be
779
     * bound to the query.
780
     *
781
     * The `$condition` parameter should be either a string (e.g. `'id=1'`) or an array.
782
     *
783
     * {@inheritdoc}
784
     *
785
     * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
786
     * @param array $params the parameters (name => value) to be bound to the query.
787
     *
788
     * @return $this the query object itself.
789
     *
790
     * {@see andWhere()}
791
     * {@see orWhere()}
792
     * {@see QueryInterface::where()}
793
     */
794 1054
    public function where($condition, array $params = []): self
795
    {
796 1054
        $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
797 1054
        $this->addParams($params);
798
799 1054
        return $this;
800
    }
801
802
    /**
803
     * Adds an additional WHERE condition to the existing one.
804
     *
805
     * The new condition and the existing one will be joined using the `AND` operator.
806
     *
807
     * @param string|array|ExpressionInterface $condition the new WHERE condition. Please refer to {@see where()} on how
808
     * to specify this parameter.
809
     * @param array $params the parameters (name => value) to be bound to the query.
810
     *
811
     * @return $this the query object itself.
812
     *
813
     * {@see where()}
814
     * {@see orWhere()}
815
     */
816 371
    public function andWhere($condition, array $params = []): self
817
    {
818 371
        if ($this->where === null) {
819 355
            $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
820 82
        } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) {
821 4
            $this->where[] = $condition;
822
        } else {
823 82
            $this->where = ['and', $this->where, $condition];
824
        }
825
826 371
        $this->addParams($params);
827
828 371
        return $this;
829
    }
830
831
    /**
832
     * Adds an additional WHERE condition to the existing one.
833
     *
834
     * The new condition and the existing one will be joined using the `OR` operator.
835
     *
836
     * @param string|array|ExpressionInterface $condition the new WHERE condition. Please refer to {@see where()} on how
837
     * to specify this parameter.
838
     * @param array $params the parameters (name => value) to be bound to the query.
839
     *
840
     * @return $this the query object itself.
841
     *
842
     * {@see where()}
843
     * {@see andWhere()}
844
     */
845 5
    public function orWhere($condition, array $params = []): self
846
    {
847 5
        if ($this->where === null) {
848
            $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
849
        } else {
850 5
            $this->where = ['or', $this->where, $condition];
851
        }
852
853 5
        $this->addParams($params);
854
855 5
        return $this;
856
    }
857
858
    /**
859
     * Adds a filtering condition for a specific column and allow the user to choose a filter operator.
860
     *
861
     * It adds an additional WHERE condition for the given field and determines the comparison operator based on the
862
     * first few characters of the given value.
863
     *
864
     * The condition is added in the same way as in {@see andFilterWhere} so {@see isEmpty()|empty values} are ignored.
865
     * The new condition and the existing one will be joined using the `AND` operator.
866
     *
867
     * The comparison operator is intelligently determined based on the first few characters in the given value.
868
     * In particular, it recognizes the following operators if they appear as the leading characters in the given value:
869
     *
870
     * - `<`: the column must be less than the given value.
871
     * - `>`: the column must be greater than the given value.
872
     * - `<=`: the column must be less than or equal to the given value.
873
     * - `>=`: the column must be greater than or equal to the given value.
874
     * - `<>`: the column must not be the same as the given value.
875
     * - `=`: the column must be equal to the given value.
876
     * - If none of the above operators is detected, the `$defaultOperator` will be used.
877
     *
878
     * @param string $name the column name.
879
     * @param string|null $value the column value optionally prepended with the comparison operator.
880
     * @param string $defaultOperator The operator to use, when no operator is given in `$value`.
881
     * Defaults to `=`, performing an exact match.
882
     *
883
     * @return $this the query object itself.
884
     */
885 4
    public function andFilterCompare(string $name, ?string $value, string $defaultOperator = '='): self
886
    {
887 4
        if (preg_match('/^(<>|>=|>|<=|<|=)/', (string) $value, $matches)) {
888 4
            $operator = $matches[1];
889 4
            $value = substr($value, strlen($operator));
890
        } else {
891 4
            $operator = $defaultOperator;
892
        }
893
894 4
        return $this->andFilterWhere([$operator, $name, $value]);
895
    }
896
897
    /**
898
     * Appends a JOIN part to the query.
899
     *
900
     * The first parameter specifies what type of join it is.
901
     *
902
     * @param string $type  the type of join, such as INNER JOIN, LEFT JOIN.
903
     * @param string|array $table the table to be joined.
904
     * Use a string to represent the name of the table to be joined.
905
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
906
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
907
     * given as a sub-query or DB expression).
908
     * Use an array to represent joining with a sub-query. The array must contain only one element.
909
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
910
     * alias for the sub-query.
911
     * @param string|array $on the join condition that should appear in the ON part.
912
     * Please refer to {@see where()} on how to specify this parameter.
913
     *
914
     * Note that the array format of {@see where()} is designed to match columns to values instead of columns to
915
     * columns, so the following would **not** work as expected: `['post.author_id' => 'user.id']`, it would match the
916
     * `post.author_id` column value against the string `'user.id'`.
917
     *
918
     * It is recommended to use the string syntax here which is more suited for a join:
919
     *
920
     * ```php
921
     * 'post.author_id = user.id'
922
     * ```
923
     * @param array $params the parameters (name => value) to be bound to the query.
924
     *
925
     * @return $this the query object itself.
926
     */
927 56
    public function join(string $type, $table, $on = '', array $params = []): self
928
    {
929 56
        $this->join[] = [$type, $table, $on];
930
931 56
        return $this->addParams($params);
932
    }
933
934
    /**
935
     * Appends an INNER JOIN part to the query.
936
     *
937
     * @param string|array $table the table to be joined.
938
     * Use a string to represent the name of the table to be joined.
939
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
940
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
941
     * given as a sub-query or DB expression).
942
     * Use an array to represent joining with a sub-query. The array must contain only one element.
943
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
944
     * alias for the sub-query.
945
     * @param string|array $on the join condition that should appear in the ON part.
946
     * Please refer to {@see join()} on how to specify this parameter.
947
     * @param array $params the parameters (name => value) to be bound to the query.
948
     *
949
     * @return $this the query object itself.
950
     */
951 4
    public function innerJoin($table, $on = '', array $params = []): self
952
    {
953 4
        $this->join[] = ['INNER JOIN', $table, $on];
954
955 4
        return $this->addParams($params);
956
    }
957
958
    /**
959
     * Appends a LEFT OUTER JOIN part to the query.
960
     *
961
     * @param string|array $table the table to be joined.
962
     * Use a string to represent the name of the table to be joined.
963
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
964
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
965
     * given as a sub-query or DB expression).
966
     * Use an array to represent joining with a sub-query. The array must contain only one element.
967
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
968
     * alias for the sub-query.
969
     * @param string|array $on the join condition that should appear in the ON part.
970
     * Please refer to {@see join()} on how to specify this parameter.
971
     * @param array $params the parameters (name => value) to be bound to the query.
972
     *
973
     * @return $this the query object itself.
974
     */
975 4
    public function leftJoin($table, $on = '', array $params = []): self
976
    {
977 4
        $this->join[] = ['LEFT JOIN', $table, $on];
978
979 4
        return $this->addParams($params);
980
    }
981
982
    /**
983
     * Appends a RIGHT OUTER JOIN part to the query.
984
     *
985
     * @param string|array $table the table to be joined.
986
     * Use a string to represent the name of the table to be joined.
987
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
988
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
989
     * given as a sub-query or DB expression).
990
     * Use an array to represent joining with a sub-query. The array must contain only one element.
991
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
992
     * alias for the sub-query.
993
     * @param string|array $on the join condition that should appear in the ON part.
994
     * Please refer to {@see join()} on how to specify this parameter.
995
     * @param array $params the parameters (name => value) to be bound to the query.
996
     *
997
     * @return $this the query object itself.
998
     */
999
    public function rightJoin($table, $on = '', array $params = []): self
1000
    {
1001
        $this->join[] = ['RIGHT JOIN', $table, $on];
1002
1003
        return $this->addParams($params);
1004
    }
1005
1006
    /**
1007
     * Sets the GROUP BY part of the query.
1008
     *
1009
     * @param string|array|ExpressionInterface $columns the columns to be grouped by.
1010
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
1011
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
1012
     * column contains a DB expression).
1013
     *
1014
     * Note that if your group-by is an expression containing commas, you should always use an array to represent the
1015
     * group-by information. Otherwise, the method will not be able to correctly determine the group-by columns.
1016
     *
1017
     * {@see ExpressionInterface} object can be passed to specify the GROUP BY part explicitly in plain SQL.
1018
     * {@see ExpressionInterface} object can be passed as well.
1019
     *
1020
     * @return $this the query object itself.
1021
     *
1022
     * {@see addGroupBy()}
1023
     */
1024 444
    public function groupBy($columns): self
1025
    {
1026 444
        if ($columns instanceof ExpressionInterface) {
1027 4
            $columns = [$columns];
1028 444
        } elseif (!is_array($columns)) {
1029 20
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1030
        }
1031 444
        $this->groupBy = $columns;
1032
1033 444
        return $this;
1034
    }
1035
1036
    /**
1037
     * Adds additional group-by columns to the existing ones.
1038
     *
1039
     * @param string|array $columns additional columns to be grouped by.
1040
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
1041
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
1042
     * column contains a DB expression).
1043
     *
1044
     * Note that if your group-by is an expression containing commas, you should always use an array to represent the
1045
     * group-by information. Otherwise, the method will not be able to correctly determine the group-by columns.
1046
     *
1047
     * {@see Expression} object can be passed to specify the GROUP BY part explicitly in plain SQL.
1048
     * {@see ExpressionInterface} object can be passed as well.
1049
     *
1050
     * @return $this the query object itself
1051
     *
1052
     * {@see groupBy()}
1053
     */
1054 4
    public function addGroupBy($columns): self
1055
    {
1056 4
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
1057
            $columns = [$columns];
1058 4
        } elseif (!is_array($columns)) {
1059 4
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1060
        }
1061 4
        if ($this->groupBy === null) {
1062
            $this->groupBy = $columns;
1063
        } else {
1064 4
            $this->groupBy = array_merge($this->groupBy, $columns);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1064
            $this->groupBy = array_merge($this->groupBy, /** @scrutinizer ignore-type */ $columns);
Loading history...
1065
        }
1066
1067 4
        return $this;
1068
    }
1069
1070
    /**
1071
     * Sets the HAVING part of the query.
1072
     *
1073
     * @param string|array|ExpressionInterface $condition the conditions to be put after HAVING.
1074
     * Please refer to {@see where()} on how to specify this parameter.
1075
     * @param array $params the parameters (name => value) to be bound to the query.
1076
     *
1077
     * @return $this the query object itself.
1078
     *
1079
     * {@see andHaving()}
1080
     * {@see orHaving()}
1081
     */
1082 444
    public function having($condition, array $params = []): self
1083
    {
1084 444
        $this->having = $condition;
1085 444
        $this->addParams($params);
1086
1087 444
        return $this;
1088
    }
1089
1090
    /**
1091
     * Adds an additional HAVING condition to the existing one.
1092
     * The new condition and the existing one will be joined using the `AND` operator.
1093
     *
1094
     * @param string|array|ExpressionInterface $condition the new HAVING condition. Please refer to {@see where()}
1095
     * on how to specify this parameter.
1096
     * @param array $params the parameters (name => value) to be bound to the query.
1097
     *
1098
     * @return $this the query object itself.
1099
     *
1100
     * {@see having()}
1101
     * {@see orHaving()}
1102
     */
1103 4
    public function andHaving($condition, array $params = []): self
1104
    {
1105 4
        if ($this->having === null) {
1106
            $this->having = $condition;
1107
        } else {
1108 4
            $this->having = ['and', $this->having, $condition];
1109
        }
1110
1111 4
        $this->addParams($params);
1112
1113 4
        return $this;
1114
    }
1115
1116
    /**
1117
     * Adds an additional HAVING condition to the existing one.
1118
     *
1119
     * The new condition and the existing one will be joined using the `OR` operator.
1120
     *
1121
     * @param string|array|ExpressionInterface $condition the new HAVING condition. Please refer to {@see where()}
1122
     * on how to specify this parameter.
1123
     * @param array $params the parameters (name => value) to be bound to the query.
1124
     *
1125
     * @return $this the query object itself.
1126
     *
1127
     * {@see having()}
1128
     * {@see andHaving()}
1129
     */
1130 4
    public function orHaving($condition, $params = []): self
1131
    {
1132 4
        if ($this->having === null) {
1133
            $this->having = $condition;
1134
        } else {
1135 4
            $this->having = ['or', $this->having, $condition];
1136
        }
1137
1138 4
        $this->addParams($params);
1139
1140 4
        return $this;
1141
    }
1142
1143
    /**
1144
     * Sets the HAVING part of the query but ignores {@see isEmpty()|empty operands}.
1145
     *
1146
     * This method is similar to {@see having()}. The main difference is that this method will remove
1147
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1148
     * based on filter values entered by users.
1149
     *
1150
     * The following code shows the difference between this method and {@see having()}:
1151
     *
1152
     * ```php
1153
     * // HAVING `age`=:age
1154
     * $query->filterHaving(['name' => null, 'age' => 20]);
1155
     * // HAVING `age`=:age
1156
     * $query->having(['age' => 20]);
1157
     * // HAVING `name` IS NULL AND `age`=:age
1158
     * $query->having(['name' => null, 'age' => 20]);
1159
     * ```
1160
     *
1161
     * Note that unlike {@see having()}, you cannot pass binding parameters to this method.
1162
     *
1163
     * @param array $condition the conditions that should be put in the HAVING part.
1164
     * See {@see having()} on how to specify this parameter.
1165
     *
1166
     * @throws NotSupportedException
1167
     *
1168
     * @return $this the query object itself.
1169
     *
1170
     * {@see having()}
1171
     * {@see andFilterHaving()}
1172
     * {@see orFilterHaving()}
1173
     */
1174 8
    public function filterHaving(array $condition): self
1175
    {
1176 8
        $condition = $this->filterCondition($condition);
1177
1178 8
        if ($condition !== []) {
1179 8
            $this->having($condition);
1180
        }
1181
1182 8
        return $this;
1183
    }
1184
1185
    /**
1186
     * Adds an additional HAVING condition to the existing one but ignores {@see isEmpty()|empty operands}.
1187
     *
1188
     * The new condition and the existing one will be joined using the `AND` operator.
1189
     *
1190
     * This method is similar to {@see andHaving()}. The main difference is that this method will remove
1191
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1192
     * based on filter values entered by users.
1193
     *
1194
     * @param array $condition the new HAVING condition. Please refer to {@see having()} on how to specify this
1195
     * parameter.
1196
     *
1197
     * @throws NotSupportedException
1198
     *
1199
     * @return $this the query object itself.
1200
     *
1201
     * {@see filterHaving()}
1202
     * {@see orFilterHaving()}
1203
     */
1204 8
    public function andFilterHaving(array $condition): self
1205
    {
1206 8
        $condition = $this->filterCondition($condition);
1207
1208 8
        if ($condition !== []) {
1209
            $this->andHaving($condition);
1210
        }
1211
1212 8
        return $this;
1213
    }
1214
1215
    /**
1216
     * Adds an additional HAVING condition to the existing one but ignores {@see isEmpty()|empty operands}.
1217
     *
1218
     * The new condition and the existing one will be joined using the `OR` operator.
1219
     *
1220
     * This method is similar to {@see orHaving()}. The main difference is that this method will remove
1221
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1222
     * based on filter values entered by users.
1223
     *
1224
     * @param array $condition the new HAVING condition. Please refer to {@see having()} on how to specify this
1225
     * parameter.
1226
     *
1227
     * @throws NotSupportedException
1228
     *
1229
     * @return $this the query object itself.
1230
     *
1231
     * {@see filterHaving()}
1232
     * {@see andFilterHaving()}
1233
     */
1234 8
    public function orFilterHaving(array $condition): self
1235
    {
1236 8
        $condition = $this->filterCondition($condition);
1237
1238 8
        if ($condition !== []) {
1239
            $this->orHaving($condition);
1240
        }
1241
1242 8
        return $this;
1243
    }
1244
1245
    /**
1246
     * Appends a SQL statement using UNION operator.
1247
     *
1248
     * @param string|Query $sql the SQL statement to be appended using UNION.
1249
     * @param bool $all TRUE if using UNION ALL and FALSE if using UNION.
1250
     *
1251
     * @return $this the query object itself.
1252
     */
1253 14
    public function union($sql, $all = false): self
1254
    {
1255 14
        $this->union[] = ['query' => $sql, 'all' => $all];
1256
1257 14
        return $this;
1258
    }
1259
1260
    /**
1261
     * Sets the parameters to be bound to the query.
1262
     *
1263
     * @param array $params list of query parameter values indexed by parameter placeholders.
1264
     * For example, `[':name' => 'Dan', ':age' => 31]`.
1265
     *
1266
     * @return $this the query object itself.
1267
     *
1268
     * {@see addParams()}
1269
     */
1270 448
    public function params(array $params): self
1271
    {
1272 448
        $this->params = $params;
1273
1274 448
        return $this;
1275
    }
1276
1277
    /**
1278
     * Adds additional parameters to be bound to the query.
1279
     *
1280
     * @param array $params list of query parameter values indexed by parameter placeholders.
1281
     * For example, `[':name' => 'Dan', ':age' => 31]`.
1282
     *
1283
     * @return $this the query object itself.
1284
     *
1285
     * {@see params()}
1286
     */
1287 1131
    public function addParams(array $params): self
1288
    {
1289 1131
        if (!empty($params)) {
1290 51
            if (empty($this->params)) {
1291 51
                $this->params = $params;
1292
            } else {
1293 8
                foreach ($params as $name => $value) {
1294 8
                    if (is_int($name)) {
1295
                        $this->params[] = $value;
1296
                    } else {
1297 8
                        $this->params[$name] = $value;
1298
                    }
1299
                }
1300
            }
1301
        }
1302
1303 1131
        return $this;
1304
    }
1305
1306
    /**
1307
     * Enables query cache for this Query.
1308
     *
1309
     * @param int|true $duration the number of seconds that query results can remain valid in cache.
1310
     * Use 0 to indicate that the cached data will never expire.
1311
     * Use a negative number to indicate that query cache should not be used.
1312
     * Use boolean `true` to indicate that {@see ConnectionInterface::queryCacheDuration} should be used.
1313
     * Defaults to `true`.
1314
     * @param Dependency|null $dependency the cache dependency associated with the cached result.
1315
     *
1316
     * @return $this the Query object itself.
1317
     */
1318 4
    public function cache($duration = true, ?Dependency $dependency = null): self
1319
    {
1320 4
        $this->queryCacheDuration = $duration;
1321 4
        $this->queryCacheDependency = $dependency;
1322
1323 4
        return $this;
1324
    }
1325
1326
    /**
1327
     * Disables query cache for this Query.
1328
     *
1329
     * @return $this the Query object itself.
1330
     */
1331 4
    public function noCache(): self
1332
    {
1333 4
        $this->queryCacheDuration = -1;
1334
1335 4
        return $this;
1336
    }
1337
1338
    /**
1339
     * Sets $command cache, if this query has enabled caching.
1340
     *
1341
     * @param Command $command
1342
     *
1343
     * @return Command
1344
     */
1345 476
    protected function setCommandCache(Command $command): Command
1346
    {
1347 476
        if ($this->queryCacheDuration !== null || $this->queryCacheDependency !== null) {
1348 4
            $duration = $this->queryCacheDuration === true ? null : $this->queryCacheDuration;
1349 4
            $command->cache($duration, $this->queryCacheDependency);
1350
        }
1351
1352 476
        return $command;
1353
    }
1354
1355
    /**
1356
     * Creates a new Query object and copies its property values from an existing one.
1357
     *
1358
     * The properties being copies are the ones to be used by query builders.
1359
     *
1360
     * @param ConnectionInterface $db the source query object.
1361
     * @param Query $from the source query object.
1362
     *
1363
     * @return $this the new Query object.
1364
     */
1365 432
    public static function create(ConnectionInterface $db, Query $from): self
1366
    {
1367 432
        return (new self($db))
1368 432
            ->where($from->where)
1369 432
            ->limit($from->limit)
1370 432
            ->offset($from->offset)
1371 432
            ->orderBy($from->orderBy)
1372 432
            ->indexBy($from->indexBy)
1373 432
            ->select($from->select)
1374 432
            ->selectOption($from->selectOption)
1375 432
            ->distinct($from->distinct)
1376 432
            ->from($from->from)
1377 432
            ->groupBy($from->groupBy)
1378 432
            ->setJoin($from->join)
1379 432
            ->having($from->having)
1380 432
            ->setUnion($from->union)
1381 432
            ->params($from->params)
1382 432
            ->withQueries($from->withQueries);
1383
    }
1384
1385
    /**
1386
     * Returns the SQL representation of Query.
1387
     *
1388
     * @return string
1389
     */
1390
    public function __toString(): string
1391
    {
1392
        return serialize($this);
1393
    }
1394
1395 1168
    public function getDistinct(): ?bool
1396
    {
1397 1168
        return $this->distinct;
1398
    }
1399
1400 1193
    public function getFrom()
1401
    {
1402 1193
        return $this->from;
1403
    }
1404
1405 1168
    public function getGroupBy(): array
1406
    {
1407 1168
        return $this->groupBy;
1408
    }
1409
1410 1176
    public function getHaving()
1411
    {
1412 1176
        return $this->having;
1413
    }
1414
1415 1168
    public function getJoin(): array
1416
    {
1417 1168
        return $this->join;
1418
    }
1419
1420 1197
    public function getParams(): array
1421
    {
1422 1197
        return $this->params;
1423
    }
1424
1425 1184
    public function getSelect(): array
1426
    {
1427 1184
        return $this->select;
1428
    }
1429
1430 1168
    public function getSelectOption(): ?string
1431
    {
1432 1168
        return $this->selectOption;
1433
    }
1434
1435 1164
    public function getUnion(): array
1436
    {
1437 1164
        return $this->union;
1438
    }
1439
1440 1164
    public function getWithQueries(): array
1441
    {
1442 1164
        return $this->withQueries;
1443
    }
1444
1445
    /**
1446
     * Prepends a SQL statement using WITH syntax.
1447
     *
1448
     * @param string|Query $query the SQL statement to be appended using UNION.
1449
     * @param string $alias query alias in WITH construction.
1450
     * @param bool $recursive TRUE if using WITH RECURSIVE and FALSE if using WITH.
1451
     *
1452
     * @return $this the query object itself.
1453
     */
1454 12
    public function withQuery($query, string $alias, bool $recursive = false): self
1455
    {
1456 12
        $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
1457
1458 12
        return $this;
1459
    }
1460
1461
    /**
1462
     * This function can be overridden to customize the returned class.
1463
     *
1464
     * @param ConnectionInterface $value
1465
     *
1466
     * @return $this
1467
     */
1468 8
    protected static function createInstance(ConnectionInterface $value): self
1469
    {
1470 8
        return new self($value);
1471
    }
1472
1473 432
    public function selectOption(?string $value): self
1474
    {
1475 432
        $this->selectOption = $value;
1476
1477 432
        return $this;
1478
    }
1479
1480 432
    public function setJoin($value): self
1481
    {
1482 432
        $this->join = $value;
1483
1484 432
        return $this;
1485
    }
1486
1487 432
    public function setUnion($value): self
1488
    {
1489 432
        $this->union = $value;
1490
1491 432
        return $this;
1492
    }
1493
1494 432
    public function withQueries(array $value): self
1495
    {
1496 432
        $this->withQueries = $value;
1497
1498 432
        return $this;
1499
    }
1500
}
1501