Passed
Push — master ( 89ce2c...1feb1e )
by Wilmer
20:10 queued 17:23
created

AbstractSchema::getTableChecks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use PDO;
8
use Throwable;
9
use Yiisoft\Cache\Dependency\TagDependency;
10
use Yiisoft\Db\Cache\SchemaCache;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
15
use function array_change_key_case;
16
use function array_map;
17
use function gettype;
18
use function is_array;
19
use function preg_match;
20
21
/**
22
 * The AbstractSchema class provides a set of methods for working with database schemas such as creating, modifying,
23
 * and inspecting tables, columns, and other database objects.
24
 *
25
 * It is a very powerful and flexible tool that allows you to perform a wide range of database operations in a
26
 * database-agnostic way.
27
 */
28
abstract class AbstractSchema implements SchemaInterface
29
{
30
    /**
31
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
32
     */
33
    protected const SCHEMA_CACHE_VERSION = 1;
34
    public const CACHE_VERSION = 'cacheVersion';
35
36
    /**
37
     * @var string|null $defaultSchema The default schema name used for the current session.
38
     */
39
    protected string|null $defaultSchema = null;
40
    private array $schemaNames = [];
41
    private array $tableNames = [];
42
    protected array $viewNames = [];
43
    private array $tableMetadata = [];
44
45
    public function __construct(protected ConnectionInterface $db, private SchemaCache $schemaCache)
46
    {
47
    }
48
49
    /**
50
     * @param string $name the table name.
51
     *
52
     * @return array The cache key for the specified table name.
53
     */
54
    abstract protected function getCacheKey(string $name): array;
55
56
    /**
57
     * @return string The cache tag name.
58
     *
59
     * This allows {@see refresh()} to invalidate all cached table schemas.
60
     */
61
    abstract protected function getCacheTag(): string;
62
63
    /**
64
     * Loads all check constraints for the given table.
65
     *
66
     * @param string $tableName The table name.
67
     *
68
     * @return array The check constraints for the given table.
69
     */
70
    abstract protected function loadTableChecks(string $tableName): array;
71
72
    /**
73
     * Loads all default value constraints for the given table.
74
     *
75
     * @param string $tableName The table name.
76
     *
77
     * @return array The default value constraints for the given table.
78
     */
79
    abstract protected function loadTableDefaultValues(string $tableName): array;
80
81
    /**
82
     * Loads all foreign keys for the given table.
83
     *
84
     * @param string $tableName The table name.
85
     *
86
     * @return array The foreign keys for the given table.
87
     */
88
    abstract protected function loadTableForeignKeys(string $tableName): array;
89
90
    /**
91
     * Loads all indexes for the given table.
92
     *
93
     * @param string $tableName The table name.
94
     *
95
     * @return array The indexes for the given table.
96
     */
97
    abstract protected function loadTableIndexes(string $tableName): array;
98
99
    /**
100
     * Loads a primary key for the given table.
101
     *
102
     * @param string $tableName The table name.
103
     *
104
     * @return Constraint|null The primary key for the given table. `null` if the table has no primary key.
105
     */
106
    abstract protected function loadTablePrimaryKey(string $tableName): Constraint|null;
107
108
    /**
109
     * Loads all unique constraints for the given table.
110
     *
111
     * @param string $tableName The table name.
112
     *
113
     * @return array The unique constraints for the given table.
114
     */
115
    abstract protected function loadTableUniques(string $tableName): array;
116
117
    /**
118
     * Loads the metadata for the specified table.
119
     *
120
     * @param string $name The table name.
121
     *
122
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table does not exist.
123
     */
124
    abstract protected function loadTableSchema(string $name): TableSchemaInterface|null;
125
126
    public function getDefaultSchema(): string|null
127
    {
128
        return $this->defaultSchema;
129
    }
130
131
    public function getPdoType(mixed $data): int
132
    {
133
        /** @psalm-var array<string, int> $typeMap */
134
        $typeMap = [
135
            // php type => PDO type
136
            SchemaInterface::PHP_TYPE_BOOLEAN => PDO::PARAM_BOOL,
137
            SchemaInterface::PHP_TYPE_INTEGER => PDO::PARAM_INT,
138
            SchemaInterface::PHP_TYPE_STRING => PDO::PARAM_STR,
139
            SchemaInterface::PHP_TYPE_RESOURCE => PDO::PARAM_LOB,
140
            SchemaInterface::PHP_TYPE_NULL => PDO::PARAM_NULL,
141
        ];
142
143
        $type = gettype($data);
144
145
        return $typeMap[$type] ?? PDO::PARAM_STR;
146
    }
147
148
    public function getRawTableName(string $name): string
149
    {
150
        if (str_contains($name, '{{')) {
151
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
152
153
            return str_replace('%', $this->db->getTablePrefix(), $name);
154
        }
155
156
        return $name;
157
    }
158
159
    /**
160
     * @throws NotSupportedException
161
     */
162
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
163
    {
164
        return $this->getSchemaMetadata($schema, SchemaInterface::CHECKS, $refresh);
165
    }
166
167
    /**
168
     * @throws NotSupportedException
169
     */
170
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
171
    {
172
        return $this->getSchemaMetadata($schema, SchemaInterface::DEFAULT_VALUES, $refresh);
173
    }
174
175
    /**
176
     * @throws NotSupportedException
177
     */
178
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
179
    {
180
        return $this->getSchemaMetadata($schema, SchemaInterface::FOREIGN_KEYS, $refresh);
181
    }
182
183
    /**
184
     * @throws NotSupportedException
185
     */
186
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
187
    {
188
        return $this->getSchemaMetadata($schema, SchemaInterface::INDEXES, $refresh);
189
    }
190
191
    public function getSchemaNames(bool $refresh = false): array
192
    {
193
        if (empty($this->schemaNames) || $refresh) {
194
            $this->schemaNames = $this->findSchemaNames();
195
        }
196
197
        return $this->schemaNames;
198
    }
199
200
    /**
201
     * @throws NotSupportedException
202
     */
203
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
204
    {
205
        return $this->getSchemaMetadata($schema, SchemaInterface::PRIMARY_KEY, $refresh);
206
    }
207
208
    /**
209
     * @throws NotSupportedException
210
     */
211
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
212
    {
213
        return $this->getSchemaMetadata($schema, SchemaInterface::UNIQUES, $refresh);
214
    }
215
216
    public function getTableChecks(string $name, bool $refresh = false): array
217
    {
218
        /** @psalm-var mixed $tableChecks */
219
        $tableChecks = $this->getTableMetadata($name, SchemaInterface::CHECKS, $refresh);
220
        return is_array($tableChecks) ? $tableChecks : [];
221
    }
222
223
    public function getTableDefaultValues(string $name, bool $refresh = false): array
224
    {
225
        /** @psalm-var mixed $tableDefaultValues */
226
        $tableDefaultValues = $this->getTableMetadata($name, SchemaInterface::DEFAULT_VALUES, $refresh);
227
        return is_array($tableDefaultValues) ? $tableDefaultValues : [];
228
    }
229
230
    public function getTableForeignKeys(string $name, bool $refresh = false): array
231
    {
232
        /** @psalm-var mixed $tableForeignKeys */
233
        $tableForeignKeys = $this->getTableMetadata($name, SchemaInterface::FOREIGN_KEYS, $refresh);
234
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
235
    }
236
237
    public function getTableIndexes(string $name, bool $refresh = false): array
238
    {
239
        /** @psalm-var mixed $tableIndexes */
240
        $tableIndexes = $this->getTableMetadata($name, SchemaInterface::INDEXES, $refresh);
241
        return is_array($tableIndexes) ? $tableIndexes : [];
242
    }
243
244
    public function getTableNames(string $schema = '', bool $refresh = false): array
245
    {
246
        if (!isset($this->tableNames[$schema]) || $refresh) {
247
            /** @psalm-var string[] */
248
            $this->tableNames[$schema] = $this->findTableNames($schema);
249
        }
250
251
        return is_array($this->tableNames[$schema]) ? $this->tableNames[$schema] : [];
252
    }
253
254
    public function getTablePrimaryKey(string $name, bool $refresh = false): Constraint|null
255
    {
256
        /** @psalm-var mixed $tablePrimaryKey */
257
        $tablePrimaryKey = $this->getTableMetadata($name, SchemaInterface::PRIMARY_KEY, $refresh);
258
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
259
    }
260
261
    public function getTableSchema(string $name, bool $refresh = false): TableSchemaInterface|null
262
    {
263
        /** @psalm-var mixed $tableSchema */
264
        $tableSchema = $this->getTableMetadata($name, SchemaInterface::SCHEMA, $refresh);
265
        return $tableSchema instanceof TableSchemaInterface ? $tableSchema : null;
266
    }
267
268
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
269
    {
270
        /** @psalm-var mixed $tableSchemas */
271
        $tableSchemas = $this->getSchemaMetadata($schema, SchemaInterface::SCHEMA, $refresh);
272
273
        return is_array($tableSchemas) ? $tableSchemas : [];
0 ignored issues
show
introduced by
The condition is_array($tableSchemas) is always true.
Loading history...
274
    }
275
276
    public function getTableUniques(string $name, bool $refresh = false): array
277
    {
278
        /** @psalm-var mixed $tableUniques */
279
        $tableUniques = $this->getTableMetadata($name, SchemaInterface::UNIQUES, $refresh);
280
        return is_array($tableUniques) ? $tableUniques : [];
281
    }
282
283
    public function isReadQuery(string $sql): bool
284
    {
285
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
286
287
        return preg_match($pattern, $sql) > 0;
288
    }
289
290
    public function refresh(): void
291
    {
292
        if ($this->schemaCache->isEnabled()) {
293
            $this->schemaCache->invalidate($this->getCacheTag());
294
        }
295
296
        $this->tableNames = [];
297
        $this->tableMetadata = [];
298
    }
299
300
    public function refreshTableSchema(string $name): void
301
    {
302
        $rawName = $this->getRawTableName($name);
303
304
        unset($this->tableMetadata[$rawName]);
305
306
        $this->tableNames = [];
307
308
        if ($this->schemaCache->isEnabled()) {
309
            $this->schemaCache->remove($this->getCacheKey($rawName));
310
        }
311
    }
312
313
    public function schemaCacheEnable(bool $value): void
314
    {
315
        $this->schemaCache->setEnable($value);
316
    }
317
318
    /**
319
     * Returns all schema names in the database, including the default one but not system schemas.
320
     *
321
     * This method should be overridden by child classes in order to support this feature because the default
322
     * implementation simply throws an exception.
323
     *
324
     * @throws NotSupportedException If this method is not supported by the DBMS.
325
     *
326
     * @return array All schema names in the database, except system schemas.
327
     */
328
    protected function findSchemaNames(): array
329
    {
330
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
331
    }
332
333
    /**
334
     * Returns all table names in the database.
335
     *
336
     * This method should be overridden by child classes in order to support this feature because the default
337
     * implementation simply throws an exception.
338
     *
339
     * @param string $schema The schema of the tables. Defaults to empty string, meaning the current or default schema.
340
     *
341
     * @throws NotSupportedException If this method is not supported by the DBMS.
342
     *
343
     * @return array All table names in the database. The names have NO schema name prefix.
344
     */
345
    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

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

519
    protected function resolveTableName(/** @scrutinizer ignore-unused */ string $name): TableSchemaInterface

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...
520
    {
521
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
522
    }
523
524
    /**
525
     * Sets the metadata of the given type for the given table.
526
     *
527
     * @param string $name The table name.
528
     * @param string $type The metadata type.
529
     * @param mixed $data The metadata to be set.
530
     *
531
     * @psalm-suppress MixedArrayAssignment
532
     */
533
    protected function setTableMetadata(string $name, string $type, mixed $data): void
534
    {
535
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
536
    }
537
538
    /**
539
     * Tries to load and populate table metadata from cache.
540
     */
541
    private function loadTableMetadataFromCache(string $rawName): void
542
    {
543
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
544
            $this->tableMetadata[$rawName] = [];
545
            return;
546
        }
547
548
        $metadata = $this->schemaCache->getOrSet(
549
            $this->getCacheKey($rawName),
550
            null,
551
            $this->schemaCache->getDuration(),
552
            new TagDependency($this->getCacheTag()),
553
        );
554
555
        if (
556
            !is_array($metadata) ||
557
            !isset($metadata[self::CACHE_VERSION]) ||
558
            $metadata[self::CACHE_VERSION] !== static::SCHEMA_CACHE_VERSION
559
        ) {
560
            $this->tableMetadata[$rawName] = [];
561
            return;
562
        }
563
564
        unset($metadata[self::CACHE_VERSION]);
565
        $this->tableMetadata[$rawName] = $metadata;
566
    }
