Passed
Push — master ( c5030b...c13ec3 )
by Wilmer
09:57 queued 27s
created

Schema::quoteColumnName()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 5
nop 1
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use JsonException;
8
use PDO;
9
use PDOException;
10
use Psr\SimpleCache\CacheInterface;
11
use Throwable;
12
use Yiisoft\Cache\Dependency\TagDependency;
13
use Yiisoft\Db\Cache\SchemaCache;
14
use Yiisoft\Db\Connection\Connection;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\IntegrityException;
17
use Yiisoft\Db\Exception\InvalidCallException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Query\QueryBuilder;
21
22
use function addcslashes;
23
use function array_change_key_case;
24
use function array_key_exists;
25
use function array_map;
26
use function explode;
27
use function gettype;
28
use function implode;
29
use function is_array;
30
use function is_string;
31
use function md5;
32
use function preg_match;
33
use function preg_replace;
34
use function serialize;
35
use function str_replace;
36
use function strlen;
37
use function strpos;
38
use function substr;
39
use function ucfirst;
40
use function version_compare;
41
42
/**
43
 * Schema is the base class for concrete DBMS-specific schema classes.
44
 *
45
 * Schema represents the database schema information that is DBMS specific.
46
 *
47
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
48
 * object. This property is read-only.
49
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
50
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is read-only.
51
 * @property string $serverVersion Server version as a string. This property is read-only.
52
 * @property string[] $tableNames All table names in the database. This property is read-only.
53
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an instance
54
 * of {@see TableSchema} or its child class. This property is read-only.
55
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
56
 * one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
57
 * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS specific
58
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
59
 */
