Passed
Pull Request — master (#163)
by Wilmer
19:50 queued 04:50
created

Command::queryInternal()   D

Complexity

Conditions 15
Paths 266

Size

Total Lines 74
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 15

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 41
c 1
b 0
f 0
nc 266
nop 2
dl 0
loc 74
ccs 32
cts 32
cp 1
crap 15
rs 4.2583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Command;
6
7
use PDO;
8
use PDOException;
9
use PDOStatement;
10
use Psr\Log\LoggerInterface;
11
use Psr\Log\LogLevel;
12
use Yiisoft\Cache\CacheInterface;
13
use Yiisoft\Cache\Dependency\Dependency;
14
use Yiisoft\Db\Data\DataReader;
15
use Yiisoft\Db\Connection\Connection;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Query\Query;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Exception\Exception;
22
use Yiisoft\Db\Pdo\PdoValue;
23
use Yiisoft\Profiler\Profiler;
24
25
/**
26
 * Command represents a SQL statement to be executed against a database.
27
 *
28
 * A command object is usually created by calling {@see Connection::createCommand()}.
29
 *
30
 * The SQL statement it represents can be set via the {@see sql} property.
31
 *
32
 * To execute a non-query SQL (such as INSERT, DELETE, UPDATE), call {@see execute()}.
33
 * To execute a SQL statement that returns a result data set (such as SELECT), use {@see queryAll()},
34
 * {@see queryOne()}, {@see queryColumn()}, {@see queryScalar()}, or {@see query()}.
35
 *
36
 * For example,
37
 *
38
 * ```php
39
 * $users = $connection->createCommand('SELECT * FROM user')->queryAll();
40
 * ```
41
 *
42
 * Command supports SQL statement preparation and parameter binding.
43
 * Call {@see bindValue()} to bind a value to a SQL parameter;
44
 * Call {@see bindParam()} to bind a PHP variable to a SQL parameter.
45
 * When binding a parameter, the SQL statement is automatically prepared.
46
 * You may also call {@see prepare()} explicitly to prepare a SQL statement.
47
 *
48
 * Command also supports building SQL statements by providing methods such as {@see insert()}, {@see update()}, etc.
49
 *
50
 * For example, the following code will create and execute an INSERT SQL statement:
51
 *
52
 * ```php
53
 * $connection->createCommand()->insert('user', [
54
 *     'name' => 'Sam',
55
 *     'age' => 30,
56
 * ])->execute();
57
 * ```
58
 *
59
 * To build SELECT SQL statements, please use {@see Query} instead.
60
 *
61
 * For more details and usage information on Command, see the [guide article on Database Access Objects](guide:db-dao).
62
 *
63
 * @property string $rawSql The raw SQL with parameter values inserted into the corresponding placeholders in
64
 * {@see sql}.
65
 * @property string $sql The SQL statement to be executed.
66
 */
67
class Command
68
{
69
    protected array $params = [];
70
    private ?Connection $db = null;
71
    private int $fetchMode = PDO::FETCH_ASSOC;
72
    private ?LoggerInterface $logger = null;
73
    private ?PDOStatement $pdoStatement = null;
74
    private ?Profiler $profiler = null;
75
    private ?int $queryCacheDuration = null;
76
77
    /**
78
     * @var Dependency the dependency to be associated with the cached query result for this command.
79
     *
80
     * {@see cache()}
81
     */
82
    private ?Dependency $queryCacheDependency = null;
83
84
    /**
85
     * @var array pending parameters to be bound to the current PDO statement.
86
     */
87
    private array $pendingParams = [];
88
89
    /**
90
     * @var string|null the SQL statement that this command represents
91
     */
92
    private ?string $sql = null;
93
94
    /**
95
     * @var string|null name of the table, which schema, should be refreshed after command execution.
96
     */
97
    private ?string $refreshTableName = null;
98
99
    /**
100
     * @var string|null the isolation level to use for this transaction.
101
     *
102
     * See {@see Transaction::begin()} for details.
103
     */
104
    private ?string $isolationLevel = null;
105
106
    /**
107
     * @var callable a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing
108
     * the command.
109
     */
110
    private $retryHandler;
111
112 525
    public function __construct(Profiler $profiler, LoggerInterface $logger, Connection $db, ?string $sql)
113
    {
114 525
        $this->db = $db;
115 525
        $this->logger = $logger;
116 525
        $this->profiler = $profiler;
117 525
        $this->sql = $sql;
118 525
    }
119
120
    /**
121
     * Enables query cache for this command.
122
     *
123
     * @param int $duration the number of seconds that query result of this command can remain valid in the cache.
124
     * If this is not set, the value of {@see Connection::queryCacheDuration} will be used instead.
125
     * Use 0 to indicate that the cached data will never expire.
126
     * @param Dependency $dependency the cache dependency associated with the cached query
127
     * result.
128
     *
129
     * @return self the command object itself
130
     */
131 6
    public function cache(?int $duration = null, ?Dependency $dependency = null): self
132
    {
133 6
        $this->queryCacheDuration = $duration ?? $this->db->getQueryCacheDuration();
0 ignored issues
show
Bug introduced by
The method getQueryCacheDuration() 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

133
        $this->queryCacheDuration = $duration ?? $this->db->/** @scrutinizer ignore-call */ getQueryCacheDuration();

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...
134 6
        $this->queryCacheDependency = $dependency;
135
136 6
        return $this;
137
    }
138
139
    /**
140
     * Disables query cache for this command.
141
     *
142
     * @return self the command object itself
143
     */
144 3
    public function noCache(): self
145
    {
146 3
        $this->queryCacheDuration = -1;
147
148 3
        return $this;
149
    }
150
151
    /**
152
     * Returns the SQL statement for this command.
153
     *
154
     * @return string|null the SQL statement to be executed
155
     */
156 507
    public function getSql(): ?string
157
    {
158 507
        return $this->sql;
159
    }
160
161
    /**
162
     * Specifies the SQL statement to be executed. The SQL statement will be quoted using
163
     * {@see Connection::quoteSql()}.
164
     *
165
     * The previous SQL (if any) will be discarded, and {@see params} will be cleared as well. See {@see reset()} for
166
     * details.
167
     *
168
     * @param string $sql the SQL statement to be set.
169
     *
170
     * @return Command this command instance
171
     *
172
     * {@see reset()}
173
     * {@see cancel()}
174
     */
175 119
    public function setSql(string $sql): self
176
    {
177 119
        if ($sql !== $this->sql) {
178 119
            $this->cancel();
179 119
            $this->reset();
180 119
            $this->sql = $this->db->quoteSql($sql);
181
        }
182
183 119
        return $this;
184
    }
185
186
    /**
187
     * Specifies the SQL statement to be executed. The SQL statement will not be modified in any way.
188
     *
189
     * The previous SQL (if any) will be discarded, and {@see params} will be cleared as well. See {@see reset()}
190
     * for details.
191
     *
192
     * @param string $sql the SQL statement to be set.
193
     *
194
     * @return Command this command instance
195
     *
196
     * {@see reset()}
197
     * {@see cancel()}
198
     */
199 26
    public function setRawSql(string $sql): self
200
    {
201 26
        if ($sql !== $this->sql) {
202 26
            $this->cancel();
203 26
            $this->reset();
204 26
            $this->sql = $sql;
205
        }
206
207 26
        return $this;
208
    }
209
210
    /**
211
     * Returns the raw SQL by inserting parameter values into the corresponding placeholders in {@see sql}.
212
     *
213
     * Note that the return value of this method should mainly be used for logging purpose.
214
     *
215
     * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders.
216
     *
217
     * @throws Exception
218
     * @throws InvalidConfigException
219
     * @throws NotSupportedException
220
     *
221
     * @return string the raw SQL with parameter values inserted into the corresponding placeholders in {@see sql}.
222
     */
223 513
    public function getRawSql(): string
224
    {
225 513
        if (empty($this->params)) {
226 378
            return $this->sql;
227
        }
228
229 300
        $params = [];
230
231 300
        foreach ($this->params as $name => $value) {
232 300
            if (\is_string($name) && \strncmp(':', $name, 1)) {
233 11
                $name = ':' . $name;
234
            }
235
236 300
            if (\is_string($value)) {
237 255
                $params[$name] = $this->db->quoteValue($value);
238 147
            } elseif (\is_bool($value)) {
239 9
                $params[$name] = ($value ? 'TRUE' : 'FALSE');
240 142
            } elseif ($value === null) {
241 61
                $params[$name] = 'NULL';
242 85
            } elseif ((!\is_object($value) && !\is_resource($value)) || $value instanceof Expression) {
243 85
                $params[$name] = $value;
244
            }
245
        }
246
247 300
        if (!isset($params[1])) {
248 300
            return \strtr($this->sql, $params);
249
        }
250
251
        $sql = '';
252
253
        foreach (\explode('?', $this->sql) as $i => $part) {
254
            $sql .= ($params[$i] ?? '') . $part;
255
        }
256
257
        return $sql;
258
    }
259
260
    /**
261
     * Prepares the SQL statement to be executed.
262
     *
263
     * For complex SQL statement that is to be executed multiple times, this may improve performance.
264
     * For SQL statement with binding parameters, this method is invoked automatically.
265
     *
266
     * @param bool|null $forRead whether this method is called for a read query. If null, it means the SQL statement
267
     * should be used to determine whether it is for read or write.
268
     *
269
     * @throws Exception if there is any DB error
270
     *
271
     * @return void
272
     */
273 498
    public function prepare(?bool $forRead = null): void
274
    {
275 498
        if ($this->pdoStatement) {
276 15
            $this->bindPendingParams();
277
278 15
            return;
279
        }
280
281 498
        $sql = $this->getSql();
282
283 498
        if ($this->db->getTransaction()) {
284
            // master is in a transaction. use the same connection.
285 16
            $forRead = false;
286
        }
287
288 498
        if ($forRead || ($forRead === null && $this->db->getSchema()->isReadQuery($sql))) {
289 486
            $pdo = $this->db->getSlavePdo();
290
        } else {
291 163
            $pdo = $this->db->getMasterPdo();
292
        }
293
294
        try {
295 498
            $this->pdoStatement = $pdo->prepare($sql);
296 498
            $this->bindPendingParams();
297 3
        } catch (\Exception $e) {
298 3
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
299 3
            $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
300
301 3
            throw new Exception($message, $errorInfo, (string) $e->getCode(), $e);
302
        }
303 498
    }
304
305
    /**
306
     * Cancels the execution of the SQL statement.
307
     *
308
     * This method mainly sets {@see pdoStatement} to be null.
309
     */
310 142
    public function cancel(): void
311
    {
312 142
        $this->pdoStatement = null;
313 142
    }
314
315
    /**
316
     * Binds a parameter to the SQL statement to be executed.
317
     *
318
     * @param string|int $name parameter identifier. For a prepared statement using named placeholders, this will be a
319
     * parameter name of the form `:name`. For a prepared statement using question mark placeholders, this will be the
320
     * 1-indexed position of the parameter.
321
     *
322
     * @param mixed $value the PHP variable to bind to the SQL statement parameter (passed by reference)
323
     * @param int|null $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
324
     * value.
325
     * @param int|null $length length of the data type
326
     * @param mixed $driverOptions the driver-specific options
327
     *
328
     * @throws Exception
329
     *
330
     * @return self the current command being executed
331
     *
332
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindParam.php}
333
     */
334 3
    public function bindParam($name, &$value, ?int $dataType = null, ?int $length = null, $driverOptions = null): self
335
    {
336 3
        $this->prepare();
337
338 3
        if ($dataType === null) {
339 3
            $dataType = $this->db->getSchema()->getPdoType($value);
340
        }
341
342 3
        if ($length === null) {
343 3
            $this->pdoStatement->bindParam($name, $value, $dataType);
344
        } elseif ($driverOptions === null) {
345
            $this->pdoStatement->bindParam($name, $value, $dataType, $length);
346
        } else {
347
            $this->pdoStatement->bindParam($name, $value, $dataType, $length, $driverOptions);
348
        }
349
350 3
        $this->params[$name] = &$value;
351
352 3
        return $this;
353
    }
354
355
    /**
356
     * Binds pending parameters that were registered via {@see bindValue()} and {@see bindValues()}.
357
     *
358
     * Note that this method requires an active {@see pdoStatement}.
359
     */
360 498
    protected function bindPendingParams(): void
361
    {
362 498
        foreach ($this->pendingParams as $name => $value) {
363 287
            $this->pdoStatement->bindValue($name, $value[0], $value[1]);
0 ignored issues
show
Bug introduced by
The method bindValue() 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

363
            $this->pdoStatement->/** @scrutinizer ignore-call */ 
364
                                 bindValue($name, $value[0], $value[1]);

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...
364
        }
365
366 498
        $this->pendingParams = [];
367 498
    }
368
369
    /**
370
     * Binds a value to a parameter.
371
     *
372
     * @param string|int $name Parameter identifier. For a prepared statement using named placeholders, this will be a
373
     * parameter name of the form `:name`. For a prepared statement using question mark placeholders, this will be the
374
     * 1-indexed position of the parameter.
375
     * @param mixed $value The value to bind to the parameter
376
     * @param int $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
377
     * value.
378
     *
379
     * @throws Exception
380
     * @throws InvalidConfigException
381
     * @throws NotSupportedException
382
     *
383
     * @return Command the current command being executed
384
     *
385
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindValue.php}
386
     */
387 6
    public function bindValue($name, $value, ?int $dataType = null): self
388
    {
389 6
        if ($dataType === null) {
390 6
            $dataType = $this->db->getSchema()->getPdoType($value);
391
        }
392
393 6
        $this->pendingParams[$name] = [$value, $dataType];
394
395 6
        $this->params[$name] = $value;
396
397 6
        return $this;
398
    }
399
400
    /**
401
     * Binds a list of values to the corresponding parameters.
402
     *
403
     * This is similar to {@see bindValue()} except that it binds multiple values at a time.
404
     *
405
     * Note that the SQL data type of each value is determined by its PHP type.
406
     *
407
     * @param array $values the values to be bound. This must be given in terms of an associative array with array keys
408
     * being the parameter names, and array values the corresponding parameter values,
409
     * e.g. `[':name' => 'John', ':age' => 25]`.
410
     * By default, the PDO type of each value is determined  by its PHP type. You may explicitly specify the PDO type by
411
     * using a {@see PdoValue} class: `new PdoValue(value, type)`,
412
     * e.g. `[':name' => 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
413
     *
414
     * @throws Exception
415
     * @throws InvalidConfigException
416
     * @throws NotSupportedException
417
     *
418
     * @return self the current command being executed
419
     */
420 525
    public function bindValues(array $values): self
421
    {
422 525
        if (empty($values)) {
423 396
            return $this;
424
        }
425
426 302
        $schema = $this->db->getSchema();
427
428 302
        foreach ($values as $name => $value) {
429 302
            if (\is_array($value)) { // TODO: Drop in Yii 2.1
430
                $this->pendingParams[$name] = $value;
431
                $this->params[$name] = $value[0];
432 302
            } elseif ($value instanceof PdoValue) {
433 1
                $this->pendingParams[$name] = [$value->getValue(), $value->getType()];
434 1
                $this->params[$name] = $value->getValue();
435
            } else {
436 302
                $type = $schema->getPdoType($value);
437
438 302
                $this->pendingParams[$name] = [$value, $type];
439 302
                $this->params[$name] = $value;
440
            }
441
        }
442
443 302
        return $this;
444
    }
445
446
    /**
447
     * Executes the SQL statement and returns query result.
448
     *
449
     * This method is for executing a SQL query that returns result set, such as `SELECT`.
450
     *
451
     * @throws Exception execution failed
452
     * @throws InvalidArgumentException
453
     *
454
     * @return DataReader the reader object for fetching the query result
455
     */
456 9
    public function query(): DataReader
457
    {
458 9
        return $this->queryInternal('');
459
    }
460
461
    /**
462
     * Executes the SQL statement and returns ALL rows at once.
463
     *
464
     * @param int $fetchMode the result fetch mode.
465
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
466
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
467
     *
468
     * @throws Exception execution failed
469
     * @throws InvalidArgumentException
470
     *
471
     * @return array all rows of the query result. Each array element is an array representing a row of data. An empty
472
     * array is returned if the query results in nothing.
473
     */
474 411
    public function queryAll(?int $fetchMode = null): array
475
    {
476 411
        return $this->queryInternal('fetchAll', $fetchMode);
477
    }
478
479
    /**
480
     * Executes the SQL statement and returns the first row of the result.
481
     *
482
     * This method is best used when only the first row of result is needed for a query.
483
     *
484
     * @param array|int|null $fetchMode the result fetch mode.
485
     * Please refer to [PHP manual](http://php.net/manual/en/pdostatement.setfetchmode.php)
486
     * for valid fetch modes. If this parameter is null, the value set in {@see fetchMode} will be used.
487
     *
488
     * @throws Exception execution failed
489
     * @throws InvalidArgumentException
490
     *
491
     * @return array|false the first row (in terms of an array) of the query result. False is returned if the query
492
     * results in nothing.
493
     */
494 46
    public function queryOne($fetchMode = null)
495
    {
496 46
        return $this->queryInternal('fetch', $fetchMode);
497
    }
498
499
    /**
500
     * Executes the SQL statement and returns the value of the first column in the first row of data.
501
     *
502
     * This method is best used when only a single value is needed for a query.
503
     *
504
     * @throws Exception execution failed
505
     * @throws InvalidArgumentException
506
     *
507
     * @return string|null|false the value of the first column in the first row of the query result. False is returned
508
     * if there is no value.
509
     */
510 111
    public function queryScalar()
511
    {
512 111
        $result = $this->queryInternal('fetchColumn', 0);
513
514 111
        if (\is_resource($result) && \get_resource_type($result) === 'stream') {
515
            return \stream_get_contents($result);
516
        }
517
518 111
        return $result;
519
    }
520
521
    /**
522
     * Executes the SQL statement and returns the first column of the result.
523
     *
524
     * This method is best used when only the first column of result (i.e. the first element in each row) is needed for
525
     * a query.
526
     *
527
     * @throws Exception execution failed
528
     * @throws InvalidArgumentException
529
     *
530
     * @return array the first column of the query result. Empty array is returned if the query results in nothing.
531
     */
532 26
    public function queryColumn(): array
533
    {
534 26
        return $this->queryInternal('fetchAll', PDO::FETCH_COLUMN);
535
    }
536
537
    /**
538
     * Creates an INSERT command.
539
     *
540
     * For example,
541
     *
542
     * ```php
543
     * $connection->createCommand()->insert('user', [
544
     *     'name' => 'Sam',
545
     *     'age' => 30,
546
     * ])->execute();
547
     * ```
548
     *
549
     * The method will properly escape the column names, and bind the values to be inserted.
550
     *
551
     * Note that the created command is not executed until {@see execute()} is called.
552
     *
553
     * @param string $table the table that new rows will be inserted into.
554
     * @param array|Query $columns the column data (name => value) to be inserted into the table or
555
     * instance of {@see Query} to perform INSERT INTO ... SELECT SQL statement.
556
     * Passing of {@see Query}.
557
     *
558
     * @throws Exception
559
     * @throws InvalidConfigException
560
     * @throws NotSupportedException
561
     *
562
     * @return self the command object itself
563
     */
564 59
    public function insert(string $table, $columns): self
565
    {
566 59
        $params = [];
567 59
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
568
569 50
        return $this->setSql($sql)->bindValues($params);
570
    }
571
572
    /**
573
     * Creates a batch INSERT command.
574
     *
575
     * For example,
576
     *
577
     * ```php
578
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
579
     *     ['Tom', 30],
580
     *     ['Jane', 20],
581
     *     ['Linda', 25],
582
     * ])->execute();
583
     * ```
584
     *
585
     * The method will properly escape the column names, and quote the values to be inserted.
586
     *
587
     * Note that the values in each row must match the corresponding column names.
588
     *
589
     * Also note that the created command is not executed until {@see execute()} is called.
590
     *
591
     * @param string $table the table that new rows will be inserted into.
592
     * @param array $columns the column names
593
     * @param iterable $rows the rows to be batch inserted into the table
594
     *
595
     * @throws Exception
596
     * @throws InvalidConfigException
597
     * @throws NotSupportedException
598
     *
599
     * @return self the command object itself
600
     */
601 26
    public function batchInsert(string $table, array $columns, iterable $rows): self
602
    {
603 26
        $table = $this->db->quoteSql($table);
604
605 26
        $columns = \array_map(fn ($column) => $this->db->quoteSql($column), $columns);
606
607 26
        $params = [];
608
609 26
        $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows, $params);
610
611 26
        $this->setRawSql($sql);
612 26
        $this->bindValues($params);
613
614 26
        return $this;
615
    }
616
617
    /**
618
     * Creates a command to insert rows into a database table if they do not already exist (matching unique constraints)
619
     * or update them if they do.
620
     *
621
     * For example,
622
     *
623
     * ```php
624
     * $sql = $queryBuilder->upsert('pages', [
625
     *     'name' => 'Front page',
626
     *     'url' => 'http://example.com/', // url is unique
627
     *     'visits' => 0,
628
     * ], [
629
     *     'visits' => new \Yiisoft\Db\Expression\Expression('visits + 1'),
630
     * ], $params);
631
     * ```
632
     *
633
     * The method will properly escape the table and column names.
634
     *
635
     * @param string $table the table that new rows will be inserted into/updated in.
636
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
637
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
638
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
639
     * If `true` is passed, the column data will be updated to match the insert column data.
640
     * If `false` is passed, no update will be performed if the column data already exists.
641
     * @param array $params the parameters to be bound to the command.
642
     *
643
     * @throws Exception
644
     * @throws InvalidConfigException
645
     * @throws NotSupportedException
646
     *
647
     * @return self the command object itself.
648
     */
649 18
    public function upsert(string $table, $insertColumns, $updateColumns = true, array $params = []): self
650
    {
651 18
        $sql = $this->db->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
652
653 18
        return $this->setSql($sql)->bindValues($params);
654
    }
655
656
    /**
657
     * Creates an UPDATE command.
658
     *
659
     * For example,
660
     *
661
     * ```php
662
     * $connection->createCommand()->update('user', ['status' => 1], 'age > 30')->execute();
663
     * ```
664
     *
665
     * or with using parameter binding for the condition:
666
     *
667
     * ```php
668
     * $minAge = 30;
669
     * $connection->createCommand()->update(
670
     *     'user',
671
     *     ['status' => 1],
672
     *     'age > :minAge',
673
     *     [':minAge' => $minAge]
674
     * )->execute();
675
     * ```
676
     *
677
     * The method will properly escape the column names and bind the values to be updated.
678
     *
679
     * Note that the created command is not executed until {@see execute()} is called.
680
     *
681
     * @param string $table the table to be updated.
682
     * @param array $columns the column data (name => value) to be updated.
683
     * @param string|array $condition the condition that will be put in the WHERE part.
684
     * Please refer to {@see Query::where()} on how to specify condition.
685
     * @param array $params the parameters to be bound to the command
686
     *
687
     * @throws Exception
688
     * @throws InvalidConfigException
689
     * @throws NotSupportedException
690
     *
691
     * @return self the command object itself
692
     */
693 4
    public function update(string $table, array $columns, $condition = '', array $params = []): self
694
    {
695 4
        $sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params);
696
697 4
        return $this->setSql($sql)->bindValues($params);
698
    }
