Passed
Push — master ( 14175d...40c9a5 )
by Alexander
09:38
created

Schema::quoteValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use JsonException;
8
use PDO;
9
use PDOException;
10
use Throwable;
11
use Yiisoft\Cache\Dependency\TagDependency;
12
use Yiisoft\Db\Cache\SchemaCache;
13
use Yiisoft\Db\Connection\Connection;
14
use Yiisoft\Db\Exception\Exception;
15
use Yiisoft\Db\Exception\IntegrityException;
16
use Yiisoft\Db\Exception\InvalidCallException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Query\QueryBuilder;
20
21
use function addcslashes;
22
use function array_change_key_case;
23
use function array_key_exists;
24
use function array_map;
25
use function explode;
26
use function gettype;
27
use function implode;
28
use function is_array;
29
use function is_string;
30
use function md5;
31
use function preg_match;
32
use function preg_replace;
33
use function serialize;
34
use function str_replace;
35
use function strlen;
36
use function strpos;
37
use function substr;
38
use function ucfirst;
39
use function version_compare;
40
41
/**
42
 * Schema is the base class for concrete DBMS-specific schema classes.
43
 *
44
 * Schema represents the database schema information that is DBMS specific.
45
 *
46
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
47
 * object. This property is read-only.
48
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
49
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is read-only.
50
 * @property string $serverVersion Server version as a string. This property is read-only.
51
 * @property string[] $tableNames All table names in the database. This property is read-only.
52
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an instance
53
 * of {@see TableSchema} or its child class. This property is read-only.
54
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
55
 * one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
56
 * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS specific
57
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
58
 */
