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

Schema::getDefaultSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
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
     * @todo Need refactoring and remove this method
192
     */
193 1322
    public function getPdoType($data): int
194
    {
195 1322
        static $typeMap = [
196
            // php type => PDO type
197
            'boolean' => PDO::PARAM_BOOL,
198
            'integer' => PDO::PARAM_INT,
199
            'string' => PDO::PARAM_STR,
200
            'resource' => PDO::PARAM_LOB,
201
            'NULL' => PDO::PARAM_NULL,
202
        ];
203
204
        $type = gettype($data);
205
206
        return $typeMap[$type] ?? PDO::PARAM_STR;
207
    }
208
209
    /**
210
     * @inheritDoc
211 13
     */
212
    public function refresh(): void
213 13
    {
214
        if ($this->schemaCache->isEnabled()) {
215
            $this->schemaCache->invalidate($this->getCacheTag());
216
        }
217
218
        $this->tableNames = [];
219
        $this->tableMetadata = [];
220
    }
221
222
    /**
223
     * @inheritDoc
224
     */
225
    public function refreshTableSchema(string $name): void
226 4
    {
227
        $rawName = $this->getRawTableName($name);
228 4
229 4
        unset($this->tableMetadata[$rawName]);
230
231
        $this->tableNames = [];
232 4
233
        if ($this->schemaCache->isEnabled()) {
234
            $this->schemaCache->remove($this->getCacheKey($rawName));
235
        }
236
    }
237
238
    /**
239
     * @inheritDoc
240
     * @todo Need refactoring and remove this method, because not all dbms supported this
241
     */
242
    public function getLastInsertID(string $sequenceName = ''): string
243
    {
244
        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

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

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

258
        return $this->db->/** @scrutinizer ignore-call */ isSavepointEnabled();
Loading history...
259
    }
260 963
261
    /**
262 963
     * @inheritDoc
263 963
     * @todo move to class Transation?
264
     */
265
    public function createSavepoint(string $name): void
266 963
    {
267
        $this->db->createCommand("SAVEPOINT $name")->execute();
268
    }
269
270
    /**
271
     * @inheritDoc
272
     * @todo move to class Transation?
273
     */
274
    public function releaseSavepoint(string $name): void
275
    {
276
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
277
    }
278 1360
279
    /**
280 1360
     * @inheritDoc
281
     * @todo move to class Transation?
282
     */
283
    public function rollBackSavepoint(string $name): void
284
    {
285
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
286
    }
287
288
    /**
289 1360
     * @inheritDoc
290
     * @todo move to class Transation?
291 1360
     */
292
    public function setTransactionIsolationLevel(string $level): void
293
    {
294
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
295
    }
296
297
    /**
298
     * @inheritDoc
299
     * @todo - need refactoring:
300 96
     * - $this->getTableSchema($table) - maybe null
301
     * - $tableSchema->getColumn($name) - maybe null
302 96
     * - $tableSchema->getColumn($name) - maybe null
303 96
     * - not all dbms support lastInsertId - method getLastInsertID must be renamed, refactoring or removed.
304
     * In mssql SCOPE_IDENTITY() - @see https://docs.microsoft.com/ru-ru/dotnet/framework/data/adonet/retrieving-identity-or-autonumber-values
305
     */
306 96
    public function insert(string $table, array $columns)
307 96
    {
308 96
        $command = $this->db->createCommand()->insert($table, $columns);
309
310
        if (!$command->execute()) {
311
            return false;
312
        }
313
314
        $tableSchema = $this->getTableSchema($table);
315
        $result = [];
316
317
        foreach ($tableSchema->getPrimaryKey() as $name) {
318 103
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
319
                $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

319
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
320 103
                break;
321
            }
322 103
323
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
324 103
        }
325
326 103
        return $result;
327 103
    }
328
329 103
    /**
330
     * @inheritDoc
331
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
332
     */
333
    public function quoteValue($str)
334
    {
335
        if (!is_string($str)) {
336
            return $str;
337
        }
338
339
        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

339
        if (($value = $this->db->/** @scrutinizer ignore-call */ getSlavePdo()->quote($str)) !== false) {
Loading history...
340
            return $value;
341
        }
342 36
343
        /** the driver doesn't support quote (e.g. oci) */
344 36
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
345 36
    }
346 36
347
    /**
348
     * @inheritDoc
349
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
350
     */
351
    public function quoteTableName(string $name): string
352
    {
353
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
354
            return $name;
355
        }
356 10
357
        if (strpos($name, '{{') !== false) {
358 10
            return $name;
359
        }
360
361
        if (strpos($name, '.') === false) {
362
            return $this->quoteSimpleTableName($name);
363
        }
364
365
        $parts = $this->getTableNameParts($name);
366
367
        foreach ($parts as $i => $part) {
368 4
            $parts[$i] = $this->quoteSimpleTableName($part);
369
        }
370 4
371 4
        return implode('.', $parts);
372
    }
373
374
    /**
375
     * @inheritDoc
376
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
377
     */
378
    public function quoteColumnName(string $name): string
379
    {
380
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
381
            return $name;
382
        }
