Passed
Pull Request — master (#197)
by Wilmer
13:46
created

Command::setQueryCacheDuration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

129
        /** @scrutinizer ignore-call */ 
130
        $this->connectionCache = $this->db->getConnectionCache();
Loading history...
130 1413
    }
131 1413
132
    /**
133
     * Enables query cache for this command.
134
     *
135
     * @param int|null $duration the number of seconds that query result of this command can remain valid in the cache.
136
     * If this is not set, the value of {@see ConnectionInterface::queryCacheDuration} will be used instead.
137
     * Use 0 to indicate that the cached data will never expire.
138
     * @param Dependency|null $dependency the cache dependency associated with the cached query result.
139
     *
140
     * @return $this the command object itself.
141
     */
142
    public function cache(?int $duration = null, Dependency $dependency = null): self
143 8
    {
144
        $this->queryCacheDuration = $duration ?? $this->connectionCache->getQueryCacheDuration();
145 8
        $this->queryCacheDependency = $dependency;
146 8
147
        return $this;
148 8
    }
149
150
    /**
151
     * Disables query cache for this command.
152
     *
153
     * @return $this the command object itself.
154
     */
155
    public function noCache(): self
156 4
    {
157
        $this->queryCacheDuration = -1;
158 4
159
        return $this;
160 4
    }
161
162
    /**
163
     * Returns the SQL statement for this command.
164
     *
165
     * @return string|null the SQL statement to be executed.
166
     */
167
    public function getSql(): ?string
168 1389
    {
169
        return $this->sql;
170 1389
    }
171
172
    /**
173
     * Specifies the SQL statement to be executed. The SQL statement will be quoted using
174
     * {@see ConnectionInterface::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 $this this command instance.
182
     *
183
     * {@see reset()}
184
     * {@see cancel()}
185
     */
186
    public function setSql(string $sql): self
187 259
    {
188
        if ($sql !== $this->sql) {
189 259
            $this->cancel();
190 259
            $this->reset();
191 259
            $this->sql = $this->db->quoteSql($sql);
0 ignored issues
show
Bug introduced by
The method quoteSql() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

191
            /** @scrutinizer ignore-call */ 
192
            $this->sql = $this->db->quoteSql($sql);
Loading history...
192 259
        }
193
194
        return $this;
195 259
    }
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 $this this command instance.
206
     *
207
     * {@see reset()}
208
     * {@see cancel()}
209
     */
210
    public function setRawSql(string $sql): self
211 34
    {
212
        if ($sql !== $this->sql) {
213 34
            $this->cancel();
214 34
            $this->reset();
215 34
            $this->sql = $sql;
216 34
        }
217
218
        return $this;
219 34
    }
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
    public function getRawSql(): string
231 1398
    {
232
        if (empty($this->params)) {
233 1398
            return $this->sql;
234 1190
        }
235
236
        $params = [];
237 1015
238
        foreach ($this->params as $name => $value) {
239 1015
            if (is_string($name) && strncmp(':', $name, 1)) {
240 1015
                $name = ':' . $name;
241 15
            }
242
243
            if (is_string($value)) {
244 1015
                $params[$name] = $this->db->quoteValue($value);
0 ignored issues
show
Bug introduced by
The method quoteValue() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

244
                /** @scrutinizer ignore-call */ 
245
                $params[$name] = $this->db->quoteValue($value);
Loading history...
245 802
            } elseif (is_bool($value)) {
246 570
                $params[$name] = ($value ? 'TRUE' : 'FALSE');
247 19
            } elseif ($value === null) {
248 562
                $params[$name] = 'NULL';
249 94
            } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) {
250 504
                $params[$name] = $value;
251 504
            }
252
        }
253
254
        if (!isset($params[1])) {
255 1015
            return strtr($this->sql, $params);
256 1015
        }
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. For SQL statement
271
     * 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
    public function prepare(?bool $forRead = null): void
279 1378
    {
280
        if (isset($this->pdoStatement)) {
281 1378
            $this->bindPendingParams();
282 39
283
            return;
284 39
        }
285
286
        $sql = $this->getSql();
287 1378
288
        if ($this->db->getTransaction()) {
0 ignored issues
show
Bug introduced by
The method getTransaction() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

288
        if ($this->db->/** @scrutinizer ignore-call */ getTransaction()) {
Loading history...
289 1378
            /** master is in a transaction. use the same connection. */
290
            $forRead = false;
291 21
        }
292
293
        if ($forRead || ($forRead === null && $this->db->getSchema()->isReadQuery($sql))) {
294 1378
            $pdo = $this->db->getSlavePdo();
0 ignored issues
show
Bug introduced by
The method getSlavePdo() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

294
            /** @scrutinizer ignore-call */ 
295
            $pdo = $this->db->getSlavePdo();
Loading history...
295 1362
        } else {
296
            $pdo = $this->db->getMasterPdo();
0 ignored issues
show
Bug introduced by
The method getMasterPdo() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

296
            /** @scrutinizer ignore-call */ 
297
            $pdo = $this->db->getMasterPdo();
Loading history...
297 325
        }
298
299
        try {
300
            $this->pdoStatement = $pdo->prepare($sql);
301 1378
            $this->bindPendingParams();
302 1378
        } catch (\Exception $e) {
303 3
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
304 3
            $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
305 3
306
            throw new Exception($message, $errorInfo, $e);
307 3
        }
308
    }
309 1378
310
    /**
311
     * Cancels the execution of the SQL statement.
312
     *
313
     * This method mainly sets {@see pdoStatement} to be null.
314
     */
315
    public function cancel(): void
316 289
    {
317
        $this->pdoStatement = null;
318 289
    }
319 289
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
     * @param mixed $value the PHP variable to bind to the SQL statement parameter (passed by reference).
327
     * @param int|null $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
328
     * value.
329
     * @param int|null $length length of the data type.
330
     * @param mixed $driverOptions the driver-specific options.
331
     *
332
     * @throws Exception
333
     *
334
     * @return $this the current command being executed.
335
     *
336
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindParam.php}
337
     */
