Passed
Pull Request — master (#259)
by Def
15:17
created

Schema::getTableNames()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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

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

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

345
        if ($this->db->/** @scrutinizer ignore-call */ isActive()) {
Loading history...
346 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

346
            return $this->db->/** @scrutinizer ignore-call */ getPDO()->lastInsertId(
Loading history...
347
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
348
            );
349
        }
350
351
        throw new InvalidCallException('DB Connection is not active.');
352
    }
353
354
    /**
355
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
356 10
     */
357
    public function supportsSavepoint(): bool
358 10
    {
359
        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

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

439
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
440
                break;
441
            }
442 4
443
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
444
        }
445 28
446
        return $result;
447
    }
448
449
    /**
450
     * Quotes a string value for use in a query.
451
     *
452
     * Note that if the parameter is not a string, it will be returned without change.
453
     *
454
     * @param int|string $str string to be quoted.
455
     *
456
     * @throws Exception
457
     *
458
     * @return int|string the properly quoted string.
459
     *
460
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
461 1075
     */
462
    public function quoteValue($str)
463 1075
    {
464 6
        if (!is_string($str)) {
465
            return $str;
466
        }
467 1075
468 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

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

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

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

1004
                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...
1005
            case SchemaInterface::UNIQUES:
1006
                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

1006
                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...
1007
            case SchemaInterface::FOREIGN_KEYS:
1008
                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

1008
                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...
1009
            case SchemaInterface::INDEXES:
1010
                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

1010
                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...
1011
            case SchemaInterface::DEFAULT_VALUES:
1012
                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

1012
                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...
1013
            case SchemaInterface::CHECKS:
1014
                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

1014
                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...
1015
        }
1016
1017
        return null;
1018
    }
1019
}
1020