Passed
Pull Request — master (#282)
by Wilmer
13:20
created

Command::alterColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Command;
6
7
use JsonException;
8
use PDO;
9
use Psr\Log\LogLevel;
10
use Throwable;
11
use Yiisoft\Cache\CacheInterface;
12
use Yiisoft\Cache\Dependency\Dependency;
13
use Yiisoft\Db\AwareTrait\LoggerAwareTrait;
14
use Yiisoft\Db\AwareTrait\ProfilerAwareTrait;
15
use Yiisoft\Db\Cache\QueryCache;
16
use Yiisoft\Db\Connection\ConnectionInterface;
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidArgumentException;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Pdo\PdoValue;
23
use Yiisoft\Db\Query\Data\DataReader;
24
use Yiisoft\Db\Query\QueryInterface;
25
use Yiisoft\Db\Transaction\TransactionInterface;
26
27
use function array_map;
28
use function call_user_func_array;
29
use function explode;
30
use function get_resource_type;
31
use function is_array;
32
use function is_bool;
33
use function is_object;
34
use function is_resource;
35
use function is_string;
36
use function stream_get_contents;
37
use function strncmp;
38
use function strtr;
39
40
/**
41
 * Command represents a SQL statement to be executed against a database.
42
 *
43
 * A command object is usually created by calling {@see ConnectionInterface::createCommand()}.
44
 *
45
 * The SQL statement it represents can be set via the {@see sql} property.
46
 *
47
 * To execute a non-query SQL (such as INSERT, DELETE, UPDATE), call {@see execute()}.
48
 * To execute a SQL statement that returns a result data set (such as SELECT), use {@see queryAll()},
49
 * {@see queryOne()}, {@see queryColumn()}, {@see queryScalar()}, or {@see query()}.
50
 *
51
 * For example,
52
 *
53
 * ```php
54
 * $users = $connectionInterface->createCommand('SELECT * FROM user')->queryAll();
55
 * ```
56
 *
57
 * Command supports SQL statement preparation and parameter binding.
58
 *
59
 * Call {@see bindValue()} to bind a value to a SQL parameter;
60
 * Call {@see bindParam()} to bind a PHP variable to a SQL parameter.
61
 *
62
 * When binding a parameter, the SQL statement is automatically prepared. You may also call {@see prepare()} explicitly
63
 * to prepare a SQL statement.
64
 *
65
 * Command also supports building SQL statements by providing methods such as {@see insert()}, {@see update()}, etc.
66
 *
67
 * For example, the following code will create and execute an INSERT SQL statement:
68
 *
69
 * ```php
70
 * $connectionInterface->createCommand()->insert('user', [
71
 *     'name' => 'Sam',
72
 *     'age' => 30,
73
 * ])->execute();
74
 * ```
75
 *
76
 * To build SELECT SQL statements, please use {@see QueryInterface} instead.
77
 *
78
 * For more details and usage information on Command, see the [guide article on Database Access Objects](guide:db-dao).
79
 *
80
 * @property string $rawSql The raw SQL with parameter values inserted into the corresponding placeholders in
81
 * {@see sql}.
82
 * @property string $sql The SQL statement to be executed.
83
 */
84
abstract class Command implements CommandInterface
85
{
86
    use CommandPdoTrait;
87
    use LoggerAwareTrait;
88
    use ProfilerAwareTrait;
89
90
    protected ?string $isolationLevel = null;
91
92
    protected ?string $refreshTableName = null;
93
    /** @var callable|null */
94
    protected $retryHandler = null;
95
    private int $fetchMode = PDO::FETCH_ASSOC;
96
    private ?int $queryCacheDuration = null;
97
    private ?string $sql = null;
98
    private ?Dependency $queryCacheDependency = null;
99
100
    public function __construct(private QueryCache $queryCache)
101
    {
102
    }
103
104
    /**
105
     * Returns the cache key for the query.
106
     *
107
     * @param string $method method of PDOStatement to be called.
108
     * @param int|null $fetchMode the result fetch mode.
109
     * Please refer to [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid
110
     * fetch modes.
111
     * @param string $rawSql the raw SQL with parameter values inserted into the corresponding placeholders.
112
     *
113
     * @throws JsonException
114
     *
115
     * @return array the cache key.
116
     */
117
    abstract protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): array;
118
119
    /**
120
     * Executes a prepared statement.
121
     *
122 1779
     * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers.
123
     *
124 1779
     * @param string|null $rawSql the rawSql if it has been created.
125 1779
     *
126 1779
     * @throws Exception|Throwable
127 1779
     */
128
    abstract protected function internalExecute(?string $rawSql): void;
129
130
    public function addCheck(string $name, string $table, string $expression): self
131
    {
132
        $sql = $this->queryBuilder()->addCheck($name, $table, $expression);
133
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
134
    }
135
136
    public function addColumn(string $table, string $column, string $type): self
137
    {
138
        $sql = $this->queryBuilder()->addColumn($table, $column, $type);
139 10
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
140
    }
141 10
142 10
    /**
143 10
     * @throws \Exception
144
     */
145
    public function addCommentOnColumn(string $table, string $column, string $comment): self
146
    {
147
        $sql = $this->queryBuilder()->addCommentOnColumn($table, $column, $comment);
148
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
149
    }
150
151 5
    /**
152
     * @throws \Exception
153 5
     */
154 5
    public function addCommentOnTable(string $table, string $comment): self
155
    {
156
        $sql = $this->queryBuilder()->addCommentOnTable($table, $comment);
157
        return $this->setSql($sql);
158
    }
159
160
    /**
161
     * @throws Exception|NotSupportedException
162 1749
     */
163
    public function addDefaultValue(string $name, string $table, string $column, mixed $value): self
164 1749
    {
165
        $sql = $this->queryBuilder()->addDefaultValue($name, $table, $column, $value);
166
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
167
    }
168
169
    /**
170
     * @throws Exception|InvalidArgumentException
171
     */
172
    public function addForeignKey(
173
        string $name,
174
        string $table,
175
        array|string $columns,
176
        string $refTable,
177
        array|string $refColumns,
178
        ?string $delete = null,
179
        ?string $update = null
180
    ): self {
181 339
        $sql = $this->queryBuilder()->addForeignKey(
182
            $name,
183 339
            $table,
184 339
            $columns,
185 339
            $refTable,
186 339
            $refColumns,
187
            $delete,
188
            $update
189 339
        );
190
191
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
192
    }
193
194
    public function addPrimaryKey(string $name, string $table, array|string $columns): self
195
    {
196
        $sql = $this->queryBuilder()->addPrimaryKey($name, $table, $columns);
197
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
198
    }
199
200
    public function addUnique(string $name, string $table, array|string $columns): self
201
    {
202
        $sql = $this->queryBuilder()->addUnique($name, $table, $columns);
203
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
204
    }
205 41
206
    public function alterColumn(string $table, string $column, string $type): self
207 41
    {
208 41
        $sql = $this->queryBuilder()->alterColumn($table, $column, $type);
209 41
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
210 41
    }
211
212
    /**
213 41
     * @throws Exception|InvalidArgumentException
214
     */
215
    public function batchInsert(string $table, array $columns, iterable $rows): self
216
    {
217
        $table = $this->queryBuilder()->quoter()->quoteSql($table);
218
        $columns = array_map(fn ($column) => $this->queryBuilder()->quoter()->quoteSql($column), $columns);
219
        $params = [];
220
        $sql = $this->queryBuilder()->batchInsert($table, $columns, $rows, $params);
221
222
        $this->setRawSql($sql);
223
        $this->bindValues($params);
224
225 929
        return $this;
226
    }
227 929
228 596
    public function bindValue(int|string $name, mixed $value, ?int $dataType = null): self
229
    {
230
        if ($dataType === null) {
231 652
            $dataType = $this->queryBuilder()->schema()->getPdoType($value);
232
        }
233 652
234 652
        $this->params[$name] = new Param($name, $value, $dataType);
235 20
236
        return $this;
237
    }
238 652
239 565
    public function bindValues(array $values): self
240 290
    {
241 10
        if (empty($values)) {
242 283
            return $this;
243 135
        }
244 171
245 171
        foreach ($values as $name => $value) {
246
            if ($value instanceof ParamInterface) {
247
                $this->params[$value->getName()] = $value;
248
            } elseif (is_array($value)) { // TODO: Drop in Yii 2.1
249 652
                $this->params[$name] = new Param($name, ...$value);
250 652
            } elseif ($value instanceof PdoValue) {
251
                $this->params[$name] = new Param($name, $value->getValue(), $value->getType());
252
            } else {
253
                $type = $this->queryBuilder()->schema()->getPdoType($value);
254
                $this->params[$name] = new Param($name, $value, $type);
255
            }
256
        }
257
258
        return $this;
259
    }
260
261
    public function cache(?int $duration = null, Dependency $dependency = null): self
262
    {
263
        $this->queryCacheDuration = $duration ?? $this->queryCache->getDuration();
264
        $this->queryCacheDependency = $dependency;
265
        return $this;
266
    }
267
268
    /**
269
     * @throws Exception|NotSupportedException
270
     */
271
    public function checkIntegrity(string $schema, string $table, bool $check = true): self
272
    {
273 1735
        $sql = $this->queryBuilder()->checkIntegrity($schema, $table, $check);
274
        return $this->setSql($sql);
275 1735
    }
276 59
277
    /**
278 59
     * @throws Exception|InvalidArgumentException
279
     */
280
    public function createIndex(string $name, string $table, array|string $columns, bool $unique = false): self
281 1735
    {
282
        $sql = $this->queryBuilder()->createIndex($name, $table, $columns, $unique);
283 1735
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
284
    }
285 26
286
    public function createTable(string $table, array $columns, ?string $options = null): self
287
    {
288 1735
        $sql = $this->queryBuilder()->createTable($table, $columns, $options);
289 1710
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
290
    }
291 427
292
    /**
293
     * @throws Exception|InvalidConfigException|NotSupportedException
294
     */
295 1735
    public function createView(string $viewName, QueryInterface|string $subquery): self
296 1735
    {
297 3
        $sql = $this->queryBuilder()->createView($viewName, $subquery);
298 3
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
299 3
    }
300
301 3
    /**
302
     * @throws Exception|InvalidArgumentException
303 1735
     */
304
    public function delete(string $table, array|string $condition = '', array $params = []): self
305
    {
306
        $sql = $this->queryBuilder()->delete($table, $condition, $params);
307
        return $this->setSql($sql)->bindValues($params);
308
    }
309
310 375
    public function dropCheck(string $name, string $table): self
311
    {
312 375
        $sql = $this->queryBuilder()->dropCheck($name, $table);
313 375
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
314
    }
315
316
    public function dropColumn(string $table, string $column): self
317
    {
318
        $sql = $this->queryBuilder()->dropColumn($table, $column);
319
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
320
    }
321
322
    public function dropCommentFromColumn(string $table, string $column): self
323
    {
324
        $sql = $this->queryBuilder()->dropCommentFromColumn($table, $column);
325
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
326
    }
327
328
    public function dropCommentFromTable(string $table): self
329
    {
330
        $sql = $this->queryBuilder()->dropCommentFromTable($table);
331
        return $this->setSql($sql);
332
    }
333 2
334
    /**
335 2
     * @throws Exception|NotSupportedException
336
     */
337 2
    public function dropDefaultValue(string $name, string $table): self
338 2
    {
339
        $sql = $this->queryBuilder()->dropDefaultValue($name, $table);
340
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
341 2
    }
342 2
343
    public function dropForeignKey(string $name, string $table): self
344
    {
345
        $sql = $this->queryBuilder()->dropForeignKey($name, $table);
346
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
347
    }
348
349 2
    public function dropIndex(string $name, string $table): self
350
    {
351 2
        $sql = $this->queryBuilder()->dropIndex($name, $table);
352
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
353
    }
354
355
    public function dropPrimaryKey(string $name, string $table): self
356
    {
357
        $sql = $this->queryBuilder()->dropPrimaryKey($name, $table);
358
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
359 1408
    }
360
361 1408
    public function dropTable(string $table): self
362 1017
    {
363
        $sql = $this->queryBuilder()->dropTable($table);
364
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
365 1408
    }
366 1408
367
    public function dropUnique(string $name, string $table): self
368
    {
369
        $sql = $this->queryBuilder()->dropUnique($name, $table);
370
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
371
    }
372
373
    public function dropView(string $viewName): self
374
    {
375
        $sql = $this->queryBuilder()->dropView($viewName);
376
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
377
    }
378
379
    /**
380
     * Executes the SQL statement.
381
     *
382 7
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
383
     * No result set will be returned.
384 7
     *
385 7
     * @throws Throwable
386
     * @throws Exception execution failed.
387
     *
388 7
     * @return int number of rows affected by the execution.
389
     */
390 7
    public function execute(): int
391
    {
392 7
        $sql = $this->getSql();
393
394
        [, $rawSql] = $this->logQuery(__METHOD__);
395
396
        if ($sql === '') {
397
            return 0;
398
        }
399
400
        $this->prepare(false);
401
402
        try {
403
            $this->profiler?->begin((string)$rawSql, [__METHOD__]);
404
405
            $this->internalExecute($rawSql);
406
            $n = $this->pdoStatement->rowCount();
0 ignored issues
show
Bug introduced by
The method rowCount() 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

406
            /** @scrutinizer ignore-call */ 
407
            $n = $this->pdoStatement->rowCount();

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...
407
408
            $this->profiler?->end((string)$rawSql, [__METHOD__]);
409
410
            $this->refreshTableSchema();
411 1779
412
            return $n;
413 1779
        } catch (Exception $e) {
414 1387
            $this->profiler?->end((string)$rawSql, [__METHOD__]);
415
            throw $e;
416
        }
417 1353
    }
418
419 1353
    /**
420 1353
     * @throws Exception|NotSupportedException
421
     */
422
    public function executeResetSequence(string $table, mixed $value = null): self
423 1353
    {
424 2
        return $this->resetSequence($table, $value);
425 2
    }
426
427 1353
    public function getFetchMode(): int
428
    {
429 1353
        return $this->fetchMode;
430 1353
    }
431
432
    public function getParams(): array
433
    {
434 1353
        return array_map(static fn (mixed $value): mixed => $value->getValue(), $this->params);
435
    }
436
437
    /**
438
     * @throws \Exception
439
     */
440
    public function getRawSql(): string
441
    {
442
        if (empty($this->params)) {
443
            return $this->sql;
444
        }
445
446
        $params = [];
447 35
448
        foreach ($this->params as $name => $value) {
449 35
            if (is_string($name) && strncmp(':', $name, 1)) {
450
                $name = ':' . $name;
451
            }
452
453
            if ($value instanceof ParamInterface) {
454
                $value = $value->getValue();
455
            }
456
457
            if (is_string($value)) {
458
                $params[$name] = $this->queryBuilder()->quoter()->quoteValue($value);
459
            } elseif (is_bool($value)) {
460
                $params[$name] = ($value ? 'TRUE' : 'FALSE');
461
            } elseif ($value === null) {
462
                $params[$name] = 'NULL';
463
            } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) {
464
                $params[$name] = $value;
465 1589
            }
466
        }
467 1589
468
        if (!isset($params[1])) {
469
            return strtr($this->sql, $params);
470
        }
471
472
        $sql = '';
473
474
        foreach (explode('?', $this->sql) as $i => $part) {
475
            $sql .= ($params[$i] ?? '') . $part;
476
        }
477
478
        return $sql;
479
    }