338
    public function bindParam($name, &$value, ?int $dataType = null, ?int $length = null, $driverOptions = null): self
339 1
    {
340
        $this->prepare();
341 1
342
        if ($dataType === null) {
343 1
            $dataType = $this->db->getSchema()->getPdoType($value);
344 1
        }
345
346
        if ($length === null) {
347 1
            $this->pdoStatement->bindParam($name, $value, $dataType);
0 ignored issues
show
Bug introduced by
The method bindParam() 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

347
            $this->pdoStatement->/** @scrutinizer ignore-call */ 
348
                                 bindParam($name, $value, $dataType);

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...
348 1
        } elseif ($driverOptions === null) {
349
            $this->pdoStatement->bindParam($name, $value, $dataType, $length);
350
        } else {
351
            $this->pdoStatement->bindParam($name, $value, $dataType, $length, $driverOptions);
352
        }
353
354
        $this->params[$name] = &$value;
355 1
356
        return $this;
357 1
    }
358
359
    /**
360
     * Binds pending parameters that were registered via {@see bindValue()} and {@see bindValues()}.
361
     *
362
     * Note that this method requires an active {@see pdoStatement}.
363
     */
364
    protected function bindPendingParams(): void
365 1378
    {
366
        foreach ($this->pendingParams as $name => $value) {
367 1378
            $this->pdoStatement->bindValue($name, $value[0], $value[1]);
368 995
        }
369
370
        $this->pendingParams = [];
371 1378
    }
372 1378
373
    /**
374
     * Binds a value to a parameter.
375
     *
376
     * @param string|int $name Parameter identifier. For a prepared statement using named placeholders, this will be a
377
     * parameter name of the form `:name`. For a prepared statement using question mark placeholders, this will be the
378
     * 1-indexed position of the parameter.
379
     * @param mixed $value The value to bind to the parameter.
380
     * @param int|null $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
381
     * value.
382
     *
383
     * @return $this the current command being executed.
384
     *
385
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindValue.php}
386
     */
387
    public function bindValue($name, $value, ?int $dataType = null): self
388 5
    {
389
        if ($dataType === null) {
390 5
            $dataType = $this->db->getSchema()->getPdoType($value);
391 5
        }
392
393
        $this->pendingParams[$name] = [$value, $dataType];
394 5
395
        $this->params[$name] = $value;
396 5
397
        return $this;
398 5
    }
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
     * @return $this the current command being executed.
415
     */
416
    public function bindValues(array $values): self
417 1413
    {
418
        if (empty($values)) {
419 1413
            return $this;
420 1213
        }
421
422
        $schema = $this->db->getSchema();
423 1019
424
        foreach ($values as $name => $value) {
425 1019
            if (is_array($value)) { // TODO: Drop in Yii 2.1
426 1019
                $this->pendingParams[$name] = $value;
427
                $this->params[$name] = $value[0];
428
            } elseif ($value instanceof PdoValue) {
429 1019
                $this->pendingParams[$name] = [$value->getValue(), $value->getType()];
430 1
                $this->params[$name] = $value->getValue();
431 1
            } else {
432
                $type = $schema->getPdoType($value);
433 1019
434
                $this->pendingParams[$name] = [$value, $type];
435 1019
                $this->params[$name] = $value;
436 1019
            }
437
        }
438
439
        return $this;
440 1019
    }
441
442
    /**
443
     * Executes the SQL statement and returns query result.
444
     *
445
     * This method is for executing a SQL query that returns result set, such as `SELECT`.
446
     *
447
     * @throws Throwable
448
     * @throws Exception execution failed.
449
     *
450
     * @return DataReader the reader object for fetching the query result.
451
     */
452
    public function query(): DataReader
453 28
    {
454
        return $this->queryInternal('');
455 28
    }
456
457
    /**
458
     * Executes the SQL statement and returns ALL rows at once.
459
     *
460
     * @param int|null $fetchMode the result fetch mode.
461
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
462
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
463
     *
464
     * @throws Throwable
465
     * @throws Exception execution failed.
466
     *
467
     * @return array all rows of the query result. Each array element is an array representing a row of data. An empty
468
     * array is returned if the query results in nothing.
469
     */
470
    public function queryAll(?int $fetchMode = null): array
471 1260
    {
472
        return $this->queryInternal('fetchAll', $fetchMode);
473 1260
    }
474
475
    /**
476
     * Executes the SQL statement and returns the first row of the result.
477
     *
478
     * This method is best used when only the first row of result is needed for a query.
479
     *
480
     * @param array|int|null $fetchMode the result fetch mode.
481
     *
482
     * Please refer to [PHP manual](http://php.net/manual/en/pdostatement.setfetchmode.php)
483
     * for valid fetch modes. If this parameter is null, the value set in {@see fetchMode} will be used.
484
     *
485
     * @throws Throwable|Exception execution failed.
486
     *
487
     * @return array|false the first row (in terms of an array) of the query result. False is returned if the query
488
     * results in nothing.
489
     */
490
    public function queryOne($fetchMode = null)
491 363
    {
492
        return $this->queryInternal('fetch', $fetchMode);
493 363
    }