60
abstract class Schema
61
{
62
    public const TYPE_PK = 'pk';
63
    public const TYPE_UPK = 'upk';
64
    public const TYPE_BIGPK = 'bigpk';
65
    public const TYPE_UBIGPK = 'ubigpk';
66
    public const TYPE_CHAR = 'char';
67
    public const TYPE_STRING = 'string';
68
    public const TYPE_TEXT = 'text';
69
    public const TYPE_TINYINT = 'tinyint';
70
    public const TYPE_SMALLINT = 'smallint';
71
    public const TYPE_INTEGER = 'integer';
72
    public const TYPE_BIGINT = 'bigint';
73
    public const TYPE_FLOAT = 'float';
74
    public const TYPE_DOUBLE = 'double';
75
    public const TYPE_DECIMAL = 'decimal';
76
    public const TYPE_DATETIME = 'datetime';
77
    public const TYPE_TIMESTAMP = 'timestamp';
78
    public const TYPE_TIME = 'time';
79
    public const TYPE_DATE = 'date';
80
    public const TYPE_BINARY = 'binary';
81
    public const TYPE_BOOLEAN = 'boolean';
82
    public const TYPE_MONEY = 'money';
83
    public const TYPE_JSON = 'json';
84
85
    /**
86
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
87
     */
88
    protected const SCHEMA_CACHE_VERSION = 1;
89
90
    /**
91
     * @var string|null the default schema name used for the current session.
92
     */
93
    protected ?string $defaultSchema = null;
94
95
    /**
96
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
97
     * class from the right part is used.
98
     */
99
    protected array $exceptionMap = [
100
        'SQLSTATE[23' => IntegrityException::class,
101
    ];
102
103
    /**
104
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
105
     * case starting and ending characters are different.
106
     */
107
    protected $tableQuoteCharacter = "'";
108
109
    /**
110
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
111
     * and ending characters are different.
112
     */
113
    protected $columnQuoteCharacter = '"';
114
    private array $schemaNames = [];
115
    private array $tableNames = [];
116
    private array $tableMetadata = [];
117
    private ?QueryBuilder $builder = null;
118
    private ?string $serverVersion = null;
119
    private Connection $db;
120
    private SchemaCache $schemaCache;
121
122 2481
    public function __construct(Connection $db, SchemaCache $schemaCache)
123
    {
124 2481
        $this->db = $db;
125 2481
        $this->schemaCache = $schemaCache;
126 2481
    }
127
128
    abstract public function createQueryBuilder(): QueryBuilder;
129
130
    /**
131
     * Resolves the table name and schema name (if any).
132
     *
133
     * @param string $name the table name.
134
     *
135
     * @throws NotSupportedException if this method is not supported by the DBMS.
136
     *
137
     * @return TableSchema with resolved table, schema, etc. names.
138
     *
139
     * {@see \Yiisoft\Db\Schema\TableSchema}
140
     */
141
    protected function resolveTableName(string $name): TableSchema
142
    {
143
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
144
    }
145
146
    /**
147
     * Returns all schema names in the database, including the default one but not system schemas.
148
     *
149
     * This method should be overridden by child classes in order to support this feature because the default
150
     * implementation simply throws an exception.
151
     *
152
     * @throws NotSupportedException if this method is not supported by the DBMS.
153
     *
154
     * @return array all schema names in the database, except system schemas.
155
     */
156
    protected function findSchemaNames(): array
157
    {
158
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
159
    }
160
161
    /**
162
     * Returns all table names in the database.
163
     *
164
     * This method should be overridden by child classes in order to support this feature because the default
165
     * implementation simply throws an exception.
166
     *
167
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
168
     *
169
     * @throws NotSupportedException if this method is not supported by the DBMS.
170
     *
171
     * @return array all table names in the database. The names have NO schema name prefix.
172
     */
173
    protected function findTableNames(string $schema = ''): array
174
    {
175
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
176
    }
177
178
    /**
179
     * Loads the metadata for the specified table.
180
     *
181
     * @param string $name table name.
182
     *
183
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
184
     */
185
    abstract protected function loadTableSchema(string $name): ?TableSchema;
186
187
    /**
188
     * Obtains the metadata for the named table.
189
     *
190
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
191
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
192
     *
193
     * @throws JsonException
194
     *
195
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
196
     */
197 1295
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
198
    {
199 1295
        return $this->getTableMetadata($name, 'schema', $refresh);
200
    }
201
202
    /**
203
     * Returns the metadata for all tables in the database.
204
     *
205
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
206
     * name.
207
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, cached data may be
208
     * returned if available.
209
     *
210
     * @throws NotSupportedException
211
     *
212
     * @return TableSchema[] the metadata for all tables in the database. Each array element is an instance of
213
     * {@see TableSchema} or its child class.
214
     */
215 13
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
216
    {
217 13
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
218
    }
219
220
    /**
221
     * Returns all schema names in the database, except system schemas.
222
     *
223
     * @param bool $refresh whether to fetch the latest available schema names. If this is false, schema names fetched
224
     * previously (if available) will be returned.
225
     *
226
     * @throws NotSupportedException
227
     *
228
     * @return string[] all schema names in the database, except system schemas.
229
     */
230 4
    public function getSchemaNames(bool $refresh = false): array
231
    {
232 4
        if (empty($this->schemaNames) || $refresh) {
233 4
            $this->schemaNames = $this->findSchemaNames();
234
        }
235
236 4
        return $this->schemaNames;
237
    }
238
239
    /**
240
     * Returns all table names in the database.
241
     *
242
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
243
     * name.
244
     * If not empty, the returned table names will be prefixed with the schema name.
245
     * @param bool $refresh whether to fetch the latest available table names. If this is false, table names fetched
246
     * previously (if available) will be returned.
247
     *
248
     * @throws NotSupportedException
249
     *
250
     * @return string[] all table names in the database.
251
     */
252 23
    public function getTableNames(string $schema = '', bool $refresh = false): array
253
    {
254 23
        if (!isset($this->tableNames[$schema]) || $refresh) {
255 23
            $this->tableNames[$schema] = $this->findTableNames($schema);
256
        }
257
258 23
        return $this->tableNames[$schema];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tableNames[$schema] returns the type string which is incompatible with the type-hinted return array.
Loading history...
259
    }
260
261
    /**
262
     * @return QueryBuilder the query builder for this connection.
263
     */
264 941
    public function getQueryBuilder(): QueryBuilder
265
    {
266 941
        if ($this->builder === null) {
267 941
            $this->builder = $this->createQueryBuilder();
268
        }
269
270 941
        return $this->builder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->builder could return the type null which is incompatible with the type-hinted return Yiisoft\Db\Query\QueryBuilder. Consider adding an additional type-check to rule them out.
Loading history...
271
    }
272
273
    /**
274
     * Determines the PDO type for the given PHP data value.
275
     *
276
     * @param mixed $data the data whose PDO type is to be determined
277
     *
278
     * @return int the PDO type
279
     *
280
     * {@see http://www.php.net/manual/en/pdo.constants.php}
281
     */
282 1336
    public function getPdoType($data): int
283
    {
284 1336
        static $typeMap = [
285
            // php type => PDO type
286
            'boolean' => PDO::PARAM_BOOL,
287
            'integer' => PDO::PARAM_INT,
288
            'string' => PDO::PARAM_STR,
289
            'resource' => PDO::PARAM_LOB,
290
            'NULL' => PDO::PARAM_NULL,
291
        ];
292
293 1336
        $type = gettype($data);
294
295 1336
        return $typeMap[$type] ?? PDO::PARAM_STR;
296
    }
297
298
    /**
299
     * Refreshes the schema.
300
     *
301
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
302
     * schema change.
303
     */
304 96
    public function refresh(): void
305
    {
306
        /* @var $cache CacheInterface */
307 96
        $cache = $this->schemaCache->getCache();
308
309 96
        if ($this->schemaCache->isEnabled()) {
310 96
            TagDependency::invalidate($cache, $this->getCacheTag());
311
        }
312
313 96
        $this->tableNames = [];
314 96
        $this->tableMetadata = [];
315 96
    }
316
317
    /**
318
     * Refreshes the particular table schema.
319
     *
320
     * This method cleans up cached table schema so that it can be re-created later to reflect the database schema
321
     * change.
322
     *
323
     * @param string $name table name.
324
     *
325
     * @throws JsonException
326
     */
327 99
    public function refreshTableSchema(string $name): void
328
    {
329 99
        $rawName = $this->getRawTableName($name);
330
331 99
        unset($this->tableMetadata[$rawName]);
332
333 99
        $this->tableNames = [];
334
335 99
        if ($this->schemaCache->isEnabled()) {
336 99
            $this->schemaCache->getCache()->delete($this->getCacheKey($rawName));
337
        }
338 99
    }
339
340
    /**
341
     * Returns the ID of the last inserted row or sequence value.
342
     *
343
     * @param string $sequenceName name of the sequence object (required by some DBMS)
344
     *
345
     * @throws InvalidCallException if the DB connection is not active
346
     *
347
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
348
     *
349
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
350
     */
351 36
    public function getLastInsertID(string $sequenceName = ''): string
352
    {
353 36
        if ($this->db->isActive()) {
354 36
            return $this->db->getPDO()->lastInsertId(
355 36
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
356
            );
357
        }
358
359
        throw new InvalidCallException('DB Connection is not active.');
360
    }
361
362
    /**
363
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
364
     */
365 10
    public function supportsSavepoint(): bool
366
    {
367 10
        return $this->db->isSavepointEnabled();
368
    }
369
370
    /**
371
     * Creates a new savepoint.
372
     *
373
     * @param string $name the savepoint name
374
     *
375
     * @throws Exception|InvalidConfigException|Throwable
376
     */
377 4
    public function createSavepoint(string $name): void
378
    {
379 4
        $this->db->createCommand("SAVEPOINT $name")->execute();
380 4
    }
381
382
    /**
383
     * Releases an existing savepoint.
384
     *
385
     * @param string $name the savepoint name
386
     *
387
     * @throws Exception|InvalidConfigException|Throwable
388
     */
389
    public function releaseSavepoint(string $name): void
390
    {
391
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
392
    }
393
394
    /**
395
     * Rolls back to a previously created savepoint.
396
     *
397
     * @param string $name the savepoint name
398
     *
399
     * @throws Exception|InvalidConfigException|Throwable
400
     */
401 4
    public function rollBackSavepoint(string $name): void
402
    {
403 4
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
404 4
    }
405
406
    /**
407
     * Sets the isolation level of the current transaction.
408
     *
409
     * @param string $level The transaction isolation level to use for this transaction.
410
     *
411
     * This can be one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
412
     * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS
413
     * specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
414
     *
415
     * @throws Exception|InvalidConfigException|Throwable
416
     *
417
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
418
     */
419 8
    public function setTransactionIsolationLevel(string $level): void
420
    {
421 8
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
422 8
    }
423
424
    /**
425
     * Executes the INSERT command, returning primary key values.
426
     *
427
     * @param string $table the table that new rows will be inserted into.
428
     * @param array $columns the column data (name => value) to be inserted into the table.
429
     *
430
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
431
     *
432
     * @return array|false primary key values or false if the command fails.
433
     */
434 28
    public function insert(string $table, array $columns)
435
    {
436 28
        $command = $this->db->createCommand()->insert($table, $columns);
437
438 28
        if (!$command->execute()) {
439
            return false;
440
        }
441
442 28
        $tableSchema = $this->getTableSchema($table);
443 28
        $result = [];
444
445 28
        foreach ($tableSchema->getPrimaryKey() as $name) {
446 26
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
447 24
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

447
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
448 24
                break;
449
            }
450
451 4
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
452
        }
453
454 28
        return $result;
455
    }
