Schema   F
last analyzed

Complexity

Total Complexity 112

Size/Duplication

Total Lines 824
Duplicated Lines 0 %

Test Coverage

Coverage 87.23%

Importance

Changes 0
Metric Value
eloc 229
dl 0
loc 824
ccs 205
cts 235
cp 0.8723
rs 2
c 0
b 0
f 0
wmc 112

43 Methods

Rating   Name   Duplication   Size   Complexity  
A getTableNameParts() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A quoteSimpleTableName() 0 8 3
A createSavepoint() 0 3 1
A getLastInsertID() 0 7 3
A getTableSchema() 0 3 1
A quoteSimpleColumnName() 0 8 4
A normalizePdoRowKeyCase() 0 13 3
A loadTableMetadataFromCache() 0 15 5
A setTransactionIsolationLevel() 0 3 1
A findTableNames() 0 3 1
A getTableNames() 0 7 3
A quoteValue() 0 12 4
A getSchemaNames() 0 7 3
A getTableSchemas() 0 3 1
A getSchemaMetadata() 0 15 4
A refreshTableSchema() 0 9 4
A isReadQuery() 0 4 1
A supportsSavepoint() 0 3 1
A rollBackSavepoint() 0 3 1
A convertException() 0 15 5
A resolveTableName() 0 3 1
A releaseSavepoint() 0 3 1
A unquoteSimpleTableName() 0 8 3
A refresh() 0 9 4
A findSchemaNames() 0 3 1
A getCacheTag() 0 6 1
A insert() 0 18 5
B getTableMetadata() 0 19 8
A getRawTableName() 0 9 2
A setTableMetadata() 0 3 1
A quoteTableName() 0 17 6
A createColumnSchema() 0 3 1
A getQueryBuilder() 0 7 2
A quoteColumnName() 0 16 5
A findUniqueIndexes() 0 3 1
A unquoteSimpleColumnName() 0 8 3
A createQueryBuilder() 0 3 1
A getServerVersion() 0 6 2
A getPdoType() 0 13 2
A getCacheKey() 0 7 1
B getColumnPhpType() 0 25 8
A saveTableMetadataToCache() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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

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

316
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ QueryBuilder::className(), [$this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
317
    }
318
319
    /**
320
     * Create a column schema builder instance giving the type and value precision.
321
     *
322
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
323
     *
324
     * @param string $type type of the column. See [[ColumnSchemaBuilder::$type]].
325
     * @param int|string|array|null $length length or precision of the column. See [[ColumnSchemaBuilder::$length]].
326
     * @return ColumnSchemaBuilder column schema builder instance
327
     * @since 2.0.6
328
     */
329 20
    public function createColumnSchemaBuilder($type, $length = null)
330
    {
331 20
        return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

331
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ ColumnSchemaBuilder::className(), [$type, $length]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
332
    }
333
334
    /**
335
     * Returns all unique indexes for the given table.
336
     *
337
     * Each array element is of the following structure:
338
     *
339
     * ```php
340
     * [
341
     *  'IndexName1' => ['col1' [, ...]],
342
     *  'IndexName2' => ['col2' [, ...]],
343
     * ]
344
     * ```
345
     *
346
     * This method should be overridden by child classes in order to support this feature
347
     * because the default implementation simply throws an exception
348
     * @param TableSchema $table the table metadata
349
     * @return array all unique indexes for the given table.
350
     * @throws NotSupportedException if this method is called
351
     */
352
    public function findUniqueIndexes($table)
353
    {
354
        throw new NotSupportedException(get_class($this) . ' does not support getting unique indexes information.');
355
    }
356
357
    /**
358
     * Returns the ID of the last inserted row or sequence value.
359
     * @param string $sequenceName name of the sequence object (required by some DBMS)
360
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
361
     * @throws InvalidCallException if the DB connection is not active
362
     * @see https://www.php.net/manual/en/function.PDO-lastInsertId.php
363
     */
364 98
    public function getLastInsertID($sequenceName = '')
365
    {
366 98
        if ($this->db->isActive) {
367 98
            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

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