494
495
    /**
496
     * Executes the SQL statement and returns the value of the first column in the first row of data.
497
     *
498
     * This method is best used when only a single value is needed for a query.
499
     *
500
     * @throws Exception|Throwable failed.
501
     *
502
     * @return string|null|false the value of the first column in the first row of the query result. False is returned
503
     * if there is no value.
504
     */
505
    public function queryScalar()
506 221
    {
507
        $result = $this->queryInternal('fetchColumn', 0);
508 221
509
        if (is_resource($result) && get_resource_type($result) === 'stream') {
510 221
            return stream_get_contents($result);
511
        }
512
513
        return $result;
514 221
    }
515
516
    /**
517
     * Executes the SQL statement and returns the first column of the result.
518
     *
519
     * This method is best used when only the first column of result (i.e. the first element in each row) is needed for
520
     * a query.
521
     *
522
     * @throws Exception|Throwable execution failed.
523
     *
524
     * @return array the first column of the query result. Empty array is returned if the query results in nothing.
525
     */
526
    public function queryColumn(): array
527 37
    {
528
        return $this->queryInternal('fetchAll', PDO::FETCH_COLUMN);
529 37
    }
530
531
    /**
532
     * Creates an INSERT command.
533
     *
534
     * For example,
535
     *
536
     * ```php
537
     * $connectionInterface->createCommand()->insert('user', [
538
     *     'name' => 'Sam',
539
     *     'age' => 30,
540
     * ])->execute();
541
     * ```
542
     *
543
     * The method will properly escape the column names, and bind the values to be inserted.
544
     *
545
     * Note that the created command is not executed until {@see execute()} is called.
546
     *
547
     * @param string $table the table that new rows will be inserted into.
548
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
549
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
550
     *
551
     * @return $this the command object itself
552
     */
553
    public function insert(string $table, $columns): self
554 118
    {
555
        $params = [];
556 118
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
0 ignored issues
show
Bug introduced by
The method getQueryBuilder() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

556
        $sql = $this->db->/** @scrutinizer ignore-call */ getQueryBuilder()->insert($table, $columns, $params);
Loading history...
557 118
558
        return $this->setSql($sql)->bindValues($params);
559 106
    }
560
561
    /**
562
     * Creates a batch INSERT command.
563
     *
564
     * For example,
565
     *
566
     * ```php
567
     * $connectionInterface->createCommand()->batchInsert('user', ['name', 'age'], [
568
     *     ['Tom', 30],
569
     *     ['Jane', 20],
570
     *     ['Linda', 25],
571
     * ])->execute();
572
     * ```
573
     *
574
     * The method will properly escape the column names, and quote the values to be inserted.
575
     *
576
     * Note that the values in each row must match the corresponding column names.
577
     *
578
     * Also note that the created command is not executed until {@see execute()} is called.
579
     *
580
     * @param string $table the table that new rows will be inserted into.
581
     * @param array $columns the column names
582
     * @param iterable $rows the rows to be batch inserted into the table.
583
     *
584
     * @return $this the command object itself.
585
     */
586
    public function batchInsert(string $table, array $columns, iterable $rows): self
587 34
    {
588
        $table = $this->db->quoteSql($table);
589 34
590
        $columns = array_map(fn ($column) => $this->db->quoteSql($column), $columns);
591 34
592
        $params = [];
593 34
594
        $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows, $params);
595 34
596
        $this->setRawSql($sql);
597 34
        $this->bindValues($params);
598 34
599
        return $this;
600 34
    }
601
602
    /**
603
     * Creates a command to insert rows into a database table if they do not already exist (matching unique constraints)
604
     * or update them if they do.
605
     *
606
     * For example,
607
     *
608
     * ```php
609
     * $sql = $queryBuilder->upsert('pages', [
610
     *     'name' => 'Front page',
611
     *     'url' => 'http://example.com/', // url is unique
612
     *     'visits' => 0,
613
     * ], [
614
     *     'visits' => new \Yiisoft\Db\Expression\Expression('visits + 1'),
615
     * ], $params);
616
     * ```
617
     *
618
     * The method will properly escape the table and column names.
619
     *
620
     * @param string $table the table that new rows will be inserted into/updated in.
621
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
622
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
623
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
624
     * If `true` is passed, the column data will be updated to match the insert column data.
625
     * If `false` is passed, no update will be performed if the column data already exists.
626
     * @param array $params the parameters to be bound to the command.
627
     *
628
     * @return $this the command object itself.
629
     */
630
    public function upsert(string $table, $insertColumns, $updateColumns = true, array $params = []): self
631 24
    {
632
        $sql = $this->db->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
633 24
634
        return $this->setSql($sql)->bindValues($params);
635 24
    }
636
637
    /**
638
     * Creates an UPDATE command.
639
     *
640
     * For example,
641
     *
642
     * ```php
643
     * $connectionInterface->createCommand()->update('user', ['status' => 1], 'age > 30')->execute();
644
     * ```
645
     *
646
     * or with using parameter binding for the condition:
647
     *
648
     * ```php
649
     * $minAge = 30;
650
     * $connectionInterface->createCommand()->update(
651
     *     'user',
652
     *     ['status' => 1],
653
     *     'age > :minAge',
654
     *     [':minAge' => $minAge]
655
     * )->execute();
656
     * ```
657
     *
658
     * The method will properly escape the column names and bind the values to be updated.
659
     *
660
     * Note that the created command is not executed until {@see execute()} is called.
661
     *
662
     * @param string $table the table to be updated.
663
     * @param array $columns the column data (name => value) to be updated.
664
     * @param string|array $condition the condition that will be put in the WHERE part.
665
     * Please refer to {@see Query::where()} on how to specify condition.
666
     * @param array $params the parameters to be bound to the command.
667
     *
668
     * @return $this the command object itself.
669
     */
