Passed
Push — master ( a8d37b...dabdd0 )
by Wilmer
10:00
created

Schema::getQueryBuilder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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