480
481
    public function getSql(): ?string
482
    {
483
        return $this->sql;
484
    }
485 471
486
    /**
487 471
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
488
     */
489
    public function insert(string $table, QueryInterface|array $columns): self
490
    {
491
        $params = [];
492
        $sql = $this->queryBuilder()->insert($table, $columns, $params);
493
        return $this->setSql($sql)->bindValues($params);
494
    }
495
496
    abstract public function insertEx(string $table, array $columns): bool|array;
497
498
    public function noCache(): self
499
    {
500 450
        $this->queryCacheDuration = -1;
501
        return $this;
502 450
    }
503
504 450
    public function query(): DataReader
505
    {
506
        return $this->queryInternal('');
507
    }
508 450
509
    public function queryAll(?int $fetchMode = null): array
510
    {
511
        return $this->queryInternal('fetchAll', $fetchMode);
512
    }
513
514
    public function queryColumn(): array
515
    {
516
        return $this->queryInternal('fetchAll', PDO::FETCH_COLUMN);
517
    }
518
519
    public function queryOne(array|int $fetchMode = null): mixed
520
    {
521 43
        return $this->queryInternal('fetch', $fetchMode);
522
    }
523 43
524
    public function queryScalar(): bool|string|null|int
525
    {
526
        $result = $this->queryInternal('fetchColumn', 0);
527
528
        if (is_resource($result) && get_resource_type($result) === 'stream') {
529
            return stream_get_contents($result);
530
        }
531
532
        return $result;
533
    }