670
    public function update(string $table, array $columns, $condition = '', array $params = []): self
671 63
    {
672
        $sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params);
673 63
674
        return $this->setSql($sql)->bindValues($params);
675 63
    }
676
677
    /**
678
     * Creates a DELETE command.
679
     *
680
     * For example,
681
     *
682
     * ```php
683
     * $connectionInterface->createCommand()->delete('user', 'status = 0')->execute();
684
     * ```
685
     *
686
     * or with using parameter binding for the condition:
687
     *
688
     * ```php
689
     * $status = 0;
690
     * $connectionInterface->createCommand()->delete('user', 'status = :status', [':status' => $status])->execute();
691
     * ```
692
     *
693
     * The method will properly escape the table and column names.
694
     *
695
     * Note that the created command is not executed until {@see execute()} is called.
696
     *
697
     * @param string $table the table where the data will be deleted from.
698
     * @param string|array $condition the condition that will be put in the WHERE part. Please refer to
699
     * {@see Query::where()} on how to specify condition.
700
     * @param array $params the parameters to be bound to the command.
701
     *
702
     * @return $this the command object itself.
703
     */
704
    public function delete(string $table, $condition = '', array $params = []): self
705 29
    {
706
        $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params);
707 29
708
        return $this->setSql($sql)->bindValues($params);
709 29
    }
710
711
    /**
712
     * Creates a SQL command for creating a new DB table.
713
     *
714
     * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), where name
715
     * stands for a column name which will be properly quoted by the method, and definition stands for the column type
716
     * which can contain an abstract DB type.
717
     *
718
     * The method {@see QueryBuilder::getColumnType()} will be called to convert the abstract column types to physical
719
     * ones. For example, `string` will be converted as `varchar(255)`, and `string not null` becomes
720
     * `varchar(255) not null`.
721
     *
722
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly inserted
723
     * into the generated SQL.
724
     *
725
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
726
     * @param array $columns the columns (name => definition) in the new table.
727
     * @param string|null $options additional SQL fragment that will be appended to the generated SQL.
728
     *
729
     * @return $this the command object itself.
730
     */
731
    public function createTable(string $table, array $columns, ?string $options = null): self
732 39
    {
733
        $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options);
734 39
735
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
736 39
    }
737
738
    /**
739
     * Creates a SQL command for renaming a DB table.
740
     *
741
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
742
     * @param string $newName the new table name. The name will be properly quoted by the method.
743
     *
744
     * @return $this the command object itself.
745
     */
746
    public function renameTable(string $table, string $newName): self
747 8
    {
748
        $sql = $this->db->getQueryBuilder()->renameTable($table, $newName);
749 8
750
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
751 8
    }
752
753
    /**
754
     * Creates a SQL command for dropping a DB table.
755
     *
756
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
757
     *
758
     * @return $this the command object itself
759
     */
760
    public function dropTable(string $table): self
761 5
    {
762
        $sql = $this->db->getQueryBuilder()->dropTable($table);
763 5
764
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
765 5
    }
766
767
    /**
768
     * Creates a SQL command for truncating a DB table.
769
     *
770
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
771
     *
772
     * @return $this the command object itself.
773
     */
774
    public function truncateTable(string $table): self
775 4
    {
776
        $sql = $this->db->getQueryBuilder()->truncateTable($table);
777 4
778
        return $this->setSql($sql);
779 4
    }
780
781
    /**
782
     * Creates a SQL command for adding a new DB column.
783
     *
784
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by
785
     * the method.
786
     * @param string $column the name of the new column. The name will be properly quoted by the method.
787
     * @param string $type the column type. {@see QueryBuilder::getColumnType()} will be called to convert the give
788
     * column type to the physical one. For example, `string` will be converted as `varchar(255)`, and `string not null`
789
     * becomes `varchar(255) not null`.
790
     *
791
     * @return $this the command object itself.
792
     */
793
    public function addColumn(string $table, string $column, string $type): self
794 1
    {
795
        $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type);
796 1
797
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
798 1
    }
799
800
    /**
801
     * Creates a SQL command for dropping a DB column.
802
     *
803
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
804
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
805
     *
806
     * @return $this the command object itself.
807
     */
808
    public function dropColumn(string $table, string $column): self
809
    {
810
        $sql = $this->db->getQueryBuilder()->dropColumn($table, $column);
811
812
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
813
    }
814
815
    /**
816
     * Creates a SQL command for renaming a column.
817
     *
818
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
819
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
820
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
821
     *
822
     * @return $this the command object itself.
823
     */
824
    public function renameColumn(string $table, string $oldName, string $newName): self
825
    {
826
        $sql = $this->db->getQueryBuilder()->renameColumn($table, $oldName, $newName);
827
828
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
829
    }
830
831
    /**
832
     * Creates a SQL command for changing the definition of a column.
833
     *
834
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
835
     * method.
836
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
837
     * @param string $type the column type. {@see QueryBuilder::getColumnType()} will be called to
838
     * convert the give column type to the physical one. For example, `string` will be converted as `varchar(255)`, and
839
     * `string not null` becomes `varchar(255) not null`.
840
     *
841
     * @return $this the command object itself.
842
     */
843
    public function alterColumn(string $table, string $column, string $type): self
844 1
    {
845
        $sql = $this->db->getQueryBuilder()->alterColumn($table, $column, $type);
846 1
847
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
848 1
    }
849
850
    /**
851
     * Creates a SQL command for adding a primary key constraint to an existing table.
852
     *
853
     * The method will properly quote the table and column names.
854
     *
855
     * @param string $name the name of the primary key constraint.
856
     * @param string $table the table that the primary key constraint will be added to.
857
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
858
     *
859
     * @return $this the command object itself.
860
     */