699
700
    /**
701
     * Creates a DELETE command.
702
     *
703
     * For example,
704
     *
705
     * ```php
706
     * $connection->createCommand()->delete('user', 'status = 0')->execute();
707
     * ```
708
     *
709
     * or with using parameter binding for the condition:
710
     *
711
     * ```php
712
     * $status = 0;
713
     * $connection->createCommand()->delete('user', 'status = :status', [':status' => $status])->execute();
714
     * ```
715
     *
716
     * The method will properly escape the table and column names.
717
     *
718
     * Note that the created command is not executed until {@see execute()} is called.
719
     *
720
     * @param string $table the table where the data will be deleted from.
721
     * @param string|array $condition the condition that will be put in the WHERE part. Please refer to
722
     * {@see Query::where()} on how to specify condition.
723
     * @param array $params the parameters to be bound to the command
724
     *
725
     * @throws Exception
726
     * @throws InvalidConfigException
727
     * @throws NotSupportedException
728
     *
729
     * @return self the command object itself
730
     */
731 4
    public function delete(string $table, $condition = '', array $params = []): self
732
    {
733 4
        $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params);
734
735 4
        return $this->setSql($sql)->bindValues($params);
736
    }
737
738
    /**
739
     * Creates a SQL command for creating a new DB table.
740
     *
741
     * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), where name
742
     * stands for a column name which will be properly quoted by the method, and definition stands for the column type
743
     * which can contain an abstract DB type.
744
     * The method {@see QueryBuilder::getColumnType()} will be called to convert the abstract column types to physical
745
     * ones. For example, `string` will be converted as `varchar(255)`, and `string not null` becomes
746
     * `varchar(255) not null`.
747
     *
748
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly inserted
749
     * into the generated SQL.
750
     *
751
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
752
     * @param array $columns the columns (name => definition) in the new table.
753
     * @param string $options additional SQL fragment that will be appended to the generated SQL.
754
     *
755
     * @throws Exception
756
     * @throws InvalidConfigException
757
     * @throws NotSupportedException
758
     *
759
     * @return self the command object itself
760
     */
