Passed
Pull Request — master (#210)
by Evgeniy
11:06
created

Command::bindPendingParams()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
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\QueryCache;
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\ProfilerInterface;
24
25
use function array_map;
26
use function call_user_func_array;
27
use function explode;
28
use function get_resource_type;
29
use function is_array;
30
use function is_bool;
31
use function is_object;
32
use function is_resource;
33
use function is_string;
34
use function stream_get_contents;
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 ConnectionInterface::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 = $connectionInterface->createCommand('SELECT * FROM user')->queryAll();
53
 * ```
54
 *
55
 * Command supports SQL statement preparation and parameter binding.
56
 *
57
 * Call {@see bindValue()} to bind a value to a SQL parameter;
58
 * Call {@see bindParam()} to bind a PHP variable to a SQL parameter.
59
 *
60
 * When binding a parameter, the SQL statement is automatically prepared. You may also call {@see prepare()} explicitly
61
 * to prepare a SQL statement.
62
 *
63
 * Command also supports building SQL statements by providing methods such as {@see insert()}, {@see update()}, etc.
64
 *
65
 * For example, the following code will create and execute an INSERT SQL statement:
66
 *
67
 * ```php
68
 * $connectionInterface->createCommand()->insert('user', [
69
 *     'name' => 'Sam',
70
 *     'age' => 30,
71
 * ])->execute();
72
 * ```
73
 *
74
 * To build SELECT SQL statements, please use {@see Query} instead.
75
 *
76
 * For more details and usage information on Command, see the [guide article on Database Access Objects](guide:db-dao).
77
 *
78
 * @property string $rawSql The raw SQL with parameter values inserted into the corresponding placeholders in
79
 * {@see sql}.
80
 * @property string $sql The SQL statement to be executed.
81
 */
82
class Command
83
{
84
    /**
85
     * @var array pending parameters to be bound to the current PDO statement.
86
     */
87
    protected array $pendingParams = [];
88
    protected array $params = [];
89
90
    /**
91
     * @var string|null the SQL statement that this command represents
92
     */
93
    private ?string $sql = null;
94
95
    /**
96
     * @var string|null name of the table, which schema, should be refreshed after command execution.
97
     */
98
    private ?string $refreshTableName = null;
99
100
    /**
101
     * @var string|null the isolation level to use for this transaction.
102
     *
103
     * See {@see Transaction::begin()} for details.
104
     */
105
    private ?string $isolationLevel = null;
106
107
    /**
108
     * @var callable a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing
109
     * the command.
110
     */
111
    private $retryHandler;
112
113
    private ProfilerInterface $profiler;
114
    private LoggerInterface $logger;
115
    private ConnectionInterface $db;
116
    private ?PDOStatement $pdoStatement = null;
117
    private int $fetchMode = PDO::FETCH_ASSOC;
118
    private ?int $queryCacheDuration = null;
119
    private ?Dependency $queryCacheDependency = null;
120
    private QueryCache $queryCache;
121
122 1748
    public function __construct(
123
        ProfilerInterface $profiler,
124
        LoggerInterface $logger,
125
        ConnectionInterface $db,
126
        QueryCache $queryCache,
127
        ?string $sql
128
    ) {
129 1748
        $this->db = $db;
130 1748
        $this->logger = $logger;
131 1748
        $this->profiler = $profiler;
132 1748
        $this->sql = $sql;
133 1748
        $this->queryCache = $queryCache;
134 1748
    }
135
136
    /**
137
     * Enables query cache for this command.
138
     *
139
     * @param int|null $duration the number of seconds that query result of this command can remain valid in the cache.
140
     * If this is not set, the value of {@see ConnectionInterface::queryCacheDuration} will be used instead.
141
     * Use 0 to indicate that the cached data will never expire.
142
     * @param Dependency|null $dependency the cache dependency associated with the cached query result.
143
     *
144
     * @return $this the command object itself.
145
     */
146 10
    public function cache(?int $duration = null, Dependency $dependency = null): self
147
    {
148 10
        $this->queryCacheDuration = $duration ?? $this->queryCache->getDuration();
149 10
        $this->queryCacheDependency = $dependency;
150
151 10
        return $this;
152
    }
153
154
    /**
155
     * Disables query cache for this command.
156
     *
157
     * @return $this the command object itself.
158
     */
159 5
    public function noCache(): self
160
    {
161 5
        $this->queryCacheDuration = -1;
162
163 5
        return $this;
164
    }
165
166
    /**
167
     * Returns the SQL statement for this command.
168
     *
169
     * @return string|null the SQL statement to be executed.
170
     */
171 1718
    public function getSql(): ?string
172
    {
173 1718
        return $this->sql;
174
    }
175
176
    /**
177
     * Specifies the SQL statement to be executed. The SQL statement will be quoted using
178
     * {@see ConnectionInterface::quoteSql()}.
179
     *
180
     * The previous SQL (if any) will be discarded, and {@see params} will be cleared as well. See {@see reset()} for
181
     * details.
182
     *
183
     * @param string $sql the SQL statement to be set.
184
     *
185
     * @return $this this command instance.
186
     *
187
     * {@see reset()}
188
     * {@see cancel()}
189
     */
190 320
    public function setSql(string $sql): self
191
    {
192 320
        if ($sql !== $this->sql) {
193 320
            $this->cancel();
194 320
            $this->reset();
195 320
            $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

195
            /** @scrutinizer ignore-call */ 
196
            $this->sql = $this->db->quoteSql($sql);
Loading history...
196
        }
197
198 320
        return $this;
199
    }
200
201
    /**
202
     * Specifies the SQL statement to be executed. The SQL statement will not be modified in any way.
203
     *
204
     * The previous SQL (if any) will be discarded, and {@see params} will be cleared as well. See {@see reset()}
205
     * for details.
206
     *
207
     * @param string $sql the SQL statement to be set.
208
     *
209
     * @return $this this command instance.
210
     *
211
     * {@see reset()}
212
     * {@see cancel()}
213
     */
214 41
    public function setRawSql(string $sql): self
215
    {
216 41
        if ($sql !== $this->sql) {
217 41
            $this->cancel();
218 41
            $this->reset();
219 41
            $this->sql = $sql;
220
        }
221
222 41
        return $this;
223
    }
224
225
    /**
226
     * Returns the raw SQL by inserting parameter values into the corresponding placeholders in {@see sql}.
227
     *
228
     * Note that the return value of this method should mainly be used for logging purpose.
229
     *
230
     * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders.
231
     *
232
     * @return string the raw SQL with parameter values inserted into the corresponding placeholders in {@see sql}.
233
     */
234 1729
    public function getRawSql(): string
235
    {
236 1729
        if (empty($this->params)) {
237 1306
            return $this->sql;
238
        }
239
240 1327
        $params = [];
241
242 1327
        foreach ($this->params as $name => $value) {
243 1327
            if (is_string($name) && strncmp(':', $name, 1)) {
244 18
                $name = ':' . $name;
245
            }
246
247 1327
            if (is_string($value)) {
248 1103
                $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

248
                /** @scrutinizer ignore-call */ 
249
                $params[$name] = $this->db->quoteValue($value);
Loading history...
249 694
            } elseif (is_bool($value)) {
250 20
                $params[$name] = ($value ? 'TRUE' : 'FALSE');
251 685
            } elseif ($value === null) {
252 103
                $params[$name] = 'NULL';
253 626
            } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) {
254 626
                $params[$name] = $value;
255
            }
256
        }
257
258 1327
        if (!isset($params[1])) {
259 1327
            return strtr($this->sql, $params);
260
        }
261
262
        $sql = '';
263
264
        foreach (explode('?', $this->sql) as $i => $part) {
265
            $sql .= ($params[$i] ?? '') . $part;
266
        }
267
268
        return $sql;
269
    }
270
271
    /**
272
     * Prepares the SQL statement to be executed.
273
     *
274
     * For complex SQL statement that is to be executed multiple times, this may improve performance. For SQL statement
275
     * with binding parameters, this method is invoked automatically.
276
     *
277
     * @param bool|null $forRead whether this method is called for a read query. If null, it means the SQL statement
278
     * should be used to determine whether it is for read or write.
279
     *
280
     * @throws Exception if there is any DB error.
281
     */
282 1704
    public function prepare(?bool $forRead = null): void
283
    {
284 1704
        if (isset($this->pdoStatement)) {
285 56
            $this->bindPendingParams();
286
287 56
            return;
288
        }
289
290 1704
        $sql = $this->getSql();
291
292 1704
        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

292
        if ($this->db->/** @scrutinizer ignore-call */ getTransaction()) {
Loading history...
293
            /** master is in a transaction. use the same connection. */
294 26
            $forRead = false;
295
        }
296
297 1704
        if ($forRead || ($forRead === null && $this->db->getSchema()->isReadQuery($sql))) {
298 1680
            $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

298
            /** @scrutinizer ignore-call */ 
299
            $pdo = $this->db->getSlavePdo();
Loading history...
299
        } else {
300 406
            $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

300
            /** @scrutinizer ignore-call */ 
301
            $pdo = $this->db->getMasterPdo();
Loading history...
301
        }
302
303
        try {
304 1704
            $this->pdoStatement = $pdo->prepare($sql);
305 1704
            $this->bindPendingParams();
306 3
        } catch (\Exception $e) {
307 3
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
308 3
            $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
309
310 3
            throw new Exception($message, $errorInfo, $e);
311
        }
312 1704
    }
313
314
    /**
315
     * Cancels the execution of the SQL statement.
316
     *
317
     * This method mainly sets {@see pdoStatement} to be null.
318
     */
319 356
    public function cancel(): void
320
    {
321 356
        $this->pdoStatement = null;
322 356
    }
323
324
    /**
325
     * Binds a parameter to the SQL statement to be executed.
326
     *
327
     * @param int|string $name parameter identifier. For a prepared statement using named placeholders, this will be a
328
     * parameter name of the form `:name`. For a prepared statement using question mark placeholders, this will be the
329
     * 1-indexed position of the parameter.
330
     * @param mixed $value the PHP variable to bind to the SQL statement parameter (passed by reference).
331
     * @param int|null $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
332
     * value.
333
     * @param int|null $length length of the data type.
334
     * @param mixed $driverOptions the driver-specific options.
335
     *
336
     * @throws Exception
337
     *
338
     * @return $this the current command being executed.
339
     *
340
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindParam.php}
341
     */
342 2
    public function bindParam($name, &$value, ?int $dataType = null, ?int $length = null, $driverOptions = null): self
343
    {
344 2
        $this->prepare();
345
346 2
        if ($dataType === null) {
347 2
            $dataType = $this->db->getSchema()->getPdoType($value);
348
        }
349
350 2
        if ($length === null) {
351 2
            $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

351
            $this->pdoStatement->/** @scrutinizer ignore-call */ 
352
                                 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...
352
        } elseif ($driverOptions === null) {
353
            $this->pdoStatement->bindParam($name, $value, $dataType, $length);
354
        } else {
355
            $this->pdoStatement->bindParam($name, $value, $dataType, $length, $driverOptions);
356
        }
357
358 2
        $this->params[$name] = &$value;
359
360 2
        return $this;
361
    }
362
363
    /**
364
     * Binds pending parameters that were registered via {@see bindValue()} and {@see bindValues()}.
365
     *
366
     * Note that this method requires an active {@see pdoStatement}.
367
     */
368 1381
    protected function bindPendingParams(): void
369
    {
370 1381
        foreach ($this->pendingParams as $name => $value) {
371 998
            $this->pdoStatement->bindValue($name, $value[0], $value[1]);
372
        }
373
374 1381
        $this->pendingParams = [];
375 1381
    }
376
377
    /**
378
     * Binds a value to a parameter.
379
     *
380
     * @param int|string $name Parameter identifier. For a prepared statement using named placeholders, this will be a
381
     * parameter name of the form `:name`. For a prepared statement using question mark placeholders, this will be the
382
     * 1-indexed position of the parameter.
383
     * @param mixed $value The value to bind to the parameter.
384
     * @param int|null $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the
385
     * value.
386
     *
387
     * @return $this the current command being executed.
388
     *
389
     * {@see http://www.php.net/manual/en/function.PDOStatement-bindValue.php}
390
     */
391 7
    public function bindValue($name, $value, ?int $dataType = null): self
392
    {
393 7
        if ($dataType === null) {
394 7
            $dataType = $this->db->getSchema()->getPdoType($value);
395
        }
396
397 7
        $this->pendingParams[$name] = [$value, $dataType];
398
399 7
        $this->params[$name] = $value;
400
401 7
        return $this;
402
    }
403
404
    /**
405
     * Binds a list of values to the corresponding parameters.
406
     *
407
     * This is similar to {@see bindValue()} except that it binds multiple values at a time.
408
     *
409
     * Note that the SQL data type of each value is determined by its PHP type.
410
     *
411
     * @param array $values the values to be bound. This must be given in terms of an associative array with array keys
412
     * being the parameter names, and array values the corresponding parameter values,
413
     * e.g. `[':name' => 'John', ':age' => 25]`.
414
     * By default, the PDO type of each value is determined  by its PHP type. You may explicitly specify the PDO type by
415
     * using a {@see PdoValue} class: `new PdoValue(value, type)`,
416
     * e.g. `[':name' => 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
417
     *
418
     * @return $this the current command being executed.
419
     */
420 1748
    public function bindValues(array $values): self
421
    {
422 1748
        if (empty($values)) {
423 1357
            return $this;
424
        }
425
426 1330
        $schema = $this->db->getSchema();
427
428 1330
        foreach ($values as $name => $value) {
429 1330
            if (is_array($value)) { // TODO: Drop in Yii 2.1
430
                $this->pendingParams[$name] = $value;
431
                $this->params[$name] = $value[0];
432 1330
            } elseif ($value instanceof PdoValue) {
433 1
                $this->pendingParams[$name] = [$value->getValue(), $value->getType()];
434 1
                $this->params[$name] = $value->getValue();
435
            } else {
436 1330
                $type = $schema->getPdoType($value);
437
438 1330
                $this->pendingParams[$name] = [$value, $type];
439 1330
                $this->params[$name] = $value;
440
            }
441
        }
442
443 1330
        return $this;
444
    }
445
446
    /**
447
     * Executes the SQL statement and returns query result.
448
     *
449
     * This method is for executing a SQL query that returns result set, such as `SELECT`.
450
     *
451
     * @throws Throwable
452
     * @throws Exception execution failed.
453
     *
454
     * @return DataReader the reader object for fetching the query result.
455
     */
456 35
    public function query(): DataReader
457
    {
458 35
        return $this->queryInternal('');
459
    }
460
461
    /**
462
     * Executes the SQL statement and returns ALL rows at once.
463
     *
464
     * @param int|null $fetchMode the result fetch mode.
465
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
466
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
467
     *
468
     * @throws Throwable
469
     * @throws Exception execution failed.
470
     *
471
     * @return array all rows of the query result. Each array element is an array representing a row of data. An empty
472
     * array is returned if the query results in nothing.
473
     */
474 1563
    public function queryAll(?int $fetchMode = null): array
475
    {
476 1563
        return $this->queryInternal('fetchAll', $fetchMode);
477
    }
478
479
    /**
480
     * Executes the SQL statement and returns the first row of the result.
481
     *
482
     * This method is best used when only the first row of result is needed for a query.
483
     *
484
     * @param array|int|null $fetchMode the result fetch mode.
485
     *
486
     * Please refer to [PHP manual](http://php.net/manual/en/pdostatement.setfetchmode.php)
487
     * for valid fetch modes. If this parameter is null, the value set in {@see fetchMode} will be used.
488
     *
489
     * @throws Exception|Throwable execution failed.
490
     *
491
     * @return array|false the first row (in terms of an array) of the query result. False is returned if the query
492
     * results in nothing.
493
     */
494 451
    public function queryOne($fetchMode = null)
495
    {
496 451
        return $this->queryInternal('fetch', $fetchMode);
497
    }
498
499
    /**
500
     * Executes the SQL statement and returns the value of the first column in the first row of data.
501
     *
502
     * This method is best used when only a single value is needed for a query.
503
     *
504
     * @throws Exception|Throwable failed.
505
     *
506
     * @return false|string|null the value of the first column in the first row of the query result. False is returned
507
     * if there is no value.
508
     */
509 447
    public function queryScalar()
510
    {
511 447
        $result = $this->queryInternal('fetchColumn', 0);
512
513 447
        if (is_resource($result) && get_resource_type($result) === 'stream') {
514
            return stream_get_contents($result);
515
        }
516
517 447
        return $result;
518
    }
519
520
    /**
521
     * Executes the SQL statement and returns the first column of the result.
522
     *
523
     * This method is best used when only the first column of result (i.e. the first element in each row) is needed for
524
     * a query.
525
     *
526
     * @throws Exception|Throwable 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 42
    public function queryColumn(): array
531
    {
532 42
        return $this->queryInternal('fetchAll', PDO::FETCH_COLUMN);
533
    }
534
535
    /**
536
     * Creates an INSERT command.
537
     *
538
     * For example,
539
     *
540
     * ```php
541
     * $connectionInterface->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
     * @return $this the command object itself
556
     */
557 138
    public function insert(string $table, $columns): self
558
    {
559 138
        $params = [];
560 138
        $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

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

1258
            if ($this->db->/** @scrutinizer ignore-call */ isProfilingEnabled()) {
Loading history...
1259 376
                $this->profiler->begin((string) $rawSql, [__METHOD__]);
1260
            }
1261
1262 376
            $this->internalExecute($rawSql);
1263 371
            $n = $this->pdoStatement->rowCount();
1264
1265 371
            if ($this->db->isProfilingEnabled()) {
1266 371
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1267
            }
1268
1269 371
            $this->refreshTableSchema();
1270
1271 371
            return $n;
1272 20
        } catch (Exception $e) {
1273 20
            if ($this->db->isProfilingEnabled()) {
1274 20
                $this->profiler->end((string) $rawSql, [__METHOD__]);
1275
            }
1276
1277 20
            throw $e;
1278
        }
1279
    }
1280
1281
    /**
1282
     * Logs the current database query if query logging is enabled and returns the profiling token if profiling is
1283
     * enabled.
1284
     *
1285
     * @param string $category the log category.
1286
     *
1287
     * @return array array of two elements, the first is boolean of whether profiling is enabled or not. The second is
1288
     * the rawSql if it has been created.
1289
     */
1290 1699
    protected function logQuery(string $category): array
1291
    {
1292 1699
        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

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

1331
                $cacheKey = $this->getCacheKey($method, /** @scrutinizer ignore-type */ $fetchMode, $rawSql);
Loading history...
1332 10
                $result = $cache->getOrSet(
1333
                    $cacheKey,
1334 10
                    static fn () => null,
1335
                );
1336
1337 10
                if (is_array($result) && isset($result[0])) {
1338 10
                    if ($this->db->isLoggingEnabled()) {
1339 10
                        $this->logger->log(
1340 10
                            LogLevel::DEBUG,
1341 10
                            'Query result served from cache',
1342 10
                            [__CLASS__ . '::query']
1343
                        );
1344
                    }
1345
1346 10
                    return $result[0];
1347
                }
1348
            }
1349
        }
