Passed
Push — master ( 9f7d35...3f1c7e )
by Wilmer
08:50 queued 06:32
created

Schema::quoteValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use 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 1292
    public function __construct(Connection $db)
98
    {
99 1292
        $this->db = $db;
100 1292
        $this->cache = $this->db->getSchemaCache();
101 1292
    }
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
     */
168 349
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
169
    {
170 349
        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
     *
181
     * @return TableSchema[] the metadata for all tables in the database. Each array element is an instance of
182
     * {@see TableSchema} or its child class.
183
     */
184 10
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
185
    {
186 10
        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
     */
197 3
    public function getSchemaNames(bool $refresh = false): array
198
    {
199 3
        if (empty($this->schemaNames) || $refresh) {
200 3
            $this->schemaNames = $this->findSchemaNames();
201
        }
202
203 3
        return $this->schemaNames;
204
    }
205
206
    /**
207
     * Returns all table names in the database.
208
     *
209
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema
210
     * name.
211
     * If not empty, the returned table names will be prefixed with the schema name.
212
     * @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
     * @return string[] all table names in the database.
216
     */
217 18
    public function getTableNames(string $schema = '', bool $refresh = false): array
218
    {
219 18
        if (!isset($this->tableNames[$schema]) || $refresh) {
220 18
            $this->tableNames[$schema] = $this->findTableNames($schema);
221
        }
222
223 18
        return $this->tableNames[$schema];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tableNames[$schema] returns the type string which is incompatible with the type-hinted return array.
Loading history...
224
    }
225
226
    /**
227
     * @return QueryBuilder the query builder for this connection.
228
     */
229 225
    public function getQueryBuilder(): QueryBuilder
230
    {
231 225
        if ($this->builder === null) {
232 225
            $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 225
        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
     * @param mixed $data the data whose PDO type is to be determined
242
     *
243
     * @return int the PDO type
244
     *
245
     * {@see http://www.php.net/manual/en/pdo.constants.php}
246
     */
247 466
    public function getPdoType($data): int
248
    {
249 466
        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 466
        $type = gettype($data);
258
259 466
        return $typeMap[$type] ?? \PDO::PARAM_STR;
260
    }
261
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
    {
270
        /* @var $cache CacheInterface */
271
        $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 79
    public function refreshTableSchema(string $name): void
290
    {
291 79
        $rawName = $this->getRawTableName($name);
292
293 79
        unset($this->tableMetadata[$rawName]);
294
295 79
        $this->tableNames = [];
296
297 79
        if ($this->db->isSchemaCacheEnabled() && $this->cache instanceof CacheInterface) {
298 79
            $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 79
    }
301
302
    /**
303
     * Returns the ID of the last inserted row or sequence value.
304
     *
305
     * @param string $sequenceName name of the sequence object (required by some DBMS)
306
     *
307
     * @throws InvalidCallException if the DB connection is not active
308
     *
309
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
310
     *
311
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
312
     */
313 12
    public function getLastInsertID(string $sequenceName = ''): string
314
    {
315 12
        if ($this->db->isActive()) {
316 12
            return $this->db->getPDO()->lastInsertId(
317 12
                $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 8
    public function supportsSavepoint(): bool
328
    {
329 8
        return $this->db->isSavepointEnabled();
330
    }
331
332
    /**
333
     * Creates a new savepoint.
334
     *
335
     * @param string $name the savepoint name
336
     */
337 3
    public function createSavepoint(string $name): void
338
    {
339 3
        $this->db->createCommand("SAVEPOINT $name")->execute();
340 3
    }
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 3
    public function rollBackSavepoint(string $name): void
358
    {
359 3
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
360 3
    }
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 9
    public function setTransactionIsolationLevel(string $level): void
374
    {
375 9
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
376 9
    }
377
378
    /**
379
     * Executes the INSERT command, returning primary key values.
380
     *
381
     * @param string $table the table that new rows will be inserted into.
382
     * @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
        }
393
394
        $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
403
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
404
        }
405
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 525
    public function quoteValue($str)
421
    {
422 525
        if (!is_string($str)) {
423 5
            return $str;
424
        }
425
426 525
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
427 525
            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
     * quoted or contains '(' or '{{', then this method will do nothing.
439
     *
440
     * @param string $name table name.
441
     *
442
     * @return string the properly quoted table name.
443
     *
444
     * {@see quoteSimpleTableName()}
445
     */
446 624
    public function quoteTableName(string $name): string
447
    {
448 624
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
449 3
            return $name;
450
        }
451
452 624
        if (strpos($name, '{{') !== false) {
453 67
            return $name;
454
        }
455
456 608
        if (strpos($name, '.') === false) {
457 562
            return $this->quoteSimpleTableName($name);
458
        }
459
460 109
        $parts = $this->getTableNameParts($name);
461
462 109
        foreach ($parts as $i => $part) {
463 109
            $parts[$i] = $this->quoteSimpleTableName($part);
464
        }
465
466 109
        return implode('.', $parts);
467
    }
468
469
    /**
470
     * Splits full table name into parts
471
     *
472
     * @param string $name
473
     *
474
     * @return array
475
     */
476 11
    protected function getTableNameParts(string $name): array
477
    {
478 11
        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
     * or contains '(', '[[' or '{{', then this method will do nothing.
486
     *
487
     * @param string $name column name.
488
     *
489
     * @return string the properly quoted column name.
490
     *
491
     * {@see quoteSimpleColumnName()}
492
     */
493 815
    public function quoteColumnName(string $name): string
494
    {
495 815
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
496 47
            return $name;
497
        }
498
499 804
        if (($pos = strrpos($name, '.')) !== false) {
500 24
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
501 24
            $name = substr($name, $pos + 1);
502
        } else {
503 800
            $prefix = '';
504
        }
505
506 804
        if (strpos($name, '{{') !== false) {
507 3
            return $name;
508
        }
509
510 804
        return $prefix . $this->quoteSimpleColumnName($name);
511
    }
512
513
    /**
514
     * 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
     * quoted, this method will do nothing.
518
     *
519
     * @param string $name table name.
520
     *
521
     * @return string the properly quoted table name.
522
     */
523 634
    public function quoteSimpleTableName(string $name): string
524
    {
525 634
        if (is_string($this->tableQuoteCharacter)) {
526 456
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
527
        } else {
528 178
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
529
        }
530
531 634
        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
     * 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
     *
540
     * @param string $name column name.
541
     *
542
     * @return string the properly quoted column name.
543
     */
544 804
    public function quoteSimpleColumnName(string $name): string
545
    {
546 804
        if (is_string($this->columnQuoteCharacter)) {
547 613
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
548
        } else {
549 191
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
550
        }
551
552 804
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
553 804
            . $endingCharacter;
554
    }
555
556
    /**
557
     * 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
     * quoted, this method will do nothing.
561
     *
562
     * @param string $name table name.
563
     *
564
     * @return string unquoted table name.
565
     */
566 5
    public function unquoteSimpleTableName(string $name): string
567
    {
568 5
        if (\is_string($this->tableQuoteCharacter)) {
569 5
            $startingCharacter = $this->tableQuoteCharacter;
570
        } else {
571
            $startingCharacter = $this->tableQuoteCharacter[0];
572
        }
573
574 5
        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
     *
585
     * @return string unquoted column name.
586
     */
587
    public function unquoteSimpleColumnName(string $name): string
588
    {
589
        if (\is_string($this->columnQuoteCharacter)) {
590
            $startingCharacter = $this->columnQuoteCharacter;
591
        } else {
592
            $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
     *
606
     * @return string the real name of the given table name.
607
     */
608 589
    public function getRawTableName(string $name): string
609
    {
610 589
        if (strpos($name, '{{') !== false) {
611 92
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
612
613 92
            return str_replace('%', $this->db->getTablePrefix(), $name);
614
        }
615
616 589
        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 327
    protected function getColumnPhpType(ColumnSchema $column): string
627
    {
628 327
        static $typeMap = [
629
            // abstract type => php type
630
            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
            self::TYPE_FLOAT    => 'double',
636
            self::TYPE_DOUBLE   => 'double',
637
            self::TYPE_BINARY   => 'resource',
638
            self::TYPE_JSON     => 'array',
639
        ];
640
641 327
        if (isset($typeMap[$column->getType()])) {
642 321
            if ($column->getType() === 'bigint') {
643 40
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
644
            }
645
646 321
            if ($column->getType() === 'integer') {
647 321
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
648
            }
649
650 191
            return $typeMap[$column->getType()];
651
        }
652
653 299
        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 31
    public function convertException(\Exception $e, string $rawSql): Exception
665
    {
666 31
        if ($e instanceof Exception) {
667
            return $e;
668
        }
669
670 31
        $exceptionClass = Exception::class;
671
672 31
        foreach ($this->exceptionMap as $error => $class) {
673 31
            if (strpos($e->getMessage(), $error) !== false) {
674 9
                $exceptionClass = $class;
675
            }
676
        }
677
678 31
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
679 31
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
680
681 31
        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
     * @param string $sql the SQL statement.
688
     *
689
     * @return bool whether a SQL statement is for read purpose.
690
     */
691 9
    public function isReadQuery($sql): bool
692
    {
693 9
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
694
695 9
        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
     */
703 163
    public function getServerVersion(): string
704
    {
705 163
        if ($this->serverVersion === null) {
706 163
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
707
        }
708
709 163
        return $this->serverVersion;
710
    }
711
712
    /**
713
     * Returns the cache key for the specified table name.
714
     *
715
     * @param string $name the table name.
716
     *
717
     * @return mixed the cache key.
718
     */
719 589
    protected function getCacheKey($name)
720
    {
721
        return [
722 589
            __CLASS__,
723 589
            $this->db->getDsn(),
724 589
            $this->db->getUsername(),
725 589
            $this->getRawTableName($name),
726
        ];
727
    }
728
729
    /**
730
     * Returns the cache tag name.
731
     *
732
     * This allows {@see refresh()} to invalidate all cached table schemas.
733
     *
734
     * @return string the cache tag name.
735
     */
736 541
    protected function getCacheTag(): string
737
    {
738 541
        return md5(serialize([
739 541
            __CLASS__,
740 541
            $this->db->getDsn(),
741 541
            $this->db->getUsername(),
742
        ]));
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
     * @param string $type metadata type.
753
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
754
     *
755
     * @return mixed metadata.
756
     */
757 589
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
758
    {
759 589
        if ($this->db->isSchemaCacheEnabled() && !\in_array($name, $this->db->getSchemaCacheExclude(), true)) {
760 589
            $schemaCache = $this->cache;
761
        }
762
763 589
        $rawName = $this->getRawTableName($name);
764
765 589
        if (!isset($this->tableMetadata[$rawName])) {
766 589
            $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
        }
768
769 589
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
770 589
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
771 541
            $this->saveTableMetadataToCache($schemaCache, $rawName);
772
        }
773
774 541
        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
     * 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
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
784
     * name.
785
     * @param string $type metadata type.
786
     * @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 10
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
792
    {
793 10
        $metadata = [];
794 10
        $methodName = 'getTable' . ucfirst($type);
795
796 10
        foreach ($this->getTableNames($schema, $refresh) as $name) {
797 10
            if ($schema !== '') {
798
                $name = $schema . '.' . $name;
799
            }
800
801 10
            $tableMetadata = $this->$methodName($name, $refresh);
802
803 10
            if ($tableMetadata !== null) {
804 10
                $metadata[] = $tableMetadata;
805
            }
806
        }
807
808 10
        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 235
    protected function setTableMetadata(string $name, string $type, $data): void
819
    {
820 235
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
821 235
    }
822
823
    /**
824
     * Changes row's array key case to lower if PDO's one is set to uppercase.
825
     *
826
     * @param array $row row's array or an array of row's arrays.
827
     * @param bool $multiple whether multiple rows or a single row passed.
828
     *
829
     * @return array normalized row or rows.
830
     */
831 269
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
832
    {
833 269
        if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
834 209
            return $row;
835
        }
836
837 60
        if ($multiple) {
838 60
            return \array_map(function (array $row) {
839 58
                return \array_change_key_case($row, CASE_LOWER);
840 60
            }, $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 589
    private function loadTableMetadataFromCache(?CacheInterface $cache, string $name): void
853
    {
854 589
        if ($cache === null) {
855
            $this->tableMetadata[$name] = [];
856
857
            return;
858
        }
859
860 589
        $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 589
        if (!\is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
863 589
            $this->tableMetadata[$name] = [];
864
865 589
            return;
866
        }
867
868
        unset($metadata['cacheVersion']);
869
        $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 541
    private function saveTableMetadataToCache(?CacheInterface $cache, string $name): void
879
    {
880 541
        if ($cache === null) {
881
            return;
882
        }
883
884 541
        $metadata = $this->tableMetadata[$name];
885
886 541
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
887
888 541
        $cache->set(
889 541
            $this->getCacheKey($name),
890
            $metadata,
891 541
            $this->db->getSchemaCacheDuration(),
892 541
            new TagDependency(['tags' => $this->getCacheTag()])
893
        );
894 541
    }
895
896 609
    public function getDb(): ?Connection
897
    {
898 609
        return $this->db;
899
    }
900
901
    public function getDefaultSchema(): ?string
902
    {
903
        return $this->defaultSchema;
904
    }
905
}
906