Passed
Push — master ( 4cfdfc...4628b9 )
by Paweł
20:57
created

framework/db/Schema.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://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
    protected function createColumnSchema()
180
    {
181 1632
        return Yii::createObject($this->columnSchemaClass);
182
    }
183 1632
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
    public function getTableSchema($name, $refresh = false)
191
    {
192 1796
        return $this->getTableMetadata($name, 'schema', $refresh);
193
    }
194 1796
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 18
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
206
    }
207 18
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 4
        if ($this->_schemaNames === null || $refresh) {
218
            $this->_schemaNames = $this->findSchemaNames();
219 4
        }
220 4
221
        return $this->_schemaNames;
222
    }
223 4
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 28
        if (!isset($this->_tableNames[$schema]) || $refresh) {
235
            $this->_tableNames[$schema] = $this->findTableNames($schema);
236 28
        }
237 28
238
        return $this->_tableNames[$schema];
239
    }
240 28
241
    /**
242
     * @return QueryBuilder the query builder for this connection.
243
     */
244
    public function getQueryBuilder()
245
    {
246 1647
        if ($this->_builder === null) {
247
            $this->_builder = $this->createQueryBuilder();
248 1647
        }
249 1580
250
        return $this->_builder;
251
    }
252 1647
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
    public function getPdoType($data)
260
    {
261 1916
        static $typeMap = [
262
            // php type => PDO type
263 1916
            'boolean' => \PDO::PARAM_BOOL,
264
            'integer' => \PDO::PARAM_INT,
265
            'string' => \PDO::PARAM_STR,
266
            'resource' => \PDO::PARAM_LOB,
267
            'NULL' => \PDO::PARAM_NULL,
268
        ];
269
        $type = gettype($data);
270
271 1916
        return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR;
272
    }
273 1916
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 72
        /* @var $cache CacheInterface */
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 72
            TagDependency::invalidate($cache, $this->getCacheTag());
285 72
        }
286 1
        $this->_tableNames = [];
287
        $this->_tableMetadata = [];
288 72
    }
289 72
290 72
    /**
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
    public function refreshTableSchema($name)
298
    {
299 287
        $rawName = $this->getRawTableName($name);
300
        unset($this->_tableMetadata[$rawName]);
301 287
        $this->_tableNames = [];
302 287
        /* @var $cache CacheInterface */
303 287
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
304
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
305 287
            $cache->delete($this->getCacheKey($rawName));
306 287
        }
307 36
    }
308
309 287
    /**
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 new QueryBuilder($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 14
        return new ColumnSchemaBuilder($type, $length);
332
    }
333 14
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
    public function getLastInsertID($sequenceName = '')
365
    {
366 96
        if ($this->db->isActive) {
367
            return $this->db->pdo->lastInsertId($sequenceName === '' ? null : $this->quoteTableName($sequenceName));
0 ignored issues
show
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 96
        }
369 96
370
        throw new InvalidCallException('DB Connection is not active.');
371
    }
372
373
    /**
374
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
375
     */
376
    public function supportsSavepoint()
377
    {
378 12
        return $this->db->enableSavepoint;
379
    }
380 12
381
    /**
382
     * Creates a new savepoint.
383
     * @param string $name the savepoint name
384
     */
385
    public function createSavepoint($name)
386
    {
387 4
        $this->db->createCommand("SAVEPOINT $name")->execute();
388
    }
389 4
390 4
    /**
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 4
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
406
    }
407 4
408 4
    /**
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 http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
415
     */
416
    public function setTransactionIsolationLevel($level)
417
    {
418 10
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
419
    }