456
457
    /**
458
     * Quotes a string value for use in a query.
459
     *
460
     * Note that if the parameter is not a string, it will be returned without change.
461
     *
462
     * @param int|string $str string to be quoted.
463
     *
464
     * @throws Exception|InvalidConfigException
465
     *
466
     * @return int|string the properly quoted string.
467
     *
468
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
469
     */
470 1411
    public function quoteValue($str)
471
    {
472 1411
        if (!is_string($str)) {
473 6
            return $str;
474
        }
475
476 1411
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
477 1411
            return $value;
478
        }
479
480
        /** the driver doesn't support quote (e.g. oci) */
481
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
482
    }
483
484
    /**
485
     * Quotes a table name for use in a query.
486
     *
487
     * If the table name contains schema prefix, the prefix will also be properly quoted. If the table name is already
488
     * quoted or contains '(' or '{{', then this method will do nothing.
489
     *
490
     * @param string $name table name.
491
     *
492
     * @return string the properly quoted table name.
493
     *
494
     * {@see quoteSimpleTableName()}
495
     */
496 1544
    public function quoteTableName(string $name): string
497
    {
498 1544
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
499 4
            return $name;
500
        }
501
502 1544
        if (strpos($name, '{{') !== false) {
503 146
            return $name;
504
        }
