Passed
Pull Request — 2.2 (#20357)
by Wilmer
12:52 queued 05:12
created

Schema::findUniqueIndexes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db;
10
11
use Yii;
12
use yii\base\BaseObject;
13
use yii\base\InvalidCallException;
14
use yii\base\InvalidConfigException;
15
use yii\base\NotSupportedException;
16
use yii\caching\Cache;
17
use yii\caching\CacheInterface;
18
use yii\caching\TagDependency;
19
20
/**
21
 * Schema is the base class for concrete DBMS-specific schema classes.
22
 *
23
 * Schema represents the database schema information that is DBMS specific.
24
 *
25
 * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from
26
 * the sequence object.
27
 * @property-read QueryBuilder $queryBuilder The query builder for this connection.
28
 * @property-read string[] $schemaNames All schema names in the database, except system schemas.
29
 * @property-read string $serverVersion Server version as a string.
30
 * @property-read string[] $tableNames All table names in the database.
31
 * @property-read TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element
32
 * is an instance of [[TableSchema]] or its child class.
33
 * @property-write string $transactionIsolationLevel The transaction isolation level to use for this
34
 * transaction. This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]],
35
 * [[Transaction::REPEATABLE_READ]] and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific
36
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
37
 *
38
 * @author Qiang Xue <[email protected]>
39
 * @author Sergey Makinen <[email protected]>
40
 * @since 2.0
41
 */
42
abstract class Schema extends BaseObject
43
{
44
    // The following are the supported abstract column data types.
45
    const TYPE_PK = 'pk';
46
    const TYPE_UPK = 'upk';
47
    const TYPE_BIGPK = 'bigpk';
48
    const TYPE_UBIGPK = 'ubigpk';
49
    const TYPE_CHAR = 'char';
50
    const TYPE_STRING = 'string';
51
    const TYPE_TEXT = 'text';
52
    const TYPE_TINYINT = 'tinyint';
53
    const TYPE_SMALLINT = 'smallint';
54
    const TYPE_INTEGER = 'integer';
55
    const TYPE_BIGINT = 'bigint';
56
    const TYPE_FLOAT = 'float';
57
    const TYPE_DOUBLE = 'double';
58
    const TYPE_DECIMAL = 'decimal';
59
    const TYPE_DATETIME = 'datetime';
60
    const TYPE_TIMESTAMP = 'timestamp';
61
    const TYPE_TIME = 'time';
62
    const TYPE_DATE = 'date';
63
    const TYPE_BINARY = 'binary';
64
    const TYPE_BOOLEAN = 'boolean';
65
    const TYPE_MONEY = 'money';
66
    const TYPE_JSON = 'json';
67
    /**
68
     * Schema cache version, to detect incompatibilities in cached values when the
69
     * data format of the cache changes.
70
     */
71
    const SCHEMA_CACHE_VERSION = 1;
72
73
    /**
74
     * @var Connection the database connection
75
     */
76
    public $db;
77
    /**
78
     * @var string the default schema name used for the current session.
79
     */
80
    public $defaultSchema;
81
    /**
82
     * @var array map of DB errors and corresponding exceptions
83
     * If left part is found in DB error message exception class from the right part is used.
84
     */
85
    public $exceptionMap = [
86
        'SQLSTATE[23' => 'yii\db\IntegrityException',
87
    ];
88
    /**
89
     * @var string|array column schema class or class config
90
     * @since 2.0.11
91
     */
92
    public $columnSchemaClass = 'yii\db\ColumnSchema';
93
94
    /**
95
     * @var string|string[] character used to quote schema, table, etc. names.
96
     * An array of 2 characters can be used in case starting and ending characters are different.
97
     * @since 2.0.14
98
     */
99
    protected $tableQuoteCharacter = "'";
100
    /**
101
     * @var string|string[] character used to quote column names.
102
     * An array of 2 characters can be used in case starting and ending characters are different.
103
     * @since 2.0.14
104
     */
105
    protected $columnQuoteCharacter = '"';
106
107
    /**
108
     * @var array list of ALL schema names in the database, except system schemas
109
     */
110
    private $_schemaNames;
111
    /**
112
     * @var array list of ALL table names in the database
113
     */
114
    private $_tableNames = [];
115
    /**
116
     * @var array list of loaded table metadata (table name => metadata type => metadata).
117
     */
118
    private $_tableMetadata = [];
119
    /**
120
     * @var QueryBuilder the query builder for this database
121
     */
122
    private $_builder;
123
    /**
124
     * @var string server version as a string.
125
     */
126
    private $_serverVersion;
127
128
129
    /**
130
     * Resolves the table name and schema name (if any).
131
     * @param string $name the table name
132
     * @return TableSchema [[TableSchema]] with resolved table, schema, etc. names.
133
     * @throws NotSupportedException if this method is not supported by the DBMS.
134
     * @since 2.0.13
135
     */
136
    protected function resolveTableName($name)
137
    {
138
        throw new NotSupportedException(get_class($this) . ' does not support resolving table names.');
139
    }
140
141
    /**
142
     * Returns all schema names in the database, including the default one but not system schemas.
143
     * This method should be overridden by child classes in order to support this feature
144
     * because the default implementation simply throws an exception.
145
     * @return array all schema names in the database, except system schemas.
146
     * @throws NotSupportedException if this method is not supported by the DBMS.
147
     * @since 2.0.4
148
     */
149
    protected function findSchemaNames()
150
    {
151
        throw new NotSupportedException(get_class($this) . ' does not support fetching all schema names.');
152
    }
153
154
    /**
155
     * Returns all table names in the database.
156
     * This method should be overridden by child classes in order to support this feature
157
     * because the default implementation simply throws an exception.
158
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
159
     * @return array all table names in the database. The names have NO schema name prefix.
160
     * @throws NotSupportedException if this method is not supported by the DBMS.
161
     */
162
    protected function findTableNames($schema = '')
163
    {
164
        throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.');
165
    }
166
167
    /**
168
     * Loads the metadata for the specified table.
169
     * @param string $name table name
170
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
171
     */
172
    abstract protected function loadTableSchema($name);
173
174
    /**
175
     * Creates a column schema for the database.
176
     * This method may be overridden by child classes to create a DBMS-specific column schema.
177
     * @return ColumnSchema column schema instance.
178
     * @throws InvalidConfigException if a column schema class cannot be created.
179
     */
180 62
    protected function createColumnSchema()
181
    {
182 62
        return Yii::createObject($this->columnSchemaClass);
183
    }
184
185
    /**
186
     * Obtains the metadata for the named table.
187
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
188
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
189
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
190
     */
191 62
    public function getTableSchema($name, $refresh = false)
192
    {
193 62
        return $this->getTableMetadata($name, 'schema', $refresh);
194
    }
195
196
    /**
197
     * Returns the metadata for all tables in the database.
198
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
199
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`,
200
     * cached data may be returned if available.
201
     * @return TableSchema[] the metadata for all tables in the database.
202
     * Each array element is an instance of [[TableSchema]] or its child class.
203
     */
204
    public function getTableSchemas($schema = '', $refresh = false)
205
    {
206
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
207
    }
208
209
    /**
210
     * Returns all schema names in the database, except system schemas.
211
     * @param bool $refresh whether to fetch the latest available schema names. If this is false,
212
     * schema names fetched previously (if available) will be returned.
213
     * @return string[] all schema names in the database, except system schemas.
214
     * @since 2.0.4
215
     */
216
    public function getSchemaNames($refresh = false)
217
    {
218
        if ($this->_schemaNames === null || $refresh) {
219
            $this->_schemaNames = $this->findSchemaNames();
220
        }
221
222
        return $this->_schemaNames;
223
    }
224
225
    /**
226
     * Returns all table names in the database.
227
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
228
     * If not empty, the returned table names will be prefixed with the schema name.
229
     * @param bool $refresh whether to fetch the latest available table names. If this is false,
230
     * table names fetched previously (if available) will be returned.
231
     * @return string[] all table names in the database.
232
     */
233
    public function getTableNames($schema = '', $refresh = false)
234
    {
235
        if (!isset($this->_tableNames[$schema]) || $refresh) {
236
            $this->_tableNames[$schema] = $this->findTableNames($schema);
237
        }
238
239
        return $this->_tableNames[$schema];
240
    }
241
242
    /**
243
     * @return QueryBuilder the query builder for this connection.
244
     */
245 80
    public function getQueryBuilder()
246
    {
247 80
        if ($this->_builder === null) {
248 80
            $this->_builder = $this->createQueryBuilder();
249
        }
250
251 80
        return $this->_builder;
252
    }
253
254
    /**
255
     * Determines the PDO type for the given PHP data value.
256
     * @param mixed $data the data whose PDO type is to be determined
257
     * @return int the PDO type
258
     * @see https://www.php.net/manual/en/pdo.constants.php
259
     */
260 32
    public function getPdoType($data)
261
    {
262 32
        static $typeMap = [
263
            // php type => PDO type
264 32
            'boolean' => \PDO::PARAM_BOOL,
265 32
            'integer' => \PDO::PARAM_INT,
266 32
            'string' => \PDO::PARAM_STR,
267 32
            'resource' => \PDO::PARAM_LOB,
268 32
            'NULL' => \PDO::PARAM_NULL,
269 32
        ];
270 32
        $type = gettype($data);
271
272 32
        return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR;
273
    }
274
275
    /**
276
     * Refreshes the schema.
277
     * This method cleans up all cached table schemas so that they can be re-created later
278
     * to reflect the database schema change.
279
     */
280
    public function refresh()
281
    {
282
        /* @var $cache CacheInterface */
283
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
284
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
285
            TagDependency::invalidate($cache, $this->getCacheTag());
286
        }
287
        $this->_tableNames = [];
288
        $this->_tableMetadata = [];
289
    }
290
291
    /**
292
     * Refreshes the particular table schema.
293
     * This method cleans up cached table schema so that it can be re-created later
294
     * to reflect the database schema change.
295
     * @param string $name table name.
296
     * @since 2.0.6
297
     */
298 69
    public function refreshTableSchema($name)
299
    {
300 69
        $rawName = $this->getRawTableName($name);
301 69
        unset($this->_tableMetadata[$rawName]);
302 69
        $this->_tableNames = [];
303
        /* @var $cache CacheInterface */
304 69
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
305 69
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
306
            $cache->delete($this->getCacheKey($rawName));
307
        }
308
    }