1350
1351 1680
        $this->prepare(true);
1352
1353
        try {
1354 1680
            if ($this->db->isProfilingEnabled()) {
1355 1680
                $this->profiler->begin((string) $rawSql, [__CLASS__ . '::query']);
1356
            }
1357
1358 1680
            $this->internalExecute($rawSql);
1359
1360 1678
            if ($method === '') {
1361 35
                $result = new DataReader($this);
1362
            } else {
1363 1663
                if ($fetchMode === null) {
1364 1579
                    $fetchMode = $this->fetchMode;
1365
                }
1366
1367 1663
                $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
1368
1369 1663
                $this->pdoStatement->closeCursor();
1370
            }
1371
1372 1678
            if ($this->db->isProfilingEnabled()) {
1373 1678
                $this->profiler->end((string) $rawSql, [__CLASS__ . '::query']);
1374
            }
1375 28
        } catch (Exception $e) {
1376 28
            if ($this->db->isProfilingEnabled()) {
1377 28
                $this->profiler->end((string) $rawSql, [__CLASS__ . '::query']);
1378
            }
1379
1380 28
            throw $e;
1381
        }
1382
1383 1678
        if (isset($cache, $cacheKey, $info)) {
1384 10
            $cache->getOrSet(
1385
                $cacheKey,
1386 10
                static fn (): array => [$result],
1387 10
                $info[1],
1388 10
                $info[2]
1389
            );
1390
1391 10
            if ($this->db->isLoggingEnabled()) {
1392 10
                $this->logger->log(
1393 10
                    LogLevel::DEBUG,
1394 10
                    'Saved query result in cache',
1395 10
                    [__CLASS__ . '::query']
1396
                );
1397
            }
1398
        }