505
506 1520
        if (strpos($name, '.') === false) {
507 1438
            return $this->quoteSimpleTableName($name);
508
        }
509
510 280
        $parts = $this->getTableNameParts($name);
511
512 280
        foreach ($parts as $i => $part) {
513 280
            $parts[$i] = $this->quoteSimpleTableName($part);
514
        }
515
516 280
        return implode('.', $parts);
517
    }
518
519
    /**
520
     * Splits full table name into parts
521
     *
522
     * @param string $name
523
     *
524
     * @return array
525
     */
526 12
    protected function getTableNameParts(string $name): array
527
    {
528 12
        return explode('.', $name);
529
    }
530
531
    /**
532
     * Quotes a column name for use in a query.
533
     *
534
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
535
     * or contains '(', '[[' or '{{', then this method will do nothing.
536
     *
537
     * @param string $name column name.
538
     *
539
     * @return string the properly quoted column name.
540
     *
541
     * {@see quoteSimpleColumnName()}
542
     */
543 1674
    public function quoteColumnName(string $name): string
544
    {
545 1674
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
546 149
            return $name;
547
        }
548
549 1659
        if (($pos = strrpos($name, '.')) !== false) {
550 232
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
551 232
            $name = substr($name, $pos + 1);
552
        } else {
553 1649
            $prefix = '';
554
        }
555
556 1659
        if (strpos($name, '{{') !== false) {
557 4
            return $name;
558
        }