309
310
    /**
311
     * Creates a query builder for the database.
312
     * This method may be overridden by child classes to create a DBMS-specific query builder.
313
     * @return QueryBuilder query builder instance
314
     */
315
    public function createQueryBuilder()
316
    {
317
        return Yii::createObject(QueryBuilder::class, [$this->db]);
318
    }
319
320
    /**
321
     * Create a column schema builder instance giving the type and value precision.
322
     *
323
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
324
     *
325
     * @param string $type type of the column. See [[ColumnSchemaBuilder::$type]].
326
     * @param int|string|array|null $length length or precision of the column. See [[ColumnSchemaBuilder::$length]].
327
     * @return ColumnSchemaBuilder column schema builder instance
328
     * @since 2.0.6
329
     */
330
    public function createColumnSchemaBuilder($type, $length = null)
331
    {
332
        return Yii::createObject(ColumnSchemaBuilder::class, [$type, $length]);
333
    }
334
335
    /**
336
     * Returns all unique indexes for the given table.
337
     *
338
     * Each array element is of the following structure:
339
     *
340
     * ```php
341
     * [
342
     *  'IndexName1' => ['col1' [, ...]],
343
     *  'IndexName2' => ['col2' [, ...]],
344
     * ]
345
     * ```
346
     *
347
     * This method should be overridden by child classes in order to support this feature
348
     * because the default implementation simply throws an exception
349
     * @param TableSchema $table the table metadata
350
     * @return array all unique indexes for the given table.
351
     * @throws NotSupportedException if this method is called
352
     */