761 34
    public function createTable(string $table, array $columns, string $options = null): self
762
    {
763 34
        $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options);
764
765 34
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
766
    }
767
768
    /**
769
     * Creates a SQL command for renaming a DB table.
770
     *
771
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
772
     * @param string $newName the new table name. The name will be properly quoted by the method.
773
     *
774
     * @throws Exception
775
     * @throws InvalidConfigException
776
     * @throws NotSupportedException
777
     *
778
     * @return self the command object itself
779
     */
780 6
    public function renameTable(string $table, string $newName): self
781
    {
782 6
        $sql = $this->db->getQueryBuilder()->renameTable($table, $newName);
783
784 6
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
785
    }
786
787
    /**
788
     * Creates a SQL command for dropping a DB table.
789
     *
790
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
791
     *
792
     * @throws Exception
793
     * @throws InvalidConfigException
794
     * @throws NotSupportedException
795
     *
796
     * @return self the command object itself
797
     */
798 9
    public function dropTable(string $table): self
799
    {
800 9
        $sql = $this->db->getQueryBuilder()->dropTable($table);
801
802 9
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
803
    }
804
805
    /**
806
     * Creates a SQL command for truncating a DB table.
807
     *
808
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
809
     *
810
     * @throws Exception
811
     * @throws InvalidConfigException
812
     * @throws NotSupportedException
813
     *
814
     * @return self the command object itself
815
     */
