Passed
Pull Request — master (#266)
by Wilmer
10:46
created

Schema::findSchemaNames()   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 0
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 Yiisoft\Cache\Dependency\TagDependency;
10
use Yiisoft\Db\Cache\SchemaCache;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\IntegrityException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
16
use function array_key_exists;
17
use function gettype;
18
use function is_array;
19
use function preg_match;
20
use function ucfirst;
21
22
abstract class Schema implements SchemaInterface
23
{
24
    public const SCHEMA = 'schema';
25
    public const PRIMARY_KEY = 'primaryKey';
26
    public const INDEXES = 'indexes';
27
    public const CHECKS = 'checks';
28
    public const FOREIGN_KEYS = 'foreignKeys';
29
    public const DEFAULT_VALUES = 'defaultValues';
30
    public const UNIQUES = 'uniques';
31
32
    public const TYPE_PK = 'pk';
33
    public const TYPE_UPK = 'upk';
34
    public const TYPE_BIGPK = 'bigpk';
35
    public const TYPE_UBIGPK = 'ubigpk';
36
    public const TYPE_CHAR = 'char';
37
    public const TYPE_STRING = 'string';
38
    public const TYPE_TEXT = 'text';
39
    public const TYPE_TINYINT = 'tinyint';
40
    public const TYPE_SMALLINT = 'smallint';
41
    public const TYPE_INTEGER = 'integer';
42
    public const TYPE_BIGINT = 'bigint';
43
    public const TYPE_FLOAT = 'float';
44
    public const TYPE_DOUBLE = 'double';
45
    public const TYPE_DECIMAL = 'decimal';
46
    public const TYPE_DATETIME = 'datetime';
47
    public const TYPE_TIMESTAMP = 'timestamp';
48
    public const TYPE_TIME = 'time';
49
    public const TYPE_DATE = 'date';
50
    public const TYPE_BINARY = 'binary';
51
    public const TYPE_BOOLEAN = 'boolean';
52
    public const TYPE_MONEY = 'money';
53
    public const TYPE_JSON = 'json';
54
55
    /**
56
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
57
     */
58
    protected const SCHEMA_CACHE_VERSION = 1;
59
60
    /**
61
     * @var string|null the default schema name used for the current session.
62
     */
63
    protected ?string $defaultSchema = null;
64
65
    /**
66
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
67
     * class from the right part is used.
68
     */
69
    protected array $exceptionMap = [
70
        'SQLSTATE[23' => IntegrityException::class,
71
    ];
72
73
    private array $schemaNames = [];
74
    private array $tableNames = [];
75
    private array $tableMetadata = [];
76
77
    public function __construct(private SchemaCache $schemaCache)
78
    {
79
    }
80
81
    /**
82
     * Returns the cache key for the specified table name.
83
     *
84
     * @param string $name the table name.
85
     *
86
     * @return array the cache key.
87
     */
88
    abstract protected function getCacheKey(string $name): array;
89
90
    /**
91
     * Returns the cache tag name.
92
     *
93
     * This allows {@see refresh()} to invalidate all cached table schemas.
94
     *
95
     * @return string the cache tag name.
96
     */
97
    abstract protected function getCacheTag(): string;
98
99
    /**
100
     * Loads all check constraints for the given table.
101
     *
102
     * @param string $tableName table name.
103
     *
104
     * @return array check constraints for the given table.
105
     */
106
    abstract protected function loadTableChecks(string $tableName): array;
107
108
    /**
109
     * Loads all default value constraints for the given table.
110 2532
     *
111
     * @param string $tableName table name.
112 2532
     *
113 2532
     * @return array default value constraints for the given table.
114 2532
     */
115
    abstract protected function loadTableDefaultValues(string $tableName): array;
116
117
    /**
118
     * Loads all foreign keys for the given table.
119
     *
120
     * @param string $tableName table name.
121 963
     *
122
     * @return array foreign keys for the given table.
123 963
     */
124 963
    abstract protected function loadTableForeignKeys(string $tableName): array;
125
126
    /**
127 963
     * Loads all indexes for the given table.
128
     *
129
     * @param string $tableName table name.
130 1665
     *
131
     * @return array indexes for the given table.
132 1665
     */
133
    abstract protected function loadTableIndexes(string $tableName): array;
134
135
    /**
136
     * Loads a primary key for the given table.
137
     *
138
     * @param string $tableName table name.
139
     *
140 18
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
141
     */
142 18
    abstract protected function loadTablePrimaryKey(string $tableName): ?Constraint;
143
144
    /**
145
     * Loads all unique constraints for the given table.
146
     *
147
     * @param string $tableName table name.
148 1322
     *
149
     * @return array unique constraints for the given table.
150 1322
     */
151
    abstract protected function loadTableUniques(string $tableName): array;
152
153
    /**
154
     * Loads the metadata for the specified table.
155
     *
156 13
     * @param string $name table name.
157
     *
158 13
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
159
     */
160
    abstract protected function loadTableSchema(string $name): ?TableSchema;
161
162
    public function convertException(\Exception $e, string $rawSql): Exception
163
    {
164 4
        if ($e instanceof Exception) {
165
            return $e;
166 4
        }
167 4
168
        $exceptionClass = Exception::class;
169
170 4
        foreach ($this->exceptionMap as $error => $class) {
171
            if (str_contains($e->getMessage(), $error)) {
172
                $exceptionClass = $class;
173
            }
174
        }
175
176 23
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
177
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
178 23
179 23
        return new $exceptionClass($message, $errorInfo, $e);
180
    }
181
182 23
    public function getDefaultSchema(): ?string
183
    {
184
        return $this->defaultSchema;
185
    }
186
187
    public function getPdoType(mixed $data): int
188 1360
    {
189
        static $typeMap = [
190 1360
            // php type => PDO type
191
            'boolean' => PDO::PARAM_BOOL,
192
            'integer' => PDO::PARAM_INT,
193
            'string' => PDO::PARAM_STR,
194
            'resource' => PDO::PARAM_LOB,
195
            'NULL' => PDO::PARAM_NULL,
196
        ];
197
198
        $type = gettype($data);
199 1360
200
        return $typeMap[$type] ?? PDO::PARAM_STR;
201 1360
    }
202
203
    public function getSchemaCache(): SchemaCache
204
    {
205
        return $this->schemaCache;
206
    }
207 96
208
    /**
209 96
     * @throws NotSupportedException
210 96
     */
211
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
212
    {
213 96
        return $this->getSchemaMetadata($schema, 'checks', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ma, '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\Constraint\Co...face::getSchemaChecks().
Loading history...
214 96
    }
215 96
216
    /**
217
     * @throws NotSupportedException
218
     */
219
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
220 103
    {
221
        return $this->getSchemaMetadata($schema, 'defaultValues', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...faultValues', $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\Constraint\Co...etSchemaDefaultValues().
Loading history...
222 103
    }
223
224 103
    /**
225
     * @throws NotSupportedException
226 103
     */
227
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
228 103
    {
229 103
        return $this->getSchemaMetadata($schema, 'foreignKeys', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...foreignKeys', $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\Constraint\Co...:getSchemaForeignKeys().
Loading history...
230
    }
231 103
232
    /**
233
     * @throws NotSupportedException
234
     */
235
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
236 36
    {
237
        return $this->getSchemaMetadata($schema, 'indexes', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...a, '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\Constraint\Co...ace::getSchemaIndexes().
Loading history...
238 36
    }
239 36
240 36
    public function getSchemaNames(bool $refresh = false): array
241
    {
242
        if (empty($this->schemaNames) || $refresh) {
243
            $this->schemaNames = $this->findSchemaNames();
244
        }
245
246
        return $this->schemaNames;
247
    }
248
249
    /**
250 10
     * @throws NotSupportedException
251
     */
252 10
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
253
    {
254
        return $this->getSchemaMetadata($schema, 'primaryKey', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...'primaryKey', $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\Constraint\Co...:getSchemaPrimaryKeys().
Loading history...
255
    }
256
257
    /**
258 4
     * @throws NotSupportedException
259
     */
260 4
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
261 4
    {
262
        return $this->getSchemaMetadata($schema, 'uniques', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...a, '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\Constraint\Co...ace::getSchemaUniques().
Loading history...
263
    }
264
265
    public function getTableChecks(string $name, bool $refresh = false): array
266
    {
267
        return $this->getTableMetadata($name, 'checks', $refresh);
268
    }
269
270
    public function getTableDefaultValues(string $name, bool $refresh = false): array
271
    {
272
        return $this->getTableMetadata($name, 'defaultValues', $refresh);
273
    }
274 4
275
    public function getTableForeignKeys(string $name, bool $refresh = false): array
276 4
    {
277 4
        return $this->getTableMetadata($name, 'foreignKeys', $refresh);
278
    }
279
280
    public function getTableIndexes(string $name, bool $refresh = false): array
281
    {
282 8
        return $this->getTableMetadata($name, 'indexes', $refresh);
283
    }
284 8
285 8
    public function getTableNames(string $schema = '', bool $refresh = false): array
286
    {
287
        if (!isset($this->tableNames[$schema]) || $refresh) {
288
            $this->tableNames[$schema] = $this->findTableNames($schema);
289
        }
290 28
291
        return $this->tableNames[$schema];
292 28
    }
293
294 28
    public function getTablePrimaryKey(string $name, bool $refresh = false): ?Constraint
295
    {
296
        return $this->getTableMetadata($name, 'primaryKey', $refresh);
297
    }
298 28
299 28
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
300
    {
301 28
        return $this->getTableMetadata($name, 'schema', $refresh);
302 26
    }
303 24
304 24
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
305
    {
306
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ma, '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...
307 4
    }
308
309
    public function getTableUniques(string $name, bool $refresh = false): array
310 28
    {
311
        return $this->getTableMetadata($name, 'uniques', $refresh);
312
    }
313
314
    /**
315
     * Returns a value indicating whether a SQL statement is for read purpose.
316 1075
     *
317
     * @param string $sql the SQL statement.
318 1075
     *
319 6
     * @return bool whether a SQL statement is for read purpose.
320
     */
321
    public function isReadQuery(string $sql): bool
322 1075
    {
323 1075
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
324
325
        return preg_match($pattern, $sql) > 0;
326
    }
327
328
    /**
329
     * Refreshes the schema.
330
     *
331
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
332
     * schema change.
333 1574
     */
334
    public function refresh(): void
335 1574
    {
336 4
        if ($this->schemaCache->isEnabled()) {
337
            $this->schemaCache->invalidate($this->getCacheTag());
338
        }
339 1574
340 146
        $this->tableNames = [];
341
        $this->tableMetadata = [];
342
    }
343 1550
344 1467
    public function refreshTableSchema(string $name): void
345
    {
346
        $rawName = $this->getRawTableName($name);
347 287
348
        unset($this->tableMetadata[$rawName]);
349 287
350 287
        $this->tableNames = [];
351
352
        if ($this->schemaCache->isEnabled()) {
353 287
            $this->schemaCache->remove($this->getCacheKey($rawName));
354
        }
355
    }
356
357
    /**
358
     * Returns all schema names in the database, including the default one but not system schemas.
359 1698
     *
360
     * This method should be overridden by child classes in order to support this feature because the default
361 1698
     * implementation simply throws an exception.
362 149
     *
363
     * @throws NotSupportedException if this method is not supported by the DBMS.
364
     *
365 1683
     * @return array all schema names in the database, except system schemas.
366 232
     */
367 232
    protected function findSchemaNames(): array
368
    {
369 1673
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
370
    }
371
372 1683
    /**
373 4
     * Returns all table names in the database.
374
     *
375
     * This method should be overridden by child classes in order to support this feature because the default
376 1683
     * implementation simply throws an exception.
377
     *
378
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
379
     *
380
     * @throws NotSupportedException if this method is not supported by the DBMS.
381
     *
382 1344
     * @return array all table names in the database. The names have NO schema name prefix.
383
     */
384 1344
    protected function findTableNames(string $schema = ''): array
385 987
    {
386
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
387 357
    }
388
389
    /**
390 1344
     * Extracts the PHP type from abstract DB type.
391
     *
392
     * @param ColumnSchema $column the column schema information.
393
     *
394
     * @return string PHP type name.
395
     */
396 1683
    protected function getColumnPhpType(ColumnSchema $column): string
397
    {
398 1683
        static $typeMap = [
399 1357
            // abstract type => php type
400
            self::TYPE_TINYINT => 'integer',
401 326
            self::TYPE_SMALLINT => 'integer',
402
            self::TYPE_INTEGER => 'integer',
403
            self::TYPE_BIGINT => 'integer',
404 1683
            self::TYPE_BOOLEAN => 'boolean',
405 1683
            self::TYPE_FLOAT => 'double',
406
            self::TYPE_DOUBLE => 'double',
407
            self::TYPE_BINARY => 'resource',
408
            self::TYPE_JSON => 'array',
409
        ];
410
411 5
        if (isset($typeMap[$column->getType()])) {
412
            if ($column->getType() === 'bigint') {
413 5
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
414 5
            }
415
416
            if ($column->getType() === 'integer') {
417
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
418
            }
419 5
420
            return $typeMap[$column->getType()];
421
        }
422
423
        return 'string';
424
    }
425
426
    /**
427
     * Returns the metadata of the given type for all tables in the given schema.
428
     *
429
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
430
     * name.
431
     * @param string $type metadata type.
432
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
433
     * returned if available.
434
     *
435
     * @throws NotSupportedException
436
     *
437
     * @return array array of metadata.
438
     */
439 1622
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
440
    {
441 1622
        $metadata = [];
442 130
443
        foreach ($this->getTableNames($schema, $refresh) as $name) {
444 130
            if ($schema !== '') {
445
                $name = $schema . '.' . $name;
446
            }
447 1622
448
            $tableMetadata = $this->getTableTypeMetadata($type, $name, $refresh);
449
450
            if ($tableMetadata !== null) {
451
                $metadata[] = $tableMetadata;
452
            }
453 50
        }
454
455 50
        return $metadata;
456
    }
457
458
    /**
459 50
     * Returns the metadata of the given type for the given table.
460
     *
461 50
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
462 50
     * table name to obtain the metadata.
463 13
     *
464
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
465
     * @param string $type metadata type.
466
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
467 50
     *
468 50
     * @return mixed metadata.
469
     */
470 50
    protected function getTableMetadata(string $name, string $type, bool $refresh = false): mixed
471
    {
472
        $rawName = $this->getRawTableName($name);
473
474
        if (!isset($this->tableMetadata[$rawName])) {
475
            $this->loadTableMetadataFromCache($rawName);
476 12
        }
477
478 12
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
479
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
480 12
            $this->saveTableMetadataToCache($rawName);
481
        }
482
483
        return $this->tableMetadata[$rawName][$type];
484
    }
485
486 375
    /**
487
     * This method returns the desired metadata type for table name (with refresh if needed)
488 375
     *
489 375
     * @param string $type
490
     * @param string $name
491
     * @param bool $refresh
492 375
     *
493
     * @return Constraint|array|TableSchema|null
494
     */
495
    protected function getTableTypeMetadata(
496
        string $type,
497
        string $name,
498 153
        bool $refresh = false
499
    ): Constraint|array|null|TableSchema {
500 153
        return match ($type) {
501
            self::SCHEMA => $this->getTableSchema($name, $refresh),
502
            self::PRIMARY_KEY => $this->getTablePrimaryKey($name, $refresh),
503
            self::UNIQUES => $this->getTableUniques($name, $refresh),
504
            self::FOREIGN_KEYS => $this->getTableForeignKeys($name, $refresh),
505
            self::INDEXES => $this->getTableIndexes($name, $refresh),
506
            self::DEFAULT_VALUES => $this->getTableDefaultValues($name, $refresh),
507
            self::CHECKS => $this->getTableChecks($name, $refresh),
508
            default => null,
509
        };
510
    }
511
512
    /**
513
     * Resolves the table name and schema name (if any).
514 20
     *
515
     * @param string $name the table name.
516 20
     *
517
     * @throws NotSupportedException if this method is not supported by the DBMS.
518
     *
519
     * @return TableSchema with resolved table, schema, etc. names.
520
     *
521
     * {@see \Yiisoft\Db\Schema\TableSchema}
522
     */
523
    protected function resolveTableName(string $name): TableSchema
524
    {
525
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
526
    }
527
528
    /**
529
     * Sets the metadata of the given type for the given table.
530 141
     *
531
     * @param string $name table name.
532 141
     * @param string $type metadata type.
533
     * @param mixed $data metadata.
534
     */
535
    protected function setTableMetadata(string $name, string $type, mixed $data): void
536
    {
537
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
538
    }
539
540
    /**
541
     * Tries to load and populate table metadata from cache.
542
     *
543
     * @param string $rawName
544
     */
545
    private function loadTableMetadataFromCache(string $rawName): void
546 156
    {
547
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
548 156
            $this->tableMetadata[$rawName] = [];
549
            return;
550
        }
551
552
        $metadata = $this->schemaCache->getOrSet(
553
            $this->getCacheKey($rawName),
554
            null,
555
            $this->schemaCache->getDuration(),
556
            new TagDependency($this->getCacheTag()),
557
        );
558
559
        if (
560
            !is_array($metadata) ||
561
            !isset($metadata['cacheVersion']) ||
562 63
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
563
        ) {
564 63
            $this->tableMetadata[$rawName] = [];
565
566
            return;
567
        }
568
569
        unset($metadata['cacheVersion']);
570
        $this->tableMetadata[$rawName] = $metadata;
571
    }
572
573
    /**
574
     * Saves table metadata to cache.
575
     *
576
     * @param string $rawName
577
     */
578 61
    private function saveTableMetadataToCache(string $rawName): void
579
    {
580 61
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
581
            return;
582
        }
583
584
        $metadata = $this->tableMetadata[$rawName];
585
586
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
587
588
        $this->schemaCache->set(
589
            $this->getCacheKey($rawName),
590
            $metadata,
591
            $this->schemaCache->getDuration(),
592
            new TagDependency($this->getCacheTag()),
593
        );
594
    }
595
}
596