Passed
Pull Request — master (#259)
by Def
28:41 queued 13:52
created

Schema::getTableType()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 16
nc 8
nop 3
dl 0
loc 20
ccs 0
cts 0
cp 0
crap 72
rs 8.4444
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 Throwable;
10
use Yiisoft\Cache\Dependency\TagDependency;
11
use Yiisoft\Db\Cache\SchemaCache;
12
use Yiisoft\Db\Connection\ConnectionInterface;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\IntegrityException;
15
use Yiisoft\Db\Exception\InvalidCallException;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\NotSupportedException;
18
use Yiisoft\Db\Query\QueryBuilder;
19
20
use function addcslashes;
21
use function array_change_key_case;
22
use function array_key_exists;
23
use function array_map;
24
use function explode;
25
use function gettype;
26
use function implode;
27
use function is_array;
28
use function is_string;
29
use function md5;
30
use function preg_match;
31
use function preg_replace;
32
use function serialize;
33
use function str_replace;
34
use function strlen;
35
use function strpos;
36
use function substr;
37
use function ucfirst;
38
use function version_compare;
39
40
/**
41
 * Schema is the base class for concrete DBMS-specific schema classes.
42
 *
43
 * Schema represents the database schema information that is DBMS specific.
44
 *
45
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
46
 * object. This property is read-only.
47
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
48
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is read-only.
49
 * @property string $serverVersion Server version as a string. This property is read-only.
50
 * @property string[] $tableNames All table names in the database. This property is read-only.
51
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an instance
52
 * of {@see TableSchema} or its child class. This property is read-only.
53
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
54
 * one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
55
 * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS specific
56
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
57
 */
58
abstract class Schema implements SchemaInterface
59
{
60
    public const TYPE_PK = 'pk';
61
    public const TYPE_UPK = 'upk';
62
    public const TYPE_BIGPK = 'bigpk';
63
    public const TYPE_UBIGPK = 'ubigpk';
64
    public const TYPE_CHAR = 'char';
65
    public const TYPE_STRING = 'string';
66
    public const TYPE_TEXT = 'text';
67
    public const TYPE_TINYINT = 'tinyint';
68
    public const TYPE_SMALLINT = 'smallint';
69
    public const TYPE_INTEGER = 'integer';
70
    public const TYPE_BIGINT = 'bigint';
71
    public const TYPE_FLOAT = 'float';
72
    public const TYPE_DOUBLE = 'double';
73
    public const TYPE_DECIMAL = 'decimal';
74
    public const TYPE_DATETIME = 'datetime';
75
    public const TYPE_TIMESTAMP = 'timestamp';
76
    public const TYPE_TIME = 'time';
77
    public const TYPE_DATE = 'date';
78
    public const TYPE_BINARY = 'binary';
79
    public const TYPE_BOOLEAN = 'boolean';
80
    public const TYPE_MONEY = 'money';
81
    public const TYPE_JSON = 'json';
82
83
    /**
84
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
85
     */
86
    protected const SCHEMA_CACHE_VERSION = 1;
87
88
    /**
89
     * @var string|null the default schema name used for the current session.
90
     */
91
    protected ?string $defaultSchema = null;
92
93
    /**
94
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
95
     * class from the right part is used.
96
     */
97
    protected array $exceptionMap = [
98
        'SQLSTATE[23' => IntegrityException::class,
99
    ];
100
101
    /**
102
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
103
     * case starting and ending characters are different.
104
     */
105
    protected $tableQuoteCharacter = "'";
106
107
    /**
108
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
109
     * and ending characters are different.
110
     */
111
    protected $columnQuoteCharacter = '"';
112
    private array $schemaNames = [];
113
    private array $tableNames = [];
114
    private array $tableMetadata = [];
115
    private ?string $serverVersion = null;
116
    private ConnectionInterface $db;
117
    private ?QueryBuilder $builder = null;
118
    private SchemaCache $schemaCache;
119
120 2532
    public function __construct(ConnectionInterface $db, SchemaCache $schemaCache)
121
    {
122 2532
        $this->db = $db;
123 2532
        $this->schemaCache = $schemaCache;
124 2532
    }
125
126
    abstract public function createQueryBuilder(): QueryBuilder;
127
128
    /**
129
     * Resolves the table name and schema name (if any).
130
     *
131
     * @param string $name the table name.
132
     *
133
     * @throws NotSupportedException if this method is not supported by the DBMS.
134
     *
135
     * @return TableSchema with resolved table, schema, etc. names.
136
     *
137
     * {@see \Yiisoft\Db\Schema\TableSchema}
138
     */
139
    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

139
    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...
140
    {
141
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
142
    }
143
144
    /**
145
     * Returns all schema names in the database, including the default one but not system schemas.
146
     *
147
     * This method should be overridden by child classes in order to support this feature because the default
148
     * implementation simply throws an exception.
149
     *
150
     * @throws NotSupportedException if this method is not supported by the DBMS.
151
     *
152
     * @return array all schema names in the database, except system schemas.
153
     */
154
    protected function findSchemaNames(): array
155
    {
156
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
157
    }
158
159
    /**
160
     * Returns all table names in the database.
161
     *
162
     * This method should be overridden by child classes in order to support this feature because the default
163
     * implementation simply throws an exception.
164
     *
165
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
166
     *
167
     * @throws NotSupportedException if this method is not supported by the DBMS.
168
     *
169
     * @return array all table names in the database. The names have NO schema name prefix.
170
     */
171
    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

171
    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...
172
    {
173
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
174
    }
175
176
    /**
177
     * Loads the metadata for the specified table.
178
     *
179
     * @param string $name table name.
180
     *
181
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
182
     */
183
    abstract protected function loadTableSchema(string $name): ?TableSchema;
184
185
    /**
186
     * Obtains the metadata for the named table.
187
     *
188
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
189
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
190
     *
191
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
192
     */
193 1322
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
194
    {
195 1322
        return $this->getTableMetadata($name, self::SCHEMA, $refresh);
196
    }
197
198
    /**
199
     * Returns the metadata for all tables in the database.
200
     *
201
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
202
     * name.
203
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, cached data may be
204
     * returned if available.
205
     *
206
     * @throws NotSupportedException
207
     *
208
     * @return TableSchema[] the metadata for all tables in the database. Each array element is an instance of
209
     * {@see TableSchema} or its child class.
210
     */
211 13
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
212
    {
213 13
        return $this->getSchemaMetadata($schema, self::SCHEMA, $refresh);
214
    }
215
216
    /**
217
     * Returns all schema names in the database, except system schemas.
218
     *
219
     * @param bool $refresh whether to fetch the latest available schema names. If this is false, schema names fetched
220
     * previously (if available) will be returned.
221
     *
222
     * @throws NotSupportedException
223
     *
224
     * @return string[] all schema names in the database, except system schemas.
225
     */
226 4
    public function getSchemaNames(bool $refresh = false): array
227
    {
228 4
        if (empty($this->schemaNames) || $refresh) {
229 4
            $this->schemaNames = $this->findSchemaNames();
230
        }
231
232 4
        return $this->schemaNames;
233
    }
234
235
    /**
236
     * Returns all table names in the database.
237
     *
238
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
239
     * name.
240
     * If not empty, the returned table names will be prefixed with the schema name.
241
     * @param bool $refresh whether to fetch the latest available table names. If this is false, table names fetched
242
     * previously (if available) will be returned.
243
     *
244
     * @throws NotSupportedException
245
     *
246
     * @return string[] all table names in the database.
247
     */
248 23
    public function getTableNames(string $schema = '', bool $refresh = false): array
249
    {
250 23
        if (!isset($this->tableNames[$schema]) || $refresh) {
251 23
            $this->tableNames[$schema] = $this->findTableNames($schema);
252
        }
253
254 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...
255
    }
256
257
    /**
258
     * @return QueryBuilder the query builder for this connection.
259
     */
260 963
    public function getQueryBuilder(): QueryBuilder
261
    {
262 963
        if ($this->builder === null) {
263 963
            $this->builder = $this->createQueryBuilder();
264
        }
265
266 963
        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...
267
    }
268
269
    /**
270
     * Determines the PDO type for the given PHP data value.
271
     *
272
     * @param mixed $data the data whose PDO type is to be determined
273
     *
274
     * @return int the PDO type
275
     *
276
     * {@see http://www.php.net/manual/en/pdo.constants.php}
277
     */
278 1360
    public function getPdoType($data): int
279
    {
280 1360
        static $typeMap = [
281
            // php type => PDO type
282
            'boolean' => PDO::PARAM_BOOL,
283
            'integer' => PDO::PARAM_INT,
284
            'string' => PDO::PARAM_STR,
285
            'resource' => PDO::PARAM_LOB,
286
            'NULL' => PDO::PARAM_NULL,
287
        ];
288
289 1360
        $type = gettype($data);
290
291 1360
        return $typeMap[$type] ?? PDO::PARAM_STR;
292
    }
293
294
    /**
295
     * Refreshes the schema.
296
     *
297
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
298
     * schema change.
299
     */
300 96
    public function refresh(): void
301
    {
302 96
        if ($this->schemaCache->isEnabled()) {
303 96
            $this->schemaCache->invalidate($this->getCacheTag());
304
        }
305
306 96
        $this->tableNames = [];
307 96
        $this->tableMetadata = [];
308 96
    }
309
310
    /**
311
     * Refreshes the particular table schema.
312
     *
313
     * This method cleans up cached table schema so that it can be re-created later to reflect the database schema
314
     * change.
315
     *
316
     * @param string $name table name.
317
     */
318 103
    public function refreshTableSchema(string $name): void
319
    {
320 103
        $rawName = $this->getRawTableName($name);
321
322 103
        unset($this->tableMetadata[$rawName]);
323
324 103
        $this->tableNames = [];
325
326 103
        if ($this->schemaCache->isEnabled()) {
327 103
            $this->schemaCache->remove($this->getCacheKey($rawName));
328
        }
329 103
    }
330
331
    /**
332
     * Returns the ID of the last inserted row or sequence value.
333
     *
334
     * @param string $sequenceName name of the sequence object (required by some DBMS)
335
     *
336
     * @throws InvalidCallException if the DB connection is not active
337
     *
338
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
339
     *
340
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
341
     */
342 36
    public function getLastInsertID(string $sequenceName = ''): string
343
    {
344 36
        if ($this->db->isActive()) {
0 ignored issues
show
Bug introduced by
The method isActive() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

344
        if ($this->db->/** @scrutinizer ignore-call */ isActive()) {
Loading history...
345 36
            return $this->db->getPDO()->lastInsertId(
0 ignored issues
show
Bug introduced by
The method getPDO() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

345
            return $this->db->/** @scrutinizer ignore-call */ getPDO()->lastInsertId(
Loading history...
346 36
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
347
            );
348
        }
349
350
        throw new InvalidCallException('DB Connection is not active.');
351
    }
352
353
    /**
354
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
355
     */
356 10
    public function supportsSavepoint(): bool
357
    {
358 10
        return $this->db->isSavepointEnabled();
0 ignored issues
show
Bug introduced by
The method isSavepointEnabled() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

358
        return $this->db->/** @scrutinizer ignore-call */ isSavepointEnabled();
Loading history...
359
    }
360
361
    /**
362
     * Creates a new savepoint.
363
     *
364
     * @param string $name the savepoint name
365
     *
366
     * @throws Exception|InvalidConfigException|Throwable
367
     */
368 4
    public function createSavepoint(string $name): void
369
    {
370 4
        $this->db->createCommand("SAVEPOINT $name")->execute();
371 4
    }
372
373
    /**
374
     * Releases an existing savepoint.
375
     *
376
     * @param string $name the savepoint name
377
     *
378
     * @throws Exception|InvalidConfigException|Throwable
379
     */
380
    public function releaseSavepoint(string $name): void
381
    {
382
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
383
    }
384
385
    /**
386
     * Rolls back to a previously created savepoint.
387
     *
388
     * @param string $name the savepoint name
389
     *
390
     * @throws Exception|InvalidConfigException|Throwable
391
     */
392 4
    public function rollBackSavepoint(string $name): void
393
    {
394 4
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
395 4
    }
396
397
    /**
398
     * Sets the isolation level of the current transaction.
399
     *
400
     * @param string $level The transaction isolation level to use for this transaction.
401
     *
402
     * This can be one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
403
     * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS
404
     * specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
405
     *
406
     * @throws Exception|InvalidConfigException|Throwable
407
     *
408
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
409
     */
410 8
    public function setTransactionIsolationLevel(string $level): void
411
    {
412 8
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
413 8
    }
414
415
    /**
416
     * Executes the INSERT command, returning primary key values.
417
     *
418
     * @param string $table the table that new rows will be inserted into.
419
     * @param array $columns the column data (name => value) to be inserted into the table.
420
     *
421
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
422
     *
423
     * @return array|false primary key values or false if the command fails.
424
     */
425 28
    public function insert(string $table, array $columns)
426
    {
427 28
        $command = $this->db->createCommand()->insert($table, $columns);
428
429 28
        if (!$command->execute()) {
430
            return false;
431
        }
432
433 28
        $tableSchema = $this->getTableSchema($table);
434 28
        $result = [];
435
436 28
        foreach ($tableSchema->getPrimaryKey() as $name) {
437 26
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
438 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

438
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
439 24
                break;
440
            }
441
442 4
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
443
        }
444
445 28
        return $result;
446
    }
447
448
    /**
449
     * Quotes a string value for use in a query.
450
     *
451
     * Note that if the parameter is not a string, it will be returned without change.
452
     *
453
     * @param int|string $str string to be quoted.
454
     *
455
     * @throws Exception
456
     *
457
     * @return int|string the properly quoted string.
458
     *
459
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
460
     */
461 1075
    public function quoteValue($str)
462
    {
463 1075
        if (!is_string($str)) {
464 6
            return $str;
465
        }
466
467 1075
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
0 ignored issues
show
Bug introduced by
The method getSlavePdo() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

467
        if (($value = $this->db->/** @scrutinizer ignore-call */ getSlavePdo()->quote($str)) !== false) {
Loading history...
468 1075
            return $value;
469
        }
470
471
        /** the driver doesn't support quote (e.g. oci) */
472
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
473
    }
474
475
    /**
476
     * Quotes a table name for use in a query.
477
     *
478
     * If the table name contains schema prefix, the prefix will also be properly quoted. If the table name is already
479
     * quoted or contains '(' or '{{', then this method will do nothing.
480
     *
481
     * @param string $name table name.
482
     *
483
     * @return string the properly quoted table name.
484
     *
485
     * {@see quoteSimpleTableName()}
486
     */
487 1574
    public function quoteTableName(string $name): string
488
    {
489 1574
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
490 4
            return $name;
491
        }
492
493 1574
        if (strpos($name, '{{') !== false) {
494 146
            return $name;
495
        }
496
497 1550
        if (strpos($name, '.') === false) {
498 1467
            return $this->quoteSimpleTableName($name);
499
        }
500
501 287
        $parts = $this->getTableNameParts($name);
502
503 287
        foreach ($parts as $i => $part) {
504 287
            $parts[$i] = $this->quoteSimpleTableName($part);
505
        }
506
507 287
        return implode('.', $parts);
508
    }
509
510
    /**
511
     * Splits full table name into parts
512
     *
513
     * @param string $name
514
     *
515
     * @return array
516
     */
517 13
    protected function getTableNameParts(string $name): array
518
    {
519 13
        return explode('.', $name);
520
    }
521
522
    /**
523
     * Quotes a column name for use in a query.
524
     *
525
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
526
     * or contains '(', '[[' or '{{', then this method will do nothing.
527
     *
528
     * @param string $name column name.
529
     *
530
     * @return string the properly quoted column name.
531
     *
532
     * {@see quoteSimpleColumnName()}
533
     */
534 1698
    public function quoteColumnName(string $name): string
535
    {
536 1698
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
537 149
            return $name;
538
        }
539
540 1683
        if (($pos = strrpos($name, '.')) !== false) {
541 232
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
542 232
            $name = substr($name, $pos + 1);
543
        } else {
544 1673
            $prefix = '';
545
        }
546
547 1683
        if (strpos($name, '{{') !== false) {
548 4
            return $name;
549
        }
550
551 1683
        return $prefix . $this->quoteSimpleColumnName($name);
552
    }
553
554
    /**
555
     * Quotes a simple table name for use in a query.
556
     *
557
     * A simple table name should contain the table name only without any schema prefix. If the table name is already
558
     * quoted, this method will do nothing.
559
     *
560
     * @param string $name table name.
561
     *
562
     * @return string the properly quoted table name.
563
     */
564 1344
    public function quoteSimpleTableName(string $name): string
565
    {
566 1344
        if (is_string($this->tableQuoteCharacter)) {
567 987
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
568
        } else {
569 357
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
570
        }
571
572 1344
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
573
    }
574
575
    /**
576
     * Quotes a simple column name for use in a query.
577
     *
578
     * A simple column name should contain the column name only without any prefix. If the column name is already quoted
579
     * or is the asterisk character '*', this method will do nothing.
580
     *
581
     * @param string $name column name.
582
     *
583
     * @return string the properly quoted column name.
584
     */
585 1683
    public function quoteSimpleColumnName(string $name): string
586
    {
587 1683
        if (is_string($this->columnQuoteCharacter)) {
588 1357
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
589
        } else {
590 326
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
591
        }
592
593 1683
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
594 1683
            . $endingCharacter;
595
    }
596
597
    /**
598
     * Unquotes a simple table name.
599
     *
600
     * A simple table name should contain the table name only without any schema prefix. If the table name is not
601
     * quoted, this method will do nothing.
602
     *
603
     * @param string $name table name.
604
     *
605
     * @return string unquoted table name.
606
     */
607 5
    public function unquoteSimpleTableName(string $name): string
608
    {
609 5
        if (is_string($this->tableQuoteCharacter)) {
610 5
            $startingCharacter = $this->tableQuoteCharacter;
611
        } else {
612
            $startingCharacter = $this->tableQuoteCharacter[0];
613
        }
614
615 5
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
616
    }
617
618
    /**
619
     * Unquotes a simple column name.
620
     *
621
     * A simple column name should contain the column name only without any prefix. If the column name is not quoted or
622
     * is the asterisk character '*', this method will do nothing.
623
     *
624
     * @param string $name column name.
625
     *
626
     * @return string unquoted column name.
627
     */
628
    public function unquoteSimpleColumnName(string $name): string
629
    {
630
        if (is_string($this->columnQuoteCharacter)) {
631
            $startingCharacter = $this->columnQuoteCharacter;
632
        } else {
633
            $startingCharacter = $this->columnQuoteCharacter[0];
634
        }
635
636
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
637
    }
638
639
    /**
640
     * Returns the actual name of a given table name.
641
     *
642
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
643
     * {@see ConnectionInterface::tablePrefix}.
644
     *
645
     * @param string $name the table name to be converted.
646
     *
647
     * @return string the real name of the given table name.
648
     */
649 1622
    public function getRawTableName(string $name): string
650
    {
651 1622
        if (strpos($name, '{{') !== false) {
652 130
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
653
654 130
            return str_replace('%', $this->db->getTablePrefix(), $name);
0 ignored issues
show
Bug introduced by
The method getTablePrefix() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

654
            return str_replace('%', $this->db->/** @scrutinizer ignore-call */ getTablePrefix(), $name);
Loading history...
655
        }
656
657 1622
        return $name;
658
    }
659
660
    /**
661
     * Extracts the PHP type from abstract DB type.
662
     *
663
     * @param ColumnSchema $column the column schema information.
664
     *
665
     * @return string PHP type name.
666
     */
667 1280
    protected function getColumnPhpType(ColumnSchema $column): string
668
    {
669 1280
        static $typeMap = [
670
            // abstract type => php type
671
            self::TYPE_TINYINT => 'integer',
672
            self::TYPE_SMALLINT => 'integer',
673
            self::TYPE_INTEGER => 'integer',
674
            self::TYPE_BIGINT => 'integer',
675
            self::TYPE_BOOLEAN => 'boolean',
676
            self::TYPE_FLOAT => 'double',
677
            self::TYPE_DOUBLE => 'double',
678
            self::TYPE_BINARY => 'resource',
679
            self::TYPE_JSON => 'array',
680
        ];
681
682 1280
        if (isset($typeMap[$column->getType()])) {
683 1269
            if ($column->getType() === 'bigint') {
684 52
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
685
            }
686
687 1269
            if ($column->getType() === 'integer') {
688 1269
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
689
            }
690
691 367
            return $typeMap[$column->getType()];
692
        }
693
694 1218
        return 'string';
695
    }
696
697
    /**
698
     * Converts a DB exception to a more concrete one if possible.
699
     *
700
     * @param \Exception $e
701
     * @param string $rawSql SQL that produced exception.
702
     *
703
     * @return Exception
704
     */
705 50
    public function convertException(\Exception $e, string $rawSql): Exception
706
    {
707 50
        if ($e instanceof Exception) {
708
            return $e;
709
        }
710
711 50
        $exceptionClass = Exception::class;
712
713 50
        foreach ($this->exceptionMap as $error => $class) {
714 50
            if (strpos($e->getMessage(), $error) !== false) {
715 13
                $exceptionClass = $class;
716
            }
717
        }
718
719 50
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
720 50
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
721
722 50
        return new $exceptionClass($message, $errorInfo, $e);
723
    }
724
725
    /**
726
     * Returns a value indicating whether a SQL statement is for read purpose.
727
     *
728
     * @param string $sql the SQL statement.
729
     *
730
     * @return bool whether a SQL statement is for read purpose.
731
     */
732 12
    public function isReadQuery(string $sql): bool
733
    {
734 12
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
735
736 12
        return preg_match($pattern, $sql) > 0;
737
    }
738
739
    /**
740
     * Returns a server version as a string comparable by {@see version_compare()}.
741
     *
742
     * @throws Exception
743
     *
744
     * @return string server version as a string.
745
     */
746 375
    public function getServerVersion(): string
747
    {
748 375
        if ($this->serverVersion === null) {
749 375
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
750
        }
751
752 375
        return $this->serverVersion;
753
    }
754
755
    /**
756
     * Returns the cache key for the specified table name.
757
     *
758
     * @param string $name the table name.
759
     *
760
     * @return array the cache key.
761
     */
762 1622
    protected function getCacheKey(string $name): array
763
    {
764
        return [
765 1622
            __CLASS__,
766 1622
            $this->db->getDsn(),
767 1622
            $this->db->getUsername(),
0 ignored issues
show
Bug introduced by
The method getUsername() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

767
            $this->db->/** @scrutinizer ignore-call */ 
768
                       getUsername(),
Loading history...
768 1622
            $this->getRawTableName($name),
769
        ];
770
    }
771
772
    /**
773
     * Returns the cache tag name.
774
     *
775
     * This allows {@see refresh()} to invalidate all cached table schemas.
776
     *
777
     * @return string the cache tag name.
778
     */
779 1622
    protected function getCacheTag(): string
780
    {
781 1622
        return md5(serialize([
782 1622
            __CLASS__,
783 1622
            $this->db->getDsn(),
784 1622
            $this->db->getUsername(),
785
        ]));
786
    }
787
788
    /**
789
     * Returns the metadata of the given type for the given table.
790
     *
791
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
792
     * table name to obtain the metadata.
793
     *
794
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
795
     * @param string $type metadata type.
796
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
797
     *
798
     * @return mixed metadata.
799
     */
800 1622
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
801
    {
802 1622
        $rawName = $this->getRawTableName($name);
803
804 1622
        if (!isset($this->tableMetadata[$rawName])) {
805 1622
            $this->loadTableMetadataFromCache($rawName);
806
        }
807
808 1622
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
809 1622
            $this->tableMetadata[$rawName][$type] = $this->loadTableType($type, $rawName);
810 1562
            $this->saveTableMetadataToCache($rawName);
811
        }
812
813 1562
        return $this->tableMetadata[$rawName][$type];
814
    }