816 3
    public function truncateTable(string $table): self
817
    {
818 3
        $sql = $this->db->getQueryBuilder()->truncateTable($table);
819
820 3
        return $this->setSql($sql);
821
    }
822
823
    /**
824
     * Creates a SQL command for adding a new DB column.
825
     *
826
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by
827
     * the method.
828
     * @param string $column the name of the new column. The name will be properly quoted by the method.
829
     * @param string $type the column type. {@see QueryBuilder::getColumnType()} will be called to convert the give
830
     * column type to the physical one. For example, `string` will be converted as `varchar(255)`, and `string not null`
831
     * becomes `varchar(255) not null`.
832
     *
833
     * @throws Exception
834
     * @throws InvalidConfigException
835
     * @throws NotSupportedException
836
     *
837
     * @return self the command object itself
838
     */
839 3
    public function addColumn(string $table, string $column, string $type): self
840
    {
841 3
        $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type);
842
843 3
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
844
    }
845
846
    /**
847
     * Creates a SQL command for dropping a DB column.
848
     *
849
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
850
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
851
     *
852
     * @throws Exception
853
     * @throws InvalidConfigException
854
     * @throws NotSupportedException
855
     *
856
     * @return self the command object itself
857
     */
858
    public function dropColumn(string $table, string $column): self