861
    public function addPrimaryKey(string $name, string $table, $columns): self
862 2
    {
863
        $sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns);
864 2
865
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
866 2
    }
867
868
    /**
869
     * Creates a SQL command for removing a primary key constraint to an existing table.
870
     *
871
     * @param string $name the name of the primary key constraint to be removed.
872
     * @param string $table the table that the primary key constraint will be removed from.
873
     *
874
     * @return $this the command object itself.
875
     */
876
    public function dropPrimaryKey(string $name, string $table): self
877 2
    {
878
        $sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table);
879 2
880
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
881 2
    }
882
883
    /**
884
     * Creates a SQL command for adding a foreign key constraint to an existing table.
885
     *
886
     * The method will properly quote the table and column names.
887
     *
888
     * @param string $name the name of the foreign key constraint.
889
     * @param string $table the table that the foreign key constraint will be added to.
890
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
891
     * multiple columns, separate them with commas.
892
     * @param string $refTable the table that the foreign key references to.
893
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
894
     * columns, separate them with commas.
895
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
896
     * SET DEFAULT, SET NULL.
897
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
898
     * SET DEFAULT, SET NULL.
899
     *
900
     * @return $this the command object itself.
901
     */
902
    public function addForeignKey(
903 5
        string $name,
904
        string $table,
905
        $columns,
906
        string $refTable,
907
        $refColumns,
908
        ?string $delete = null,
909
        ?string $update = null
910
    ): self {
911
        $sql = $this->db->getQueryBuilder()->addForeignKey(
912 5
            $name,
913 5
            $table,
914
            $columns,
915
            $refTable,
916
            $refColumns,
917
            $delete,
918
            $update
919
        );
920
921
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
922 5
    }
923
924
    /**
925
     * Creates a SQL command for dropping a foreign key constraint.
926
     *
927
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by
928
     * the method.
929
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
930
     *
931
     * @return $this the command object itself.
932
     */
933
    public function dropForeignKey(string $name, string $table): self
934 4
    {
935
        $sql = $this->db->getQueryBuilder()->dropForeignKey($name, $table);
936 4
937
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
938 4
    }
939
940
    /**
941
     * Creates a SQL command for creating a new index.
942
     *
943
     * @param string $name the name of the index. The name will be properly quoted by the method.
944
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
945
     * the method.
946
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
947
     * please separate them by commas. The column names will be properly quoted by the method.
948
     * @param bool $unique whether to add UNIQUE constraint on the created index.
949
     *
950
     * @return $this the command object itself.
951
     */
952
    public function createIndex(string $name, string $table, $columns, bool $unique = false): self
953 4
    {
954
        $sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique);
955 4
956
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
957 4
    }
958
959
    /**
960
     * Creates a SQL command for dropping an index.
961
     *
962
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
963
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
964
     *
965
     * @return $this the command object itself.
966
     */
967
    public function dropIndex(string $name, string $table): self
968 4
    {
969
        $sql = $this->db->getQueryBuilder()->dropIndex($name, $table);
970 4
971
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
972 4
    }
973
974
    /**
975
     * Creates a SQL command for adding an unique constraint to an existing table.
976
     *
977
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
978
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
979
     * the method.
980
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
981
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
982
     *
983
     * @return $this the command object itself.
984
     */
985
    public function addUnique(string $name, string $table, $columns): self
986 4
    {
987
        $sql = $this->db->getQueryBuilder()->addUnique($name, $table, $columns);
988 4
989
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
990 4
    }
991
992
    /**
993
     * Creates a SQL command for dropping an unique constraint.
994
     *
995
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
996
     * method.
997
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by
998
     * the method.
999
     *
1000
     * @return $this the command object itself.
1001
     */
1002
    public function dropUnique(string $name, string $table): self
1003 4
    {
1004
        $sql = $this->db->getQueryBuilder()->dropUnique($name, $table);
1005 4
1006
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1007 4
    }
1008
1009
    /**
1010
     * Creates a SQL command for adding a check constraint to an existing table.
1011
     *
1012
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
1013
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
1014
     * the method.
1015
     * @param string $expression the SQL of the `CHECK` constraint.
1016
     *
1017
     * @return $this the command object itself.
1018
     */
1019
    public function addCheck(string $name, string $table, string $expression): self
1020 2
    {
1021
        $sql = $this->db->getQueryBuilder()->addCheck($name, $table, $expression);
1022 2
1023
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1024 2
    }
1025
1026
    /**
1027
     * Creates a SQL command for dropping a check constraint.
1028
     *
1029
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
1030
     * method.
1031
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
1032
     * method.
1033
     *
1034
     * @return $this the command object itself.
1035
     */
1036
    public function dropCheck(string $name, string $table): self
1037 2
    {
1038
        $sql = $this->db->getQueryBuilder()->dropCheck($name, $table);
1039 2
1040
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1041 2
    }
1042
1043
    /**
1044
     * Creates a SQL command for adding a default value constraint to an existing table.
1045
     *
1046
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
1047
     * @param string $table the table that the default value constraint will be added to. The name will be properly
1048
     * quoted by the method.
1049
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
1050
     * quoted by the method.
1051
     * @param mixed $value default value.
1052
     *
1053
     * @return $this the command object itself.
1054
     */
1055
    public function addDefaultValue(string $name, string $table, string $column, $value): self
1056 1
    {
1057
        $sql = $this->db->getQueryBuilder()->addDefaultValue($name, $table, $column, $value);
1058 1
1059
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1060 1
    }