59
abstract class Schema
60
{
61
    public const TYPE_PK = 'pk';
62
    public const TYPE_UPK = 'upk';
63
    public const TYPE_BIGPK = 'bigpk';
64
    public const TYPE_UBIGPK = 'ubigpk';
65
    public const TYPE_CHAR = 'char';
66
    public const TYPE_STRING = 'string';
67
    public const TYPE_TEXT = 'text';
68
    public const TYPE_TINYINT = 'tinyint';
69
    public const TYPE_SMALLINT = 'smallint';
70
    public const TYPE_INTEGER = 'integer';
71
    public const TYPE_BIGINT = 'bigint';
72
    public const TYPE_FLOAT = 'float';
73
    public const TYPE_DOUBLE = 'double';
74
    public const TYPE_DECIMAL = 'decimal';
75
    public const TYPE_DATETIME = 'datetime';
76
    public const TYPE_TIMESTAMP = 'timestamp';
77
    public const TYPE_TIME = 'time';
78
    public const TYPE_DATE = 'date';
79
    public const TYPE_BINARY = 'binary';
80
    public const TYPE_BOOLEAN = 'boolean';
81
    public const TYPE_MONEY = 'money';
82
    public const TYPE_JSON = 'json';
83
84
    /**
85
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
86
     */
87
    protected const SCHEMA_CACHE_VERSION = 1;
88
89
    /**
90
     * @var string|null the default schema name used for the current session.
91
     */
92
    protected ?string $defaultSchema = null;
93
94
    /**
95
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
96
     * class from the right part is used.
97
     */
98
    protected array $exceptionMap = [
99
        'SQLSTATE[23' => IntegrityException::class,
100
    ];
101
102
    /**
103
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
104
     * case starting and ending characters are different.
105
     */
106
    protected $tableQuoteCharacter = "'";
107
108
    /**
109
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
110
     * and ending characters are different.
111
     */
112
    protected $columnQuoteCharacter = '"';
113
    private array $schemaNames = [];
114
    private array $tableNames = [];
115
    private array $tableMetadata = [];
116
    private ?QueryBuilder $builder = null;
117
    private ?string $serverVersion = null;
118
    private Connection $db;
119
    private SchemaCache $schemaCache;
120
121 2481
    public function __construct(Connection $db, SchemaCache $schemaCache)
122
    {
123 2481
        $this->db = $db;
124 2481
        $this->schemaCache = $schemaCache;
125 2481
    }
126
127
    abstract public function createQueryBuilder(): QueryBuilder;
128
129
    /**
130
     * Resolves the table name and schema name (if any).
131
     *
132
     * @param string $name the table name.
133
     *
134
     * @throws NotSupportedException if this method is not supported by the DBMS.
135
     *
136
     * @return TableSchema with resolved table, schema, etc. names.
137
     *
138
     * {@see \Yiisoft\Db\Schema\TableSchema}
139
     */
140
    protected function resolveTableName(string $name): TableSchema
141
    {
142
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
143
    }
144
145
    /**
146
     * Returns all schema names in the database, including the default one but not system schemas.
147
     *
148
     * This method should be overridden by child classes in order to support this feature because the default
149
     * implementation simply throws an exception.
150
     *
151
     * @throws NotSupportedException if this method is not supported by the DBMS.
152
     *
153
     * @return array all schema names in the database, except system schemas.
154
     */
155
    protected function findSchemaNames(): array
156
    {
157
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
158
    }
159
160
    /**
161
     * Returns all table names in the database.
162
     *
163
     * This method should be overridden by child classes in order to support this feature because the default
164
     * implementation simply throws an exception.
165
     *
166
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
167
     *
168
     * @throws NotSupportedException if this method is not supported by the DBMS.
169
     *
170
     * @return array all table names in the database. The names have NO schema name prefix.
171
     */
172
    protected function findTableNames(string $schema = ''): array
173
    {
174
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
175
    }
176
177
    /**
178
     * Loads the metadata for the specified table.
179
     *
180
     * @param string $name table name.
181
     *
182
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
183
     */
184
    abstract protected function loadTableSchema(string $name): ?TableSchema;
185
186
    /**
187
     * Obtains the metadata for the named table.
188
     *
189
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
190
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
191
     *
192
     * @throws JsonException
193
     *
194
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
195
     */
196 1295
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
197
    {
198 1295
        return $this->getTableMetadata($name, 'schema', $refresh);
199
    }
200
201
    /**
202
     * Returns the metadata for all tables in the database.
203
     *
204
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
205
     * name.
206
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, cached data may be
207
     * returned if available.
208
     *
209
     * @throws NotSupportedException
210
     *
211
     * @return TableSchema[] the metadata for all tables in the database. Each array element is an instance of
212
     * {@see TableSchema} or its child class.
213
     */
214 13
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
215
    {
216 13
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
217
    }
218
219
    /**
220
     * Returns all schema names in the database, except system schemas.
221
     *
222
     * @param bool $refresh whether to fetch the latest available schema names. If this is false, schema names fetched
223
     * previously (if available) will be returned.
224
     *
225
     * @throws NotSupportedException
226
     *
227
     * @return string[] all schema names in the database, except system schemas.
228
     */
229 4
    public function getSchemaNames(bool $refresh = false): array
230
    {
231 4
        if (empty($this->schemaNames) || $refresh) {
232 4
            $this->schemaNames = $this->findSchemaNames();
233
        }
234
235 4
        return $this->schemaNames;
236
    }
237
238
    /**
239
     * Returns all table names in the database.
240
     *
241
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
242
     * name.
243
     * If not empty, the returned table names will be prefixed with the schema name.
244
     * @param bool $refresh whether to fetch the latest available table names. If this is false, table names fetched
245
     * previously (if available) will be returned.
246
     *
247
     * @throws NotSupportedException
248
     *
249
     * @return string[] all table names in the database.
250
     */
251 23
    public function getTableNames(string $schema = '', bool $refresh = false): array
252
    {
253 23
        if (!isset($this->tableNames[$schema]) || $refresh) {
254 23
            $this->tableNames[$schema] = $this->findTableNames($schema);
255
        }
256
257 23
        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...
258
    }
259
260
    /**
261
     * @return QueryBuilder the query builder for this connection.
262
     */
263 941
    public function getQueryBuilder(): QueryBuilder
264
    {
265 941
        if ($this->builder === null) {
266 941
            $this->builder = $this->createQueryBuilder();
267
        }
268
269 941
        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...
270
    }
271
272
    /**
273
     * Determines the PDO type for the given PHP data value.
274
     *
275
     * @param mixed $data the data whose PDO type is to be determined
276
     *
277
     * @return int the PDO type
278
     *
279
     * {@see http://www.php.net/manual/en/pdo.constants.php}
280
     */
281 1336
    public function getPdoType($data): int
282
    {
283 1336
        static $typeMap = [
284
            // php type => PDO type
285
            'boolean' => PDO::PARAM_BOOL,
286
            'integer' => PDO::PARAM_INT,
287
            'string' => PDO::PARAM_STR,
288
            'resource' => PDO::PARAM_LOB,
289
            'NULL' => PDO::PARAM_NULL,
290
        ];
291
292 1336
        $type = gettype($data);
293
294 1336
        return $typeMap[$type] ?? PDO::PARAM_STR;
295
    }
296
297
    /**
298
     * Refreshes the schema.
299
     *
300
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
301
     * schema change.
302
     */
303 96
    public function refresh(): void
304
    {
305 96
        if ($this->schemaCache->isEnabled()) {
306 96
            $this->schemaCache->invalidate($this->getCacheTag());
307
        }
308
309 96
        $this->tableNames = [];
310 96
        $this->tableMetadata = [];
311 96
    }
312
313
    /**
314
     * Refreshes the particular table schema.
315
     *
316
     * This method cleans up cached table schema so that it can be re-created later to reflect the database schema
317
     * change.
318
     *
319
     * @param string $name table name.
320
     *
321
     * @throws JsonException
322
     */
323 99
    public function refreshTableSchema(string $name): void
324
    {
325 99
        $rawName = $this->getRawTableName($name);
326
327 99
        unset($this->tableMetadata[$rawName]);
328
329 99
        $this->tableNames = [];
330
331 99
        if ($this->schemaCache->isEnabled()) {
332 99
            $this->schemaCache->delete($this->getCacheKey($rawName));
333
        }
334 99
    }
335
336
    /**
337
     * Returns the ID of the last inserted row or sequence value.
338
     *
339
     * @param string $sequenceName name of the sequence object (required by some DBMS)
340
     *
341
     * @throws InvalidCallException if the DB connection is not active
342
     *
343
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
344
     *
345
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
346
     */
347 36
    public function getLastInsertID(string $sequenceName = ''): string
348
    {
349 36
        if ($this->db->isActive()) {
350 36
            return $this->db->getPDO()->lastInsertId(
351 36
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
352
            );
353
        }
354
355
        throw new InvalidCallException('DB Connection is not active.');
356
    }
357
358
    /**
359
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
360
     */
361 10
    public function supportsSavepoint(): bool
362
    {
363 10
        return $this->db->isSavepointEnabled();
364
    }
365
366
    /**
367
     * Creates a new savepoint.
368
     *
369
     * @param string $name the savepoint name
370
     *
371
     * @throws Exception|InvalidConfigException|Throwable
372
     */
373 4
    public function createSavepoint(string $name): void
374
    {
375 4
        $this->db->createCommand("SAVEPOINT $name")->execute();
376 4
    }
377
378
    /**
379
     * Releases an existing savepoint.
380
     *
381
     * @param string $name the savepoint name
382
     *
383
     * @throws Exception|InvalidConfigException|Throwable
384
     */
385
    public function releaseSavepoint(string $name): void
386
    {
387
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
388
    }
389
390
    /**
391
     * Rolls back to a previously created savepoint.
392
     *
393
     * @param string $name the savepoint name
394
     *
395
     * @throws Exception|InvalidConfigException|Throwable
396
     */
397 4
    public function rollBackSavepoint(string $name): void
398
    {
399 4
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
400 4
    }
401
402
    /**
403
     * Sets the isolation level of the current transaction.
404
     *
405
     * @param string $level The transaction isolation level to use for this transaction.
406
     *
407
     * This can be one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
408
     * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS
409
     * specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
410
     *
411
     * @throws Exception|InvalidConfigException|Throwable
412
     *
413
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
414
     */
415 8
    public function setTransactionIsolationLevel(string $level): void
416
    {
417 8
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
418 8
    }
419
420
    /**
421
     * Executes the INSERT command, returning primary key values.
422
     *
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
     *
426
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
427
     *
428
     * @return array|false primary key values or false if the command fails.
429
     */
430 28
    public function insert(string $table, array $columns)
431
    {
432 28
        $command = $this->db->createCommand()->insert($table, $columns);
433
434 28
        if (!$command->execute()) {
435
            return false;
436
        }
437
438 28
        $tableSchema = $this->getTableSchema($table);
439 28
        $result = [];
440
441 28
        foreach ($tableSchema->getPrimaryKey() as $name) {
442 26
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
443 24
                $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

443
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
444 24
                break;
445
            }
446
447 4
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
448
        }
449
450 28
        return $result;
451
    }
