Passed
Push — master ( 3c1199...cd3c11 )
by Alexander
05:28
created

Schema::getColumnPhpType()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 8

Importance

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

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

299
            $this->cache->delete(/** @scrutinizer ignore-type */ $this->getCacheKey($rawName));
Loading history...
300
        }
301 80
    }
302
303
    /**
304
     * Returns the ID of the last inserted row or sequence value.
305
     *
306
     * @param string $sequenceName name of the sequence object (required by some DBMS)
307
     *
308
     * @throws InvalidCallException if the DB connection is not active
309
     *
310
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
311
     *
312
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
313
     */
314 34
    public function getLastInsertID(string $sequenceName = ''): string
315
    {
316 34
        if ($this->db->isActive()) {
317 34
            return $this->db->getPDO()->lastInsertId(
318 34
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
319
            );
320
        }
321
322
        throw new InvalidCallException('DB Connection is not active.');
323
    }
324
325
    /**
326
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
327
     */
328 8
    public function supportsSavepoint(): bool
329
    {
330 8
        return $this->db->isSavepointEnabled();
331
    }
332
333
    /**
334
     * Creates a new savepoint.
335
     *
336
     * @param string $name the savepoint name
337
     */
338 3
    public function createSavepoint(string $name): void
339
    {
340 3
        $this->db->createCommand("SAVEPOINT $name")->execute();
341 3
    }
342
343
    /**
344
     * Releases an existing savepoint.
345
     *
346
     * @param string $name the savepoint name
347
     */
348
    public function releaseSavepoint(string $name): void
349
    {
350
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
351
    }
352
353
    /**
354
     * Rolls back to a previously created savepoint.
355
     *
356
     * @param string $name the savepoint name
357
     */
358 3
    public function rollBackSavepoint(string $name): void
359
    {
360 3
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
361 3
    }
362
363
    /**
364
     * Sets the isolation level of the current transaction.
365
     *
366
     * @param string $level The transaction isolation level to use for this transaction.
367
     *
368
     * This can be one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
369
     * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS
370
     * specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
371
     *
372
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
373
     */
374 9
    public function setTransactionIsolationLevel(string $level): void
375
    {
376 9
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
377 9
    }
378
379
    /**
380
     * Executes the INSERT command, returning primary key values.
381
     *
382
     * @param string $table the table that new rows will be inserted into.
383
     * @param array $columns the column data (name => value) to be inserted into the table.
384
     *
385
     * @return array|false primary key values or false if the command fails.
386
     */
387 26
    public function insert(string $table, array $columns)
388
    {
389 26
        $command = $this->db->createCommand()->insert($table, $columns);
390
391 26
        if (!$command->execute()) {
392
            return false;
393
        }
394
395 26
        $tableSchema = $this->getTableSchema($table);
396 26
        $result = [];
397
398 26
        foreach ($tableSchema->getPrimaryKey() as $name) {
399 24
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
400 22
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

400
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
401 22
                break;
402
            }
403
404 4
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
405
        }
406
407 26
        return $result;
408
    }
409
410
    /**
411
     * Quotes a string value for use in a query.
412
     *
413
     * Note that if the parameter is not a string, it will be returned without change.
414
     *
415
     * @param string|int $str string to be quoted.
416
     *
417
     * @return string|int the properly quoted string.
418
     *
419
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
420
     */
421 893
    public function quoteValue($str)
422
    {
423 893
        if (!is_string($str)) {
424 5
            return $str;
425
        }
426
427 893
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
428 893
            return $value;
429
        }
430
431
        /** the driver doesn't support quote (e.g. oci) */
432
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
433
    }
434
435
    /**
436
     * Quotes a table name for use in a query.
437
     *
438
     * If the table name contains schema prefix, the prefix will also be properly quoted. If the table name is already
439
     * quoted or contains '(' or '{{', then this method will do nothing.
440
     *
441
     * @param string $name table name.
442
     *
443
     * @return string the properly quoted table name.
444
     *
445
     * {@see quoteSimpleTableName()}
446
     */
447 1134
    public function quoteTableName(string $name): string
448
    {
449 1134
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
450 3
            return $name;
451
        }
452
453 1134
        if (strpos($name, '{{') !== false) {
454 114
            return $name;
455
        }
