Passed
Pull Request — master (#259)
by Def
11:38
created

Schema::rollBackSavepoint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use PDO;
8
use PDOException;
9
use Throwable;
10
use Yiisoft\Cache\Dependency\TagDependency;
11
use Yiisoft\Db\Cache\SchemaCache;
12
use Yiisoft\Db\Connection\ConnectionInterface;
13
use Yiisoft\Db\Constraint\CheckConstraint;
14
use Yiisoft\Db\Constraint\Constraint;
15
use Yiisoft\Db\Constraint\DefaultValueConstraint;
16
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
17
use Yiisoft\Db\Constraint\IndexConstraint;
18
use Yiisoft\Db\Exception\Exception;
19
use Yiisoft\Db\Exception\IntegrityException;
20
use Yiisoft\Db\Exception\InvalidCallException;
21
use Yiisoft\Db\Exception\InvalidConfigException;
22
use Yiisoft\Db\Exception\NotSupportedException;
23
use Yiisoft\Db\Query\QueryBuilder;
24
25
use function addcslashes;
26
use function array_change_key_case;
27
use function array_key_exists;
28
use function array_map;
29
use function explode;
30
use function gettype;
31
use function implode;
32
use function is_array;
33
use function is_string;
34
use function md5;
35
use function preg_match;
36
use function preg_replace;
37
use function serialize;
38
use function str_replace;
39
use function strlen;
40
use function strpos;
41
use function substr;
42
use function version_compare;
43
44
/**
45
 * Schema is the base class for concrete DBMS-specific schema classes.
46
 *
47
 * Schema represents the database schema information that is DBMS specific.
48
 *
49
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
50
 * object. This property is read-only.
51
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
52
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is read-only.
53
 * @property string $serverVersion Server version as a string. This property is read-only.
54
 * @property string[] $tableNames All table names in the database. This property is read-only.
55
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an instance
56
 * of {@see TableSchema} or its child class. This property is read-only.
57
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
58
 * one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
59
 * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS specific
60
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
61
 *
62
 * @property CheckConstraint[] $schemaChecks Check constraints for all tables in the database. Each array element is an
63
 * array of {@see CheckConstraint} or its child classes. This property is read-only.
64
 * @property DefaultValueConstraint[] $schemaDefaultValues Default value constraints for all tables in the database.
65
 * Each array element is an array of {@see DefaultValueConstraint} or its child classes. This property is read-only.
66
 * @property ForeignKeyConstraint[] $schemaForeignKeys Foreign keys for all tables in the database. Each array element
67
 * is an array of {@see ForeignKeyConstraint} or its child classes. This property is read-only.
68
 * @property IndexConstraint[] $schemaIndexes Indexes for all tables in the database. Each array element is an array of
69
 * {@see IndexConstraint} or its child classes. This property is read-only.
70
 * @property Constraint[] $schemaPrimaryKeys Primary keys for all tables in the database. Each array element is an
71
 * instance of {@see Constraint} or its child class. This property is read-only.
72
 * @property IndexConstraint[] $schemaUniques Unique constraints for all tables in the database. Each array element is
73
 * an array of {@see IndexConstraint} or its child classes. This property is read-only.
74
 */
75
abstract class Schema implements SchemaInterface
76
{
77
    /**
78
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
79
     */
80
    protected const SCHEMA_CACHE_VERSION = 1;
81
82
    /**
83
     * @var string|null the default schema name used for the current session.
84
     */
85
    protected ?string $defaultSchema = null;
86
87
    /**
88
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
89
     * class from the right part is used.
90
     */
91
    protected array $exceptionMap = [
92
        'SQLSTATE[23' => IntegrityException::class,
93
    ];
94
95
    /**
96
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
97
     * case starting and ending characters are different.
98
     */
99
    protected $tableQuoteCharacter = "'";
100
101
    /**
102
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
103
     * and ending characters are different.
104
     */
105
    protected $columnQuoteCharacter = '"';
106
    private array $schemaNames = [];
107
    private array $tableNames = [];
108
    private array $tableMetadata = [];
109
    private ?string $serverVersion = null;
110
    private ConnectionInterface $db;
111
    private ?QueryBuilder $builder = null;
112
    private SchemaCache $schemaCache;
113
114
    public function __construct(ConnectionInterface $db, SchemaCache $schemaCache)
115
    {
116
        $this->db = $db;
117
        $this->schemaCache = $schemaCache;
118
    }
119
120 2532
    abstract public function createQueryBuilder(): QueryBuilder;
121
122 2532
    /**
123 2532
     * @inheritDoc
124 2532
     */
125
    public function getQueryBuilder(): QueryBuilder
126
    {
127
        if ($this->builder === null) {
128
            $this->builder = $this->createQueryBuilder();
129
        }
130
131
        return $this->builder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->builder could return the type null which is incompatible with the type-hinted return Yiisoft\Db\Query\QueryBuilder. Consider adding an additional type-check to rule them out.
Loading history...
132
    }
133
134
    public function getDb(): ConnectionInterface
135
    {
136
        return $this->db;
137
    }
138
139
    public function getDefaultSchema(): ?string
140
    {
141
        return $this->defaultSchema;
142
    }
143
144
    public function getSchemaCache(): SchemaCache
145
    {
146
        return $this->schemaCache;
147
    }
148
149
    /**
150
     * @inheritDoc
151
     */
152
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
153
    {
154
        return $this->getTableMetadata($name, self::SCHEMA, $refresh);
155
    }
156
157
    /**
158
     * @inheritDoc
159
     */
160
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
161
    {
162
        return $this->getSchemaMetadata($schema, self::SCHEMA, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...self::SCHEMA, $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Constraint|array which are incompatible with the return type Yiisoft\Db\Schema\TableSchema mandated by Yiisoft\Db\Schema\Schema...face::getTableSchemas().
Loading history...
163
    }
164
165
    /**
166
     * @inheritDoc
167
     */
168
    public function getSchemaNames(bool $refresh = false): array
169
    {
170
        if (empty($this->schemaNames) || $refresh) {
171
            $this->schemaNames = $this->findSchemaNames();
172
        }
173
174
        return $this->schemaNames;
175
    }
176
177
    /**
178
     * @inheritDoc
179
     */
180
    public function getTableNames(string $schema = '', bool $refresh = false): array
181
    {
182
        if (!isset($this->tableNames[$schema]) || $refresh) {
183
            $this->tableNames[$schema] = $this->findTableNames($schema);
184
        }
185
186
        return $this->tableNames[$schema];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tableNames[$schema] returns the type string which is incompatible with the type-hinted return array.
Loading history...
187
    }
188
189
    /**
190
     * @inheritDoc
191
     */
192
    public function getPdoType($data): int
193 1322
    {
194
        static $typeMap = [
195 1322
            // php type => PDO type
196
            'boolean' => PDO::PARAM_BOOL,
197
            'integer' => PDO::PARAM_INT,
198
            'string' => PDO::PARAM_STR,
199
            'resource' => PDO::PARAM_LOB,
200
            'NULL' => PDO::PARAM_NULL,
201
        ];
202
203
        $type = gettype($data);
204
205
        return $typeMap[$type] ?? PDO::PARAM_STR;
206
    }
207
208
    /**
209
     * @inheritDoc
210
     */
211 13
    public function refresh(): void
212
    {
213 13
        if ($this->schemaCache->isEnabled()) {
214
            $this->schemaCache->invalidate($this->getCacheTag());
215
        }
216
217
        $this->tableNames = [];
218
        $this->tableMetadata = [];
219
    }
220
221
    /**
222
     * @inheritDoc
223
     */
224
    public function refreshTableSchema(string $name): void
225
    {
226 4
        $rawName = $this->getRawTableName($name);
227
228 4
        unset($this->tableMetadata[$rawName]);
229 4
230
        $this->tableNames = [];
231
232 4
        if ($this->schemaCache->isEnabled()) {
233
            $this->schemaCache->remove($this->getCacheKey($rawName));
234
        }
235
    }
236
237
    /**
238
     * @inheritDoc
239
     */
240
    public function getLastInsertID(string $sequenceName = ''): string
241
    {
242
        if ($this->db->isActive()) {
0 ignored issues
show
Bug introduced by
The method isActive() 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

242
        if ($this->db->/** @scrutinizer ignore-call */ isActive()) {
Loading history...
243
            return $this->db->getPDO()->lastInsertId(
0 ignored issues
show
Bug introduced by
The method getPDO() 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

243
            return $this->db->/** @scrutinizer ignore-call */ getPDO()->lastInsertId(
Loading history...
244
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
245
            );
246
        }
247
248 23
        throw new InvalidCallException('DB Connection is not active.');
249
    }
250 23
251 23
    /**
252
     * @inheritDoc
253
     */
254 23
    public function supportsSavepoint(): bool
255
    {
256
        return $this->db->isSavepointEnabled();
0 ignored issues
show
Bug introduced by
The method isSavepointEnabled() 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

256
        return $this->db->/** @scrutinizer ignore-call */ isSavepointEnabled();
Loading history...
257
    }
258
259
    /**
260 963
     * @inheritDoc
261
     */
262 963
    public function createSavepoint(string $name): void
263 963
    {
264
        $this->db->createCommand("SAVEPOINT $name")->execute();
265
    }
266 963
267
    /**
268
     * @inheritDoc
269
     */
270
    public function releaseSavepoint(string $name): void
271
    {
272
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
273
    }
274
275
    /**
276
     * @inheritDoc
277
     */
278 1360
    public function rollBackSavepoint(string $name): void
279
    {
280 1360
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
281
    }
282
283
    /**
284
     * @inheritDoc
285
     */
286
    public function setTransactionIsolationLevel(string $level): void
287
    {
288
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
289 1360
    }
290
291 1360
    /**
292
     * @inheritDoc
293
     */
294
    public function insert(string $table, array $columns)
295
    {
296
        $command = $this->db->createCommand()->insert($table, $columns);
297
298
        if (!$command->execute()) {
299
            return false;
300 96
        }
301
302 96
        $tableSchema = $this->getTableSchema($table);
303 96
        $result = [];
304
305
        foreach ($tableSchema->getPrimaryKey() as $name) {
306 96
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
307 96
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, 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

307
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
308 96
                break;
309
            }
310
311
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
312
        }
313
314
        return $result;
315
    }
316
317
    /**
318 103
     * @inheritDoc
319
     */
320 103
    public function quoteValue($str)
321
    {
322 103
        if (!is_string($str)) {
323
            return $str;
324 103
        }
325
326 103
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
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

326
        if (($value = $this->db->/** @scrutinizer ignore-call */ getSlavePdo()->quote($str)) !== false) {
Loading history...
327 103
            return $value;
328
        }
329 103
330
        /** the driver doesn't support quote (e.g. oci) */
331
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
332
    }
333
334
    /**
335
     * @inheritDoc
336
     */
337
    public function quoteTableName(string $name): string
338
    {
339
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
340
            return $name;
341
        }
342 36
343
        if (strpos($name, '{{') !== false) {
344 36
            return $name;
345 36
        }
346 36
347
        if (strpos($name, '.') === false) {
348
            return $this->quoteSimpleTableName($name);
349
        }
350
351
        $parts = $this->getTableNameParts($name);
352
353
        foreach ($parts as $i => $part) {
354
            $parts[$i] = $this->quoteSimpleTableName($part);
355
        }
356 10
357
        return implode('.', $parts);
358 10
    }
359
360
    /**
361
     * @inheritDoc
362
     */
363
    public function quoteColumnName(string $name): string
364
    {
365
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
366
            return $name;
367
        }
368 4
369
        if (($pos = strrpos($name, '.')) !== false) {
370 4
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
371 4
            $name = substr($name, $pos + 1);
372
        } else {
373
            $prefix = '';
374
        }
375
376
        if (strpos($name, '{{') !== false) {
377
            return $name;
378
        }
379
380
        return $prefix . $this->quoteSimpleColumnName($name);
381
    }
382
383
    /**
384
     * @inheritDoc
385
     */
386
    public function quoteSimpleTableName(string $name): string
387
    {
388
        if (is_string($this->tableQuoteCharacter)) {
389
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
390
        } else {
391
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
392 4
        }
393
394 4
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
395 4
    }
396
397
    /**
398
     * @inheritDoc
399
     */
400
    public function quoteSimpleColumnName(string $name): string
401
    {
402
        if (is_string($this->columnQuoteCharacter)) {
403
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
404
        } else {
405
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
406
        }
407
408
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
409
            . $endingCharacter;
410 8
    }
411
412 8
    /**
413 8
     * @inheritDoc
414
     */
415
    public function unquoteSimpleTableName(string $name): string
416
    {
417
        if (is_string($this->tableQuoteCharacter)) {
418
            $startingCharacter = $this->tableQuoteCharacter;
419
        } else {
420
            $startingCharacter = $this->tableQuoteCharacter[0];
421
        }
422
423
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
424
    }
425 28
426
    /**
427 28
     * @inheritDoc
428
     */
429 28
    public function unquoteSimpleColumnName(string $name): string
430
    {
431
        if (is_string($this->columnQuoteCharacter)) {
432
            $startingCharacter = $this->columnQuoteCharacter;
433 28
        } else {
434 28
            $startingCharacter = $this->columnQuoteCharacter[0];
435
        }
436 28
437 26
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
438 24
    }
439 24
440
    /**
441
     * @inheritDoc
442 4
     */
443
    public function getRawTableName(string $name): string
444
    {
445 28
        if (strpos($name, '{{') !== false) {
446
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
447
448
            return str_replace('%', $this->db->getTablePrefix(), $name);
0 ignored issues
show
Bug introduced by
The method getTablePrefix() 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

448
            return str_replace('%', $this->db->/** @scrutinizer ignore-call */ getTablePrefix(), $name);
Loading history...
449
        }
450
451
        return $name;
452
    }
453
454
    /**
455
     * @inheritDoc
456
     */
457
    public function convertException(\Exception $e, string $rawSql): Exception
458
    {
459
        if ($e instanceof Exception) {
460
            return $e;
461 1075
        }
462
463 1075
        $exceptionClass = Exception::class;
464 6
465
        foreach ($this->exceptionMap as $error => $class) {
466
            if (strpos($e->getMessage(), $error) !== false) {
467 1075
                $exceptionClass = $class;
468 1075
            }
469
        }
470
471
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
472
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
473
474
        return new $exceptionClass($message, $errorInfo, $e);
475
    }
476
477
    /**
478
     * @inheritDoc
479
     */
480
    public function isReadQuery(string $sql): bool
481
    {
482
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
483
484
        return preg_match($pattern, $sql) > 0;
485
    }
486
487 1574
    /**
488
     * @inheritDoc
489 1574
     */
490 4
    public function getServerVersion(): string
491
    {
492
        if ($this->serverVersion === null) {
493 1574
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
494 146
        }
495
496
        return $this->serverVersion;
497 1550
    }
498 1467
499
    /**
500
     * @inheritDoc
501 287
     */
502
    public function getTablePrimaryKey(string $name, bool $refresh = false): ?Constraint
503 287
    {
504 287
        return $this->getTableMetadata($name, SchemaInterface::PRIMARY_KEY, $refresh);
505
    }
506
507 287
    /**
508
     * @inheritDoc
509
     */
510
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
511
    {
512
        return $this->getSchemaMetadata($schema, SchemaInterface::PRIMARY_KEY, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...:PRIMARY_KEY, $refresh) returns an array which contains values of type Yiisoft\Db\Schema\TableSchema|array which are incompatible with the return type Yiisoft\Db\Constraint\Constraint mandated by Yiisoft\Db\Schema\Schema...:getSchemaPrimaryKeys().
Loading history...
513
    }
514
515
    /**
516
     * @inheritDoc
517 13
     */
518
    public function getTableForeignKeys(string $name, bool $refresh = false): array
519 13
    {
520
        return $this->getTableMetadata($name, SchemaInterface::FOREIGN_KEYS, $refresh);
521
    }
522
523
    /**
524
     * @inheritDoc
525
     */
526
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
527
    {
528
        return $this->getSchemaMetadata($schema, SchemaInterface::FOREIGN_KEYS, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...FOREIGN_KEYS, $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\ForeignKeyConstraint[] mandated by Yiisoft\Db\Schema\Schema...:getSchemaForeignKeys().
Loading history...
529
    }
530
531
    /**
532
     * @inheritDoc
533
     */
534 1698
    public function getTableIndexes(string $name, bool $refresh = false): array
535
    {
536 1698
        return $this->getTableMetadata($name, SchemaInterface::INDEXES, $refresh);
537 149
    }
538
539
    /**
540 1683
     * @inheritDoc
541 232
     */
542 232
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
543
    {
544 1673
        return $this->getSchemaMetadata($schema, SchemaInterface::INDEXES, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ace::INDEXES, $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\IndexConstraint[] mandated by Yiisoft\Db\Schema\Schema...ace::getSchemaIndexes().
Loading history...
545
    }
546
547 1683
    /**
548 4
     * @inheritDoc
549
     */
550
    public function getTableUniques(string $name, bool $refresh = false): array
551 1683
    {
552
        return $this->getTableMetadata($name, SchemaInterface::UNIQUES, $refresh);
553
    }
554
555
    /**
556
     * @inheritDoc
557
     */
558
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
559
    {
560
        return $this->getSchemaMetadata($schema, SchemaInterface::UNIQUES, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ace::UNIQUES, $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\Constraint[] mandated by Yiisoft\Db\Schema\Schema...ace::getSchemaUniques().
Loading history...
561
    }
562
563
    /**
564 1344
     * @inheritDoc
565
     */
566 1344
    public function getTableChecks(string $name, bool $refresh = false): array
567 987
    {
568
        return $this->getTableMetadata($name, SchemaInterface::CHECKS, $refresh);
569 357
    }
570
571
    /**
572 1344
     * @inheritDoc
573
     */
574
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
575
    {
576
        return $this->getSchemaMetadata($schema, SchemaInterface::CHECKS, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...face::CHECKS, $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\CheckConstraint[] mandated by Yiisoft\Db\Schema\Schema...face::getSchemaChecks().
Loading history...
577
    }
578
579
    /**
580
     * @inheritDoc
581
     */
582
    public function getTableDefaultValues(string $name, bool $refresh = false): array
583
    {
584
        return $this->getTableMetadata($name, SchemaInterface::DEFAULT_VALUES, $refresh);
585 1683
    }
586
587 1683
    /**
588 1357
     * @inheritDoc
589
     */
590 326
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
591
    {
592
        return $this->getSchemaMetadata($schema, SchemaInterface::DEFAULT_VALUES, $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...FAULT_VALUES, $refresh) returns an array which contains values of type Yiisoft\Db\Schema\TableSchema|array which are incompatible with the return type Yiisoft\Db\Constraint\DefaultValueConstraint mandated by Yiisoft\Db\Schema\Schema...etSchemaDefaultValues().
Loading history...
593 1683
    }
594 1683
595
    /**
596
     * Resolves the table name and schema name (if any).
597
     *
598
     * @param string $name the table name.
599
     *
600
     * @throws NotSupportedException if this method is not supported by the DBMS.
601
     *
602
     * @return TableSchema with resolved table, schema, etc. names.
603
     *
604
     * {@see \Yiisoft\Db\Schema\TableSchema}
605
     */
606
    protected function resolveTableName(string $name): TableSchema
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed. ( Ignorable by Annotation )

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

606
    protected function resolveTableName(/** @scrutinizer ignore-unused */ string $name): TableSchema

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
607 5
    {
608
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
609 5
    }
610 5
611
    /**
612
     * Returns all schema names in the database, including the default one but not system schemas.
613
     *
614
     * This method should be overridden by child classes in order to support this feature because the default
615 5
     * implementation simply throws an exception.
616
     *
617
     * @throws NotSupportedException if this method is not supported by the DBMS.
618
     *
619
     * @return array all schema names in the database, except system schemas.
620
     */
621
    protected function findSchemaNames(): array
622
    {
623
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
624
    }
625
626
    /**
627
     * Returns all table names in the database.
628
     *
629
     * This method should be overridden by child classes in order to support this feature because the default
630
     * implementation simply throws an exception.
631
     *
632
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
633
     *
634
     * @throws NotSupportedException if this method is not supported by the DBMS.
635
     *
636
     * @return array all table names in the database. The names have NO schema name prefix.
637
     */
638
    protected function findTableNames(string $schema = ''): array
0 ignored issues
show
Unused Code introduced by
The parameter $schema is not used and could be removed. ( Ignorable by Annotation )

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

638
    protected function findTableNames(/** @scrutinizer ignore-unused */ string $schema = ''): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
639
    {
640
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
641
    }
642
643
    /**
644
     * Loads the metadata for the specified table.
645
     *
646
     * @param string $name table name.
647
     *
648
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
649 1622
     */
650
    abstract protected function loadTableSchema(string $name): ?TableSchema;
651 1622
652 130
    /**
653
     * Splits full table name into parts
654 130
     *
655
     * @param string $name
656
     *
657 1622
     * @return array
658
     */
659
    protected function getTableNameParts(string $name): array
660
    {
661
        return explode('.', $name);
662
    }
663
664
    /**
665
     * Extracts the PHP type from abstract DB type.
666
     *
667 1280
     * @param ColumnSchema $column the column schema information.
668
     *
669 1280
     * @return string PHP type name.
670
     */
671
    protected function getColumnPhpType(ColumnSchema $column): string
672
    {
673
        static $typeMap = [
674
            // abstract type => php type
675
            self::TYPE_TINYINT => 'integer',
676
            self::TYPE_SMALLINT => 'integer',
677
            self::TYPE_INTEGER => 'integer',
678
            self::TYPE_BIGINT => 'integer',
679
            self::TYPE_BOOLEAN => 'boolean',
680
            self::TYPE_FLOAT => 'double',
681
            self::TYPE_DOUBLE => 'double',
682 1280
            self::TYPE_BINARY => 'resource',
683 1269
            self::TYPE_JSON => 'array',
684 52
        ];
685
686
        if (isset($typeMap[$column->getType()])) {
687 1269
            if ($column->getType() === 'bigint') {
688 1269
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
689
            }
690
691 367
            if ($column->getType() === 'integer') {
692
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
693
            }
694 1218
695
            return $typeMap[$column->getType()];
696
        }
697
698
        return 'string';
699
    }
700
701
    /**
702
     * Returns the cache key for the specified table name.
703
     *
704
     * @param string $name the table name.
705 50
     *
706
     * @return array the cache key.
707 50
     */
708
    protected function getCacheKey(string $name): array
709
    {
710
        return [
711 50
            __CLASS__,
712
            $this->db->getDsn(),
713 50
            $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

713
            $this->db->/** @scrutinizer ignore-call */ 
714
                       getUsername(),
Loading history...
714 50
            $this->getRawTableName($name),
715 13
        ];
716
    }
717
718
    /**
719 50
     * Returns the cache tag name.
720 50
     *
721
     * This allows {@see refresh()} to invalidate all cached table schemas.
722 50
     *
723
     * @return string the cache tag name.
724
     */
725
    protected function getCacheTag(): string
726
    {
727
        return md5(serialize([
728
            __CLASS__,
729
            $this->db->getDsn(),
730
            $this->db->getUsername(),
731
        ]));
732 12
    }
733
734 12
    /**
735
     * Returns the metadata of the given type for the given table.
736 12
     *
737
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
738
     * @param string $type metadata type.
739
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
740
     *
741
     * @return mixed metadata.
742
     */
743
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
744
    {
745
        $rawName = $this->getRawTableName($name);
746 375
747
        if (!isset($this->tableMetadata[$rawName])) {
748 375
            $this->loadTableMetadataFromCache($rawName);
749 375
        }
750
751
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
752 375
            $this->tableMetadata[$rawName][$type] = $this->loadTableTypeMetadata($type, $rawName);
753
            $this->saveTableMetadataToCache($rawName);
754
        }
755
756
        return $this->tableMetadata[$rawName][$type];
757
    }
758
759
    /**
760
     * Returns the metadata of the given type for all tables in the given schema.
761
     *
762 1622
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
763
     * name.
764
     * @param string $type metadata type.
765 1622
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
766 1622
     * returned if available.
767 1622
     *
768 1622
     * @throws NotSupportedException
769
     *
770
     * @return array array of metadata.
771
     */
772
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
773
    {
774
        $metadata = [];
775
776
        foreach ($this->getTableNames($schema, $refresh) as $name) {
777
            if ($schema !== '') {
778
                $name = $schema . '.' . $name;
779 1622
            }
780
781 1622
            $tableMetadata = $this->getTableTypeMetadata($type, $name, $refresh);
782 1622
783 1622
            if ($tableMetadata !== null) {
784 1622
                $metadata[] = $tableMetadata;
785
            }
786
        }
787
788
        return $metadata;
789
    }
790
791
    /**
792
     * Sets the metadata of the given type for the given table.
793
     *
794
     * @param string $name table name.
795
     * @param string $type metadata type.
796
     * @param mixed $data metadata.
797
     */
798
    protected function setTableMetadata(string $name, string $type, $data): void
799
    {
800 1622
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
801
    }
802 1622
803
    /**
804 1622
     * Changes row's array key case to lower if PDO's one is set to uppercase.
805 1622
     *
806
     * @param array $row row's array or an array of row's arrays.
807
     * @param bool $multiple whether multiple rows or a single row passed.
808 1622
     *
809 1622
     * @throws Exception
810 1562
     *
811
     * @return array normalized row or rows.
812
     */
813 1562
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
814
    {
815
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
816
            return $row;
817
        }
818
819
        if ($multiple) {
820
            return array_map(static function (array $row) {
821
                return array_change_key_case($row, CASE_LOWER);
822
            }, $row);
823
        }
824
825
        return array_change_key_case($row, CASE_LOWER);
826
    }
827
828
    /**
829
     * Tries to load and populate table metadata from cache.
830
     *
831
     * @param string $rawName
832 13
     */
833
    private function loadTableMetadataFromCache(string $rawName): void
834 13
    {
835 13
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
836
            $this->tableMetadata[$rawName] = [];
837 13
            return;
838 13
        }
839
840
        $metadata = $this->schemaCache->getOrSet(
841
            $this->getCacheKey($rawName),
842 13
            null,
843
            $this->schemaCache->getDuration(),
844 13
            new TagDependency($this->getCacheTag()),
845 13
        );
846
847
        if (
848
            !is_array($metadata) ||
849 13
            !isset($metadata['cacheVersion']) ||
850
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
851
        ) {
852
            $this->tableMetadata[$rawName] = [];
853
854
            return;
855
        }
856
857
        unset($metadata['cacheVersion']);
858
        $this->tableMetadata[$rawName] = $metadata;
859 297
    }
860
861 297
    /**
862 297
     * Saves table metadata to cache.
863
     *
864
     * @param string $rawName
865
     */
866
    private function saveTableMetadataToCache(string $rawName): void
867
    {
868
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
869
            return;
870
        }
871
872
        $metadata = $this->tableMetadata[$rawName];
873
874 341
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
875
876 341
        $this->schemaCache->set(
877 265
            $this->getCacheKey($rawName),
878
            $metadata,
879
            $this->schemaCache->getDuration(),
880 76
            new TagDependency($this->getCacheTag()),
881 76
        );
882 73
    }
883 76
884
    /**
885
     * This method returns the desired metadata type for the table name.
886
     *
887
     * @param string $type
888
     * @param string $name
889
     *
890
     * @return mixed
891
     */
892
    protected function loadTableTypeMetadata(string $type, string $name)
893
    {
894 1622
        switch ($type) {
895
            case SchemaInterface::SCHEMA:
896 1622
                return $this->loadTableSchema($name);
897
            case SchemaInterface::PRIMARY_KEY:
898
                return $this->loadTablePrimaryKey($name);
899
            case SchemaInterface::UNIQUES:
900
                return $this->loadTableUniques($name);
901 1622
            case SchemaInterface::FOREIGN_KEYS:
902 1622
                return $this->loadTableForeignKeys($name);
903 1622
            case SchemaInterface::INDEXES:
904 1622
                return $this->loadTableIndexes($name);
905 1622
            case SchemaInterface::DEFAULT_VALUES:
906
                return $this->loadTableDefaultValues($name);
907
            case SchemaInterface::CHECKS:
908
                return $this->loadTableChecks($name);
909 1622
        }
910 1622
911 1622
        return null;
912
    }
913 1622
914
    /**
915 1622
     *
916
     * This method returns the desired metadata type for table name (with refresh if needed)
917
     *
918 841
     * @param string $type
919 841
     * @param string $name
920 841
     * @param bool $refresh
921
     *
922
     * @return mixed
923
     */
924
    protected function getTableTypeMetadata(string $type, string $name, bool $refresh = false)
925
    {
926
        switch ($type) {
927 1562
            case SchemaInterface::SCHEMA:
928
                return $this->getTableSchema($name, $refresh);
929 1562
            case SchemaInterface::PRIMARY_KEY:
930
                return $this->getTablePrimaryKey($name, $refresh);
931
            case SchemaInterface::UNIQUES:
932
                return $this->getTableUniques($name, $refresh);
933 1562
            case SchemaInterface::FOREIGN_KEYS:
934
                return $this->getTableForeignKeys($name, $refresh);
935 1562
            case SchemaInterface::INDEXES:
936
                return $this->getTableIndexes($name, $refresh);
937 1562
            case SchemaInterface::DEFAULT_VALUES:
938 1562
                return $this->getTableDefaultValues($name, $refresh);
939
            case SchemaInterface::CHECKS:
940 1562
                return $this->getTableChecks($name, $refresh);
941 1562
        }
942
943 1562
        return null;
944
    }
945 1665
946
    /**
947 1665
     * Loads a primary key for the given table.
948
     *
949
     * @param string $tableName table name.
950
     *
951
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
952
     */
953
    abstract protected function loadTablePrimaryKey(string $tableName): ?Constraint;
954
955 18
    /**
956
     * Loads all foreign keys for the given table.
957 18
     *
958
     * @param string $tableName table name.
959
     *
960
     * @return array foreign keys for the given table.
961
     */
962
    abstract protected function loadTableForeignKeys(string $tableName): array;
963
964
    /**
965
     * Loads all indexes for the given table.
966
     *
967
     * @param string $tableName table name.
968
     *
969
     * @return array indexes for the given table.
970
     */
971
    abstract protected function loadTableIndexes(string $tableName): array;
972
973
    /**
974
     * Loads all unique constraints for the given table.
975
     *
976
     * @param string $tableName table name.
977
     *
978
     * @return array unique constraints for the given table.
979
     */
980
    abstract protected function loadTableUniques(string $tableName): array;
981
982
    /**
983
     * Loads all check constraints for the given table.
984
     *
985
     * @param string $tableName table name.
986
     *
987
     * @return array check constraints for the given table.
988
     */
989
    abstract protected function loadTableChecks(string $tableName): array;
990
991
    /**
992
     * Loads all default value constraints for the given table.
993
     *
994
     * @param string $tableName table name.
995
     *
996
     * @return array default value constraints for the given table.
997
     */
998
    abstract protected function loadTableDefaultValues(string $tableName): array;
999
}
1000