1399
1400 1678
        return $result;
1401
    }
1402
1403
    /**
1404
     * Returns the cache key for the query.
1405
     *
1406
     * @param string $method method of PDOStatement to be called.
1407
     * @param int|null $fetchMode the result fetch mode.
1408
     * Please refer to [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid
1409
     * fetch modes.
1410
     * @param string $rawSql the raw SQL with parameter values inserted into the corresponding placeholders.
1411
     *
1412
     * @throws JsonException
1413
     *
1414
     * @return array the cache key.
1415
     */
1416 10
    protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): array
1417
    {
1418
        return [
1419 10
            __CLASS__,
1420 10
            $method,
1421 10
            $fetchMode,
1422 10
            $this->db->getDsn(),
1423 10
            $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

1423
            $this->db->/** @scrutinizer ignore-call */ 
1424
                       getUsername(),
Loading history...
1424 10
            $rawSql,
1425
        ];
1426
    }
1427
1428
    /**
1429
     * Marks a specified table schema to be refreshed after command execution.
1430
     *
1431
     * @param string $name name of the table, which schema should be refreshed.
1432
     *
1433
     * @return $this this command instance.
1434
     */
1435 75
    protected function requireTableSchemaRefresh(string $name): self
1436
    {
1437 75
        $this->refreshTableName = $name;
1438
1439 75
        return $this;
1440
    }