456
457 1118
        if (strpos($name, '.') === false) {
458 1068
            return $this->quoteSimpleTableName($name);
459
        }
460
461 217
        $parts = $this->getTableNameParts($name);
462
463 217
        foreach ($parts as $i => $part) {
464 217
            $parts[$i] = $this->quoteSimpleTableName($part);
465
        }
466
467 217
        return implode('.', $parts);
468
    }
469
470
    /**
471
     * Splits full table name into parts
472
     *
473
     * @param string $name
474
     *
475
     * @return array
476
     */
477 11
    protected function getTableNameParts(string $name): array
478
    {
479 11
        return explode('.', $name);
480
    }
481
482
    /**
483
     * Quotes a column name for use in a query.
484
     *
485
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
486
     * or contains '(', '[[' or '{{', then this method will do nothing.
487
     *
488
     * @param string $name column name.
489
     *
490
     * @return string the properly quoted column name.
491
     *
492
     * {@see quoteSimpleColumnName()}
493
     */
494 1286
    public function quoteColumnName(string $name): string
495
    {
496 1286
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
497 86
            return $name;
498
        }
499
500 1275
        if (($pos = strrpos($name, '.')) !== false) {
501 160
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
502 160
            $name = substr($name, $pos + 1);
503
        } else {
504 1267
            $prefix = '';
505
        }
506
507 1275
        if (strpos($name, '{{') !== false) {
508 3
            return $name;
509
        }
510
511 1275
        return $prefix . $this->quoteSimpleColumnName($name);
512
    }
513
514
    /**
515
     * Quotes a simple table name for use in a query.
516
     *
517
     * A simple table name should contain the table name only without any schema prefix. If the table name is already
518
     * quoted, this method will do nothing.
519
     *
520
     * @param string $name table name.
521
     *
522
     * @return string the properly quoted table name.
523
     */
524 1148
    public function quoteSimpleTableName(string $name): string
525
    {
526 1148
        if (is_string($this->tableQuoteCharacter)) {
527 846
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
528
        } else {
529 302
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
530
        }
531
532 1148
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
533
    }
534
535
    /**
536
     * Quotes a simple column name for use in a query.
537
     *
538
     * A simple column name should contain the column name only without any prefix. If the column name is already quoted
539
     * or is the asterisk character '*', this method will do nothing.
540
     *
541
     * @param string $name column name.
542
     *
543
     * @return string the properly quoted column name.
544
     */
545 1275
    public function quoteSimpleColumnName(string $name): string
546
    {
547 1275
        if (is_string($this->columnQuoteCharacter)) {
548 970
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
549
        } else {
550 305
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
551
        }
552
553 1275
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
554 1275
            . $endingCharacter;
555
    }
556
557
    /**
558
     * Unquotes a simple table name.
559
     *
560
     * A simple table name should contain the table name only without any schema prefix. If the table name is not
561
     * quoted, this method will do nothing.
562
     *
563
     * @param string $name table name.
564
     *
565
     * @return string unquoted table name.
566
     */
567 5
    public function unquoteSimpleTableName(string $name): string
568
    {
569 5
        if (\is_string($this->tableQuoteCharacter)) {
570 5
            $startingCharacter = $this->tableQuoteCharacter;
571
        } else {
572
            $startingCharacter = $this->tableQuoteCharacter[0];
573
        }
574
575 5
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
576
    }
577
578
    /**
579
     * Unquotes a simple column name.
580
     *
581
     * A simple column name should contain the column name only without any prefix. If the column name is not quoted or
582
     * is the asterisk character '*', this method will do nothing.
583
     *
584
     * @param string $name column name.
585
     *
586
     * @return string unquoted column name.
587
     */
588
    public function unquoteSimpleColumnName(string $name): string
589
    {
590
        if (\is_string($this->columnQuoteCharacter)) {
591
            $startingCharacter = $this->columnQuoteCharacter;
592
        } else {
593
            $startingCharacter = $this->columnQuoteCharacter[0];
594
        }
595
596
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
597
    }
598
599
    /**
600
     * Returns the actual name of a given table name.
601
     *
602
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
603
     * {@see ConnectionInterface::tablePrefix}.
604
     *
605
     * @param string $name the table name to be converted.
606
     *
607
     * @return string the real name of the given table name.
608
     */
609 1034
    public function getRawTableName(string $name): string