452
453
    /**
454
     * Quotes a string value for use in a query.
455
     *
456
     * Note that if the parameter is not a string, it will be returned without change.
457
     *
458
     * @param int|string $str string to be quoted.
459
     *
460
     * @throws Exception|InvalidConfigException
461
     *
462
     * @return int|string the properly quoted string.
463
     *
464
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
465
     */
466 1411
    public function quoteValue($str)
467
    {
468 1411
        if (!is_string($str)) {
469 6
            return $str;
470
        }
471
472 1411
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
473 1411
            return $value;
474
        }
475
476
        /** the driver doesn't support quote (e.g. oci) */
477
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
478
    }
479
480
    /**
481
     * Quotes a table name for use in a query.
482
     *
483
     * If the table name contains schema prefix, the prefix will also be properly quoted. If the table name is already
484
     * quoted or contains '(' or '{{', then this method will do nothing.
485
     *
486
     * @param string $name table name.
487
     *
488
     * @return string the properly quoted table name.
489
     *
490
     * {@see quoteSimpleTableName()}
491
     */
492 1544
    public function quoteTableName(string $name): string
493
    {
494 1544
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
495 4
            return $name;
496
        }
497
498 1544
        if (strpos($name, '{{') !== false) {
499 146
            return $name;
500
        }