559
560 1659
        return $prefix . $this->quoteSimpleColumnName($name);
561
    }
562
563
    /**
564
     * Quotes a simple table name for use in a query.
565
     *
566
     * A simple table name should contain the table name only without any schema prefix. If the table name is already
567
     * quoted, this method will do nothing.
568
     *
569
     * @param string $name table name.
570
     *
571
     * @return string the properly quoted table name.
572
     */
573 1316
    public function quoteSimpleTableName(string $name): string
574
    {
575 1316
        if (is_string($this->tableQuoteCharacter)) {
576 965
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
577
        } else {
578 351
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
579
        }
580
581 1316
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
582
    }
583
584
    /**
585
     * Quotes a simple column name for use in a query.
586
     *
587
     * A simple column name should contain the column name only without any prefix. If the column name is already quoted
588
     * or is the asterisk character '*', this method will do nothing.
589
     *
590
     * @param string $name column name.
591
     *
592
     * @return string the properly quoted column name.
593
     */
594 1659
    public function quoteSimpleColumnName(string $name): string
595
    {
596 1659
        if (is_string($this->columnQuoteCharacter)) {
597 1338
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
598
        } else {
599 321
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
600
        }
601
602 1659
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
603 1659
            . $endingCharacter;
604
    }
605
606
    /**
607
     * Unquotes a simple table name.
608
     *
609
     * A simple table name should contain the table name only without any schema prefix. If the table name is not
610
     * quoted, this method will do nothing.
611
     *
612
     * @param string $name table name.
613
     *
614
     * @return string unquoted table name.
615
     */
616 5
    public function unquoteSimpleTableName(string $name): string
617
    {
618 5
        if (is_string($this->tableQuoteCharacter)) {
619 5
            $startingCharacter = $this->tableQuoteCharacter;
620
        } else {
621
            $startingCharacter = $this->tableQuoteCharacter[0];
622
        }
623
624 5
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
625
    }
626
627
    /**
628
     * Unquotes a simple column name.
629
     *
630
     * A simple column name should contain the column name only without any prefix. If the column name is not quoted or
631
     * is the asterisk character '*', this method will do nothing.
632
     *
633
     * @param string $name column name.
634
     *
635
     * @return string unquoted column name.
636
     */
637
    public function unquoteSimpleColumnName(string $name): string
638
    {
639
        if (is_string($this->columnQuoteCharacter)) {
640
            $startingCharacter = $this->columnQuoteCharacter;
641
        } else {
642
            $startingCharacter = $this->columnQuoteCharacter[0];
643
        }
644
645
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
646
    }
647
648
    /**
649
     * Returns the actual name of a given table name.
650
     *
651
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
652
     * {@see ConnectionInterface::tablePrefix}.
653
     *
654
     * @param string $name the table name to be converted.
655
     *
656
     * @return string the real name of the given table name.
657
     */
658 1595
    public function getRawTableName(string $name): string
659
    {
660 1595
        if (strpos($name, '{{') !== false) {
661 130
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
662
663 130
            return str_replace('%', $this->db->getTablePrefix(), $name);
664
        }
665
666 1595
        return $name;
667
    }
668
669
    /**
670
     * Extracts the PHP type from abstract DB type.
671
     *
672
     * @param ColumnSchema $column the column schema information.
673
     *
674
     * @return string PHP type name.
675
     */
676 1254
    protected function getColumnPhpType(ColumnSchema $column): string
677
    {
678 1254
        static $typeMap = [
679
            // abstract type => php type
680
            self::TYPE_TINYINT => 'integer',
681
            self::TYPE_SMALLINT => 'integer',
682
            self::TYPE_INTEGER => 'integer',
683
            self::TYPE_BIGINT => 'integer',
684
            self::TYPE_BOOLEAN => 'boolean',
685
            self::TYPE_FLOAT => 'double',
686
            self::TYPE_DOUBLE => 'double',
687
            self::TYPE_BINARY => 'resource',
688
            self::TYPE_JSON => 'array',
689
        ];
690
691 1254
        if (isset($typeMap[$column->getType()])) {
692 1245
            if ($column->getType() === 'bigint') {
693 42
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
694
            }
695
696 1245
            if ($column->getType() === 'integer') {
697 1245
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
698
            }
699
700 347
            return $typeMap[$column->getType()];
701
        }
702
703 1201
        return 'string';
704
    }