534
535
    public function renameColumn(string $table, string $oldName, string $newName): self
536
    {
537
        $sql = $this->queryBuilder()->renameColumn($table, $oldName, $newName);
538
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
539
    }
540
541
    public function renameTable(string $table, string $newName): self
542
    {
543
        $sql = $this->queryBuilder()->renameTable($table, $newName);
544
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
545
    }
546
547
    /**
548 152
     * @throws Exception|NotSupportedException
549
     */
550 152
    public function resetSequence(string $table, mixed $value = null): self
551 152
    {
552 137
        $sql = $this->queryBuilder()->resetSequence($table, $value);
553
        return $this->setSql($sql);
554
    }
555
556
    public function setFetchMode(int $value): void
557
    {
558
        $this->fetchMode = $value;
559
    }
560
561
    public function setParams(array $value): void
562
    {
563
        $this->params = $value;
564
    }
565
566
    public function setRawSql(string $sql): self
567
    {
568
        if ($sql !== $this->sql) {
569
            $this->cancel();
570
            $this->reset();
571
            $this->sql = $sql;
572
        }
573
574
        return $this;
575
    }
576
577
    public function setSql(string $sql): self
578
    {
579
        $this->cancel();
580 41
        $this->reset();
581
        $this->sql = $this->queryBuilder()->quoter()->quoteSql($sql);
582 41
583
        return $this;
584 41
    }