1441
1442
    /**
1443
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
1444
     */
1445 371
    protected function refreshTableSchema(): void
1446
    {
1447 371
        if ($this->refreshTableName !== null) {
1448 70
            $this->db->getSchema()->refreshTableSchema($this->refreshTableName);
1449
        }
1450 371
    }
1451
1452
    /**
1453
     * Marks the command to be executed in transaction.
1454
     *
1455
     * @param string|null $isolationLevel The isolation level to use for this transaction.
1456
     *
1457
     * See {@see Transaction::begin()} for details.
1458
     *
1459
     * @return $this this command instance.
1460
     */
1461 5
    protected function requireTransaction(?string $isolationLevel = null): self
1462
    {
1463 5
        $this->isolationLevel = $isolationLevel;
1464
1465 5
        return $this;
1466
    }
1467
1468
    /**
1469
     * Sets a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing the
1470
     * command. The signature of the callable should be:.
1471
     *
1472
     * ```php
1473
     * function (Exceptions $e, $attempt)
1474
     * {
1475
     *     // return true or false (whether to retry the command or rethrow $e)
1476
     * }
1477
     * ```
1478
     *
1479
     * The callable will receive a database exception thrown and a current attempt (to execute the command) number
1480
     * starting from 1.
1481
     *
1482
     * @param callable $handler a PHP callback to handle database exceptions.
1483
     *
1484
     * @return $this this command instance.
1485
     */
1486 5
    protected function setRetryHandler(callable $handler): self
1487
    {
1488 5
        $this->retryHandler = $handler;
1489
1490 5
        return $this;
1491
    }
1492
1493
    /**
1494
     * Executes a prepared statement.
1495
     *
1496
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
1497
     *
1498
     * @param string|null $rawSql the rawSql if it has been created.
1499
     *
1500
     * @throws Exception|Throwable
1501
     */
1502 1699
    protected function internalExecute(?string $rawSql): void
1503
    {
1504 1699
        $attempt = 0;
1505
1506 1699
        while (true) {
1507
            try {
1508
                if (
1509 1699
                    ++$attempt === 1
1510 1699
                    && $this->isolationLevel !== null
1511 1699
                    && $this->db->getTransaction() === null
1512
                ) {
1513
                    $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

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