567
568
    /**
569
     * Saves table metadata to cache.
570
     */
571
    private function saveTableMetadataToCache(string $rawName): void
572
    {
573
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
574
            return;
575
        }
576
577
        /** @psalm-var array<string, array<TableSchemaInterface|int>> $metadata */
578
        $metadata = $this->tableMetadata[$rawName];
579
        /** @var int */
580
        $metadata[self::CACHE_VERSION] = static::SCHEMA_CACHE_VERSION;
581
582
        $this->schemaCache->set(
583
            $this->getCacheKey($rawName),
584
            $metadata,
585
            $this->schemaCache->getDuration(),
586
            new TagDependency($this->getCacheTag()),
587
        );
588
    }
589
590
    /**
591
     * Find the view names for the database.
592
     *
593
     * @param string $schema the schema of the views. Defaults to empty string, meaning the current or default schema.
594
     *
595
     * @return array The names of all views in the database.
596
     */
597
    protected function findViewNames(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

597
    protected function findViewNames(/** @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...
598
    {
599
        return [];
600
    }
601
602
    /**
603
     * @throws Throwable
604
     *
605
     * @return array The view names for the database.
606
     */
607
    public function getViewNames(string $schema = '', bool $refresh = false): array
608
    {
609
        if (!isset($this->viewNames[$schema]) || $refresh) {
610
            $this->viewNames[$schema] = $this->findViewNames($schema);
611
        }
612
613
        return (array) $this->viewNames[$schema];
614
    }
615
}
616