859
    {
860
        $sql = $this->db->getQueryBuilder()->dropColumn($table, $column);
861
862
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
863
    }
864
865
    /**
866
     * Creates a SQL command for renaming a column.
867
     *
868
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
869
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
870
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
871
     *
872
     * @throws Exception
873
     * @throws InvalidConfigException
874
     * @throws NotSupportedException
875
     *
876
     * @return self the command object itself
877
     */
878
    public function renameColumn(string $table, string $oldName, string $newName): self
879
    {
880
        $sql = $this->db->getQueryBuilder()->renameColumn($table, $oldName, $newName);
881
882
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
883
    }
884
885
    /**
886
     * Creates a SQL command for changing the definition of a column.
887
     *
888
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
889
     * method.
890
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
891
     * @param string $type the column type. {@see QueryBuilder::getColumnType()} will be called to
892
     * convert the give column type to the physical one. For example, `string` will be converted as `varchar(255)`, and
893
     * `string not null` becomes `varchar(255) not null`.
894
     *
895
     * @throws Exception
896
     * @throws InvalidConfigException
897
     * @throws NotSupportedException
898
     *
899
     * @return self the command object itself
900
     */
901 2
    public function alterColumn(string $table, string $column, string $type): self
902
    {
903 2
        $sql = $this->db->getQueryBuilder()->alterColumn($table, $column, $type);
904
905 2
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
906
    }
907
908
    /**
909
     * Creates a SQL command for adding a primary key constraint to an existing table.
910
     *
911
     * The method will properly quote the table and column names.
912
     *
913
     * @param string $name the name of the primary key constraint.
914
     * @param string $table the table that the primary key constraint will be added to.
915
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
916
     *
917
     * @throws Exception
918
     * @throws InvalidConfigException
919
     * @throws NotSupportedException
920
     *
921
     * @return self the command object itself.
922
     */
923 2
    public function addPrimaryKey(string $name, string $table, $columns): self
924
    {
925 2
        $sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns);
926
927 2
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
928
    }
929
930
    /**
931
     * Creates a SQL command for removing a primary key constraint to an existing table.
932
     *
933
     * @param string $name the name of the primary key constraint to be removed.
934
     * @param string $table the table that the primary key constraint will be removed from.
935
     *
936
     * @throws Exception
937
     * @throws InvalidConfigException
938
     * @throws NotSupportedException
939
     *
940
     * @return self the command object itself
941
     */
942 2
    public function dropPrimaryKey(string $name, string $table): self
943
    {
944 2
        $sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table);
945
946 2
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
947
    }
948
949
    /**
950
     * Creates a SQL command for adding a foreign key constraint to an existing table.
951
     *
952
     * The method will properly quote the table and column names.
953
     *
954
     * @param string $name the name of the foreign key constraint.
955
     * @param string $table the table that the foreign key constraint will be added to.
956
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
957
     * multiple columns, separate them with commas.
958
     * @param string $refTable the table that the foreign key references to.
959
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
960
     * columns, separate them with commas.
961
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
962
     * SET DEFAULT, SET NULL
963
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
964
     * SET DEFAULT, SET NULL
965
     *
966
     * @throws Exception
967
     * @throws InvalidConfigException
968
     * @throws NotSupportedException
969
     *
970
     * @return self the command object itself
971
     */
972 6
    public function addForeignKey(
973
        string $name,
974
        string $table,
975
        $columns,
976
        string $refTable,
977
        $refColumns,
978
        ?string $delete = null,
979
        ?string $update = null
980
    ): self {
981 6
        $sql = $this->db->getQueryBuilder()->addForeignKey(
982 6
            $name,
983
            $table,
984
            $columns,
985
            $refTable,
986
            $refColumns,
987
            $delete,
988
            $update
989
        );
990
991 6
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
992
    }
993
994
    /**
995
     * Creates a SQL command for dropping a foreign key constraint.
996
     *
997
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by
998
     * the method.
999
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
1000
     *
1001
     * @throws Exception
1002
     * @throws InvalidConfigException
1003
     * @throws NotSupportedException
1004
     *
1005
     * @return self the command object itself
1006
     */
1007 5
    public function dropForeignKey(string $name, string $table): self
1008
    {
1009 5
        $sql = $this->db->getQueryBuilder()->dropForeignKey($name, $table);
1010
1011 5
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1012
    }
1013
1014
    /**
1015
     * Creates a SQL command for creating a new index.
1016
     *
1017
     * @param string $name the name of the index. The name will be properly quoted by the method.
1018
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
1019
     * the method.
1020
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
1021
     * please separate them by commas. The column names will be properly quoted by the method.
1022
     * @param bool $unique whether to add UNIQUE constraint on the created index.
1023
     *
1024
     * @throws Exception
1025
     * @throws InvalidConfigException
1026
     * @throws NotSupportedException
1027
     *
1028
     * @return Command the command object itself
1029
     */
1030 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): self
1031
    {
1032 6
        $sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique);
1033
1034 6
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1035
    }
1036
1037
    /**
1038
     * Creates a SQL command for dropping an index.
1039
     *
1040
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
1041
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
1042
     *
1043
     * @throws Exception
1044
     * @throws InvalidConfigException
1045
     * @throws NotSupportedException
1046
     *
1047
     * @return self the command object itself
1048
     */
1049 3
    public function dropIndex(string $name, string $table): self
1050
    {
1051 3
        $sql = $this->db->getQueryBuilder()->dropIndex($name, $table);
1052
1053 3
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1054
    }
1055
1056
    /**
1057
     * Creates a SQL command for adding an unique constraint to an existing table.
1058
     *
1059
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
1060
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
1061
     * the method.
1062
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
1063
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
1064
     *
1065
     * @throws Exception
1066
     * @throws InvalidConfigException
1067
     * @throws NotSupportedException
1068
     *
1069
     * @return self the command object itself.
1070
     */
1071 3
    public function addUnique(string $name, string $table, $columns): self
1072
    {
1073 3
        $sql = $this->db->getQueryBuilder()->addUnique($name, $table, $columns);
1074
1075 3
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1076
    }
1077
1078
    /**
1079
     * Creates a SQL command for dropping an unique constraint.
1080
     *
1081
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
1082
     * method.
1083
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by
1084
     * the method.
1085
     *
1086
     * @throws Exception
1087
     * @throws InvalidConfigException
1088
     * @throws NotSupportedException
1089
     *
1090
     * @return self the command object itself.
1091
     */
1092 3
    public function dropUnique(string $name, string $table): self
1093
    {
1094 3
        $sql = $this->db->getQueryBuilder()->dropUnique($name, $table);
1095
1096 3
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1097
    }