353
    public function findUniqueIndexes($table)
354
    {
355
        throw new NotSupportedException(get_class($this) . ' does not support getting unique indexes information.');
356
    }
357
358
    /**
359
     * Returns the ID of the last inserted row or sequence value.
360
     * @param string $sequenceName name of the sequence object (required by some DBMS)
361
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
362
     * @throws InvalidCallException if the DB connection is not active
363
     * @see https://www.php.net/manual/en/function.PDO-lastInsertId.php
364
     */
365 24
    public function getLastInsertID($sequenceName = '')
366
    {
367 24
        if ($this->db->isActive) {
368 24
            return $this->db->pdo->lastInsertId($sequenceName === '' ? null : $this->quoteTableName($sequenceName));
0 ignored issues
show
Bug introduced by
The method lastInsertId() does not exist on null. ( Ignorable by Annotation )

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

368
            return $this->db->pdo->/** @scrutinizer ignore-call */ lastInsertId($sequenceName === '' ? null : $this->quoteTableName($sequenceName));

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...
369
        }
370
371
        throw new InvalidCallException('DB Connection is not active.');
372
    }
373
374
    /**
375
     * @return bool whether this DBMS supports [savepoint](https://en.wikipedia.org/wiki/Savepoint).
376
     */
377
    public function supportsSavepoint()