610
    {
611 1034
        if (strpos($name, '{{') !== false) {
612 107
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
613
614 107
            return str_replace('%', $this->db->getTablePrefix(), $name);
615
        }
616
617 1034
        return $name;
618
    }
619
620
    /**
621
     * Extracts the PHP type from abstract DB type.
622
     *
623
     * @param ColumnSchema $column the column schema information.
624
     *
625
     * @return string PHP type name.
626
     */
627 772
    protected function getColumnPhpType(ColumnSchema $column): string
628
    {
629 772
        static $typeMap = [
630
            // abstract type => php type
631
            self::TYPE_TINYINT  => 'integer',
632
            self::TYPE_SMALLINT => 'integer',
633
            self::TYPE_INTEGER  => 'integer',
634
            self::TYPE_BIGINT   => 'integer',
635
            self::TYPE_BOOLEAN  => 'boolean',
636
            self::TYPE_FLOAT    => 'double',
637
            self::TYPE_DOUBLE   => 'double',
638
            self::TYPE_BINARY   => 'resource',
639
            self::TYPE_JSON     => 'array',
640
        ];
641
642 772
        if (isset($typeMap[$column->getType()])) {
643 766
            if ($column->getType() === 'bigint') {
644 42
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
645
            }
646
647 766
            if ($column->getType() === 'integer') {
648 766
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
649
            }
650
651 276
            return $typeMap[$column->getType()];
652
        }
653
654 738
        return 'string';
655
    }
656
657
    /**
658
     * Converts a DB exception to a more concrete one if possible.
659
     *
660
     * @param \Exception $e
661
     * @param string $rawSql SQL that produced exception.
662
     *
663
     * @return Exception
664
     */
665 31
    public function convertException(\Exception $e, string $rawSql): Exception
666
    {
667 31
        if ($e instanceof Exception) {
668
            return $e;
669
        }
670
671 31
        $exceptionClass = Exception::class;
672
673 31
        foreach ($this->exceptionMap as $error => $class) {
674 31
            if (strpos($e->getMessage(), $error) !== false) {
675 9
                $exceptionClass = $class;
676
            }
677
        }
678
679 31
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
680 31
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
681
682 31
        return new $exceptionClass($message, $errorInfo, $e->getCode(), $e);
683
    }
684
685
    /**
686
     * Returns a value indicating whether a SQL statement is for read purpose.
687
     *
688
     * @param string $sql the SQL statement.
689
     *
690
     * @return bool whether a SQL statement is for read purpose.
691
     */
692 9
    public function isReadQuery($sql): bool
693
    {
694 9
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
695
696 9
        return preg_match($pattern, $sql) > 0;
697
    }
698
699
    /**
700
     * Returns a server version as a string comparable by {@see \version_compare()}.
701
     *
702
     * @return string server version as a string.
703
     */
704 298
    public function getServerVersion(): string
705
    {
706 298
        if ($this->serverVersion === null) {
707 298
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
708
        }
709
710 298
        return $this->serverVersion;
711
    }
712
713
    /**
714
     * Returns the cache key for the specified table name.
715
     *
716
     * @param string $name the table name.
717
     *
718
     * @return mixed the cache key.
719
     */
720 1034
    protected function getCacheKey($name)
721
    {
722
        return [
723 1034
            __CLASS__,
724 1034
            $this->db->getDsn(),
725 1034
            $this->db->getUsername(),
726 1034
            $this->getRawTableName($name),
727
        ];
728
    }
729
730
    /**
731
     * Returns the cache tag name.
732
     *
733
     * This allows {@see refresh()} to invalidate all cached table schemas.
734
     *
735
     * @return string the cache tag name.
736
     */
737 986
    protected function getCacheTag(): string
738
    {
739 986
        return md5(serialize([
740 986
            __CLASS__,
741 986
            $this->db->getDsn(),
742 986
            $this->db->getUsername(),
743
        ]));
744
    }
745
746
    /**
747
     * Returns the metadata of the given type for the given table.
748
     *
749
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
750
     * table name to obtain the metadata.
751
     *
752
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
753
     * @param string $type metadata type.
754
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
755
     *
756
     * @return mixed metadata.
757
     */