1061
1062
    /**
1063
     * Creates a SQL command for dropping a default value constraint.
1064
     *
1065
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
1066
     * the method.
1067
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
1068
     * by the method.
1069
     *
1070
     * @return $this the command object itself.
1071
     */
1072
    public function dropDefaultValue(string $name, string $table): self
1073 1
    {
1074
        $sql = $this->db->getQueryBuilder()->dropDefaultValue($name, $table);
1075 1
1076
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1077 1
    }
1078
1079
    /**
1080
     * Creates a SQL command for resetting the sequence value of a table's primary key.
1081
     *
1082
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
1083
     * or 1.
1084
     *
1085
     * @param string $table the name of the table whose primary key sequence will be reset.
1086
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
1087
     * row's primary key will have a value 1.
1088
     *
1089
     * @return $this the command object itself.
1090
     */
1091
    public function resetSequence(string $table, $value = null): self
1092
    {
1093
        $sql = $this->db->getQueryBuilder()->resetSequence($table, $value);
1094
1095
        return $this->setSql($sql);
1096
    }
1097
1098
    /**
1099
     * Executes a db command resetting the sequence value of a table's primary key.
1100
     *
1101
     * Reason for execute is that some databases (Oracle) need several queries to do so.
1102
     *
1103
     * The sequence is reset such that the primary key of the next new row inserted will have the specified value or the
1104
     * maximum existing value +1.
1105
     *
1106
     * @param string $table the name of the table whose primary key sequence is reset.
1107
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
1108
     * row's primary key will have the maximum existing value +1.
1109
     *
1110
     * @return $this
1111
     */
1112
    public function executeResetSequence(string $table, $value = null): self
1113
    {
1114
        return $this->resetSequence($table, $value);
1115
    }
1116
1117
    /**
1118
     * Builds a SQL command for enabling or disabling integrity check.
1119
     *
1120
     * @param string $schema the schema name of the tables. Defaults to empty string, meaning the current or default
1121
     * schema.
1122
     * @param string $table the table name.
1123
     * @param bool $check whether to turn on or off the integrity check.
1124
     *
1125
     * @return $this the command object itself
1126
     */
1127
    public function checkIntegrity(string $schema, string $table, bool $check = true): self
1128
    {
1129
        $sql = $this->db->getQueryBuilder()->checkIntegrity($schema, $table, $check);
1130
1131
        return $this->setSql($sql);
1132
    }
1133
1134
    /**
1135
     * Builds a SQL command for adding comment to column.
1136
     *
1137
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1138
     * method.
1139
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1140
     * method.
1141
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1142
     *
1143
     * @return $this the command object itself.
1144
     */
1145
    public function addCommentOnColumn(string $table, string $column, string $comment): self
1146
    {
1147
        $sql = $this->db->getQueryBuilder()->addCommentOnColumn($table, $column, $comment);
1148
1149
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1150
    }
1151
1152
    /**
1153
     * Builds a SQL command for adding comment to table.
1154
     *
1155
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1156
     * method.
1157
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1158
     *
1159
     * @return $this the command object itself.
1160
     */
1161
    public function addCommentOnTable(string $table, string $comment): self
1162
    {
1163
        $sql = $this->db->getQueryBuilder()->addCommentOnTable($table, $comment);
1164
1165
        return $this->setSql($sql);
1166
    }
1167
1168
    /**
1169
     * Builds a SQL command for dropping comment from column.
1170
     *
1171
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1172
     * method.
1173
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1174
     * method.
1175
     *
1176
     * @return $this the command object itself.
1177
     */
1178
    public function dropCommentFromColumn(string $table, string $column): self
1179
    {
1180
        $sql = $this->db->getQueryBuilder()->dropCommentFromColumn($table, $column);
1181
1182
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
1183
    }
1184
1185
    /**
1186
     * Builds a SQL command for dropping comment from table.
1187
     *
1188
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1189
     * method.
1190
     *
1191
     * @return $this the command object itself.
1192
     */
1193
    public function dropCommentFromTable(string $table): self
1194
    {
1195
        $sql = $this->db->getQueryBuilder()->dropCommentFromTable($table);
1196
1197
        return $this->setSql($sql);
1198
    }
1199
1200
    /**
1201
     * Creates a SQL View.
1202
     *
1203
     * @param string $viewName the name of the view to be created.
1204
     * @param string|Query $subquery the select statement which defines the view. This can be either a string or a
1205
     * {@see Query} object.
1206
     *
1207
     * @return $this the command object itself.
1208
     */
1209
    public function createView(string $viewName, $subquery): self
1210 4
    {
1211
        $sql = $this->db->getQueryBuilder()->createView($viewName, $subquery);
1212 4
1213
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
1214 4
    }
1215
1216
    /**
1217
     * Drops a SQL View.
1218
     *
1219
     * @param string $viewName the name of the view to be dropped.
1220
     *
1221
     * @return $this the command object itself.
1222
     */
1223
    public function dropView(string $viewName): self
1224 4
    {
1225
        $sql = $this->db->getQueryBuilder()->dropView($viewName);
1226 4
1227
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
1228 4
    }
1229
1230
    /**
1231
     * Executes the SQL statement.
1232
     *
1233
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
1234
     * No result set will be returned.
1235
     *
1236
     * @throws Throwable
1237
     * @throws Exception execution failed.
1238
     *
1239
     * @return int number of rows affected by the execution.
1240
     */
1241
    public function execute(): int