378
    {
379
        return $this->db->enableSavepoint;
380
    }
381
382
    /**
383
     * Creates a new savepoint.
384
     * @param string $name the savepoint name
385
     */
386
    public function createSavepoint($name)
387
    {
388
        $this->db->createCommand("SAVEPOINT $name")->execute();
389
    }
390
391
    /**
392
     * Releases an existing savepoint.
393
     * @param string $name the savepoint name
394
     */
395
    public function releaseSavepoint($name)
396
    {
397
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
398
    }
399
400
    /**
401
     * Rolls back to a previously created savepoint.
402
     * @param string $name the savepoint name
403
     */
404
    public function rollBackSavepoint($name)
405
    {
406
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
407
    }
408
409
    /**
410
     * Sets the isolation level of the current transaction.
411
     * @param string $level The transaction isolation level to use for this transaction.
412
     * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]]
413
     * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used
414
     * after `SET TRANSACTION ISOLATION LEVEL`.
415
     * @see https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
416
     */
417
    public function setTransactionIsolationLevel($level)
418
    {
419
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
420
    }
421
422
    /**
423
     * Executes the INSERT command, returning primary key values.
424
     * @param string $table the table that new rows will be inserted into.
425
     * @param array $columns the column data (name => value) to be inserted into the table.
426
     * @return array|false primary key values or false if the command fails
427
     * @since 2.0.4
428
     */
429 25
    public function insert($table, $columns)
430
    {
431 25
        $command = $this->db->createCommand()->insert($table, $columns);
432 25
        if (!$command->execute()) {
433
            return false;
434
        }
435 25
        $tableSchema = $this->getTableSchema($table);
436 25
        $result = [];
437 25
        foreach ($tableSchema->primaryKey as $name) {
438 24
            if ($tableSchema->columns[$name]->autoIncrement) {
439 24
                $result[$name] = $this->getLastInsertID($tableSchema->sequenceName);
440 24
                break;
441
            }
442
443
            $result[$name] = isset($columns[$name]) ? $columns[$name] : $tableSchema->columns[$name]->defaultValue;
444
        }
445
446 25
        return $result;
447
    }
448
449
    /**
450
     * Quotes a string value for use in a query.
451
     * Note that if the parameter is not a string, it will be returned without change.
452
     * @param string $str string to be quoted
453
     * @return string the properly quoted string
454
     * @see https://www.php.net/manual/en/function.PDO-quote.php
455
     */
456 25
    public function quoteValue($str)
457
    {
458 25
        if (!is_string($str)) {
0 ignored issues
show
introduced by
The condition is_string($str) is always true.
Loading history...
459
            return $str;
460
        }
461
462 25
        if (mb_stripos((string)$this->db->dsn, 'odbc:') === false && ($value = $this->db->getSlavePdo(true)->quote($str)) !== false) {
463 25
            return $value;
464
        }
465
466
        // the driver doesn't support quote (e.g. oci)
467
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
468
    }
469
470
    /**
471
     * Quotes a table name for use in a query.
472
     * If the table name contains schema prefix, the prefix will also be properly quoted.
473
     * If the table name is already quoted or contains '(' or '{{',
474
     * then this method will do nothing.
475
     * @param string $name table name
476
     * @return string the properly quoted table name
477
     * @see quoteSimpleTableName()
478
     */
479 80
    public function quoteTableName($name)