585
586 41
    public function truncateTable(string $table): self
587
    {
588 41
        $sql = $this->queryBuilder()->truncateTable($table);
589
        return $this->setSql($sql);
590 41
    }
591 41
592
    /**
593 41
     * @throws Exception|InvalidArgumentException
594
     */
595
    public function update(string $table, array $columns, array|string $condition = '', array $params = []): self
596
    {
597
        $sql = $this->queryBuilder()->update($table, $columns, $condition, $params);
598
        return $this->setSql($sql)->bindValues($params);
599
    }
600
601
    /**
602
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException
603
     */
604
    public function upsert(
605
        string $table,
606
        QueryInterface|array $insertColumns,
607
        bool|array $updateColumns = true,
608
        array $params = []
609
    ): self {
610
        $sql = $this->queryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
611
        return $this->setSql($sql)->bindValues($params);
612
    }
613
614
    /**
615
     * Logs the current database query if query logging is enabled and returns the profiling token if profiling is
616
     * enabled.
617
     *
618
     * @param string $category The log category.
619
     *
620
     * @throws \Exception
621
     *
622
     * @return array Two elements, the first is boolean of whether profiling is enabled or not. The second is
623
     * the rawSql if it has been created.
624 30
     */
625
    protected function logQuery(string $category): array