1242 302
    {
1243
        $sql = $this->getSql();
1244 302
1245
        [$profile, $rawSql] = $this->logQuery(__METHOD__);
1246 302
1247
        if ($sql === '') {
1248 302
            return 0;
1249 8
        }
1250
1251
        $this->prepare(false);
1252 298
1253
        try {
1254
            if ($this->db->isProfilingEnabled()) {
0 ignored issues
show
Bug introduced by
The method isProfilingEnabled() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

1254
            if ($this->db->/** @scrutinizer ignore-call */ isProfilingEnabled()) {
Loading history...
1255 298
                $this->profiler->begin((string) $rawSql, [__METHOD__]);
1256 298
            }
1257
1258
            $this->internalExecute($rawSql);
1259 298
            $n = $this->pdoStatement->rowCount();
1260 294
1261
            if ($this->db->isProfilingEnabled()) {
1262 294
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1263 294
            }
1264
1265
            $this->refreshTableSchema();
1266 294
1267
            return $n;
1268 294
        } catch (Exception $e) {
1269 15
            if ($this->db->isProfilingEnabled()) {
1270 15
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1271 15
            }
1272
1273
            throw $e;
1274 15
        }
1275
    }
1276
1277
    /**
1278
     * Logs the current database query if query logging is enabled and returns the profiling token if profiling is
1279
     * enabled.
1280
     *
1281
     * @param string $category the log category.
1282
     *
1283
     * @return array array of two elements, the first is boolean of whether profiling is enabled or not. The second is
1284
     * the rawSql if it has been created.
1285
     */
1286
    protected function logQuery(string $category): array
1287 1374
    {
1288
        if ($this->db->isLoggingEnabled()) {
0 ignored issues
show
Bug introduced by
The method isLoggingEnabled() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

1288
        if ($this->db->/** @scrutinizer ignore-call */ isLoggingEnabled()) {
Loading history...
1289 1374
            $rawSql = $this->getRawSql();
1290 1374
            $this->logger->log(LogLevel::INFO, $rawSql, [$category]);
1291 1374
        }
1292
1293
        if (!$this->db->isProfilingEnabled()) {
1294 1374
            return [false, $rawSql ?? null];
1295 8
        }
1296
1297
        return [true, $rawSql ?? $this->getRawSql()];
1298 1374
    }
1299
1300
    /**
1301
     * Performs the actual DB query of a SQL statement.
1302
     *
1303
     * @param string $method method of PDOStatement to be called.
1304
     * @param array|int|null $fetchMode the result fetch mode.
1305
     *
1306
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
1307
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
1308
     *
1309
     *
1310
     * @throws Throwable|Exception if the query causes any problem.
1311
     *
1312
     * @return mixed the method execution result.
1313
     */
1314
    protected function queryInternal(string $method, $fetchMode = null)
1315 1358
    {
1316
        [, $rawSql] = $this->logQuery(__CLASS__ . '::query');
1317 1358
1318
        if ($method !== '') {
1319 1358
            $info = $this->connectionCache->queryCacheInfo(
1320 1346
                $this->queryCacheDuration,
1321
                $this->queryCacheDependency
1322 1346
            );
1323
1324 8
            if (is_array($info)) {
1325 8
                /* @var $cache CacheInterface */
1326 8
                $cache = $info[0];
1327
                $rawSql = $rawSql ?: $this->getRawSql();
1328
                $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

1328
                $cacheKey = $this->getCacheKey($method, /** @scrutinizer ignore-type */ $fetchMode, $rawSql);
Loading history...
1329 8
1330
1331 8
                $result = $cache->get($cacheKey);
1332 8
1333 8
                if (is_array($result) && isset($result[0])) {
1334 8
                    if ($this->db->isLoggingEnabled()) {
1335 8
                        $this->logger->log(
1336 8
                            LogLevel::DEBUG,
1337
                            'Query result served from cache',
1338
                            [__CLASS__ . '::query']
1339
                        );
1340 8
                    }
1341
1342
                    return $result[0];
1343
                }
1344
            }
1345 1358
        }
1346
1347
        $this->prepare(true);
1348 1358
1349 1358
        try {
1350
            if ($this->db->isProfilingEnabled()) {
1351
                $this->profiler->begin((string) $rawSql, [__CLASS__ . '::query']);
1352 1358
            }
1353
1354 1356
            $this->internalExecute($rawSql);
1355 28
1356
            if ($method === '') {
1357 1344
                $result = new DataReader($this);
1358 1272
            } else {
1359
                if ($fetchMode === null) {
1360
                    $fetchMode = $this->fetchMode;
1361 1344
                }
1362
1363 1344
                $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
1364
1365
                $this->pdoStatement->closeCursor();
1366 1356
            }
1367 1356
1368
            if ($this->db->isProfilingEnabled()) {
1369 25
                $this->profiler->end((string) $rawSql, [__CLASS__ . '::query']);
1370 25
            }
1371 25
        } catch (Exception $e) {
1372
            if ($this->db->isProfilingEnabled()) {
1373
                $this->profiler->end((string) $rawSql, [__CLASS__ . '::query']);
1374 25
            }
1375
1376
            throw $e;
1377 1356
        }
1378 8
1379 8
        if (isset($cache, $cacheKey, $info)) {
1380 8
            $cache->set($cacheKey, [$result], $info[1], $info[2]);
1381 8
            if ($this->db->isLoggingEnabled()) {
1382 8
                $this->logger->log(
1383 8
                    LogLevel::DEBUG,
1384
                    'Saved query result in cache',
1385
                    [__CLASS__ . '::query']
1386
                );
1387
            }
1388 1356
        }
1389
1390
        return $result;
1391
    }
1392
1393
    /**
1394
     * Returns the cache key for the query.
1395
     *
1396
     * @param string $method method of PDOStatement to be called.
1397
     * @param int|null $fetchMode the result fetch mode.
1398
     * Please refer to [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid
1399
     * fetch modes.
1400
     * @param string $rawSql the raw SQL with parameter values inserted into the corresponding placeholders.
1401
     *
1402 8
     * @throws JsonException
1403
     *
1404
     * @return string the cache key.
1405 8
     */
1406 8
    protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): string
1407 8
    {
1408 8
        $key = [
1409 8
            __CLASS__,
1410 8
            $method,
1411
            $fetchMode,
1412
            $this->db->getDsn(),
1413 8
            $this->db->getUsername(),
0 ignored issues
show
Bug introduced by
The method getUsername() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

1413
            $this->db->/** @scrutinizer ignore-call */ 
1414
                       getUsername(),
Loading history...
1414
            $rawSql
1415 8
        ];
1416
1417
        return $this->connectionCache->normalize($key);
1418
    }
1419
1420
    /**
1421
     * Marks a specified table schema to be refreshed after command execution.
1422
     *
1423
     * @param string $name name of the table, which schema should be refreshed.
1424
     *
1425 55
     * @return $this this command instance.
1426
     */
1427 55
    protected function requireTableSchemaRefresh(string $name): self
1428
    {
1429 55
        $this->refreshTableName = $name;
1430
1431
        return $this;
1432
    }
1433
1434
    /**
1435 294
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
1436
     */
1437 294
    protected function refreshTableSchema(): void
1438 51
    {
1439
        if ($this->refreshTableName !== null) {
1440 294
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
1441
        }
1442
    }
1443
1444
    /**
1445
     * Marks the command to be executed in transaction.
1446
     *
1447
     * @param string|null $isolationLevel The isolation level to use for this transaction.
1448
     *
1449
     * See {@see Transaction::begin()} for details.
1450
     *
1451 4
     * @return $this this command instance.
1452
     */
1453 4
    protected function requireTransaction(?string $isolationLevel = null): self
1454
    {
1455 4
        $this->isolationLevel = $isolationLevel;
1456
1457
        return $this;
1458
    }
1459
1460
    /**
1461
     * Sets a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing the
1462
     * command. The signature of the callable should be:.
1463
     *
1464
     * ```php
1465
     * function (Exceptions $e, $attempt)
1466
     * {
1467
     *     // return true or false (whether to retry the command or rethrow $e)
1468
     * }
1469
     * ```
1470
     *
1471
     * The callable will receive a database exception thrown and a current attempt (to execute the command) number
1472
     * starting from 1.
1473
     *
1474
     * @param callable $handler a PHP callback to handle database exceptions.
1475
     *
1476 4
     * @return $this this command instance.
1477
     */
1478 4
    protected function setRetryHandler(callable $handler): self
1479
    {
1480 4
        $this->retryHandler = $handler;
1481
1482
        return $this;
1483
    }
1484
1485
    /**
1486
     * Executes a prepared statement.
1487
     *
1488
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
1489
     *
1490
     * @param string|null $rawSql the rawSql if it has been created.
1491
     *
1492 1374
     * @throws Exception|Throwable
1493
     */
1494 1374
    protected function internalExecute(?string $rawSql): void
1495
    {
1496 1374
        $attempt = 0;
1497
1498
        while (true) {
1499 1374
            try {
1500 1374
                if (
1501 1374
                    ++$attempt === 1
1502
                    && $this->isolationLevel !== null
1503
                    && $this->db->getTransaction() === null
1504
                ) {
1505 1374
                    $this->db->transaction(fn ($rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
0 ignored issues
show
Bug introduced by
The method transaction() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

1505
                    $this->db->/** @scrutinizer ignore-call */ 
1506
                               transaction(fn ($rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
Loading history...
1506
                } else {
1507 1371
                    $this->pdoStatement->execute();
1508 37
                }
1509 37
                break;
1510 37
            } catch (\Exception $e) {
1511
                $rawSql = $rawSql ?: $this->getRawSql();
1512 37
                $e = $this->db->getSchema()->convertException($e, $rawSql);
1513 37
1514
                if ($this->retryHandler === null || !call_user_func($this->retryHandler, $e, $attempt)) {
1515
                    throw $e;
1516
                }
1517 1371
            }
1518
        }
1519 285
    }
1520
1521 285
    protected function reset(): void
1522 285
    {
1523 285
        $this->sql = null;
1524 285
        $this->pendingParams = [];
1525 285
        $this->params = [];
1526 285
        $this->refreshTableName = null;
1527 285
        $this->isolationLevel = null;
1528
        $this->retryHandler = null;
1529 12
    }
1530
1531 12
    public function getFetchMode(): int
1532
    {
1533
        return $this->fetchMode;
1534
    }
1535
1536
    public function getParams(): array
1537
    {
1538
        return $this->params;
1539 27
    }
1540
1541 27
    public function getPdoStatement(): ?PDOStatement
1542
    {
1543
        return $this->pdoStatement;
1544 69
    }
1545
1546 69
    /**
1547
     * The default fetch mode for this command.
1548
     *
1549
     * @param int $value
1550
     *
1551
     * {@see http://www.php.net/manual/en/pdostatement.setfetchmode.php}
1552
     */
1553
    public function setFetchMode(int $value): void
1554
    {
1555
        $this->fetchMode = $value;
1556 4
    }
1557
1558 4
    /**
1559 4
     * The parameters (name => value) that are bound to the current PDO statement.
1560
     *
1561
     * This property is maintained by methods such as {@see bindValue()}. It is mainly provided for logging purpose and
1562
     * is used to generate {@see rawSql}. Do not modify it directly.
1563
     *
1564
     * @param $value
1565
     */
1566
    public function setParams(array $value): void
1567
    {
1568
        $this->params = $value;
1569
    }
1570
}
1571