Passed
Pull Request — master (#163)
by Wilmer
12:24
created

Schema::createColumnSchemaBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 Yiisoft\Db\Connection\Connection;
8
use Yiisoft\Db\Exception\Exception;
9
use Yiisoft\Db\Exception\IntegrityException;
10
use Yiisoft\Db\Exception\InvalidCallException;
11
use Yiisoft\Db\Exception\NotSupportedException;
12
use Yiisoft\Db\Query\QueryBuilder;
13
use Yiisoft\Cache\CacheInterface;
14
use Yiisoft\Cache\Dependency\TagDependency;
15
16
/**
17
 * Schema is the base class for concrete DBMS-specific schema classes.
18
 *
19
 * Schema represents the database schema information that is DBMS specific.
20
 *
21
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
22
 * object. This property is read-only.
23
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
24
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is read-only.
25
 * @property string $serverVersion Server version as a string. This property is read-only.
26
 * @property string[] $tableNames All table names in the database. This property is read-only.
27
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an instance
28
 * of {@see TableSchema} or its child class. This property is read-only.
29
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
30
 * one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
31
 * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS specific
32
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
33
 */
34
abstract class Schema
35
{
36
    public const TYPE_PK = 'pk';
37
    public const TYPE_UPK = 'upk';
38
    public const TYPE_BIGPK = 'bigpk';
39
    public const TYPE_UBIGPK = 'ubigpk';
40
    public const TYPE_CHAR = 'char';
41
    public const TYPE_STRING = 'string';
42
    public const TYPE_TEXT = 'text';
43
    public const TYPE_TINYINT = 'tinyint';
44
    public const TYPE_SMALLINT = 'smallint';
45
    public const TYPE_INTEGER = 'integer';
46
    public const TYPE_BIGINT = 'bigint';
47
    public const TYPE_FLOAT = 'float';
48
    public const TYPE_DOUBLE = 'double';
49
    public const TYPE_DECIMAL = 'decimal';
50
    public const TYPE_DATETIME = 'datetime';
51
    public const TYPE_TIMESTAMP = 'timestamp';
52
    public const TYPE_TIME = 'time';
53
    public const TYPE_DATE = 'date';
54
    public const TYPE_BINARY = 'binary';
55
    public const TYPE_BOOLEAN = 'boolean';
56
    public const TYPE_MONEY = 'money';
57
    public const TYPE_JSON = 'json';
58
59
    /**
60
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
61
     */
62
    protected const SCHEMA_CACHE_VERSION = 1;
63
64
    /**
65
     * @var string|null the default schema name used for the current session.
66
     */
67
    protected ?string $defaultSchema = null;
68
69
    /**
70
     * @var array map of DB errors and corresponding exceptions. If left part is found in DB error message exception
71
     * class from the right part is used.
72
     */
73
    protected array $exceptionMap = [
74
        'SQLSTATE[23' => IntegrityException::class,
75
    ];
76
77
    /**
78
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
79
     * case starting and ending characters are different.
80
     */
81
    protected $tableQuoteCharacter = "'";
82
83
    /**
84
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
85
     * and ending characters are different.
86
     */
87
    protected $columnQuoteCharacter = '"';
88
89
    private array $schemaNames = [];
90
    private array $tableNames = [];
91
    private array $tableMetadata = [];
92
    private ?QueryBuilder $builder = null;
93
    private ?string $serverVersion = null;
94
    private CacheInterface $cache;
95
    private ?Connection $db = null;
96
97 900
    public function __construct(Connection $db)
98
    {
99 900
        $this->db = $db;
100 900
        $this->cache = $this->db->getSchemaCache();
101 900
    }
102
103
    /**
104
     * Resolves the table name and schema name (if any).
105
     *
106
     * @param string $name the table name.
107
     *
108
     * @return void with resolved table, schema, etc. names.
109
     *
110
     * @throws NotSupportedException if this method is not supported by the DBMS.
111
     *
112
     * {@see \Yiisoft\Db\Schema\TableSchema}
113
     */
114
    protected function resolveTableName(string $name): TableSchema
115
    {
116
        throw new NotSupportedException(get_class($this) . ' does not support resolving table names.');
117
    }
118
119
    /**
120
     * Returns all schema names in the database, including the default one but not system schemas.
121
     *
122
     * This method should be overridden by child classes in order to support this feature because the default
123
     * implementation simply throws an exception.
124
     *
125
     * @return void all schema names in the database, except system schemas.
126
     *
127
     * @throws NotSupportedException if this method is not supported by the DBMS.
128
     */
129
    protected function findSchemaNames()
130
    {
131
        throw new NotSupportedException(get_class($this) . ' does not support fetching all schema names.');
132
    }
133
134
    /**
135
     * Returns all table names in the database.
136
     *
137
     * This method should be overridden by child classes in order to support this feature because the default
138
     * implementation simply throws an exception.
139
     *
140
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
141
     *
142
     * @return void all table names in the database. The names have NO schema name prefix.
143
     *
144
     * @throws NotSupportedException if this method is not supported by the DBMS.
145
     */
146
    protected function findTableNames(string $schema = ''): array
147
    {
148
        throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.');
149
    }
150
151
    /**
152
     * Loads the metadata for the specified table.
153
     *
154
     * @param string $name table name.
155
     *
156
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
157
     */
158
    abstract protected function loadTableSchema(string $name): ?TableSchema;
159
160
    /**
161
     * Obtains the metadata for the named table.
162
     *
163
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
164
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
165
     *
166
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
167 75
     */
168
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
169 75
    {
170
        return $this->getTableMetadata($name, 'schema', $refresh);
171
    }
172
173
    /**
174
     * Returns the metadata for all tables in the database.
175
     *
176
     * @param string $schema  the schema of the tables. Defaults to empty string, meaning the current or default schema
177
     * name.
178
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, cached data may be
179
     * returned if available.
180 263
     *
181
     * @return TableSchema[] the metadata for all tables in the database. Each array element is an instance of
182 263
     * {@see TableSchema} or its child class.
183
     */
184
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
185
    {
186
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
187
    }
188
189
    /**
190
     * Returns all schema names in the database, except system schemas.
191
     *
192
     * @param bool $refresh whether to fetch the latest available schema names. If this is false, schema names fetched
193
     * previously (if available) will be returned.
194
     *
195
     * @return string[] all schema names in the database, except system schemas.
196 9
     */
197
    public function getSchemaNames(bool $refresh = false): array
198 9
    {
199
        if (empty($this->schemaNames) || $refresh) {
200
            $this->schemaNames = $this->findSchemaNames();
201
        }
202
203
        return $this->schemaNames;
204
    }
205
206
    /**
207
     * Returns all table names in the database.
208
     *
209 2
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
210
     * name.
211 2
     * If not empty, the returned table names will be prefixed with the schema name.
212 2
     * @param bool $refresh whether to fetch the latest available table names. If this is false, table names fetched
213
     * previously (if available) will be returned.
214
     *
215 2
     * @return string[] all table names in the database.
216
     */
217
    public function getTableNames(string $schema = '', bool $refresh = false): array
218
    {
219
        if (!isset($this->tableNames[$schema]) || $refresh) {
220
            $this->tableNames[$schema] = $this->findTableNames($schema);
221
        }
222
223
        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...
224
    }
225
226
    /**
227
     * @return QueryBuilder the query builder for this connection.
228
     */
229 15
    public function getQueryBuilder(): QueryBuilder
230
    {
231 15
        if ($this->builder === null) {
232 15
            $this->builder = $this->createQueryBuilder();
0 ignored issues
show
Bug introduced by
The method createQueryBuilder() does not exist on Yiisoft\Db\Schema\Schema. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Schema\Schema. ( Ignorable by Annotation )

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

232
            /** @scrutinizer ignore-call */ 
233
            $this->builder = $this->createQueryBuilder();
Loading history...
233
        }
234
235 15
        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...
236
    }
237
238
    /**
239
     * Determines the PDO type for the given PHP data value.
240
     *
241 179
     * @param mixed $data the data whose PDO type is to be determined
242
     *
243 179
     * @return int the PDO type
244 179
     *
245
     * {@see http://www.php.net/manual/en/pdo.constants.php}
246
     */
247 179
    public function getPdoType($data): int
248
    {
249
        static $typeMap = [
250
            // php type => PDO type
251
            'boolean'  => \PDO::PARAM_BOOL,
252
            'integer'  => \PDO::PARAM_INT,
253
            'string'   => \PDO::PARAM_STR,
254
            'resource' => \PDO::PARAM_LOB,
255
            'NULL'     => \PDO::PARAM_NULL,
256
        ];
257
        $type = gettype($data);
258
259 308
        return $typeMap[$type] ?? \PDO::PARAM_STR;
260
    }
261 308
262
    /**
263
     * Refreshes the schema.
264
     *
265
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
266
     * schema change.
267
     */
268
    public function refresh(): void
269 308
    {
270
        /* @var $cache CacheInterface */
271 308
        $cache = \is_string($this->db->getSchemaCache()) ? $this->cache : $this->db->getSchemaCache();
0 ignored issues
show
Bug introduced by
The method getSchemaCache() does not exist on null. ( Ignorable by Annotation )

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

271
        $cache = \is_string($this->db->/** @scrutinizer ignore-call */ getSchemaCache()) ? $this->cache : $this->db->getSchemaCache();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
introduced by
The condition is_string($this->db->getSchemaCache()) is always false.
Loading history...
272
273
        if ($this->db->isSchemaCacheEnabled() && $cache instanceof CacheInterface) {
274
            TagDependency::invalidate($cache, $this->getCacheTag());
275
        }
276
277
        $this->tableNames = [];
278
        $this->tableMetadata = [];
279
    }
280
281
    /**
282
     * Refreshes the particular table schema.
283
     *
284
     * This method cleans up cached table schema so that it can be re-created later to reflect the database schema
285
     * change.
286
     *
287
     * @param string $name table name.
288
     */
289
    public function refreshTableSchema(string $name): void
290
    {
291
        $rawName = $this->getRawTableName($name);
292
293
        unset($this->tableMetadata[$rawName]);
294
295
        $this->tableNames = [];
296
297
        if ($this->db->isSchemaCacheEnabled() && $this->cache instanceof CacheInterface) {
298
            $this->cache->delete($this->getCacheKey($rawName));
0 ignored issues
show
Bug introduced by
$this->getCacheKey($rawName) of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::delete(). ( Ignorable by Annotation )

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

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

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

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

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