705
706
    /**
707
     * Converts a DB exception to a more concrete one if possible.
708
     *
709
     * @param \Exception $e
710
     * @param string $rawSql SQL that produced exception.
711
     *
712
     * @return Exception
713
     */
714 44
    public function convertException(\Exception $e, string $rawSql): Exception
715
    {
716 44
        if ($e instanceof Exception) {
717
            return $e;
718
        }
719
720 44
        $exceptionClass = Exception::class;
721
722 44
        foreach ($this->exceptionMap as $error => $class) {
723 44
            if (strpos($e->getMessage(), $error) !== false) {
724 11
                $exceptionClass = $class;
725
            }
726
        }
727
728 44
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
729 44
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
730
731 44
        return new $exceptionClass($message, $errorInfo, $e);
732
    }
733
734
    /**
735
     * Returns a value indicating whether a SQL statement is for read purpose.
736
     *
737
     * @param string $sql the SQL statement.
738
     *
739
     * @return bool whether a SQL statement is for read purpose.
740
     */
741 12
    public function isReadQuery(string $sql): bool
742
    {
743 12
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
744
745 12
        return preg_match($pattern, $sql) > 0;
746
    }
747
748
    /**
749
     * Returns a server version as a string comparable by {@see version_compare()}.
750
     *
751
     * @throws Exception|InvalidConfigException
752
     *
753
     * @return string server version as a string.
754
     */
755 367
    public function getServerVersion(): string
756
    {
757 367
        if ($this->serverVersion === null) {
758 367
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
759
        }
760
761 367
        return $this->serverVersion;
762
    }
763
764
    /**
765
     * Returns the cache key for the specified table name.
766
     *
767
     * @param string $name the table name.
768
     *
769
     * @throws JsonException
770
     *
771
     * @return mixed the cache key.
772
     */
773 1595
    protected function getCacheKey(string $name)
774
    {
775
        $key = [
776 1595
            __CLASS__,
777 1595
            $this->db->getDsn(),
778 1595
            $this->db->getUsername(),
779 1595
            $this->getRawTableName($name),
780
        ];
781
782 1595
        $jsonKey = json_encode($key, JSON_THROW_ON_ERROR);
783
784 1595
        return md5($jsonKey);
785
    }
786
787
    /**
788
     * Returns the cache tag name.
789
     *
790
     * This allows {@see refresh()} to invalidate all cached table schemas.
791
     *
792
     * @return string the cache tag name.
793
     */
794 1535
    protected function getCacheTag(): string
795
    {
796 1535
        return md5(serialize([
797 1535
            __CLASS__,
798 1535
            $this->db->getDsn(),
799 1535
            $this->db->getUsername(),
800
        ]));
801
    }
802
803
    /**
804
     * Returns the metadata of the given type for the given table.
805
     *
806
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
807
     * table name to obtain the metadata.
808
     *
809
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
810
     * @param string $type metadata type.
811
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
812
     *
813
     * @throws JsonException
814
     *
815
     * @return mixed metadata.
816
     */
817 1595
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
818
    {
819 1595
        if ($this->schemaCache->isEnabled() && $this->schemaCache->isExclude($name)) {
820 1595
            $schemaCache = $this->schemaCache->getCache();
821
        }
822
823 1595
        $rawName = $this->getRawTableName($name);
824
825 1595
        if (!isset($this->tableMetadata[$rawName])) {
826 1595
            $this->loadTableMetadataFromCache($schemaCache, $rawName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $schemaCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
827
        }
828
829 1595
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
830 1595
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
831 1535
            $this->saveTableMetadataToCache($schemaCache, $rawName);
832
        }
833
834 1535
        return $this->tableMetadata[$rawName][$type];
835
    }
