Passed
Pull Request — master (#174)
by Wilmer
11:06
created

Query::getUnion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
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 Yiisoft\Db\Command\Command;
8
use Yiisoft\Db\Connection\ConnectionInterface;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\Expression;
14
use Yiisoft\Db\Expression\ExpressionInterface;
15
use Yiisoft\Arrays\ArrayHelper;
16
use Yiisoft\Cache\Dependency\Dependency;
17
18
/**
19
 * Query represents a SELECT SQL statement in a way that is independent of DBMS.
20
 *
21
 * Query provides a set of methods to facilitate the specification of different clauses in a SELECT statement. These
22
 * methods can be chained together.
23
 *
24
 * By calling {@see createCommand()}, we can get a {@see Command} instance which can be further used to perform/execute
25
 * the DB query against a database.
26
 *
27
 * For example,
28
 *
29
 * ```php
30
 * $query = new Query;
31
 * // compose the query
32
 * $query->select('id, name')
33
 *     ->from('user')
34
 *     ->limit(10);
35
 * // build and execute the query
36
 * $rows = $query->all();
37
 * // alternatively, you can create DB command and execute it
38
 * $command = $query->createCommand();
39
 * // $command->sql returns the actual SQL
40
 * $rows = $command->queryAll();
41
 * ```
42
 *
43
 * Query internally uses the {@see QueryBuilder} class to generate the SQL statement.
44
 *
45
 * A more detailed usage guide on how to work with Query can be found in the
46
 * [guide article on Query Builder](guide:db-query-builder).
47
 *
48
 * @property string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only.
49
 */
50
class Query implements QueryInterface, ExpressionInterface
51
{
52
    use QueryTrait;
53
54
    protected array $select = [];
55
    protected ?string $selectOption = null;
56
    protected ?bool $distinct = null;
57
    protected $from;
58
    protected array $groupBy = [];
59
    protected array $join = [];
60
    protected $having;
61
    protected array $union = [];
62
    protected array $withQueries = [];
63
    protected array $params = [];
64
    private $queryCacheDuration;
65
    private ?Dependency $queryCacheDependency = null;
66
    private ?ConnectionInterface $db = null;
67
68 233
    public function __construct(ConnectionInterface $db)
69
    {
70 233
        $this->db = $db;
71 233
    }
72
73
    /**
74
     * Creates a DB command that can be used to execute this query.
75
     *
76
     * If this parameter is not given, the `db` application component will be used.
77
     *
78
     * @throws InvalidConfigException
79
     * @throws Exception
80
     * @throws NotSupportedException
81
     *
82
     * @return Command the created DB command instance.
83
     */
84 19
    public function createCommand(): Command
85
    {
86 19
        [$sql, $params] = $this->db->getQueryBuilder()->build($this);
0 ignored issues
show
Bug introduced by
The method getQueryBuilder() does not exist on null. ( Ignorable by Annotation )

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

86
        [$sql, $params] = $this->db->/** @scrutinizer ignore-call */ getQueryBuilder()->build($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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

86
        [$sql, $params] = $this->db->/** @scrutinizer ignore-call */ getQueryBuilder()->build($this);
Loading history...
87
88 19
        $command = $this->db->createCommand($sql, $params);
89
90 19
        $this->setCommandCache($command);
91
92 19
        return $command;
93
    }
94
95
    /**
96
     * Prepares for building SQL.
97
     *
98
     * This method is called by {@see QueryBuilder} when it starts to build SQL from a query object.
99
     * You may override this method to do some final preparation work when converting a query into a SQL statement.
100
     *
101
     * @param QueryBuilder $builder
102
     *
103
     * @return self a prepared query instance which will be used by {@see QueryBuilder} to build the SQL.
104
     */
105 211
    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

105
    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...
106
    {
107 211
        return $this;
108
    }
109
110
    /**
111
     * Starts a batch query.
112
     *
113
     * A batch query supports fetching data in batches, which can keep the memory usage under a limit.
114
     *
115
     * This method will return a {@see BatchQueryResult} object which implements the {@see \Iterator} interface and can
116
     * be traversed to retrieve the data in batches.
117
     *
118
     * For example,
119
     *
120
     * ```php
121
     * $query = (new Query)->from('user');
122
     * foreach ($query->batch() as $rows) {
123
     *     // $rows is an array of 100 or fewer rows from user table
124
     * }
125
     * ```
126
     *
127
     * @param int $batchSize the number of records to be fetched in each batch.
128
     *
129
     * @return BatchQueryResult the batch query result. It implements the {@see \Iterator} interface and can be
130
     * traversed to retrieve the data in batches.
131
     */
132 3
    public function batch(int $batchSize = 100): BatchQueryResult
133
    {
134 3
        $bq = (new BatchQueryResult())
135 3
            ->query($this)
136 3
            ->batchSize($batchSize)
137 3
            ->db($this->db)
0 ignored issues
show
Bug introduced by
It seems like $this->db can also be of type null; however, parameter $value of Yiisoft\Db\Query\BatchQueryResult::db() does only seem to accept Yiisoft\Db\Connection\ConnectionInterface, 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

137
            ->db(/** @scrutinizer ignore-type */ $this->db)
Loading history...
138 3
            ->each(false);
139
140 3
        return $bq;
141
    }
142
143
    /**
144
     * Starts a batch query and retrieves data row by row.
145
     *
146
     * This method is similar to {@see batch()} except that in each iteration of the result, only one row of data is
147
     * returned. For example,
148
     *
149
     * ```php
150
     * $query = (new Query)->from('user');
151
     * foreach ($query->each() as $row) {
152
     * }
153
     * ```
154
     *
155
     * @param int $batchSize the number of records to be fetched in each batch.
156
     *
157
     * @return BatchQueryResult the batch query result. It implements the {@see \Iterator} interface and can be
158
     * traversed to retrieve the data in batches.
159
     */
160 1
    public function each(int $batchSize = 100): BatchQueryResult
161
    {
162 1
        $bq = (new BatchQueryResult())
163 1
            ->query($this)
164 1
            ->batchSize($batchSize)
165 1
            ->db($this->db)
0 ignored issues
show
Bug introduced by
It seems like $this->db can also be of type null; however, parameter $value of Yiisoft\Db\Query\BatchQueryResult::db() does only seem to accept Yiisoft\Db\Connection\ConnectionInterface, 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

165
            ->db(/** @scrutinizer ignore-type */ $this->db)
Loading history...
166 1
            ->each(true);
167
168 1
        return $bq;
169
    }
170
171
    /**
172
     * Executes the query and returns all results as an array.
173
     *
174
     * If this parameter is not given, the `db` application component will be used.
175
     *
176
     * @throws Exception
177
     * @throws InvalidArgumentException
178
     * @throws InvalidConfigException
179
     * @throws NotSupportedException
180
     *
181
     * @return array the query results. If the query results in nothing, an empty array will be returned.
182
     */
183 1
    public function all(): array
184
    {
185 1
        if ($this->emulateExecution) {
186 1
            return [];
187
        }
188
189
        $rows = $this->createCommand()->queryAll();
190
191
        return $this->populate($rows);
192
    }
193
194
    /**
195
     * Converts the raw query results into the format as specified by this query.
196
     *
197
     * This method is internally used to convert the data fetched from database into the format as required by this
198
     * query.
199
     *
200
     * @param array $rows the raw query result from database.
201
     *
202
     * @return array the converted query result.
203
     */
204 3
    public function populate(array $rows): array
205
    {
206 3
        if ($this->indexBy === null) {
207 2
            return $rows;
208
        }
209
210 2
        $result = [];
211
212 2
        foreach ($rows as $row) {
213 2
            $result[ArrayHelper::getValueByPath($row, $this->indexBy)] = $row;
214
        }
215
216 2
        return $result;
217
    }
218
219
    /**
220
     * Executes the query and returns a single row of result.
221
     *
222
     * If this parameter is not given, the `db` application component will be used.
223
     *
224
     * @throws Exception
225
     * @throws InvalidArgumentException
226
     * @throws InvalidConfigException
227
     * @throws NotSupportedException
228
     *
229
     * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query
230
     * results in nothing.
231
     */
232 9
    public function one()
233
    {
234 9
        if ($this->emulateExecution) {
235 1
            return false;
236
        }
237
238 8
        return $this->createCommand()->queryOne();
239
    }
240
241
    /**
242
     * Returns the query result as a scalar value.
243
     *
244
     * The value returned will be the first column in the first row of the query results.
245
     *
246
     * @throws Exception
247
     * @throws InvalidArgumentException
248
     * @throws InvalidConfigException
249
     * @throws NotSupportedException
250
     *
251
     * @return string|null|false the value of the first column in the first row of the query result. False is returned
252
     * if the query result is empty.
253
     */
254 2
    public function scalar()
255
    {
256 2
        if ($this->emulateExecution) {
257 1
            return null;
258
        }
259
260 1
        return $this->createCommand()->queryScalar();
261
    }
262
263
    /**
264
     * Executes the query and returns the first column of the result.
265
     *
266
     * If this parameter is not given, the `db` application component will be used.
267
     *
268
     * @throws Exception
269
     * @throws InvalidArgumentException
270
     * @throws InvalidConfigException
271
     * @throws NotSupportedException
272
     *
273
     * @return array the first column of the query result. An empty array is returned if the query results in nothing.
274
     */
275 3
    public function column(): array
276
    {
277 3
        if ($this->emulateExecution) {
278 1
            return [];
279
        }
280
281 2
        if ($this->indexBy === null) {
282 2
            return $this->createCommand()->queryColumn();
283
        }
284
285 1
        if (\is_string($this->indexBy) && \is_array($this->select) && \count($this->select) === 1) {
286 1
            if (\strpos($this->indexBy, '.') === false && \count($tables = $this->getTablesUsedInFrom()) > 0) {
287 1
                $this->select[] = \key($tables) . '.' . $this->indexBy;
288
            } else {
289
                $this->select[] = $this->indexBy;
290
            }
291
        }
292
293 1
        $rows = $this->createCommand()->queryAll();
294 1
        $results = [];
295 1
        foreach ($rows as $row) {
296 1
            $value = \reset($row);
297
298 1
            if ($this->indexBy instanceof \Closure) {
299 1
                $results[\call_user_func($this->indexBy, $row)] = $value;
300
            } else {
301 1
                $results[$row[$this->indexBy]] = $value;
302
            }
303
        }
304
305 1
        return $results;
306
    }
307
308
    /**
309
     * Returns the number of records.
310
     *
311
     * @param string $q the COUNT expression. Defaults to '*'.
312
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
313
     *
314
     * @throws Exception
315
     * @throws InvalidArgumentException
316
     * @throws InvalidConfigException
317
     * @throws NotSupportedException
318
     *
319
     * @return mixed number of records. The result may be a string depending on the underlying database engine and to
320
     * support integer values higher than a 32bit PHP integer can handle.
321
     */
322 4
    public function count(string $q = '*')
323
    {
324 4
        if ($this->emulateExecution) {
325 1
            return 0;
326
        }
327
328 4
        return $this->queryScalar("COUNT($q)");
329
    }
330
331
    /**
332
     * Returns the sum of the specified column values.
333
     *
334
     * @param string $q the column name or expression.
335
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
336
     *
337
     * @throws Exception
338
     * @throws InvalidArgumentException
339
     * @throws InvalidConfigException
340
     * @throws NotSupportedException
341
     *
342
     * @return mixed the sum of the specified column values.
343
     */
344 1
    public function sum(string $q)
345
    {
346 1
        if ($this->emulateExecution) {
347 1
            return 0;
348
        }
349
350
        return $this->queryScalar("SUM($q)");
351
    }
352
353
    /**
354
     * Returns the average of the specified column values.
355
     *
356
     * @param string $q the column name or expression.
357
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
358
     *
359
     * @throws \Throwable
360
     *
361
     * @return mixed the average of the specified column values.
362
     */
363 1
    public function average(string $q)
364
    {
365 1
        if ($this->emulateExecution) {
366 1
            return 0;
367
        }
368
369
        return $this->queryScalar("AVG($q)");
370
    }
371
372
    /**
373
     * Returns the minimum of the specified column values.
374
     *
375
     * @param string $q the column name or expression.
376
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
377
     *
378
     * @throws Exception
379
     * @throws InvalidArgumentException
380
     * @throws InvalidConfigException
381
     * @throws NotSupportedException
382
     *
383
     * @return mixed the minimum of the specified column values.
384
     */
385 1
    public function min(string $q)
386
    {
387 1
        return $this->queryScalar("MIN($q)");
388
    }
389
390
    /**
391
     * Returns the maximum of the specified column values.
392
     *
393
     * @param string $q the column name or expression.
394
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
395
     *
396
     * @throws Exception
397
     * @throws InvalidArgumentException
398
     * @throws InvalidConfigException
399
     * @throws NotSupportedException
400
     *
401
     * @return mixed the maximum of the specified column values.
402
     */
403 1
    public function max(string $q)
404
    {
405 1
        return $this->queryScalar("MAX($q)");
406
    }
407
408
    /**
409
     * Returns a value indicating whether the query result contains any row of data.
410
     *
411
     * @throws Exception
412
     * @throws InvalidArgumentException
413
     * @throws InvalidConfigException
414
     * @throws NotSupportedException
415
     *
416
     * @return bool whether the query result contains any row of data.
417
     */
418 2
    public function exists(): bool
419
    {
420 2
        if ($this->emulateExecution) {
421 1
            return false;
422
        }
423
424 1
        $command = $this->createCommand();
425 1
        $params = $command->getParams();
426 1
        $command->setSql($command->getDb()->getQueryBuilder()->selectExists($command->getSql()));
427 1
        $command->bindValues($params);
428
429 1
        return (bool) $command->queryScalar();
430
    }
431
432
    /**
433
     * Queries a scalar value by setting {@see select} first.
434
     *
435
     * Restores the value of select to make this query reusable.
436
     *
437
     * @param string|ExpressionInterface $selectExpression
438
     *
439
     * @throws Exception
440
     * @throws InvalidArgumentException
441
     * @throws InvalidConfigException
442
     * @throws NotSupportedException
443
     *
444
     * @return bool|string
445
     */
446 4
    protected function queryScalar($selectExpression)
447
    {
448 4
        if ($this->emulateExecution) {
449 1
            return null;
450
        }
451
452
        if (
453 4
            !$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...
454 4
            && empty($this->groupBy)
455 4
            && empty($this->having)
456 4
            && empty($this->union)
457 4
            && empty($this->with)
458
        ) {
459 4
            $select = $this->select;
460 4
            $order = $this->orderBy;
461 4
            $limit = $this->limit;
462 4
            $offset = $this->offset;
463
464 4
            $this->select = [$selectExpression];
465 4
            $this->orderBy = [];
466 4
            $this->limit = null;
467 4
            $this->offset = null;
468
469
            try {
470 4
                $command = $this->createCommand();
471
            } catch (\Exception $e) {
472
                // throw it later
473
            } catch (Throwable $e) {
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\Throwable was not found. Did you mean Throwable? If so, make sure to prefix the type with \.
Loading history...
474
                // throw it later
475
            }
476
477 4
            $this->select = $select;
478 4
            $this->orderBy = $order;
479 4
            $this->limit = $limit;
480 4
            $this->offset = $offset;
481
482 4
            if (!empty($e)) {
483
                throw $e;
484
            }
485
486 4
            return $command->queryScalar();
487
        }
488
489 1
        $command = static::createInstance($this->db)
0 ignored issues
show
Bug introduced by
It seems like $this->db can also be of type null; however, parameter $value of Yiisoft\Db\Query\Query::createInstance() does only seem to accept Yiisoft\Db\Connection\ConnectionInterface, 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

489
        $command = static::createInstance(/** @scrutinizer ignore-type */ $this->db)
Loading history...
490 1
            ->select([$selectExpression])
491 1
            ->from(['c' => $this])
492 1
            ->createCommand();
493
494 1
        $this->setCommandCache($command);
495
496 1
        return $command->queryScalar();
497
    }
498
499
    /**
500
     * Returns table names used in {@see from} indexed by aliases.
501
     *
502
     * Both aliases and names are enclosed into {{ and }}.
503
     *
504
     * @throws InvalidArgumentException
505
     * @throws InvalidConfigException
506
     *
507
     * @return array table names indexed by aliases
508
     */
509 11
    public function getTablesUsedInFrom(): array
510
    {
511 11
        if (empty($this->from)) {
512
            return [];
513
        }
514
515 11
        if (\is_array($this->from)) {
516 10
            $tableNames = $this->from;
517 1
        } elseif (\is_string($this->from)) {
518
            $tableNames = \preg_split('/\s*,\s*/', \trim($this->from), -1, PREG_SPLIT_NO_EMPTY);
519 1
        } elseif ($this->from instanceof Expression) {
520
            $tableNames = [$this->from];
521
        } else {
522 1
            throw new InvalidConfigException(\gettype($this->from) . ' in $from is not supported.');
523
        }
524
525 10
        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

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

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