480
    {
481
482 80
        if (strncmp($name, '(', 1) === 0 && strpos($name, ')') === strlen($name) - 1) {
483
            return $name;
484
        }
485 80
        if (strpos($name, '{{') !== false) {
486 4
            return $name;
487
        }
488 80
        if (strpos($name, '.') === false) {
489 80
            return $this->quoteSimpleTableName($name);
490
        }
491
        $parts = $this->getTableNameParts($name);
492
        foreach ($parts as $i => $part) {
493
            $parts[$i] = $this->quoteSimpleTableName($part);
494
        }
495
        return implode('.', $parts);
496
    }
497
498
    /**
499
     * Splits full table name into parts
500
     * @param string $name
501
     * @return array
502
     * @since 2.0.22
503
     */
504
    protected function getTableNameParts($name)
505
    {
506
        return explode('.', $name);
507
    }
508
509
    /**
510
     * Quotes a column name for use in a query.
511
     * If the column name contains prefix, the prefix will also be properly quoted.
512
     * If the column name is already quoted or contains '(', '[[' or '{{',
513
     * then this method will do nothing.
514
     * @param string $name column name
515
     * @return string the properly quoted column name
516
     * @see quoteSimpleColumnName()
517
     */
518 77
    public function quoteColumnName($name)
519
    {
520 77
        if ($name === null) {
0 ignored issues
show
introduced by
The condition $name === null is always false.
Loading history...
521
            return '';
522
        }
523
524 77
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
525 4
            return $name;
526
        }
527 77
        if (($pos = strrpos($name, '.')) !== false) {
528 4
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
529 4
            $name = substr($name, $pos + 1);
530
        } else {
531 77
            $prefix = '';
532
        }
533 77
        if (strpos($name, '{{') !== false) {
534
            return $name;
535
        }
536
537 77
        return $prefix . $this->quoteSimpleColumnName($name);
538
    }
539
540
    /**
541
     * Quotes a simple table name for use in a query.
542
     * A simple table name should contain the table name only without any schema prefix.
543
     * If the table name is already quoted, this method will do nothing.
544
     * @param string $name table name
545
     * @return string the properly quoted table name
546
     */
547 80
    public function quoteSimpleTableName($name)
548
    {
549 80
        if (is_string($this->tableQuoteCharacter)) {
550 80
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
551
        } else {
552
            list($startingCharacter, $endingCharacter) = $this->tableQuoteCharacter;
553
        }
554 80
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
555
    }
556
557
    /**
558
     * Quotes a simple column name for use in a query.
559
     * A simple column name should contain the column name only without any prefix.
560
     * If the column name is already quoted or is the asterisk character '*', this method will do nothing.
561
     * @param string $name column name
562
     * @return string the properly quoted column name
563
     */
564 77
    public function quoteSimpleColumnName($name)
565
    {
566 77
        if (is_string($this->columnQuoteCharacter)) {
567 77
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
568
        } else {
569
            list($startingCharacter, $endingCharacter) = $this->columnQuoteCharacter;
570
        }
571 77
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
572
    }
573
574
    /**
575
     * Unquotes a simple table name.
576
     * A simple table name should contain the table name only without any schema prefix.
577
     * If the table name is not quoted, this method will do nothing.
578
     * @param string $name table name.
579
     * @return string unquoted table name.
580
     * @since 2.0.14
581
     */
582
    public function unquoteSimpleTableName($name)
583
    {
584
        if (is_string($this->tableQuoteCharacter)) {
585
            $startingCharacter = $this->tableQuoteCharacter;
586
        } else {
587
            $startingCharacter = $this->tableQuoteCharacter[0];
588
        }
589
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
590
    }
591
592
    /**
593
     * Unquotes a simple column name.
594
     * A simple column name should contain the column name only without any prefix.
595
     * If the column name is not quoted or is the asterisk character '*', this method will do nothing.
596
     * @param string $name column name.
597
     * @return string unquoted column name.
598
     * @since 2.0.14
599
     */
600
    public function unquoteSimpleColumnName($name)
601
    {
602
        if (is_string($this->columnQuoteCharacter)) {
603
            $startingCharacter = $this->columnQuoteCharacter;
604
        } else {
605
            $startingCharacter = $this->columnQuoteCharacter[0];
606
        }
607
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
608
    }