815
816
    /**
817
     * Returns the metadata of the given type for all tables in the given schema.
818
     *
819
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
820
     * obtain the metadata.
821
     *
822
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
823
     * name.
824
     * @param string $type metadata type.
825
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
826
     * returned if available.
827
     *
828
     * @throws NotSupportedException
829
     *
830
     * @return array array of metadata.
831
     */
832 13
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
833
    {
834 13
        $metadata = [];
835 13
836
        foreach ($this->getTableNames($schema, $refresh) as $name) {
837 13
            if ($schema !== '') {
838 13
                $name = $schema . '.' . $name;
839
            }
840
841
            $tableMetadata = $this->getTableType($type, $name, $refresh);
842 13
843
            if ($tableMetadata !== null) {
844 13
                $metadata[] = $tableMetadata;
845 13
            }
846
        }
847
848
        return $metadata;
849 13
    }
850
851
    /**
852
     * Sets the metadata of the given type for the given table.
853
     *
854
     * @param string $name table name.
855
     * @param string $type metadata type.
856
     * @param mixed $data metadata.
857
     */
858
    protected function setTableMetadata(string $name, string $type, $data): void
859 297
    {
860
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
861 297
    }
862 297
863
    /**
864
     * Changes row's array key case to lower if PDO's one is set to uppercase.
865
     *
866
     * @param array $row row's array or an array of row's arrays.
867
     * @param bool $multiple whether multiple rows or a single row passed.
868
     *
869
     * @throws Exception
870
     *
871
     * @return array normalized row or rows.
872
     */
