Passed
Branch dev (f56f10)
by Wilmer
04:41 queued 01:34
created

Schema::findSchemaNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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
21
abstract class Schema implements SchemaInterface
22
{
23
    public const SCHEMA = 'schema';
24
    public const PRIMARY_KEY = 'primaryKey';
25
    public const INDEXES = 'indexes';
26
    public const CHECKS = 'checks';
27
    public const FOREIGN_KEYS = 'foreignKeys';
28
    public const DEFAULT_VALUES = 'defaultValues';
29
    public const UNIQUES = 'uniques';
30
31
    public const TYPE_PK = 'pk';
32
    public const TYPE_UPK = 'upk';
33
    public const TYPE_BIGPK = 'bigpk';
34
    public const TYPE_UBIGPK = 'ubigpk';
35
    public const TYPE_CHAR = 'char';
36
    public const TYPE_STRING = 'string';
37
    public const TYPE_TEXT = 'text';
38
    public const TYPE_TINYINT = 'tinyint';
39
    public const TYPE_SMALLINT = 'smallint';
40
    public const TYPE_INTEGER = 'integer';
41
    public const TYPE_BIGINT = 'bigint';
42
    public const TYPE_FLOAT = 'float';
43
    public const TYPE_DOUBLE = 'double';
44
    public const TYPE_DECIMAL = 'decimal';
45
    public const TYPE_DATETIME = 'datetime';
46
    public const TYPE_TIMESTAMP = 'timestamp';
47
    public const TYPE_TIME = 'time';
48
    public const TYPE_DATE = 'date';
49
    public const TYPE_BINARY = 'binary';
50
    public const TYPE_BOOLEAN = 'boolean';
51
    public const TYPE_MONEY = 'money';
52
    public const TYPE_JSON = 'json';
53
54
    /**
55
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
56
     */
57
    protected const SCHEMA_CACHE_VERSION = 1;
58
59
    /**
60
     * @var string|null the default schema name used for the current session.
61
     */
62
    protected ?string $defaultSchema = null;
63
64
    /**
65
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
66
     * class from the right part is used.
67
     */
68
    protected array $exceptionMap = [
69
        'SQLSTATE[23' => IntegrityException::class,
70
    ];
71
72
    private array $schemaNames = [];
73
    private array $tableNames = [];
74
    private array $tableMetadata = [];
75
76
    public function __construct(private SchemaCache $schemaCache)
77
    {
78
    }
79
80
    /**
81
     * Returns the cache key for the specified table name.
82
     *
83
     * @param string $name the table name.
84
     *
85
     * @return array the cache key.
86
     */
87
    abstract protected function getCacheKey(string $name): array;
88
89
    /**
90
     * Returns the cache tag name.
91
     *
92
     * This allows {@see refresh()} to invalidate all cached table schemas.
93
     *
94
     * @return string the cache tag name.
95
     */
96
    abstract protected function getCacheTag(): string;
97
98
    /**
99
     * Loads all check constraints for the given table.
100
     *
101
     * @param string $tableName table name.
102
     *
103
     * @return array check constraints for the given table.
104
     */
105
    abstract protected function loadTableChecks(string $tableName): array;
106
107
    /**
108
     * Loads all default value constraints for the given table.
109
     *
110
     * @param string $tableName table name.
111
     *
112
     * @return array default value constraints for the given table.
113
     */
114
    abstract protected function loadTableDefaultValues(string $tableName): array;
115
116
    /**
117
     * Loads all foreign keys for the given table.
118
     *
119
     * @param string $tableName table name.
120
     *
121
     * @return array foreign keys for the given table.
122
     */
123
    abstract protected function loadTableForeignKeys(string $tableName): array;
124
125
    /**
126
     * Loads all indexes for the given table.
127
     *
128
     * @param string $tableName table name.
129
     *
130
     * @return array indexes for the given table.
131
     */
132
    abstract protected function loadTableIndexes(string $tableName): array;
133
134
    /**
135
     * Loads a primary key for the given table.
136
     *
137
     * @param string $tableName table name.
138
     *
139
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
140
     */
141
    abstract protected function loadTablePrimaryKey(string $tableName): ?Constraint;
142
143
    /**
144
     * Loads all unique constraints for the given table.
145
     *
146
     * @param string $tableName table name.
147
     *
148
     * @return array unique constraints for the given table.
149
     */
150
    abstract protected function loadTableUniques(string $tableName): array;
151
152
    /**
153
     * Loads the metadata for the specified table.
154
     *
155
     * @param string $name table name.
156
     *
157
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
158
     */
159
    abstract protected function loadTableSchema(string $name): ?TableSchema;
160
161
    public function convertException(\Exception $e, string $rawSql): Exception
162
    {
163
        if ($e instanceof Exception) {
164
            return $e;
165
        }
166
167
        $exceptionClass = Exception::class;
168
169
        foreach ($this->exceptionMap as $error => $class) {
170
            if (str_contains($e->getMessage(), $error)) {
171
                $exceptionClass = $class;
172
            }
173
        }
174
175
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
176
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
177
178
        return new $exceptionClass($message, $errorInfo, $e);
179
    }
180
181
    public function getDefaultSchema(): ?string
182
    {
183
        return $this->defaultSchema;
184
    }
185
186
    public function getPdoType(mixed $data): int
187
    {
188
        static $typeMap = [
189
            // php type => PDO type
190
            'boolean' => PDO::PARAM_BOOL,
191
            'integer' => PDO::PARAM_INT,
192
            'string' => PDO::PARAM_STR,
193
            'resource' => PDO::PARAM_LOB,
194
            'NULL' => PDO::PARAM_NULL,
195
        ];
196
197
        $type = gettype($data);
198
199
        return $typeMap[$type] ?? PDO::PARAM_STR;
200
    }
201
202
    public function getSchemaCache(): SchemaCache
203
    {
204
        return $this->schemaCache;
205
    }
206
207
    /**
208
     * @throws NotSupportedException
209
     */
210
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
211
    {
212
        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...
213
    }
214
215
    /**
216
     * @throws NotSupportedException
217
     */
218
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
219
    {
220
        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...
221
    }
222
223
    /**
224
     * @throws NotSupportedException
225
     */
226
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
227
    {
228
        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...
229
    }
230
231
    /**
232
     * @throws NotSupportedException
233
     */
234
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
235
    {
236
        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...
237
    }
238
239
    public function getSchemaNames(bool $refresh = false): array
240
    {
241
        if (empty($this->schemaNames) || $refresh) {
242
            $this->schemaNames = $this->findSchemaNames();
243
        }
244
245
        return $this->schemaNames;
246
    }
247
248
    /**
249
     * @throws NotSupportedException
250
     */
251
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
252
    {
253
        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...
254
    }
255
256
    /**
257
     * @throws NotSupportedException
258
     */
259
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
260
    {
261
        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...
262
    }
263
264
    public function getTableChecks(string $name, bool $refresh = false): array
265
    {
266
        return $this->getTableMetadata($name, 'checks', $refresh);
267
    }
268
269
    public function getTableDefaultValues(string $name, bool $refresh = false): array
270
    {
271
        return $this->getTableMetadata($name, 'defaultValues', $refresh);
272
    }
273
274
    public function getTableForeignKeys(string $name, bool $refresh = false): array
275
    {
276
        return $this->getTableMetadata($name, 'foreignKeys', $refresh);
277
    }
278
279
    public function getTableIndexes(string $name, bool $refresh = false): array
280
    {
281
        return $this->getTableMetadata($name, 'indexes', $refresh);
282
    }
283
284
    public function getTableNames(string $schema = '', bool $refresh = false): array
285
    {
286
        if (!isset($this->tableNames[$schema]) || $refresh) {
287
            $this->tableNames[$schema] = $this->findTableNames($schema);
288
        }
289
290
        return $this->tableNames[$schema];
291
    }
292
293
    public function getTablePrimaryKey(string $name, bool $refresh = false): ?Constraint
294
    {
295
        return $this->getTableMetadata($name, 'primaryKey', $refresh);
296
    }
297
298
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
299
    {
300
        return $this->getTableMetadata($name, 'schema', $refresh);
301
    }
302
303
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
304
    {
305
        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...
306
    }
307
308
    public function getTableUniques(string $name, bool $refresh = false): array
309
    {
310
        return $this->getTableMetadata($name, 'uniques', $refresh);
311
    }
312
313
    /**
314
     * Returns a value indicating whether a SQL statement is for read purpose.
315
     *
316
     * @param string $sql the SQL statement.
317
     *
318
     * @return bool whether a SQL statement is for read purpose.
319
     */
320
    public function isReadQuery(string $sql): bool
321
    {
322
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
323
324
        return preg_match($pattern, $sql) > 0;
325
    }
326
327
    /**
328
     * Refreshes the schema.
329
     *
330
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
331
     * schema change.
332
     */
333
    public function refresh(): void
334
    {
335
        if ($this->schemaCache->isEnabled()) {
336
            $this->schemaCache->invalidate($this->getCacheTag());
337
        }
338
339
        $this->tableNames = [];
340
        $this->tableMetadata = [];
341
    }
342
343
    public function refreshTableSchema(string $name): void
344
    {
345
        $rawName = $this->getRawTableName($name);
346
347
        unset($this->tableMetadata[$rawName]);
348
349
        $this->tableNames = [];
350
351
        if ($this->schemaCache->isEnabled()) {
352
            $this->schemaCache->remove($this->getCacheKey($rawName));
353
        }
354
    }
355
356
    /**
357
     * Returns all schema names in the database, including the default one but not system schemas.
358
     *
359
     * This method should be overridden by child classes in order to support this feature because the default
360
     * implementation simply throws an exception.
361
     *
362
     * @throws NotSupportedException if this method is not supported by the DBMS.
363
     *
364
     * @return array all schema names in the database, except system schemas.
365
     */
366
    protected function findSchemaNames(): array
367
    {
368
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
369
    }
370
371
    /**
372
     * Returns all table names in the database.
373
     *
374
     * This method should be overridden by child classes in order to support this feature because the default
375
     * implementation simply throws an exception.
376
     *
377
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
378
     *
379
     * @throws NotSupportedException if this method is not supported by the DBMS.
380
     *
381
     * @return array all table names in the database. The names have NO schema name prefix.
382
     */
383
    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

383
    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...
384
    {
385
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
386
    }
387
388
    /**
389
     * Extracts the PHP type from abstract DB type.
390
     *
391
     * @param ColumnSchema $column the column schema information.
392
     *
393
     * @return string PHP type name.
394
     */
395
    protected function getColumnPhpType(ColumnSchema $column): string
396
    {
397
        static $typeMap = [
398
            // abstract type => php type
399
            self::TYPE_TINYINT => 'integer',
400
            self::TYPE_SMALLINT => 'integer',
401
            self::TYPE_INTEGER => 'integer',
402
            self::TYPE_BIGINT => 'integer',
403
            self::TYPE_BOOLEAN => 'boolean',
404
            self::TYPE_FLOAT => 'double',
405
            self::TYPE_DOUBLE => 'double',
406
            self::TYPE_BINARY => 'resource',
407
            self::TYPE_JSON => 'array',
408
        ];
409
410
        if (isset($typeMap[$column->getType()])) {
411
            if ($column->getType() === 'bigint') {
412
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
413
            }
414
415
            if ($column->getType() === 'integer') {
416
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
417
            }
418
419
            return $typeMap[$column->getType()];
420
        }
421
422
        return 'string';
423
    }
424
425
    /**
426
     * Returns the metadata of the given type for all tables in the given schema.
427
     *
428
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
429
     * name.
430
     * @param string $type metadata type.
431
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
432
     * returned if available.
433
     *
434
     * @throws NotSupportedException
435
     *
436
     * @return array array of metadata.
437
     */
438
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
439
    {
440
        $metadata = [];
441
442
        foreach ($this->getTableNames($schema, $refresh) as $name) {
443
            if ($schema !== '') {
444
                $name = $schema . '.' . $name;
445
            }
446
447
            $tableMetadata = $this->getTableTypeMetadata($type, $name, $refresh);
448
449
            if ($tableMetadata !== null) {
450
                $metadata[] = $tableMetadata;
451
            }
452
        }
453
454
        return $metadata;
455
    }
456
457
    /**
458
     * Returns the metadata of the given type for the given table.
459
     *
460
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
461
     * @param string $type metadata type.
462
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
463
     *
464
     * @return mixed metadata.
465
     */
466
    protected function getTableMetadata(string $name, string $type, bool $refresh = false): mixed
467
    {
468
        $rawName = $this->getRawTableName($name);
469
470
        if (!isset($this->tableMetadata[$rawName])) {
471
            $this->loadTableMetadataFromCache($rawName);
472
        }
473
474
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
475
            $this->tableMetadata[$rawName][$type] = $this->loadTableTypeMetadata($type, $rawName);
476
            $this->saveTableMetadataToCache($rawName);
477
        }
478
479
        return $this->tableMetadata[$rawName][$type];
480
    }
