Passed
Pull Request — master (#466)
by Def
02:13
created

AbstractCommand::renameColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Command;
6
7
use Closure;
8
use JsonException;
9
use Psr\Log\LoggerAwareTrait;
10
use Psr\Log\LogLevel;
11
use Throwable;
12
use Yiisoft\Db\Connection\ConnectionInterface;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Query\Data\DataReaderInterface;
16
use Yiisoft\Db\Query\QueryInterface;
17
use Yiisoft\Db\Transaction\TransactionInterface;
18
use Yiisoft\Profiler\ProfilerAwareInterface;
19
use Yiisoft\Profiler\ProfilerAwareTrait;
20
21
use function explode;
22
use function get_resource_type;
23
use function is_array;
24
use function is_bool;
25
use function is_object;
26
use function is_resource;
27
use function is_string;
28
use function stream_get_contents;
29
use function strncmp;
30
use function strtr;
31
32
/**
33
 * Command represents a SQL statement to be executed against a database.
34
 *
35
 * A command object is usually created by calling {@see ConnectionInterface::createCommand()}.
36
 *
37
 * The SQL statement it represents can be set via the {@see sql} property.
38
 *
39
 * To execute a non-query SQL (such as INSERT, DELETE, UPDATE), call {@see execute()}.
40
 * To execute a SQL statement that returns a result data set (such as SELECT), use {@see queryAll()}, {@see queryOne()},
41
 * {@see queryColumn()}, {@see queryScalar()}, or {@see query()}.
42
 *
43
 * For example,
44
 *
45
 * ```php
46
 * $users = $connectionInterface->createCommand('SELECT * FROM user')->queryAll();
47
 * ```
48
 *
49
 * Command supports SQL statement preparation and parameter binding.
50
 *
51
 * Call {@see bindValue()} to bind a value to a SQL parameter;
52
 * Call {@see bindParam()} to bind a PHP variable to a SQL parameter.
53
 *
54
 * When binding a parameter, the SQL statement is automatically prepared. You may also call {@see prepare()} explicitly
55
 * to prepare a SQL statement.
56
 *
57
 * Command also supports building SQL statements by providing methods such as {@see insert()}, {@see update()}, etc.
58
 *
59
 * For example, the following code will create and execute an INSERT SQL statement:
60
 *
61
 * ```php
62
 * $connectionInterface->createCommand()->insert(
63
 *     'user',
64
 *     ['name' => 'Sam', 'age' => 30],
65
 * )->execute();
66
 * ```
67
 *
68
 * To build SELECT SQL statements, please use {@see QueryInterface} instead.
69
 */
70
abstract class AbstractCommand implements CommandInterface, ProfilerAwareInterface
71
{
72
    use LoggerAwareTrait;
73
    use ProfilerAwareTrait;
74
75
    protected string|null $isolationLevel = null;
76
    protected string|null $refreshTableName = null;
77
    protected Closure|null $retryHandler = null;
78
    private string $sql = '';
79
    /** * @var ParamInterface[] */
80
    protected array $params = [];
81
82
    public function addCheck(string $name, string $table, string $expression): static
83
    {
84
        $sql = $this->queryBuilder()->addCheck($name, $table, $expression);
85
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
86
    }
87
88
    public function addColumn(string $table, string $column, string $type): static
89
    {
90
        $sql = $this->queryBuilder()->addColumn($table, $column, $type);
91
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
92
    }
93
94
    public function addCommentOnColumn(string $table, string $column, string $comment): static
95
    {
96
        $sql = $this->queryBuilder()->addCommentOnColumn($table, $column, $comment);
97
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
98
    }
99
100
    public function addCommentOnTable(string $table, string $comment): static
101
    {
102
        $sql = $this->queryBuilder()->addCommentOnTable($table, $comment);
103
        return $this->setSql($sql);
104
    }
105
106
    public function addDefaultValue(string $name, string $table, string $column, mixed $value): static
107
    {
108
        $sql = $this->queryBuilder()->addDefaultValue($name, $table, $column, $value);
109
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
110
    }
111
112
    public function addForeignKey(
113
        string $name,
114
        string $table,
115
        array|string $columns,
116
        string $refTable,
117
        array|string $refColumns,
118
        string $delete = null,
119
        string $update = null
120
    ): static {
121
        $sql = $this->queryBuilder()->addForeignKey(
122
            $name,
123
            $table,
124
            $columns,
125
            $refTable,
126
            $refColumns,
127
            $delete,
128
            $update
129
        );
130
131
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
132
    }
133
134
    public function addPrimaryKey(string $name, string $table, array|string $columns): static
135
    {
136
        $sql = $this->queryBuilder()->addPrimaryKey($name, $table, $columns);
137
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
138
    }
139
140
    public function addUnique(string $name, string $table, array|string $columns): static
141
    {
142
        $sql = $this->queryBuilder()->addUnique($name, $table, $columns);
143
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
144
    }
145
146
    public function alterColumn(string $table, string $column, string $type): static
147
    {
148
        $sql = $this->queryBuilder()->alterColumn($table, $column, $type);
149
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
150
    }
151
152
    public function batchInsert(string $table, array $columns, iterable $rows): static
153
    {
154
        $table = $this->queryBuilder()->quoter()->quoteSql($table);
155
156
        /** @psalm-var string[] $columns */
157
        foreach ($columns as &$column) {
158
            $column = $this->queryBuilder()->quoter()->quoteSql($column);
159
        }
160
161
        unset($column);
162
163
        $params = [];
164
        $sql = $this->queryBuilder()->batchInsert($table, $columns, $rows, $params);
165
166
        $this->setRawSql($sql);
167
        $this->bindValues($params);
168
        return $this;
169
    }
170
171
    abstract public function bindValue(int|string $name, mixed $value, int $dataType = null): static;
172
173
    abstract public function bindValues(array $values): static;
174
175
    public function checkIntegrity(string $schema, string $table, bool $check = true): static
176
    {
177
        $sql = $this->queryBuilder()->checkIntegrity($schema, $table, $check);
178
        return $this->setSql($sql);
179
    }
180
181
    public function createIndex(
182
        string $name,
183
        string $table,
184
        array|string $columns,
185
        string $indexType = null,
186
        string $indexMethod = null
187
    ): static {
188
        $sql = $this->queryBuilder()->createIndex($name, $table, $columns, $indexType, $indexMethod);
189
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
190
    }
191
192
    public function createTable(string $table, array $columns, string $options = null): static
193
    {
194
        $sql = $this->queryBuilder()->createTable($table, $columns, $options);
195
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
196
    }
197
198
    public function createView(string $viewName, QueryInterface|string $subquery): static
199
    {
200
        $sql = $this->queryBuilder()->createView($viewName, $subquery);
201
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
202
    }
203
204
    public function delete(string $table, array|string $condition = '', array $params = []): static
205
    {
206
        $sql = $this->queryBuilder()->delete($table, $condition, $params);
207
        return $this->setSql($sql)->bindValues($params);
208
    }
209
210
    public function dropCheck(string $name, string $table): static
211
    {
212
        $sql = $this->queryBuilder()->dropCheck($name, $table);
213
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
214
    }
215
216
    public function dropColumn(string $table, string $column): static
217
    {
218
        $sql = $this->queryBuilder()->dropColumn($table, $column);
219
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
220
    }
221
222
    public function dropCommentFromColumn(string $table, string $column): static
223
    {
224
        $sql = $this->queryBuilder()->dropCommentFromColumn($table, $column);
225
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
226
    }
227
228
    public function dropCommentFromTable(string $table): static
229
    {
230
        $sql = $this->queryBuilder()->dropCommentFromTable($table);
231
        return $this->setSql($sql);
232
    }
233
234
    public function dropDefaultValue(string $name, string $table): static
235
    {
236
        $sql = $this->queryBuilder()->dropDefaultValue($name, $table);
237
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
238
    }
239
240
    public function dropForeignKey(string $name, string $table): static
241
    {
242
        $sql = $this->queryBuilder()->dropForeignKey($name, $table);
243
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
244
    }
245
246
    public function dropIndex(string $name, string $table): static
247
    {
248
        $sql = $this->queryBuilder()->dropIndex($name, $table);
249
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
250
    }
251
252
    public function dropPrimaryKey(string $name, string $table): static
253
    {
254
        $sql = $this->queryBuilder()->dropPrimaryKey($name, $table);
255
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
256
    }
257
258
    public function dropTable(string $table): static
259
    {
260
        $sql = $this->queryBuilder()->dropTable($table);
261
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
262
    }
263
264
    public function dropUnique(string $name, string $table): static
265
    {
266
        $sql = $this->queryBuilder()->dropUnique($name, $table);
267
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
268
    }
269
270
    public function dropView(string $viewName): static
271
    {
272
        $sql = $this->queryBuilder()->dropView($viewName);
273
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
274
    }
275
276
    public function getParams(bool $asValues = true): array
277
    {
278
        if (!$asValues) {
279
            return $this->params;
280
        }
281
282
        $buildParams = [];
283
284
        foreach ($this->params as $name => $value) {
285
            /** @psalm-var mixed */
286
            $buildParams[$name] = $value->getValue();
287
        }
288
289
        return $buildParams;
290
    }
291
292
    public function getRawSql(): string
293
    {
294
        if (empty($this->params)) {
295
            return $this->sql;
296
        }
297
298
        $params = [];
299
300
        /** @var mixed $value */
301
        foreach ($this->params as $name => $value) {
302
            if (is_string($name) && strncmp(':', $name, 1)) {
303
                $name = ':' . $name;
304
            }
305
306
            if ($value instanceof ParamInterface) {
307
                /** @psalm-var mixed $value */
308
                $value = $value->getValue();
309
            }
310
311
            if (is_string($value)) {
312
                /** @psalm-var mixed */
313
                $params[$name] = $this->queryBuilder()->quoter()->quoteValue($value);
314
            } elseif (is_bool($value)) {
315
                /** @psalm-var string */
316
                $params[$name] = $value ? 'TRUE' : 'FALSE';
317
            } elseif ($value === null) {
318
                $params[$name] = 'NULL';
319
            } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) {
320
                /** @psalm-var mixed */
321
                $params[$name] = $value;
322
            }
323
        }
324
325
        if (!isset($params[0])) {
326
            return strtr($this->sql, $params);
327
        }
328
329
        // Support unnamed placeholders should be dropped
330
        $sql = '';
331
332
        foreach (explode('?', $this->sql) as $i => $part) {
333
            $sql .= $part . (string) ($params[$i] ?? '');
334
        }
335
336
        return $sql;
337
    }
338
339
    public function getSql(): string
340
    {
341
        return $this->sql;
342
    }
343
344
    public function insert(string $table, QueryInterface|array $columns): static
345
    {
346
        $params = [];
347
        $sql = $this->queryBuilder()->insert($table, $columns, $params);
348
        return $this->setSql($sql)->bindValues($params);
349
    }
350
351
    public function insertWithReturningPks(string $table, array $columns): bool|array
352
    {
353
        $params = [];
354
355
        $sql = $this->queryBuilder()->insertWithReturningPks($table, $columns, $params);
356
357
        $this->setSql($sql)->bindValues($params);
358
359
        /** @psalm-var array|bool $result */
360
        $result = $this->queryInternal(self::QUERY_MODE_ROW | self::QUERY_MODE_EXECUTE);
361
362
        return is_array($result) ? $result : false;
363
    }
364
365
    /**
366
     * @psalm-suppress MixedReturnStatement
367
     * @psalm-suppress MixedInferredReturnType
368
     */
369
    public function execute(): int
370
    {
371
        $sql = $this->getSql();
372
373
        if ($sql === '') {
374
            return 0;
375
        }
376
377
        return $this->queryInternal(self::QUERY_MODE_EXECUTE);
378
    }
379
380
    /**
381
     * @psalm-suppress MixedReturnStatement
382
     * @psalm-suppress MixedInferredReturnType
383
     */
384
    public function query(): DataReaderInterface
385
    {
386
        return $this->queryInternal(self::QUERY_MODE_CURSOR);
387
    }
388
389
    public function queryAll(): array
390
    {
391
        /** @psalm-var array<array-key, array>|null $results */
392
        $results = $this->queryInternal(self::QUERY_MODE_ALL);
393
394
        return $results ?? [];
395
    }
396
397
    public function queryColumn(): array
398
    {
399
        /** @psalm-var mixed $results */
400
        $results = $this->queryInternal(self::QUERY_MODE_COLUMN);
401
402
        return is_array($results) ? $results : [];
403
    }
404
405
    public function queryOne(): array|null
406
    {
407
        /** @psalm-var mixed $results */
408
        $results = $this->queryInternal(self::QUERY_MODE_ROW);
409
410
        return is_array($results) ? $results : null;
411
    }
412
413
    /**
414
     * @psalm-suppress MixedReturnStatement
415
     * @psalm-suppress MixedInferredReturnType
416
     */
417
    public function queryScalar(): bool|string|null|int|float
418
    {
419
        $firstRow = $this->queryInternal(self::QUERY_MODE_ROW);
420
421
        if (!is_array($firstRow)) {
422
            return false;
423
        }
424
425
        /** @psalm-var mixed $result */
426
        $result = current($firstRow);
427
428
        if (is_resource($result) && get_resource_type($result) === 'stream') {
429
            return stream_get_contents($result);
430
        }
431
432
        return $result;
433
    }
434
435
    public function renameColumn(string $table, string $oldName, string $newName): static
436
    {
437
        $sql = $this->queryBuilder()->renameColumn($table, $oldName, $newName);
438
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
439
    }
440
441
    public function renameTable(string $table, string $newName): static
442
    {
443
        $sql = $this->queryBuilder()->renameTable($table, $newName);
444
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
445
    }
446
447
    public function resetSequence(string $table, int|string $value = null): static
448
    {
449
        $sql = $this->queryBuilder()->resetSequence($table, $value);
450
        return $this->setSql($sql);
451
    }
452
453
    public function setRawSql(string $sql): static
454
    {
455
        if ($sql !== $this->sql) {
456
            $this->cancel();
457
            $this->reset();
458
            $this->sql = $sql;
459
        }
460
461
        return $this;
462
    }
463
464
    public function setSql(string $sql): static
465
    {
466
        $this->cancel();
467
        $this->reset();
468
        $this->sql = $this->queryBuilder()->quoter()->quoteSql($sql);
469
470
        return $this;
471
    }
472
473
    public function setRetryHandler(Closure|null $handler): static
474
    {
475
        $this->retryHandler = $handler;
476
        return $this;
477
    }
478
479
    public function truncateTable(string $table): static
480
    {
481
        $sql = $this->queryBuilder()->truncateTable($table);
482
        return $this->setSql($sql);
483
    }
484
485
    public function update(string $table, array $columns, array|string $condition = '', array $params = []): static
486
    {
487
        $sql = $this->queryBuilder()->update($table, $columns, $condition, $params);
488
        return $this->setSql($sql)->bindValues($params);
489
    }
490
491
    public function upsert(
492
        string $table,
493
        QueryInterface|array $insertColumns,
494
        bool|array $updateColumns = true,
495
        array $params = []
496
    ): static {
497
        $sql = $this->queryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
498
        return $this->setSql($sql)->bindValues($params);
499
    }
500
501
    /**
502
     * Executes a prepared statement.
503
     *
504
     * @param string|null $rawSql the rawSql if it has been created.
505
     *
506
     * @throws Exception
507
     * @throws Throwable
508
     */
509
    abstract protected function internalExecute(string|null $rawSql): void;
510
511
    protected function is(int $value, int $flag): bool
512
    {
513
        return ($value & $flag) === $flag;
514
    }
515
516
    /**
517
     * Returns the query result.
518
     *
519
     * @param int $queryMode One from modes QUERY_MODE_*.
520
     *
521
     * @throws Exception
522
     * @throws Throwable
523
     */
524
    abstract protected function internalGetQueryResult(int $queryMode): mixed;
525
526
    /**
527
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
528
     */
529
    abstract protected function refreshTableSchema(): void;
530
531
    /**
532
     * The method is called after the query is executed.
533
     *
534
     * @param int $queryMode One from modes QUERY_MODE_*.
535
     *
536
     * @throws Exception
537
     * @throws Throwable
538
     */
539
    protected function queryInternal(int $queryMode): mixed
540
    {
541
        $rawSql = $this->getRawSql();
542
543
        $isReadMode = $this->isReadMode($queryMode);
544
        $logCategory = self::class . '::' . ($isReadMode ? 'query' : 'execute');
545
546
        $this->logQuery($rawSql, $logCategory);
547
548
        $this->prepare($isReadMode);
549
550
        try {
551
            $this->profiler?->begin($rawSql, [$logCategory]);
552
553
            $this->internalExecute($rawSql);
554
555
            /** @psalm-var mixed $result */
556
            $result = $this->internalGetQueryResult($queryMode);
557
558
            $this->profiler?->end($rawSql, [$logCategory]);
559
560
            // @todo set isReadMode to false for DDL query
561
            if (!$isReadMode) {
562
                $this->refreshTableSchema();
563
            }
564
        } catch (Exception $e) {
565
            $this->profiler?->end($rawSql, [$logCategory]);
566
            throw $e;
567
        }
568
569
        return $result;
570
    }
571
572
    /**
573
     * Logs the current database query if query logging is enabled and returns the profiling token if profiling is
574
     * enabled.
575
     */
576
    protected function logQuery(string $rawSql, string $category): void
577
    {
578
        $this->logger?->log(LogLevel::INFO, $rawSql, [$category]);
579
    }
580
581
    /**
582
     * Marks a specified table schema to be refreshed after command execution.
583
     *
584
     * @param string $name Name of the table, which schema should be refreshed.
585
     *
586
     * @return static The command object itself.
587
     */
588
    protected function requireTableSchemaRefresh(string $name): static
589
    {
590
        $this->refreshTableName = $name;
591
        return $this;
592
    }
593
594
    /**
595
     * Marks the command to be executed in transaction.
596
     *
597
     * @param string|null $isolationLevel The isolation level to use for this transaction.
598
     *
599
     * {@see TransactionInterface::begin()} for details.
600
     *
601
     * @return static The command object itself.
602
     */
603
    protected function requireTransaction(string $isolationLevel = null): static
604
    {
605
        $this->isolationLevel = $isolationLevel;
606
        return $this;
607
    }
608
609
    /**
610
     * Resets the command object, so it can be reused to build another SQL statement.
611
     */
612
    protected function reset(): void
613
    {
614
        $this->sql = '';
615
        $this->params = [];
616
        $this->refreshTableName = null;
617
        $this->isolationLevel = null;
618
        $this->retryHandler = null;
619
    }
620
621
    private function isReadMode(int $queryMode): bool
622
    {
623
        return !$this->is($queryMode, self::QUERY_MODE_EXECUTE);
624
    }
625
}
626