383
384
        if (($pos = strrpos($name, '.')) !== false) {
385
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
386
            $name = substr($name, $pos + 1);
387
        } else {
388
            $prefix = '';
389
        }
390
391
        if (strpos($name, '{{') !== false) {
392 4
            return $name;
393
        }
394 4
395 4
        return $prefix . $this->quoteSimpleColumnName($name);
396
    }
397
398
    /**
399
     * @inheritDoc
400
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
401
     */
402
    public function quoteSimpleTableName(string $name): string
403
    {
404
        if (is_string($this->tableQuoteCharacter)) {
405
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
406
        } else {
407
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
408
        }
409
410 8
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
411
    }
412 8
413 8
    /**
414
     * @inheritDoc
415
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
416
     */
417
    public function quoteSimpleColumnName(string $name): string
418
    {
419
        if (is_string($this->columnQuoteCharacter)) {
420
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
421
        } else {
422
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
423
        }
424
425 28
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
426
            . $endingCharacter;
427 28
    }
428
429 28
    /**
430
     * @inheritDoc
431
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
432
     */
433 28
    public function unquoteSimpleTableName(string $name): string
434 28
    {
435
        if (is_string($this->tableQuoteCharacter)) {
436 28
            $startingCharacter = $this->tableQuoteCharacter;
437 26
        } else {
438 24
            $startingCharacter = $this->tableQuoteCharacter[0];
439 24
        }
440
441
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
442 4
    }
443
444
    /**
445 28
     * @inheritDoc
446
     * @todo - need remove and create QuoterHelper for this functional (also remove from Connection)
447
     */
448
    public function unquoteSimpleColumnName(string $name): string
449
    {
450
        if (is_string($this->columnQuoteCharacter)) {
451
            $startingCharacter = $this->columnQuoteCharacter;
452
        } else {
453
            $startingCharacter = $this->columnQuoteCharacter[0];
454
        }
455
456
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
457
    }
458
459
    /**
460
     * @inheritDoc
461 1075
     * @todo - move to QuoterHelper?
462
     */
463 1075
    public function getRawTableName(string $name): string
464 6
    {
465
        if (strpos($name, '{{') !== false) {
466
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
467 1075
468 1075
            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

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

628
    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...
629
    {
630
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
631
    }
632
633
    /**
634
     * Returns all schema names in the database, including the default one but not system schemas.
635
     *
636
     * This method should be overridden by child classes in order to support this feature because the default
637
     * implementation simply throws an exception.
638
     *
639
     * @throws NotSupportedException if this method is not supported by the DBMS.
640
     *
641
     * @return array all schema names in the database, except system schemas.
642
     */
643
    protected function findSchemaNames(): array
644
    {
645
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
646
    }
647
648
    /**
649 1622
     * Returns all table names in the database.
650
     *
651 1622
     * This method should be overridden by child classes in order to support this feature because the default
652 130
     * implementation simply throws an exception.
653
     *
654 130
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
655
     *
656
     * @throws NotSupportedException if this method is not supported by the DBMS.
657 1622
     *
658
     * @return array all table names in the database. The names have NO schema name prefix.
659
     */
660
    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

660
    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...
661
    {
662
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
663
    }
664
665
    /**
666
     * Loads the metadata for the specified table.
667 1280
     *
668
     * @param string $name table name.
669 1280
     *
670
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
671
     */
672
    abstract protected function loadTableSchema(string $name): ?TableSchema;
673
674
    /**
675
     * Splits full table name into parts
676
     *
677
     * @param string $name
678
     *
679
     * @return array
680
     */
681
    protected function getTableNameParts(string $name): array
682 1280
    {
683 1269
        return explode('.', $name);
684 52
    }
685
686
    /**
687 1269
     * Extracts the PHP type from abstract DB type.
688 1269
     *
689
     * @param ColumnSchema $column the column schema information.
690
     *
691 367
     * @return string PHP type name.
692
     */
693
    protected function getColumnPhpType(ColumnSchema $column): string
694 1218
    {
695
        static $typeMap = [
696
            // abstract type => php type
697
            self::TYPE_TINYINT => 'integer',
698
            self::TYPE_SMALLINT => 'integer',
699
            self::TYPE_INTEGER => 'integer',
700
            self::TYPE_BIGINT => 'integer',
701
            self::TYPE_BOOLEAN => 'boolean',
702
            self::TYPE_FLOAT => 'double',
703
            self::TYPE_DOUBLE => 'double',
704
            self::TYPE_BINARY => 'resource',
705 50
            self::TYPE_JSON => 'array',
706
        ];
707 50
708
        if (isset($typeMap[$column->getType()])) {
709
            if ($column->getType() === 'bigint') {
710
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
711 50
            }
712
713 50
            if ($column->getType() === 'integer') {
714 50
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
715 13
            }
716
717
            return $typeMap[$column->getType()];
718
        }
719 50
720 50
        return 'string';
721
    }
722 50
723
    /**
724
     * Returns the cache key for the specified table name.
725
     *
726
     * @param string $name the table name.
727
     *
728
     * @return array the cache key.
729
     */
730
    protected function getCacheKey(string $name): array
731
    {
732 12
        return [
733
            __CLASS__,
734 12
            $this->db->getDsn(),
735
            $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

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