420 10
421 10
    /**
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
    public function insert($table, $columns)
429
    {
430 88
        $command = $this->db->createCommand()->insert($table, $columns);
431
        if (!$command->execute()) {
432 88
            return false;
433 88
        }
434
        $tableSchema = $this->getTableSchema($table);
435
        $result = [];
436 88
        foreach ($tableSchema->primaryKey as $name) {
437 88
            if ($tableSchema->columns[$name]->autoIncrement) {
438 88
                $result[$name] = $this->getLastInsertID($tableSchema->sequenceName);
439 85
                break;
440 81
            }
441 81
442
            $result[$name] = isset($columns[$name]) ? $columns[$name] : $tableSchema->columns[$name]->defaultValue;
443
        }
444 6
445
        return $result;
446
    }
447 88
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
    public function quoteValue($str)
456
    {
457 1876
        if (!is_string($str)) {
458
            return $str;
459 1876
        }
460 20
461
        if (mb_stripos($this->db->dsn, 'odbc:') === false && ($value = $this->db->getSlavePdo()->quote($str)) !== false) {
462
            return $value;
463 1868
        }
464 1868
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
    public function quoteTableName($name)
479
    {
480 2190
481
        if (strncmp($name, '(', 1) === 0 && strpos($name, ')') === strlen($name) - 1) {
482
            return $name;
483 2190
        }
484 6
        if (strpos($name, '{{') !== false) {
485
            return $name;
486 2190
        }
487 533
        if (strpos($name, '.') === false) {
488
            return $this->quoteSimpleTableName($name);
489 2174
        }
490 2073
        $parts = $this->getTableNameParts($name);
491
        foreach ($parts as $i => $part) {
492 592
            $parts[$i] = $this->quoteSimpleTableName($part);
493 592
        }
494 592
        return implode('.', $parts);
495
    }
496 592
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 14
        return explode('.', $name);
506
    }
507 14
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
    public function quoteColumnName($name)
518
    {
519 2348
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
520
            return $name;
521 2348
        }
522 256
        if (($pos = strrpos($name, '.')) !== false) {
523
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
524 2335
            $name = substr($name, $pos + 1);
525 317
        } else {
526 317
            $prefix = '';
527
        }
528 2322
        if (strpos($name, '{{') !== false) {
529
            return $name;
530 2335
        }
531 6
532
        return $prefix . $this->quoteSimpleColumnName($name);
533
    }
534 2335
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
    public function quoteSimpleTableName($name)
543
    {
544 2290
        if (is_string($this->tableQuoteCharacter)) {
545
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
546 2290
        } else {
547 1496
            list($startingCharacter, $endingCharacter) = $this->tableQuoteCharacter;
548
        }
549 794
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
550
    }
551 2290
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
    public function quoteSimpleColumnName($name)
560
    {
561 2335
        if (is_string($this->columnQuoteCharacter)) {
562
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
563 2335
        } else {
564 1545
            list($startingCharacter, $endingCharacter) = $this->columnQuoteCharacter;
565
        }
566 790
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
567
    }
568 2335
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
    public function getRawTableName($name)
613
    {
614 2104
        if (strpos($name, '{{') !== false) {
615
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
616 2104
617 615
            return str_replace('%', $this->db->tablePrefix, $name);
618
        }
619 615
620
        return $name;
621
    }
622 1677
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
    protected function getColumnPhpType($column)
629
    {
630 1632
        static $typeMap = [
631
            // abstract type => php type
632 1632
            self::TYPE_TINYINT => 'integer',
633
            self::TYPE_SMALLINT => 'integer',
634
            self::TYPE_INTEGER => 'integer',
635
            self::TYPE_BIGINT => 'integer',
636
            self::TYPE_BOOLEAN => 'boolean',
637
            self::TYPE_FLOAT => 'double',
638
            self::TYPE_DOUBLE => 'double',
639
            self::TYPE_BINARY => 'resource',
640
            self::TYPE_JSON => 'array',
641
        ];
642
        if (isset($typeMap[$column->type])) {
643
            if ($column->type === 'bigint') {
644 1632
                return PHP_INT_SIZE === 8 && !$column->unsigned ? 'integer' : 'string';
645 1622
            } elseif ($column->type === 'integer') {
646 54
                return PHP_INT_SIZE === 4 && $column->unsigned ? 'string' : 'integer';
647 1622
            }
648 1622
649
            return $typeMap[$column->type];
650
        }
651 632
652
        return 'string';
653
    }
654 1572
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
    public function convertException(\Exception $e, $rawSql)
663
    {
664 56
        if ($e instanceof Exception) {
665
            return $e;
666 56
        }
667
668
        $exceptionClass = '\yii\db\Exception';
669
        foreach ($this->exceptionMap as $error => $class) {
670 56
            if (strpos($e->getMessage(), $error) !== false) {
671 56
                $exceptionClass = $class;
672 56
            }
673 56
        }
674
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
675
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
676 56
        return new $exceptionClass($message, $errorInfo, $e->getCode(), $e);
677 56
    }
678 56
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
    public function isReadQuery($sql)
685
    {
686 13
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
687
        return preg_match($pattern, $sql) > 0;
688 13
    }
689 13
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
    public function getServerVersion()
696
    {
697 604
        if ($this->_serverVersion === null) {
698
            $this->_serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
699 604
        }
700 604
        return $this->_serverVersion;
701
    }
702 604
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
    protected function getCacheKey($name)
709
    {
710 41
        return [
711
            __CLASS__,
712
            $this->db->dsn,
713 41
            $this->db->username,
714 41
            $this->getRawTableName($name),
715 41
        ];
716 41
    }
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
    protected function getCacheTag()
724
    {
725 41
        return md5(serialize([
726
            __CLASS__,
727 41
            $this->db->dsn,
728 41
            $this->db->username,
729 41
        ]));
730 41
    }
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
    protected function getTableMetadata($name, $type, $refresh)
743
    {
744 2099
        $cache = null;
745
        if ($this->db->enableSchemaCache && !in_array($name, $this->db->schemaCacheExclude, true)) {
746 2099
            $schemaCache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
747 2099
            if ($schemaCache instanceof CacheInterface) {
748 41
                $cache = $schemaCache;
749 41
            }
750 41
        }
751
        $rawName = $this->getRawTableName($name);
752
        if (!isset($this->_tableMetadata[$rawName])) {
753 2099
            $this->loadTableMetadataFromCache($cache, $rawName);
754 2099
        }
755 2055
        if ($refresh || !array_key_exists($type, $this->_tableMetadata[$rawName])) {
756
            $this->_tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
757 2099
            $this->saveTableMetadataToCache($cache, $rawName);
758 2055
        }
759 2007
760
        return $this->_tableMetadata[$rawName][$type];
761
    }
762 2051
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
    protected function getSchemaMetadata($schema, $type, $refresh)
775
    {
776 12
        $metadata = [];
777
        $methodName = 'getTable' . ucfirst($type);
778 12
        foreach ($this->getTableNames($schema, $refresh) as $name) {
779 12
            if ($schema !== '') {
780 12
                $name = $schema . '.' . $name;
781 12
            }
782
            $tableMetadata = $this->$methodName($name, $refresh);
783
            if ($tableMetadata !== null) {
784 12
                $metadata[] = $tableMetadata;
785 12
            }
786 12
        }
787
788
        return $metadata;
789
    }
790 12
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
    protected function setTableMetadata($name, $type, $data)
799
    {
800 413
        $this->_tableMetadata[$this->getRawTableName($name)][$type] = $data;
801
    }
802 413
803 413
    /**
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
    protected function normalizePdoRowKeyCase(array $row, $multiple)
811
    {
812 456
        if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
813
            return $row;
814 456
        }
815 375
816
        if ($multiple) {
817
            return array_map(function (array $row) {
818 81
                return array_change_key_case($row, CASE_LOWER);
819 81
            }, $row);
820 78
        }
821 81
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
    private function loadTableMetadataFromCache($cache, $name)
831
    {
832 2055
        if ($cache === null) {
833
            $this->_tableMetadata[$name] = [];
834 2055
            return;
835 2014
        }
836 2014
837
        $metadata = $cache->get($this->getCacheKey($name));
838
        if (!is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
839 41
            $this->_tableMetadata[$name] = [];
840 41
            return;
841 41
        }
842 41
843
        unset($metadata['cacheVersion']);
844
        $this->_tableMetadata[$name] = $metadata;
845 5
    }
846 5
847 5
    /**
848
     * Saves table metadata to cache.
849
     * @param Cache|null $cache
850
     * @param string $name
851
     */
852
    private function saveTableMetadataToCache($cache, $name)
853
    {
854 2007
        if ($cache === null) {
855
            return;
856 2007
        }
857 1966
858
        $metadata = $this->_tableMetadata[$name];
859
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
860 41
        $cache->set(
861 41
            $this->getCacheKey($name),
862 41
            $metadata,
863 41
            $this->db->schemaCacheDuration,
864 41
            new TagDependency(['tags' => $this->getCacheTag()])
865 41
        );
866 41
    }
867
}
868