501
502 1520
        if (strpos($name, '.') === false) {
503 1438
            return $this->quoteSimpleTableName($name);
504
        }
505
506 280
        $parts = $this->getTableNameParts($name);
507
508 280
        foreach ($parts as $i => $part) {
509 280
            $parts[$i] = $this->quoteSimpleTableName($part);
510
        }
511
512 280
        return implode('.', $parts);
513
    }
514
515
    /**
516
     * Splits full table name into parts
517
     *
518
     * @param string $name
519
     *
520
     * @return array
521
     */
522 12
    protected function getTableNameParts(string $name): array
523
    {
524 12
        return explode('.', $name);
525
    }
526
527
    /**
528
     * Quotes a column name for use in a query.
529
     *
530
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
531
     * or contains '(', '[[' or '{{', then this method will do nothing.
532
     *
533
     * @param string $name column name.
534
     *
535
     * @return string the properly quoted column name.
536
     *
537
     * {@see quoteSimpleColumnName()}
538
     */
539 1674
    public function quoteColumnName(string $name): string
540
    {
541 1674
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
542 149
            return $name;
543
        }
544
545 1659
        if (($pos = strrpos($name, '.')) !== false) {
546 232
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
547 232
            $name = substr($name, $pos + 1);
548
        } else {
549 1649
            $prefix = '';
550
        }