626 30
    {
627 30
        if ($this->logger !== null) {
628
            $rawSql = $this->getRawSql();
629
            $this->logger->log(LogLevel::INFO, $rawSql, [$category]);
630
        }
631
632
        if ($this->profiler === null) {
633
            return [false, $rawSql ?? null];
634
        }
635
636
        return [true, $rawSql ?? $this->getRawSql()];
637
    }
638
639
    /**
640
     * Performs the actual DB query of a SQL statement.
641
     *
642
     * @param string $method Method of PDOStatement to be called.
643
     * @param array|int|null $fetchMode The result fetch mode.
644
     *
645
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
646
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
647
     *
648
     * @throws Exception|Throwable If the query causes any problem.
649
     *
650
     * @return mixed The method execution result.
651
     */
652
    protected function queryInternal(string $method, array|int $fetchMode = null): mixed
653
    {
654
        [, $rawSql] = $this->logQuery(__CLASS__ . '::query');
655
656
        if ($method !== '') {
657
            $info = $this->queryCache->info($this->queryCacheDuration, $this->queryCacheDependency);
658
659
            if (is_array($info)) {
660
                /* @var $cache CacheInterface */
661
                $cache = $info[0];
662
                $rawSql = $rawSql ?: $this->getRawSql();
663 77
                $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

663
                $cacheKey = $this->getCacheKey($method, /** @scrutinizer ignore-type */ $fetchMode, $rawSql);
Loading history...
664
                $result = $cache->getOrSet(
665 77
                    $cacheKey,
666 77
                    static fn () => null,
667
                );
668
669
                if (is_array($result) && isset($result[0])) {
670
                    $this->logger?->log(LogLevel::DEBUG, 'Query result served from cache', [__CLASS__ . '::query']);
671
672
                    return $result[0];
673
                }
674
            }
675
        }
676
677
        $this->prepare(true);
678
679
        try {
680
            $this->profiler?->begin((string)$rawSql, [__CLASS__ . '::query']);
681
682
            $this->internalExecute($rawSql);
683
684
            if ($method === '') {
685
                $result = new DataReader($this);
686
            } else {
687
                if ($fetchMode === null) {
688
                    $fetchMode = $this->fetchMode;
689
                }
690
691
                $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
692
693
                $this->pdoStatement->closeCursor();
694
            }
695
696 42
            $this->profiler?->end((string)$rawSql, [__CLASS__ . '::query']);
697
        } catch (Exception $e) {
698 42
            $this->profiler?->end((string)$rawSql, [__CLASS__ . '::query']);
699 42
            throw $e;
700
        }
701
702
        if (isset($cache, $cacheKey, $info)) {
703
            $cache->getOrSet(
704
                $cacheKey,
705
                static fn (): array => [$result],
706
                $info[1],
707
                $info[2]
708
            );
709
710
            $this->logger?->log(LogLevel::DEBUG, 'Saved query result in cache', [__CLASS__ . '::query']);
711
        }
712
713
        return $result;
714
    }
