Passed
Pull Request — master (#259)
by Def
12:48
created

Schema::loadTableTypeMetadata()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 8.7021

Importance

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

238
        if ($this->db->/** @scrutinizer ignore-call */ isActive()) {
Loading history...
239
            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

239
            return $this->db->/** @scrutinizer ignore-call */ getPDO()->lastInsertId(
Loading history...
240
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
241
            );
242
        }
243
244
        throw new InvalidCallException('DB Connection is not active.');
245
    }
246
247
    /**
248 23
     * @inheritDoc
249
     */
250 23
    public function supportsSavepoint(): bool
251 23
    {
252
        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

252
        return $this->db->/** @scrutinizer ignore-call */ isSavepointEnabled();
Loading history...
253
    }
254 23
255
    /**
256
     * @inheritDoc
257
     */
258
    public function createSavepoint(string $name): void
259
    {
260 963
        $this->db->createCommand("SAVEPOINT $name")->execute();
261
    }
262 963
263 963
    /**
264
     * @inheritDoc
265
     */
266 963
    public function releaseSavepoint(string $name): void
267
    {
268
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
269
    }
270
271
    /**
272
     * @inheritDoc
273
     */
274
    public function rollBackSavepoint(string $name): void
275
    {
276
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
277
    }
278 1360
279
    /**
280 1360
     * @inheritDoc
281
     */
282
    public function setTransactionIsolationLevel(string $level): void
283
    {
284
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
285
    }
286
287
    /**
288
     * @inheritDoc
289 1360
     */
290
    public function insert(string $table, array $columns)
291 1360
    {
292
        $command = $this->db->createCommand()->insert($table, $columns);
293
294
        if (!$command->execute()) {
295
            return false;
296
        }
297
298
        $tableSchema = $this->getTableSchema($table);
299
        $result = [];
300 96
301
        foreach ($tableSchema->getPrimaryKey() as $name) {
302 96
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
303 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

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

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

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

602
    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...
603
    {
604
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
605
    }
606
607 5
    /**
608
     * Returns all schema names in the database, including the default one but not system schemas.
609 5
     *
610 5
     * This method should be overridden by child classes in order to support this feature because the default
611
     * implementation simply throws an exception.
612
     *
613
     * @throws NotSupportedException if this method is not supported by the DBMS.
614
     *
615 5
     * @return array all schema names in the database, except system schemas.
616
     */
617
    protected function findSchemaNames(): array
618
    {
619
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
620
    }
621
622
    /**
623
     * Returns all table names in the database.
624
     *
625
     * This method should be overridden by child classes in order to support this feature because the default
626
     * implementation simply throws an exception.
627
     *
628
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
629
     *
630
     * @throws NotSupportedException if this method is not supported by the DBMS.
631
     *
632
     * @return array all table names in the database. The names have NO schema name prefix.
633
     */
634
    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

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

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