551
552 1659
        if (strpos($name, '{{') !== false) {
553 4
            return $name;
554
        }
555
556 1659
        return $prefix . $this->quoteSimpleColumnName($name);
557
    }
558
559
    /**
560
     * Quotes a simple table name for use in a query.
561
     *
562
     * A simple table name should contain the table name only without any schema prefix. If the table name is already
563
     * quoted, this method will do nothing.
564
     *
565
     * @param string $name table name.
566
     *
567
     * @return string the properly quoted table name.
568
     */
569 1316
    public function quoteSimpleTableName(string $name): string
570
    {
571 1316
        if (is_string($this->tableQuoteCharacter)) {
572 965
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
573
        } else {
574 351
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
575
        }
576
577 1316
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
578
    }
579
580
    /**
581
     * Quotes a simple column name for use in a query.
582
     *
583
     * A simple column name should contain the column name only without any prefix. If the column name is already quoted
584
     * or is the asterisk character '*', this method will do nothing.
585
     *
586
     * @param string $name column name.
587
     *
588
     * @return string the properly quoted column name.
589
     */
590 1659
    public function quoteSimpleColumnName(string $name): string
591
    {
592 1659
        if (is_string($this->columnQuoteCharacter)) {
593 1338
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
594
        } else {
595 321
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
596
        }
597
598 1659
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
599 1659
            . $endingCharacter;
600
    }
601
602
    /**
603
     * Unquotes a simple table name.
604
     *
605
     * A simple table name should contain the table name only without any schema prefix. If the table name is not
606
     * quoted, this method will do nothing.
607
     *
608
     * @param string $name table name.
609
     *
610
     * @return string unquoted table name.
611
     */
612 5
    public function unquoteSimpleTableName(string $name): string
613
    {
614 5
        if (is_string($this->tableQuoteCharacter)) {
615 5
            $startingCharacter = $this->tableQuoteCharacter;
616
        } else {
617
            $startingCharacter = $this->tableQuoteCharacter[0];
618
        }
619
620 5
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
621
    }
622
623
    /**
624
     * Unquotes a simple column name.
625
     *
626
     * A simple column name should contain the column name only without any prefix. If the column name is not quoted or
627
     * is the asterisk character '*', this method will do nothing.
628
     *
629
     * @param string $name column name.
630
     *
631
     * @return string unquoted column name.
632
     */
633
    public function unquoteSimpleColumnName(string $name): string
634
    {
635
        if (is_string($this->columnQuoteCharacter)) {
636
            $startingCharacter = $this->columnQuoteCharacter;
637
        } else {
638
            $startingCharacter = $this->columnQuoteCharacter[0];
639
        }
640
641
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
642
    }
643
644
    /**
645
     * Returns the actual name of a given table name.
646
     *
647
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
648
     * {@see ConnectionInterface::tablePrefix}.
649
     *
650
     * @param string $name the table name to be converted.
651
     *
652
     * @return string the real name of the given table name.
653
     */
654 1595
    public function getRawTableName(string $name): string
655
    {
656 1595
        if (strpos($name, '{{') !== false) {
657 130
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
658
659 130
            return str_replace('%', $this->db->getTablePrefix(), $name);
660
        }
661
662 1595
        return $name;
663
    }
664
665
    /**
666
     * Extracts the PHP type from abstract DB type.
667
     *
668
     * @param ColumnSchema $column the column schema information.
669
     *
670
     * @return string PHP type name.
671
     */
672 1254
    protected function getColumnPhpType(ColumnSchema $column): string