481
482
    /**
483
     * This method returns the desired metadata type for the table name.
484
     *
485
     * @param string $type
486
     * @param string $name
487
     *
488
     * @return array|Constraint|TableSchema|null
489
     */
490
    protected function loadTableTypeMetadata(string $type, string $name): Constraint|array|TableSchema|null
491
    {
492
        return match ($type) {
493
            self::SCHEMA => $this->loadTableSchema($name),
494
            self::PRIMARY_KEY => $this->loadTablePrimaryKey($name),
495
            self::UNIQUES => $this->loadTableUniques($name),
496
            self::FOREIGN_KEYS => $this->loadTableForeignKeys($name),
497
            self::INDEXES => $this->loadTableIndexes($name),
498
            self::DEFAULT_VALUES => $this->loadTableDefaultValues($name),
499
            self::CHECKS => $this->loadTableChecks($name),
500
            default => null,
501
        };
502
    }
503
504
    /**
505
     * This method returns the desired metadata type for table name (with refresh if needed)
506
     *
507
     * @param string $type
508
     * @param string $name
509
     * @param bool $refresh
510
     *
511
     * @return array|Constraint|TableSchema|null
512
     */
513
    protected function getTableTypeMetadata(
514
        string $type,
515
        string $name,
516
        bool $refresh = false
517
    ): Constraint|array|null|TableSchema {
518
        return match ($type) {
519
            self::SCHEMA => $this->getTableSchema($name, $refresh),
520
            self::PRIMARY_KEY => $this->getTablePrimaryKey($name, $refresh),
521
            self::UNIQUES => $this->getTableUniques($name, $refresh),
522
            self::FOREIGN_KEYS => $this->getTableForeignKeys($name, $refresh),
523
            self::INDEXES => $this->getTableIndexes($name, $refresh),
524
            self::DEFAULT_VALUES => $this->getTableDefaultValues($name, $refresh),
525
            self::CHECKS => $this->getTableChecks($name, $refresh),
526
            default => null,
527
        };
528
    }