609
610
    /**
611
     * Returns the actual name of a given table name.
612
     * This method will strip off curly brackets from the given table name
613
     * and replace the percentage character '%' with [[Connection::tablePrefix]].
614
     * @param string $name the table name to be converted
615
     * @return string the real name of the given table name
616
     */
617 69
    public function getRawTableName($name)
618
    {
619 69
        if (strpos($name, '{{') !== false) {
620
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
621
622
            return str_replace('%', $this->db->tablePrefix, $name);
623
        }
624
625 69
        return $name;
626
    }
627
628
    /**
629
     * Extracts the PHP type from abstract DB type.
630
     * @param ColumnSchema $column the column schema information
631
     * @return string PHP type name
632
     */
633 62
    protected function getColumnPhpType($column)
634
    {
635 62
        static $typeMap = [
636
            // abstract type => php type
637 62
            self::TYPE_TINYINT => 'integer',
638 62
            self::TYPE_SMALLINT => 'integer',
639 62
            self::TYPE_INTEGER => 'integer',
640 62
            self::TYPE_BIGINT => 'integer',
641 62
            self::TYPE_BOOLEAN => 'boolean',
642 62
            self::TYPE_FLOAT => 'double',
643 62
            self::TYPE_DOUBLE => 'double',
644 62
            self::TYPE_BINARY => 'resource',
645 62
            self::TYPE_JSON => 'array',
646 62
        ];
647 62
        if (isset($typeMap[$column->type])) {
648 62
            if ($column->type === 'bigint') {
649
                return PHP_INT_SIZE === 8 && !$column->unsigned ? 'integer' : 'string';
650 62
            } elseif ($column->type === 'integer') {
651 62
                return PHP_INT_SIZE === 4 && $column->unsigned ? 'string' : 'integer';
652
            }
653
654 8
            return $typeMap[$column->type];
655
        }
656
657 53
        return 'string';
658
    }
659
660
    /**
661
     * Converts a DB exception to a more concrete one if possible.
662
     *
663
     * @param \Exception $e
664
     * @param string $rawSql SQL that produced exception
665
     * @return Exception
666
     */
667
    public function convertException(\Exception $e, $rawSql)
668
    {
669
        if ($e instanceof Exception) {
670
            return $e;
671
        }
672
673
        $exceptionClass = '\yii\db\Exception';
674
        foreach ($this->exceptionMap as $error => $class) {
675
            if (strpos($e->getMessage(), $error) !== false) {
676
                $exceptionClass = $class;
677
            }
678
        }
679
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
680
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
681
        return new $exceptionClass($message, $errorInfo, $e->getCode(), $e);
682
    }
683
684
    /**
685
     * Returns a value indicating whether a SQL statement is for read purpose.
686
     * @param string $sql the SQL statement
687
     * @return bool whether a SQL statement is for read purpose.
688
     */
689
    public function isReadQuery($sql)
690
    {
691
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
692
        return preg_match($pattern, $sql) > 0;
693
    }
694
695
    /**
696
     * Returns a server version as a string comparable by [[\version_compare()]].
697
     * @return string server version as a string.
698
     * @since 2.0.14
699
     */
700
    public function getServerVersion()
701
    {
702
        if ($this->_serverVersion === null) {
703
            $this->_serverVersion = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
704
        }
705
        return $this->_serverVersion;
706
    }
707
708
    /**
709
     * Returns the cache key for the specified table name.
710
     * @param string $name the table name.
711
     * @return mixed the cache key.
712
     */
713
    protected function getCacheKey($name)
714
    {
715
        return [
716
            __CLASS__,
717
            $this->db->dsn,
718
            $this->db->username,
719
            $this->getRawTableName($name),
720
        ];
721
    }
722
723
    /**
724
     * Returns the cache tag name.
725
     * This allows [[refresh()]] to invalidate all cached table schemas.
726
     * @return string the cache tag name
727
     */
728
    protected function getCacheTag()
729
    {
730
        return md5(serialize([
731
            __CLASS__,
732
            $this->db->dsn,
733
            $this->db->username,
734
        ]));
735
    }
736
737
    /**
738
     * Returns the metadata of the given type for the given table.
739
     * If there's no metadata in the cache, this method will call
740
     * a `'loadTable' . ucfirst($type)` named method with the table name to obtain the metadata.
741
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
742
     * @param string $type metadata type.
743
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
744
     * @return mixed metadata.
745
     * @since 2.0.13
746
     */
