Passed
Branch psalm-3 (d5d890)
by Wilmer
02:56
created

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

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

509
    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...
510
    {
511
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
512
    }
513
514
    /**
515
     * Sets the metadata of the given type for the given table.
516
     *
517
     * @param string $name table name.
518
     * @param string $type metadata type.
519
     * @param mixed $data metadata.
520
     */
521
    protected function setTableMetadata(string $name, string $type, mixed $data): void
522
    {
523
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
524
    }
525
526
    /**
527
     * Tries to load and populate table metadata from cache.
528
     *
529
     * @param string $rawName
530
     */
531
    private function loadTableMetadataFromCache(string $rawName): void
532
    {
533
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
534
            $this->tableMetadata[$rawName] = [];
535
            return;
536
        }
537
538
        $metadata = $this->schemaCache->getOrSet(
539
            $this->getCacheKey($rawName),
540
            null,
541
            $this->schemaCache->getDuration(),
542
            new TagDependency($this->getCacheTag()),
543
        );
544
545
        if (
546
            !is_array($metadata) ||
547
            !isset($metadata['cacheVersion']) ||
548
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
549
        ) {
550
            $this->tableMetadata[$rawName] = [];
551
552
            return;
553
        }
554
555
        unset($metadata['cacheVersion']);
556
        $this->tableMetadata[$rawName] = $metadata;
557
    }
558
559
    /**
560
     * Saves table metadata to cache.
561
     *
562
     * @param string $rawName
563
     */
564
    private function saveTableMetadataToCache(string $rawName): void
565
    {
566
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
567
            return;
568
        }
569
570
        $metadata = $this->tableMetadata[$rawName];
571
572
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
573
574
        $this->schemaCache->set(
575
            $this->getCacheKey($rawName),
576
            $metadata,
577
            $this->schemaCache->getDuration(),
578
            new TagDependency($this->getCacheTag()),
579
        );
580
    }
581
}
582