673
    {
674 1254
        static $typeMap = [
675
            // abstract type => php type
676
            self::TYPE_TINYINT => 'integer',
677
            self::TYPE_SMALLINT => 'integer',
678
            self::TYPE_INTEGER => 'integer',
679
            self::TYPE_BIGINT => 'integer',
680
            self::TYPE_BOOLEAN => 'boolean',
681
            self::TYPE_FLOAT => 'double',
682
            self::TYPE_DOUBLE => 'double',
683
            self::TYPE_BINARY => 'resource',
684
            self::TYPE_JSON => 'array',
685
        ];
686
687 1254
        if (isset($typeMap[$column->getType()])) {
688 1245
            if ($column->getType() === 'bigint') {
689 42
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
690
            }
691
692 1245
            if ($column->getType() === 'integer') {
693 1245
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
694
            }
695
696 347
            return $typeMap[$column->getType()];
697
        }
698
699 1201
        return 'string';
700
    }
701
702
    /**
703
     * Converts a DB exception to a more concrete one if possible.
704
     *
705
     * @param \Exception $e
706
     * @param string $rawSql SQL that produced exception.
707
     *
708
     * @return Exception
709
     */
710 44
    public function convertException(\Exception $e, string $rawSql): Exception
711
    {
712 44
        if ($e instanceof Exception) {
713
            return $e;
714
        }
715
716 44
        $exceptionClass = Exception::class;
717
718 44
        foreach ($this->exceptionMap as $error => $class) {
719 44
            if (strpos($e->getMessage(), $error) !== false) {
720 11
                $exceptionClass = $class;
721
            }
722
        }
723
724 44
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
725 44
        $errorInfo = $e instanceof PDOException ? $e->errorInfo : null;
726
727 44
        return new $exceptionClass($message, $errorInfo, $e);
728
    }
729
730
    /**
731
     * Returns a value indicating whether a SQL statement is for read purpose.
732
     *
733
     * @param string $sql the SQL statement.
734
     *
735
     * @return bool whether a SQL statement is for read purpose.
736
     */
737 12
    public function isReadQuery(string $sql): bool
738
    {
739 12
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
740
741 12
        return preg_match($pattern, $sql) > 0;
742
    }
743
744
    /**
745
     * Returns a server version as a string comparable by {@see version_compare()}.
746
     *
747
     * @throws Exception|InvalidConfigException
748
     *
749
     * @return string server version as a string.
750
     */
751 367
    public function getServerVersion(): string
752
    {
753 367
        if ($this->serverVersion === null) {
754 367
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
755
        }
756
757 367
        return $this->serverVersion;
758
    }
759
760
    /**
761
     * Returns the cache key for the specified table name.
762
     *
763
     * @param string $name the table name.
764
     *
765
     * @throws JsonException
766
     *
767
     * @return string the cache key.
768
     */
769 1595
    protected function getCacheKey(string $name): string
770
    {
771
        $key = [
772 1595
            __CLASS__,
773 1595
            $this->db->getDsn(),
774 1595
            $this->db->getUsername(),
775 1595
            $this->getRawTableName($name),
776
        ];
777
778 1595
        return $this->schemaCache->normalize($key);
779
    }
780
781
    /**
782
     * Returns the cache tag name.
783
     *
784
     * This allows {@see refresh()} to invalidate all cached table schemas.
785
     *
786
     * @return string the cache tag name.
787
     */
788 1535
    protected function getCacheTag(): string
789
    {
790 1535
        return md5(serialize([
791 1535
            __CLASS__,
792 1535
            $this->db->getDsn(),
793 1535
            $this->db->getUsername(),
794
        ]));
795
    }
796
797
    /**
798
     * Returns the metadata of the given type for the given table.
799
     *
800
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
801
     * table name to obtain the metadata.
802
     *
803
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
804
     * @param string $type metadata type.
805
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
806
     *
807
     * @throws JsonException
808
     *
809
     * @return mixed metadata.
810
     */
811 1595
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
812
    {
813 1595
        $rawName = $this->getRawTableName($name);
814
815 1595
        if (!isset($this->tableMetadata[$rawName])) {
816 1595
            $this->loadTableMetadataFromCache($rawName);
817
        }
818
819 1595
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
820 1595
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
821 1535
            $this->saveTableMetadataToCache($rawName);
822
        }
