Passed
Pull Request — master (#240)
by Wilmer
12:51
created

Schema::getSchemaCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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