Schema::resolveTableName()   A
last analyzed

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
 * @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 65
    protected function createColumnSchema()
180
    {
181 65
        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 65
    public function getTableSchema($name, $refresh = false)
191
    {
192 65
        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
    public function getTableSchemas($schema = '', $refresh = false)
204
    {
205
        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
    public function getSchemaNames($refresh = false)
216
    {
217
        if ($this->_schemaNames === null || $refresh) {
218
            $this->_schemaNames = $this->findSchemaNames();
219
        }
220
221
        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
    public function getTableNames($schema = '', $refresh = false)
233
    {
234
        if (!isset($this->_tableNames[$schema]) || $refresh) {
235
            $this->_tableNames[$schema] = $this->findTableNames($schema);
236
        }
237
238
        return $this->_tableNames[$schema];
239
    }
240
241
    /**
242
     * @return QueryBuilder the query builder for this connection.
243
     */
244 83
    public function getQueryBuilder()
245
    {
246 83
        if ($this->_builder === null) {
247 83
            $this->_builder = $this->createQueryBuilder();
248
        }
249
250 83
        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 32
    public function getPdoType($data)
260
    {
261 32
        static $typeMap = [
262
            // php type => PDO type
263 32
            'boolean' => \PDO::PARAM_BOOL,
264 32
            'integer' => \PDO::PARAM_INT,
265 32
            'string' => \PDO::PARAM_STR,
266 32
            'resource' => \PDO::PARAM_LOB,
267 32
            'NULL' => \PDO::PARAM_NULL,
268 32
        ];
269 32
        $type = gettype($data);
270
271 32
        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
    public function refresh()
280
    {
281
        /** @var CacheInterface $cache */
282
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
283
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
284
            TagDependency::invalidate($cache, $this->getCacheTag());
285
        }
286
        $this->_tableNames = [];
287
        $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 72
    public function refreshTableSchema($name)
298
    {
299 72
        $rawName = $this->getRawTableName($name);
300 72
        unset($this->_tableMetadata[$rawName]);
301 72
        $this->_tableNames = [];
302
        /** @var CacheInterface $cache */
303 72
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
304 72
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
305
            $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::class, [$this->db]);
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
    public function createColumnSchemaBuilder($type, $length = null)
330
    {
331
        return Yii::createObject(ColumnSchemaBuilder::class, [$type, $length]);
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 24
    public function getLastInsertID($sequenceName = '')
365
    {
366 24
        if ($this->db->isActive) {
367 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

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