1098
1099
    /**
1100
     * Creates a SQL command for adding a check constraint to an existing table.
1101
     *
1102
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
1103
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
1104
     * the method.
1105
     * @param string $expression the SQL of the `CHECK` constraint.
1106
     *
1107
     * @throws Exception
1108
     * @throws InvalidConfigException
1109
     * @throws NotSupportedException
1110
     *
1111
     * @return self the command object itself.
1112
     */
1113 1
    public function addCheck(string $name, string $table, string $expression): self
1114
    {
1115 1
        $sql = $this->db->getQueryBuilder()->addCheck($name, $table, $expression);
1116
1117 1
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1118
    }
1119
1120
    /**
1121
     * Creates a SQL command for dropping a check constraint.
1122
     *
1123
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
1124
     * method.
1125
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
1126
     * method.
1127
     *
1128
     * @throws Exception
1129
     * @throws InvalidConfigException
1130
     * @throws NotSupportedException
1131
     *
1132
     * @return self the command object itself.
1133
     */
1134 1
    public function dropCheck(string $name, string $table): self
1135
    {
1136 1
        $sql = $this->db->getQueryBuilder()->dropCheck($name, $table);
1137
1138 1
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1139
    }
1140
1141
    /**
1142
     * Creates a SQL command for adding a default value constraint to an existing table.
1143
     *
1144
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
1145
     * @param string $table the table that the default value constraint will be added to. The name will be properly
1146
     * quoted by the method.
1147
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
1148
     * quoted by the method.
1149
     * @param mixed $value default value.
1150
     *
1151
     * @throws Exception
1152
     * @throws InvalidConfigException
1153
     * @throws NotSupportedException
1154
     *
1155
     * @return self the command object itself.
1156
     */
1157
    public function addDefaultValue(string $name, string $table, string $column, $value): self
1158
    {
1159
        $sql = $this->db->getQueryBuilder()->addDefaultValue($name, $table, $column, $value);
1160
1161
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1162
    }
1163
1164
    /**
1165
     * Creates a SQL command for dropping a default value constraint.
1166
     *
1167
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
1168
     * the method.
1169
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
1170
     * by the method.
1171
     *
1172
     * @throws Exception
1173
     * @throws InvalidConfigException
1174
     * @throws NotSupportedException
1175
     *
1176
     * @return self the command object itself.
1177
     */
1178
    public function dropDefaultValue(string $name, string $table): self
1179
    {
1180
        $sql = $this->db->getQueryBuilder()->dropDefaultValue($name, $table);
1181
1182
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1183
    }
1184
1185
    /**
1186
     * Creates a SQL command for resetting the sequence value of a table's primary key.
1187
     *
1188
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
1189
     * or 1.
1190
     *
1191
     * @param string $table the name of the table whose primary key sequence will be reset
1192
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
1193
     * row's primary key will have a value 1.
1194
     *
1195
     * @throws Exception
1196
     * @throws InvalidConfigException
1197
     * @throws NotSupportedException
1198
     *
1199
     * @return self the command object itself
1200
     */
1201
    public function resetSequence(string $table, $value = null): self
1202
    {
1203
        $sql = $this->db->getQueryBuilder()->resetSequence($table, $value);
1204
1205
        return $this->setSql($sql);
1206
    }
1207
1208
    /**
1209
     * Executes a db command resetting the sequence value of a table's primary key.
1210
     *
1211
     * Reason for execute is that some databases (Oracle) need several queries to do so.
1212
     * The sequence is reset such that the primary key of the next new row inserted will have the specified value or the
1213
     * maximum existing value +1.
1214
     *
1215
     * @param string $table the name of the table whose primary key sequence is reset
1216
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
1217
     * row's primary key will have the maximum existing value +1.
1218
     *
1219
     * @return self
1220
     */
1221
    public function executeResetSequence(string $table, $value = null): self
1222
    {
1223
        return $this->resetSequence($table, $value);
1224
    }
1225
1226
    /**
1227
     * Builds a SQL command for enabling or disabling integrity check.
1228
     *
1229
     * @param string $schema the schema name of the tables. Defaults to empty string, meaning the current or default
1230
     * schema.
1231
     * @param string $table the table name.
1232
     *
1233
     * @param bool $check whether to turn on or off the integrity check.
1234
     *
1235
     * @throws Exception
1236
     * @throws InvalidConfigException
1237
     * @throws NotSupportedException
1238
     *
1239
     * @return self the command object itself
1240
     */
1241
    public function checkIntegrity(string $schema, string $table, bool $check = true): self
1242
    {
1243
        $sql = $this->db->getQueryBuilder()->checkIntegrity($schema, $table, $check);
1244
1245
        return $this->setSql($sql);
1246
    }
1247
1248
    /**
1249
     * Builds a SQL command for adding comment to column.
1250
     *
1251
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1252
     * method.
1253
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1254
     * method.
1255
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1256
     *
1257
     * @throws Exception
1258
     * @throws InvalidConfigException
1259
     * @throws NotSupportedException
1260
     *
1261
     * @return self the command object itself
1262
     */
1263 2
    public function addCommentOnColumn(string $table, string $column, string $comment): self
1264
    {
1265 2
        $sql = $this->db->getQueryBuilder()->addCommentOnColumn($table, $column, $comment);
1266
1267 2
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1268
    }
1269
1270
    /**
1271
     * Builds a SQL command for adding comment to table.
1272
     *
1273
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1274
     * method.
1275
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1276
     *
1277
     * @throws Exception
1278
     * @throws InvalidConfigException
1279
     * @throws NotSupportedException
1280
     *
1281
     * @return self the command object itself
1282
     */
1283
    public function addCommentOnTable(string $table, string $comment): self
1284
    {
1285
        $sql = $this->db->getQueryBuilder()->addCommentOnTable($table, $comment);
1286
1287
        return $this->setSql($sql);
1288
    }
1289
1290
    /**
1291
     * Builds a SQL command for dropping comment from column.
1292
     *
1293
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1294
     * method.
1295
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1296
     * method.
1297
     *
1298
     * @throws Exception
1299
     * @throws InvalidConfigException
1300
     * @throws NotSupportedException
1301
     *
1302
     * @return self the command object itself
1303
     */
1304 2
    public function dropCommentFromColumn(string $table, string $column): self
1305
    {
1306 2
        $sql = $this->db->getQueryBuilder()->dropCommentFromColumn($table, $column);
1307
1308 2
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1309
    }
1310
1311
    /**
1312
     * Builds a SQL command for dropping comment from table.
1313
     *
1314
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1315
     * method.
1316
     *
1317
     * @throws Exception
1318
     * @throws InvalidConfigException
1319
     * @throws NotSupportedException
1320
     *
1321
     * @return self the command object itself
1322
     */
1323
    public function dropCommentFromTable(string $table): self
1324
    {
1325
        $sql = $this->db->getQueryBuilder()->dropCommentFromTable($table);
1326
1327
        return $this->setSql($sql);
1328
    }