823
824 1535
        return $this->tableMetadata[$rawName][$type];
825
    }
826
827
    /**
828
     * Returns the metadata of the given type for all tables in the given schema.
829
     *
830
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
831
     * obtain the metadata.
832
     *
833
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
834
     * name.
835
     * @param string $type metadata type.
836
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
837
     * returned if available.
838
     *
839
     * @throws NotSupportedException
840
     *
841
     * @return array array of metadata.
842
     */
843 13
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
844
    {
845 13
        $metadata = [];
846 13
        $methodName = 'getTable' . ucfirst($type);
847
848 13
        foreach ($this->getTableNames($schema, $refresh) as $name) {
849 13
            if ($schema !== '') {
850
                $name = $schema . '.' . $name;
851
            }
852
853 13
            $tableMetadata = $this->$methodName($name, $refresh);
854
855 13
            if ($tableMetadata !== null) {
856 13
                $metadata[] = $tableMetadata;
857
            }
858
        }
859
860 13
        return $metadata;
861
    }
862
863
    /**
864
     * Sets the metadata of the given type for the given table.
865
     *
866
     * @param string $name table name.
867
     * @param string $type metadata type.
868
     * @param mixed  $data metadata.
869
     */
870 295
    protected function setTableMetadata(string $name, string $type, $data): void
871
    {
872 295
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
873 295
    }
874
875
    /**
876
     * Changes row's array key case to lower if PDO's one is set to uppercase.
877
     *
878
     * @param array $row row's array or an array of row's arrays.
879
     * @param bool $multiple whether multiple rows or a single row passed.
880
     *
881
     * @throws Exception|InvalidConfigException
882
     *
883
     * @return array normalized row or rows.
884
     */
885 339
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
886
    {
887 339
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
888 263
            return $row;
889
        }
890
891 76
        if ($multiple) {
892 76
            return array_map(static function (array $row) {
893 73
                return array_change_key_case($row, CASE_LOWER);
894 76
            }, $row);
895
        }
896
897
        return array_change_key_case($row, CASE_LOWER);
898
    }
899
900
    /**
901
     * Tries to load and populate table metadata from cache.
902
     *
903
     * @param string $rawName
904
     *
905
     * @throws JsonException
906
     */
907 1595
    private function loadTableMetadataFromCache(string $rawName): void
908
    {
909 1595
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
910
            $this->tableMetadata[$rawName] = [];
911
912
            return;
913
        }
914
915 1595
        $metadata = $this->schemaCache->get($this->getCacheKey($rawName));
916
917
        if (
918 1595
            !is_array($metadata) ||
919 1595
            !isset($metadata['cacheVersion']) ||
920 1595
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
921
        ) {
922 1595
            $this->tableMetadata[$rawName] = [];
923
924 1595
            return;
925
        }
926
927 501
        unset($metadata['cacheVersion']);
928 501
        $this->tableMetadata[$rawName] = $metadata;
929 501
    }
930
931
    /**
932
     * Saves table metadata to cache.
933
     *
934
     * @param string $rawName
935
     *
936
     * @throws JsonException
937
     */
938 1535
    private function saveTableMetadataToCache(string $rawName): void
939
    {
940 1535
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
941
            return;
942
        }
943
944 1535
        $metadata = $this->tableMetadata[$rawName];
945
946 1535
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
947
948 1535
        $this->schemaCache->set(
949 1535
            $this->getCacheKey($rawName),
950
            $metadata,
951 1535
            $this->schemaCache->getDuration(),
952 1535
            new TagDependency(['tags' => $this->getCacheTag()]),
953
        );
954 1535
    }
955
956 1640
    public function getDb(): Connection
957
    {
958 1640
        return $this->db;
959
    }
960
961
    public function getDefaultSchema(): ?string
962
    {
963
        return $this->defaultSchema;
964
    }
965
}
966