Passed
Pull Request — master (#800)
by
unknown
09:35 queued 06:39
created

AbstractCommand::refreshMaterializedView()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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