747 62
    protected function getTableMetadata($name, $type, $refresh)
748
    {
749 62
        $cache = null;
750 62
        if ($this->db->enableSchemaCache && !in_array($name, $this->db->schemaCacheExclude, true)) {
751
            $schemaCache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
752
            if ($schemaCache instanceof CacheInterface) {
753
                $cache = $schemaCache;
754
            }
755
        }
756 62
        $rawName = $this->getRawTableName($name);
757 62
        if (!isset($this->_tableMetadata[$rawName])) {
758 62
            $this->loadTableMetadataFromCache($cache, $rawName);
759
        }
760 62
        if ($refresh || !array_key_exists($type, $this->_tableMetadata[$rawName])) {
761 62
            $this->_tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
762 62
            $this->saveTableMetadataToCache($cache, $rawName);
763
        }
764
765 62
        return $this->_tableMetadata[$rawName][$type];
766
    }
767
768
    /**
769
     * Returns the metadata of the given type for all tables in the given schema.
770
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name
771
     * and the refresh flag to obtain the metadata.
772
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema name.
773
     * @param string $type metadata type.
774
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`,
775
     * cached data may be returned if available.
776
     * @return array array of metadata.
777
     * @since 2.0.13
778
     */
779
    protected function getSchemaMetadata($schema, $type, $refresh)
780
    {
781
        $metadata = [];
782
        $methodName = 'getTable' . ucfirst($type);
783
        foreach ($this->getTableNames($schema, $refresh) as $name) {
784
            if ($schema !== '') {
785
                $name = $schema . '.' . $name;
786
            }
787
            $tableMetadata = $this->$methodName($name, $refresh);
788
            if ($tableMetadata !== null) {
789
                $metadata[] = $tableMetadata;
790
            }
791
        }
792
793
        return $metadata;
794
    }
795
796
    /**
797
     * Sets the metadata of the given type for the given table.
798
     * @param string $name table name.
799
     * @param string $type metadata type.
800
     * @param mixed $data metadata.
801
     * @since 2.0.13
802
     */
803
    protected function setTableMetadata($name, $type, $data)
804
    {
805
        $this->_tableMetadata[$this->getRawTableName($name)][$type] = $data;
806
    }
807
808
    /**
809
     * Changes row's array key case to lower if PDO's one is set to uppercase.
810
     * @param array $row row's array or an array of row's arrays.
811
     * @param bool $multiple whether multiple rows or a single row passed.
812
     * @return array normalized row or rows.
813
     * @since 2.0.13
814
     */
815
    protected function normalizePdoRowKeyCase(array $row, $multiple)
816
    {
817
        if ($this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
818
            return $row;
819
        }
820
821
        if ($multiple) {
822
            return array_map(function (array $row) {
823
                return array_change_key_case($row, CASE_LOWER);
824
            }, $row);
825
        }
826
827
        return array_change_key_case($row, CASE_LOWER);
828
    }
829
830
    /**
831
     * Tries to load and populate table metadata from cache.
832
     * @param Cache|null $cache
833
     * @param string $name
834
     */
835 62
    private function loadTableMetadataFromCache($cache, $name)
836
    {
837 62
        if ($cache === null) {
838 62
            $this->_tableMetadata[$name] = [];
839 62
            return;
840
        }
841
842
        $metadata = $cache->get($this->getCacheKey($name));
843
        if (!is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
844
            $this->_tableMetadata[$name] = [];
845
            return;
846
        }
847
848
        unset($metadata['cacheVersion']);
849
        $this->_tableMetadata[$name] = $metadata;
850
    }
851
852
    /**
853
     * Saves table metadata to cache.
854
     * @param Cache|null $cache
855
     * @param string $name
856
     */
857 62
    private function saveTableMetadataToCache($cache, $name)
858
    {
859 62
        if ($cache === null) {
860 62
            return;
861
        }
862
863
        $metadata = $this->_tableMetadata[$name];
864
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
865
        $cache->set(
866
            $this->getCacheKey($name),
867
            $metadata,
868
            $this->db->schemaCacheDuration,
869
            new TagDependency(['tags' => $this->getCacheTag()])
870
        );
871
    }
872
}
873