1329
1330
    /**
1331
     * Creates a SQL View.
1332
     *
1333
     * @param string $viewName the name of the view to be created.
1334
     * @param string|Query $subquery the select statement which defines the view. This can be either a string or a
1335
     * {@see Query} object.
1336
     *
1337
     * @throws Exception
1338
     * @throws InvalidConfigException
1339
     * @throws NotSupportedException
1340
     *
1341
     * @return self the command object itself.
1342
     */
1343 3
    public function createView(string $viewName, $subquery): self
1344
    {
1345 3
        $sql = $this->db->getQueryBuilder()->createView($viewName, $subquery);
1346
1347 3
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
1348
    }
1349
1350
    /**
1351
     * Drops a SQL View.
1352
     *
1353
     * @param string $viewName the name of the view to be dropped.
1354
     *
1355
     * @throws Exception
1356
     * @throws InvalidConfigException
1357
     * @throws NotSupportedException
1358
     *
1359
     * @return self the command object itself.
1360
     */
1361 3
    public function dropView(string $viewName): self
1362
    {
1363 3
        $sql = $this->db->getQueryBuilder()->dropView($viewName);
1364
1365 3
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
1366
    }
1367
1368
    /**
1369
     * Executes the SQL statement.
1370
     *
1371
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
1372
     * No result set will be returned.
1373
     *
1374
     * @throws Exception execution failed
1375
     *
1376
     * @return int number of rows affected by the execution.
1377
     */
1378 154
    public function execute(): int
1379
    {
1380 154
        $sql = $this->getSql();
1381
1382 154
        [$profile, $rawSql] = $this->logQuery(__METHOD__);
1383
1384 154
        if ($sql === '') {
1385 6
            return 0;
1386
        }
1387
1388 151
        $this->prepare(false);
1389
1390
        try {
1391 151
            if ($this->db->isProfilingEnabled()) {
1392
                $this->profiler->begin((string) $rawSql, [__METHOD__]);
0 ignored issues
show
Bug introduced by
The method begin() 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

1392
                $this->profiler->/** @scrutinizer ignore-call */ 
1393
                                 begin((string) $rawSql, [__METHOD__]);

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...
1393 151
            }
1394 151
1395
            $this->internalExecute($rawSql);
1396 151
            $n = $this->pdoStatement->rowCount();
1397
1398 151
            if ($this->db->isProfilingEnabled()) {
1399
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1400 151
            }
1401 13
1402 13
            $this->refreshTableSchema();
1403
1404 13
            return $n;
1405
        } catch (Exception $e) {
1406
            if ($this->db->isProfilingEnabled()) {
1407
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1408
            }
1409
1410
            throw $e;
1411
        }
1412
    }
1413
1414
    /**
1415
     * Logs the current database query if query logging is enabled and returns the profiling token if profiling is
1416
     * enabled.
1417
     *
1418
     * @param string $category the log category.
1419
     *
1420
     * @throws Exception
1421 495
     * @throws InvalidConfigException
1422
     * @throws NotSupportedException
1423 495
     *
1424 495
     * @return array array of two elements, the first is boolean of whether profiling is enabled or not. The second is
1425
     * the rawSql if it has been created.
1426 495
     */
1427
    protected function logQuery(string $category): array
1428 495
    {
1429 2
        if ($this->db->isLoggingEnabled()) {
1430
            $rawSql = $this->getRawSql();
1431
            $this->logger->log(LogLevel::INFO, $rawSql, [$category]);
0 ignored issues
show
Bug introduced by
The method log() 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

1431
            $this->logger->/** @scrutinizer ignore-call */ 
1432
                           log(LogLevel::INFO, $rawSql, [$category]);

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...
1432 495
        }
1433
1434
        if (!$this->db->isProfilingEnabled()) {
1435
            return [false, $rawSql ?? null];
1436
        }
1437
1438
        return [true, $rawSql ?? $this->getRawSql()];
1439
    }
1440
1441
    /**
1442
     * Performs the actual DB query of a SQL statement.
1443
     *
1444
     * @param string $method method of PDOStatement to be called
1445
     * @param array|int|null $fetchMode the result fetch mode.
1446
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php)
1447
     * for valid fetch modes. If this parameter is null, the value set in {@see fetchMode} will be used.
1448
     *
1449
     *
1450
     * @throws Exception if the query causes any problem
1451 483
     * @throws InvalidConfigException
1452
     * @throws NotSupportedException
1453 483
     * @throws \Throwable
1454
     *
1455 483
     * @return mixed the method execution result
1456 477
     */
1457
    protected function queryInternal(string $method, $fetchMode = null)
1458 477
    {
1459
        [$profile, $rawSql] = $this->logQuery('\Yiisoft\Db\Command\Command::query');
1460 6
1461 6
        if ($method !== '') {
1462 6
            $info = $this->db->getQueryCacheInfo($this->queryCacheDuration, $this->queryCacheDependency);
1463
1464
            if (is_array($info)) {
1465 6
                /* @var $cache CacheInterface */
1466
                $cache = $info[0];
1467 6
                $rawSql = $rawSql ?: $this->getRawSql();
1468 6
                $cacheKey = $this->getCacheKey($method, $fetchMode, $rawSql);
0 ignored issues
show
Bug introduced by
It seems like $fetchMode can also be of type array; however, parameter $fetchMode of Yiisoft\Db\Command\Command::getCacheKey() does only seem to accept integer|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

1468
                $cacheKey = $this->getCacheKey($method, /** @scrutinizer ignore-type */ $fetchMode, $rawSql);
Loading history...
1469 6
1470 6
1471 6
                $result = $cache->get($cacheKey);
0 ignored issues
show
Bug introduced by
$cacheKey of type array<integer,integer|null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::get(). ( Ignorable by Annotation )

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

1471
                $result = $cache->get(/** @scrutinizer ignore-type */ $cacheKey);
Loading history...
1472
1473
                if (is_array($result) && isset($result[0])) {
1474 6
                    if ($this->db->isLoggingEnabled()) {
1475
                        $this->logger->log(
1476
                            LogLevel::DEBUG,
1477
                            'Query result served from cache',
1478
                            ['\Yiisoft\Db\Command\Command::query']
1479 483
                        );
1480
                    }
1481
1482 483
                    return $result[0];
1483
                }
1484 483
            }
1485
        }
1486 481
1487 9
        $this->prepare(true);
1488
1489 475
        try {
1490 423
            if ($this->db->isProfilingEnabled()) {
1491
                $this->profiler->begin((string) $rawSql, ['\Yiisoft\Db\Command\Command::query']);
1492
            }
1493 475
1494
            $this->internalExecute($rawSql);
1495 475
1496
            if ($method === '') {
1497
                $result = new DataReader($this);
1498 481
            } else {
1499 17
                if ($fetchMode === null) {
1500 17
                    $fetchMode = $this->fetchMode;
1501
                }
1502 17
1503
                $result = \call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
1504
1505 481
                $this->pdoStatement->closeCursor();
1506 6
            }
1507 6
1508 6
            if ($this->db->isProfilingEnabled()) {
1509 6
                $this->profiler->end((string) $rawSql, ['\Yiisoft\Db\Command\Command::query']);
1510 6
            }
1511
        } catch (Exception $e) {
1512
            if ($this->db->isProfilingEnabled()) {
1513
                $this->profiler->end((string) $rawSql, ['\Yiisoft\Db\Command\Command::query']);
1514 481
            }
1515
1516
            throw $e;
1517
        }
1518
1519
        if (isset($cache, $cacheKey, $info)) {
1520
            $cache->set($cacheKey, [$result], $info[1], $info[2]);
1521
            if ($this->db->isLoggingEnabled()) {
1522
                $this->logger->log(
1523
                    LogLevel::DEBUG,
1524
                    'Saved query result in cache',
1525
                    ['\Yiisoft\Db\Command\Command::query']
1526
                );
1527
            }
1528 6
        }
1529
1530
        return $result;
1531 6
    }