836
837
    /**
838
     * Returns the metadata of the given type for all tables in the given schema.
839
     *
840
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
841
     * obtain the metadata.
842
     *
843
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
844
     * name.
845
     * @param string $type metadata type.
846
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
847
     * returned if available.
848
     *
849
     * @throws NotSupportedException
850
     *
851
     * @return array array of metadata.
852
     */
853 13
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
854
    {
855 13
        $metadata = [];
856 13
        $methodName = 'getTable' . ucfirst($type);
857
858 13
        foreach ($this->getTableNames($schema, $refresh) as $name) {
859 13
            if ($schema !== '') {
860
                $name = $schema . '.' . $name;
861
            }
862
863 13
            $tableMetadata = $this->$methodName($name, $refresh);
864
865 13
            if ($tableMetadata !== null) {
866 13
                $metadata[] = $tableMetadata;
867
            }
868
        }
869
870 13
        return $metadata;
871
    }
872
873
    /**
874
     * Sets the metadata of the given type for the given table.
875
     *
876
     * @param string $name table name.
877
     * @param string $type metadata type.
878
     * @param mixed  $data metadata.
879
     */
880 295
    protected function setTableMetadata(string $name, string $type, $data): void
881
    {
882 295
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
883 295
    }
884
885
    /**
886
     * Changes row's array key case to lower if PDO's one is set to uppercase.
887
     *
888
     * @param array $row row's array or an array of row's arrays.
889
     * @param bool $multiple whether multiple rows or a single row passed.
890
     *
891
     * @throws Exception|InvalidConfigException
892
     *
893
     * @return array normalized row or rows.
894
     */
895 339
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
896
    {
897 339
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
898 263
            return $row;
899
        }
900
901 76
        if ($multiple) {
902 76
            return array_map(static function (array $row) {
903 73
                return array_change_key_case($row, CASE_LOWER);
904 76
            }, $row);
905
        }
906
907
        return array_change_key_case($row, CASE_LOWER);
908
    }
909
910
    /**
911
     * Tries to load and populate table metadata from cache.
912
     *
913
     * @param CacheInterface|null $cache
914
     * @param string $name
915
     *
916
     * @throws JsonException
917
     */
918 1595
    private function loadTableMetadataFromCache(?CacheInterface $cache, string $name): void
919
    {
920 1595
        if ($cache === null) {
921
            $this->tableMetadata[$name] = [];
922
923
            return;
924
        }
925
926 1595
        $metadata = $cache->get($this->getCacheKey($name));
927
928
        if (
929 1595
            !is_array($metadata) ||
930 1595
            !isset($metadata['cacheVersion']) ||
931 1595
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
932
        ) {
933 1595
            $this->tableMetadata[$name] = [];
934
935 1595
            return;
936
        }
937
938 501
        unset($metadata['cacheVersion']);
939 501
        $this->tableMetadata[$name] = $metadata;
940 501
    }
941
942
    /**
943
     * Saves table metadata to cache.
944
     *
945
     * @param CacheInterface|null $cache
946
     * @param string $name
947
     *
948
     * @throws JsonException
949
     */
950 1535
    private function saveTableMetadataToCache(?CacheInterface $cache, string $name): void
951
    {
952 1535
        if ($cache === null) {
953
            return;
954
        }
955
956 1535
        $metadata = $this->tableMetadata[$name];
957
958 1535
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
959
960 1535
        $cache->set(
961 1535
            $this->getCacheKey($name),
962
            $metadata,
963 1535
            $this->schemaCache->getDuration(),
964 1535
            new TagDependency(['tags' => $this->getCacheTag()]),
0 ignored issues
show
Unused Code introduced by
The call to Psr\SimpleCache\CacheInterface::set() has too many arguments starting with new Yiisoft\Cache\Depend... $this->getCacheTag())). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

964
        $cache->/** @scrutinizer ignore-call */ 
965
                set(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
965
        );
966 1535
    }
967
968 1640
    public function getDb(): Connection
969
    {
970 1640
        return $this->db;
971
    }
972
973
    public function getDefaultSchema(): ?string
974
    {
975
        return $this->defaultSchema;
976
    }
977
}
978