758 1034
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
759
    {
760 1034
        if ($this->db->isSchemaCacheEnabled() && !\in_array($name, $this->db->getSchemaCacheExclude(), true)) {
761 1034
            $schemaCache = $this->cache;
762
        }
763
764 1034
        $rawName = $this->getRawTableName($name);
765
766 1034
        if (!isset($this->tableMetadata[$rawName])) {
767 1034
            $this->loadTableMetadataFromCache($schemaCache, $rawName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $schemaCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
768
        }
769
770 1034
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
771 1034
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
772 986
            $this->saveTableMetadataToCache($schemaCache, $rawName);
773
        }
774
775 986
        return $this->tableMetadata[$rawName][$type];
776
    }
777
778
    /**
779
     * Returns the metadata of the given type for all tables in the given schema.
780
     *
781
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
782
     * obtain the metadata.
783
     *
784
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
785
     * name.
786
     * @param string $type metadata type.
787
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
788
     * returned if available.
789
     *
790
     * @return array array of metadata.
791
     */
792 10
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
793
    {
794 10
        $metadata = [];
795 10
        $methodName = 'getTable' . ucfirst($type);
796
797 10
        foreach ($this->getTableNames($schema, $refresh) as $name) {
798 10
            if ($schema !== '') {
799
                $name = $schema . '.' . $name;
800
            }
801
802 10
            $tableMetadata = $this->$methodName($name, $refresh);
803
804 10
            if ($tableMetadata !== null) {
805 10
                $metadata[] = $tableMetadata;
806
            }
807
        }
808
809 10
        return $metadata;
810
    }
811
812
    /**
813
     * Sets the metadata of the given type for the given table.
814
     *
815
     * @param string $name table name.
816
     * @param string $type metadata type.
817
     * @param mixed  $data metadata.
818
     */
819 235
    protected function setTableMetadata(string $name, string $type, $data): void
820
    {
821 235
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
822 235
    }
823
824
    /**
825
     * Changes row's array key case to lower if PDO's one is set to uppercase.
826
     *
827
     * @param array $row row's array or an array of row's arrays.
828
     * @param bool $multiple whether multiple rows or a single row passed.
829
     *
830
     * @return array normalized row or rows.
831
     */
832 269
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
833
    {
834 269
        if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
835 209
            return $row;
836
        }
837
838 60
        if ($multiple) {
839 60
            return \array_map(function (array $row) {
840 58
                return \array_change_key_case($row, CASE_LOWER);
841 60
            }, $row);
842
        }
843
844
        return \array_change_key_case($row, CASE_LOWER);
845
    }
846
847
    /**
848
     * Tries to load and populate table metadata from cache.
849
     *
850
     * @param CacheInterface|null $cache
851
     * @param string $name
852
     */
853 1034
    private function loadTableMetadataFromCache(?CacheInterface $cache, string $name): void
854
    {
855 1034
        if ($cache === null) {
856
            $this->tableMetadata[$name] = [];
857
858
            return;
859
        }
860
861 1034
        $metadata = $cache->get($this->getCacheKey($name));
0 ignored issues
show
Bug introduced by
$this->getCacheKey($name) of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::get(). ( Ignorable by Annotation )

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

861
        $metadata = $cache->get(/** @scrutinizer ignore-type */ $this->getCacheKey($name));
Loading history...
862
863 1034
        if (!\is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
864 1034
            $this->tableMetadata[$name] = [];
865
866 1034
            return;
867
        }
868
869
        unset($metadata['cacheVersion']);
870
        $this->tableMetadata[$name] = $metadata;
871
    }
872
873
    /**
874
     * Saves table metadata to cache.
875
     *
876
     * @param CacheInterface|null $cache
877
     * @param string $name
878
     */
879 986
    private function saveTableMetadataToCache(?CacheInterface $cache, string $name): void
880
    {
881 986
        if ($cache === null) {
882
            return;
883
        }
884
885 986
        $metadata = $this->tableMetadata[$name];
886
887 986
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
888
889 986
        $cache->set(
890 986
            $this->getCacheKey($name),
891
            $metadata,
892 986
            $this->db->getSchemaCacheDuration(),
893 986
            new TagDependency(['tags' => $this->getCacheTag()])
894
        );
895 986
    }
896
897 1130
    public function getDb(): Connection
898
    {
899 1130
        return $this->db;
900
    }
901
902
    public function getDefaultSchema(): ?string
903
    {
904
        return $this->defaultSchema;
905
    }
906
}
907