1532 6
1533 6
    /**
1534 6
     * Returns the cache key for the query.
1535 6
     *
1536 6
     * @param string $method method of PDOStatement to be called.
1537
     * @param int $fetchMode the result fetch mode.
1538
     * Please refer to [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid
1539
     * fetch modes.
1540
     * @param string $rawSql the raw SQL with parameter values inserted into the corresponding placeholders.
1541
     *
1542
     * @return array the cache key
1543
     */
1544
    protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): array
1545
    {
1546
        return [
1547 46
            __CLASS__,
1548
            $method,
1549 46
            $fetchMode,
1550
            $this->db->getDsn(),
1551 46
            $this->db->getUsername(),
1552
            $rawSql,
1553
        ];
1554
    }
1555
1556
    /**
1557 151
     * Marks a specified table schema to be refreshed after command execution.
1558
     *
1559 151
     * @param string $name name of the table, which schema should be refreshed.
1560 43
     *
1561
     * @return self this command instance
1562 151
     */
1563
    protected function requireTableSchemaRefresh(string $name): self
1564
    {
1565
        $this->refreshTableName = $name;
1566
1567
        return $this;
1568
    }
1569
1570
    /**
1571
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
1572
     */
1573 3
    protected function refreshTableSchema(): void
1574
    {
1575 3
        if ($this->refreshTableName !== null) {
1576
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
1577 3
        }
1578
    }
1579
1580
    /**
1581
     * Marks the command to be executed in transaction.
1582
     *
1583
     * @param string|null $isolationLevel The isolation level to use for this transaction.
1584
     *
1585
     * See {@see Transaction::begin()} for details.
1586
     *
1587
     * @return self this command instance.
1588
     */
1589
    protected function requireTransaction(?string $isolationLevel = null): self
1590
    {
1591
        $this->isolationLevel = $isolationLevel;
1592
1593
        return $this;
1594
    }
1595
1596
    /**
1597
     * Sets a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing the
1598 3
     * command. The signature of the callable should be:.
1599
     *
1600 3
     * ```php
1601
     * function (\Yiisoft\Db\Exception\Exceptions $e, $attempt)
1602 3
     * {
1603
     *     // return true or false (whether to retry the command or rethrow $e)
1604
     * }
1605
     * ```
1606
     *
1607
     * The callable will receive a database exception thrown and a current attempt (to execute the command) number
1608
     * starting from 1.
1609
     *
1610
     * @param callable $handler a PHP callback to handle database exceptions.
1611
     *
1612
     * @return self this command instance.
1613
     */
1614
    protected function setRetryHandler(callable $handler): self
1615
    {
1616
        $this->retryHandler = $handler;
1617
1618
        return $this;
1619 495
    }
1620
1621 495
    /**
1622
     * Executes a prepared statement.
1623 495
     *
1624
     * It's a wrapper around {@see \PDOStatement::execute()} to support transactions and retry handlers.
1625
     *
1626 495
     * @param string|null $rawSql the rawSql if it has been created.
1627 495
     *
1628 495
     * @throws Exception
1629
     * @throws InvalidConfigException
1630
     * @throws NotSupportedException
1631
     * @throws \Throwable
1632 495
     *
1633
     * @return void
1634 494
     */
1635 28
    protected function internalExecute(?string $rawSql): void
1636 28
    {
1637 28
        $attempt = 0;
1638
1639 28
        while (true) {
1640 28
            try {
1641
                if (
1642
                    ++$attempt === 1
1643
                    && $this->isolationLevel !== null
1644 494
                    && $this->db->getTransaction() === null
1645
                ) {
1646 139
                    $this->db->transaction(fn ($rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
1647
                } else {
1648 139
                    $this->pdoStatement->execute();
1649 139
                }
1650 139
                break;
1651 139
            } catch (\Exception $e) {
1652 139
                $rawSql = $rawSql ?: $this->getRawSql();
1653 139
                $e = $this->db->getSchema()->convertException($e, $rawSql);
1654 139
1655
                if ($this->retryHandler === null || !\call_user_func($this->retryHandler, $e, $attempt)) {
1656 3
                    throw $e;
1657
                }
1658 3
            }
1659
        }
1660
    }
1661
1662
    protected function reset(): void
1663
    {
1664
        $this->sql = null;
1665
        $this->pendingParams = [];
1666 15
        $this->params = [];
1667
        $this->refreshTableName = null;
1668 15
        $this->isolationLevel = null;
1669
        $this->retryHandler = null;
1670
    }
1671 15
1672
    public function getDb(): ?Connection
1673 15
    {
1674
        return $this->db;
1675
    }
1676
1677
    public function getFetchMode(): int
1678
    {
1679
        return $this->fetchMode;
1680
    }
1681
1682
    public function getParams(): array
1683
    {
1684
        return $this->params;
1685 3
    }
1686
1687 3
    public function getPdoStatement(): ?PDOStatement
1688 3
    {
1689
        return $this->pdoStatement;
1690
    }
1691
1692
    /**
1693
     * The default fetch mode for this command.
1694
     *
1695
     * @param int $value
1696
     *
1697
     * @return void
1698
     *
1699
     * {@see http://www.php.net/manual/en/pdostatement.setfetchmode.php}
1700
     */
1701
    public function setFetchMode(int $value): void
1702
    {
1703
        $this->fetchMode = $value;
1704
    }
1705
1706
    /**
1707
     * The parameters (name => value) that are bound to the current PDO statement.
1708
     *
1709
     * This property is maintained by methods such as {@see bindValue()}. It is mainly provided for logging purpose and
1710
     * is used to generate {@see rawSql}. Do not modify it directly.
1711
     *
1712
     * @param $value
1713
     *
1714
     * @return void
1715
     */
1716
    public function setParams(array $value): void
1717
    {
1718
        $this->params = $value;
1719
    }
1720 1
1721
    /**
1722
     * The default number of seconds that query results can remain valid in cache.
1723
     *
1724
     * @param int $value If this is not set, the value will be used instead. Use 0 to indicate that the cached data will
1725
     * never expire. And use a negative number to indicate query cache should not be used.
1726
     *
1727
     * @return self
1728
     */
1729
    public function setQueryCacheDuration(int $value): self
1730
    {
1731
        $this->queryCacheDuration = $value;
1732
1733
        return $this;
1734
    }
1735
}
1736