873
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
874 341
    {
875
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
876 341
            return $row;
877 265
        }
878
879
        if ($multiple) {
880 76
            return array_map(static function (array $row) {
881 76
                return array_change_key_case($row, CASE_LOWER);
882 73
            }, $row);
883 76
        }
884
885
        return array_change_key_case($row, CASE_LOWER);
886
    }
887
888
    /**
889
     * Tries to load and populate table metadata from cache.
890
     *
891
     * @param string $rawName
892
     */
893
    private function loadTableMetadataFromCache(string $rawName): void
894 1622
    {
895
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
896 1622
            $this->tableMetadata[$rawName] = [];
897
            return;
898
        }
899
900
        $metadata = $this->schemaCache->getOrSet(
901 1622
            $this->getCacheKey($rawName),
902 1622
            null,
903 1622
            $this->schemaCache->getDuration(),
904 1622
            new TagDependency($this->getCacheTag()),
905 1622
        );
906
907
        if (
908
            !is_array($metadata) ||
909 1622
            !isset($metadata['cacheVersion']) ||
910 1622
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
911 1622
        ) {
912
            $this->tableMetadata[$rawName] = [];
913 1622
914
            return;
915 1622
        }
916
917
        unset($metadata['cacheVersion']);
918 841
        $this->tableMetadata[$rawName] = $metadata;
