Passed
Push — master ( 9f7d35...3f1c7e )
by Wilmer
08:50 queued 06:32
created

Command::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

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

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

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

1430
            $this->logger->/** @scrutinizer ignore-call */ 
1431
                           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...
1431
        }
1432
1433 660
        if (!$this->db->isProfilingEnabled()) {
1434 8
            return [false, $rawSql ?? null];
1435
        }
1436
1437 660
        return [true, $rawSql ?? $this->getRawSql()];
1438
    }
1439
1440
    /**
1441
     * Performs the actual DB query of a SQL statement.
1442
     *
1443
     * @param string $method method of PDOStatement to be called.
1444
     * @param array|int|null $fetchMode the result fetch mode.
1445
     *
1446
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
1447
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
1448
     *
1449
     *
1450
     * @throws Throwable
1451
     * @throws InvalidArgumentException
1452
     * @throws Exception if the query causes any problem.
1453
     *
1454
     * @return mixed the method execution result.
1455
     */
1456 644
    protected function queryInternal(string $method, $fetchMode = null)
1457
    {
1458 644
        [$profile, $rawSql] = $this->logQuery('\Yiisoft\Db\Command\Command::query');
1459
1460 644
        if ($method !== '') {
1461 632
            $info = $this->db->getQueryCacheInfo($this->queryCacheDuration, $this->queryCacheDependency);
1462
1463 632
            if (is_array($info)) {
1464
                /* @var $cache CacheInterface */
1465 8
                $cache = $info[0];
1466 8
                $rawSql = $rawSql ?: $this->getRawSql();
1467 8
                $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

1467
                $cacheKey = $this->getCacheKey($method, /** @scrutinizer ignore-type */ $fetchMode, $rawSql);
Loading history...
1468
1469
1470 8
                $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

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