529
530
    /**
531
     * Resolves the table name and schema name (if any).
532
     *
533
     * @param string $name the table name.
534
     *
535
     * @throws NotSupportedException if this method is not supported by the DBMS.
536
     *
537
     * @return TableSchema with resolved table, schema, etc. names.
538
     *
539
     * {@see \Yiisoft\Db\Schema\TableSchema}
540
     */
541
    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

541
    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...
542
    {
543
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
544
    }
545
546
    /**
547
     * Sets the metadata of the given type for the given table.
548
     *
549
     * @param string $name table name.
550
     * @param string $type metadata type.
551
     * @param mixed $data metadata.
552
     */
553
    protected function setTableMetadata(string $name, string $type, mixed $data): void
554
    {
555
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
556
    }
557
558
    /**
559
     * Tries to load and populate table metadata from cache.
560
     *
561
     * @param string $rawName
562
     */
563
    private function loadTableMetadataFromCache(string $rawName): void
564
    {
565
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
566
            $this->tableMetadata[$rawName] = [];
567
            return;
568
        }
569
570
        $metadata = $this->schemaCache->getOrSet(
571
            $this->getCacheKey($rawName),
572
            null,
573
            $this->schemaCache->getDuration(),
574
            new TagDependency($this->getCacheTag()),
575
        );
576
577
        if (
578
            !is_array($metadata) ||
579
            !isset($metadata['cacheVersion']) ||
580
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
581
        ) {
582
            $this->tableMetadata[$rawName] = [];
583
584
            return;
585
        }
586
587
        unset($metadata['cacheVersion']);
588
        $this->tableMetadata[$rawName] = $metadata;
589
    }
590
591
    /**
592
     * Saves table metadata to cache.
593
     *
594
     * @param string $rawName
595
     */
596
    private function saveTableMetadataToCache(string $rawName): void
597
    {
598
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
599
            return;
600
        }
601
602
        $metadata = $this->tableMetadata[$rawName];
603
604
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
605
606
        $this->schemaCache->set(
607
            $this->getCacheKey($rawName),
608
            $metadata,
609
            $this->schemaCache->getDuration(),
610
            new TagDependency($this->getCacheTag()),
611
        );
612
    }
613
}
614