919 841
    }
920 841
921
    /**
922
     * Saves table metadata to cache.
923
     *
924
     * @param string $rawName
925
     */
926
    private function saveTableMetadataToCache(string $rawName): void
927 1562
    {
928
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
929 1562
            return;
930
        }
931
932
        $metadata = $this->tableMetadata[$rawName];
933 1562
934
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
935 1562
936
        $this->schemaCache->set(
937 1562
            $this->getCacheKey($rawName),
938 1562
            $metadata,
939
            $this->schemaCache->getDuration(),
940 1562
            new TagDependency($this->getCacheTag()),
941 1562
        );
942
    }
943 1562
944
    public function getDb(): ConnectionInterface
945 1665
    {
946
        return $this->db;
947 1665
    }
948
949
    public function getDefaultSchema(): ?string
950
    {
951
        return $this->defaultSchema;
952
    }
953
954
    public function getSchemaCache(): SchemaCache
955 18
    {
956
        return $this->schemaCache;
957 18
    }
958
959
    /**
960
     * This method will call a `'loadTable' . ucfirst($type)` named method with the table name to obtain the metadata.
961
     *
962
     * @param string $type
963
     * @param string $name
964
     *
965
     * @return mixed
966
     */
967
    protected function loadTableType(string $type, string $name)