715
716
    /**
717
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
718
     */
719
    protected function refreshTableSchema(): void
720
    {
721
        if ($this->refreshTableName !== null) {
722 58
            $this->queryBuilder()->schema()->refreshTableSchema($this->refreshTableName);
723
        }
724 58
    }
725 58
726
    /**
727
     * Marks a specified table schema to be refreshed after command execution.
728
     *
729
     * @param string $name Name of the table, which schema should be refreshed.
730
     *
731
     * @return static
732
     */
733
    protected function requireTableSchemaRefresh(string $name): self
734
    {
735
        $this->refreshTableName = $name;
736 10
        return $this;
737
    }
738 10
739 10
    /**
740
     * Marks the command to be executed in transaction.
741
     *
742
     * @param string|null $isolationLevel The isolation level to use for this transaction.
743
     *
744
     * {@see TransactionInterface::begin()} for details.
745
     *
746
     * @return static
747
     */
748
    protected function requireTransaction(?string $isolationLevel = null): self
749 12
    {
750
        $this->isolationLevel = $isolationLevel;
751 12
        return $this;
752 12
    }
753
754
    protected function reset(): void
755
    {
756
        $this->sql = null;
757
        $this->params = [];
758
        $this->refreshTableName = null;
759
        $this->isolationLevel = null;
760
        $this->retryHandler = null;
761
    }
762 5
763
    /**
764 5
     * Sets a callable (e.g. anonymous function) that is called when {@see Exception} is thrown when executing the
765 5
     * command. The signature of the callable should be:.
766
     *
767
     * ```php
768
     * function (Exceptions $e, $attempt)
769
     * {
770
     *     // return true or false (whether to retry the command or rethrow $e)
771
     * }
772
     * ```
773
     *
774
     * The callable will receive a database exception thrown and a current attempt (to execute the command) number
775
     * starting from 1.
776
     *
777
     * @param callable|null $handler A PHP callback to handle database exceptions.
778
     *
779
     * @return static
780 2
     */
781
    protected function setRetryHandler(?callable $handler): self
782 2
    {
783 2
        $this->retryHandler = $handler;
784
        return $this;
785
    }
786
}
787