968
    {
969
        switch ($type) {
970
            case SchemaInterface::SCHEMA:
971
                return $this->loadTableSchema($name);
972
            case SchemaInterface::PRIMARY_KEY:
973
                return $this->loadTablePrimaryKey($name);
0 ignored issues
show
Bug introduced by
The method loadTablePrimaryKey() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

973
                return $this->/** @scrutinizer ignore-call */ loadTablePrimaryKey($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
974
            case SchemaInterface::UNIQUES:
975
                return $this->loadTableUniques($name);
0 ignored issues
show
Bug introduced by
The method loadTableUniques() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean loadTableType()? ( Ignorable by Annotation )

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

975
                return $this->/** @scrutinizer ignore-call */ loadTableUniques($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
976
            case SchemaInterface::FOREIGN_KEYS:
977
                return $this->loadTableForeignKeys($name);
0 ignored issues
show
Bug introduced by
The method loadTableForeignKeys() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

977
                return $this->/** @scrutinizer ignore-call */ loadTableForeignKeys($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
978
            case SchemaInterface::INDEXES:
979
                return $this->loadTableIndexes($name);
0 ignored issues
show
Bug introduced by
The method loadTableIndexes() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean loadTableType()? ( Ignorable by Annotation )

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

979
                return $this->/** @scrutinizer ignore-call */ loadTableIndexes($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
980
            case SchemaInterface::DEFAULT_VALUES:
981
                return $this->loadTableDefaultValues($name);
0 ignored issues
show
Bug introduced by
The method loadTableDefaultValues() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

981
                return $this->/** @scrutinizer ignore-call */ loadTableDefaultValues($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
982
            case SchemaInterface::CHECKS:
983
                return $this->loadTableChecks($name);
0 ignored issues
show
Bug introduced by
The method loadTableChecks() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean loadTableSchema()? ( Ignorable by Annotation )

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

983
                return $this->/** @scrutinizer ignore-call */ loadTableChecks($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
984
        }
985
986
        return null;
987
    }
988
989
    /**
990
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
991
     * obtain the metadata.
992
     *
993
     * @param string $type
994
     * @param string $name
995
     * @param bool $refresh
996
     *
997
     * @return mixed
998
     */
999
    protected function getTableType(string $type, string $name, bool $refresh = false)
1000
    {
1001
        switch ($type) {
1002
            case SchemaInterface::SCHEMA:
1003
                return $this->getTableSchema($name, $refresh);
1004
            case SchemaInterface::PRIMARY_KEY:
1005
                return $this->getTablePrimaryKey($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTablePrimaryKey() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

1005
                return $this->/** @scrutinizer ignore-call */ getTablePrimaryKey($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1006
            case SchemaInterface::UNIQUES:
1007
                return $this->getTableUniques($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTableUniques() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean getTableNames()? ( Ignorable by Annotation )

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

1007
                return $this->/** @scrutinizer ignore-call */ getTableUniques($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1008
            case SchemaInterface::FOREIGN_KEYS:
1009
                return $this->getTableForeignKeys($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTableForeignKeys() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

1009
                return $this->/** @scrutinizer ignore-call */ getTableForeignKeys($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1010
            case SchemaInterface::INDEXES:
1011
                return $this->getTableIndexes($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTableIndexes() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean getTableNames()? ( Ignorable by Annotation )

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

1011
                return $this->/** @scrutinizer ignore-call */ getTableIndexes($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1012
            case SchemaInterface::DEFAULT_VALUES:
1013
                return $this->getTableDefaultValues($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTableDefaultValues() does not exist on Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

1013
                return $this->/** @scrutinizer ignore-call */ getTableDefaultValues($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1014
            case SchemaInterface::CHECKS:
1015
                return $this->getTableChecks($name, $refresh);
0 ignored issues
show
Bug introduced by
The method getTableChecks() does not exist on Yiisoft\Db\Schema\Schema. Did you maybe mean getTableSchemas()? ( Ignorable by Annotation )

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

1015
                return $this->/** @scrutinizer ignore-call */ getTableChecks($name, $refresh);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1016
        }
1017
1018
        return null;
1019
    }
1020
}
1021