Passed
Push — master ( d42673...c7102b )
by Wilmer
09:45 queued 08:06
created

Schema   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 489
Duplicated Lines 0 %

Test Coverage

Coverage 92.08%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 222
c 1
b 0
f 0
dl 0
loc 489
ccs 186
cts 202
cp 0.9208
rs 2.96
wmc 68

18 Methods

Rating   Name   Duplication   Size   Complexity  
A createQueryBuilder() 0 3 1
A loadTableUniques() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A loadTablePrimaryKey() 0 3 1
A findUniqueIndexes() 0 21 4
A loadTableIndexes() 0 3 1
A loadTableForeignKeys() 0 27 2
A setTransactionIsolationLevel() 0 12 3
A findColumns() 0 24 6
B loadTableChecks() 0 41 6
A findTableNames() 0 5 1
C loadColumnSchema() 0 50 17
A isSystemIdentifier() 0 3 1
A findConstraints() 0 13 3
A loadTableColumnsInfo() 0 9 1
F loadTableConstraints() 0 90 16
A loadTableSchema() 0 14 2
A loadTableDefaultValues() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\CheckConstraint;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Schema\Schema as AbstractSchema;
17
use Yiisoft\Db\Schema\ColumnSchema;
18
use Yiisoft\Db\Sqlite\Query\QueryBuilder;
19
use Yiisoft\Db\Sqlite\Token\SqlToken;
20
use Yiisoft\Db\Sqlite\Token\SqlTokenizer;
21
use Yiisoft\Db\Transaction\Transaction;
22
23
/**
24
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
25
 *
26
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction.
27
 * This can be either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
28
 */
29
class Schema extends AbstractSchema implements ConstraintFinderInterface
30
{
31
    use ConstraintFinderTrait;
32
33
    protected string $tableQuoteCharacter = '`';
34
    protected string $columnQuoteCharacter = '`';
35
36
    /**
37
     * @var array mapping from physical column types (keys) to abstract column types (values)
38
     */
39
    private array $typeMap = [
40
        'tinyint' => self::TYPE_TINYINT,
41
        'bit' => self::TYPE_SMALLINT,
42
        'boolean' => self::TYPE_BOOLEAN,
43
        'bool' => self::TYPE_BOOLEAN,
44
        'smallint' => self::TYPE_SMALLINT,
45
        'mediumint' => self::TYPE_INTEGER,
46
        'int' => self::TYPE_INTEGER,
47
        'integer' => self::TYPE_INTEGER,
48
        'bigint' => self::TYPE_BIGINT,
49
        'float' => self::TYPE_FLOAT,
50
        'double' => self::TYPE_DOUBLE,
51
        'real' => self::TYPE_FLOAT,
52
        'decimal' => self::TYPE_DECIMAL,
53
        'numeric' => self::TYPE_DECIMAL,
54
        'tinytext' => self::TYPE_TEXT,
55
        'mediumtext' => self::TYPE_TEXT,
56
        'longtext' => self::TYPE_TEXT,
57
        'text' => self::TYPE_TEXT,
58
        'varchar' => self::TYPE_STRING,
59
        'string' => self::TYPE_STRING,
60
        'char' => self::TYPE_CHAR,
61
        'blob' => self::TYPE_BINARY,
62
        'datetime' => self::TYPE_DATETIME,
63
        'year' => self::TYPE_DATE,
64
        'date' => self::TYPE_DATE,
65
        'time' => self::TYPE_TIME,
66
        'timestamp' => self::TYPE_TIMESTAMP,
67
        'enum' => self::TYPE_STRING,
68
    ];
69
70 5
    protected function findTableNames($schema = '')
71
    {
72 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
73
74 5
        return $this->getDb()->createCommand($sql)->queryColumn();
75
    }
76
77 77
    protected function loadTableSchema(string $name): ?TableSchema
78
    {
79 77
        $table = new TableSchema();
80
81 77
        $table->name($name);
82 77
        $table->fullName($name);
83
84 77
        if ($this->findColumns($table)) {
85 75
            $this->findConstraints($table);
86
87 75
            return $table;
88
        }
89
90 10
        return null;
91
    }
92
93 30
    protected function loadTablePrimaryKey($tableName)
94
    {
95 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
96
    }
97
98 3
    protected function loadTableForeignKeys($tableName)
99
    {
100 3
        $foreignKeys = $this->getDb()->createCommand(
101 3
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
102 3
        )->queryAll();
103
104 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
105
106 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
107
108 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
109
110 3
        $result = [];
111
112 3
        foreach ($foreignKeys as $table => $foreignKey) {
113 3
            $fk = new ForeignKeyConstraint();
114
115 3
            $fk->setColumnNames(ArrayHelper::getColumn($foreignKey, 'from'));
116 3
            $fk->setForeignTableName($table);
117 3
            $fk->setForeignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'));
118 3
            $fk->setOnDelete($foreignKey[0]['on_delete'] ?? null);
119 3
            $fk->setOnUpdate($foreignKey[0]['on_update'] ?? null);
120
121 3
            $result[] = $fk;
122
        }
123
124 3
        return $result;
125
    }
126
127 10
    protected function loadTableIndexes($tableName)
128
    {
129 10
        return $this->loadTableConstraints($tableName, 'indexes');
130
    }
131
132 12
    protected function loadTableUniques($tableName)
133
    {
134 12
        return $this->loadTableConstraints($tableName, 'uniques');
135
    }
136
137 12
    protected function loadTableChecks($tableName)
138
    {
139 12
        $sql = $this->getDb()->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
140 12
            ':tableName' => $tableName,
141 12
        ])->queryScalar();
142
143
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
144 12
        $code = (new SqlTokenizer($sql))->tokenize();
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type false and null; however, parameter $sql of Yiisoft\Db\Sqlite\Token\...okenizer::__construct() 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

144
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
145 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
146
147 12
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lastMatchIndex seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $firstMatchIndex seems to be never defined.
Loading history...
148
            return [];
149
        }
150
151 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
152 12
        $result = [];
153 12
        $offset = 0;
154
155 12
        while (true) {
156 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
157
158 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
159 12
                break;
160
            }
161
162 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
163 3
            $name = null;
164 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
165
166 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
167
                $name = $createTableToken[$firstMatchIndex - 1]->content;
168
            }
169
170 3
            $ck = new CheckConstraint();
171 3
            $ck->setName($name);
172 3
            $ck->setExpression($checkSql);
173
174 3
            $result[] = $ck;
175
        }
176
177 12
        return $result;
178
    }
179
180 12
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

180
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
181
    {
182 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
183
    }
184
185
    /**
186
     * Creates a query builder for the MySQL database.
187
     *
188
     * This method may be overridden by child classes to create a DBMS-specific query builder.
189
     *
190
     * @return QueryBuilder query builder instance
191
     */
192 50
    public function createQueryBuilder()
193
    {
194 50
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Sqlite\Query\...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, 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

194
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
195
    }
196
197
    /**
198
     * @return ColumnSchemaBuilder column schema builder instance
199
     */
200 3
    public function createColumnSchemaBuilder($type, $length = null)
201
    {
202 3
        return new ColumnSchemaBuilder($type, $length);
203
    }
204
205
    /**
206
     * Collects the table column metadata.
207
     *
208
     * @param TableSchema $table the table metadata
209
     *
210
     * @return bool whether the table exists in the database
211
     */
212 77
    protected function findColumns($table): bool
213
    {
214 77
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

214
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
215 77
        $columns = $this->getDb()->createCommand($sql)->queryAll();
216
217 77
        if (empty($columns)) {
218 10
            return false;
219
        }
220
221 75
        foreach ($columns as $info) {
222 75
            $column = $this->loadColumnSchema($info);
223 75
            $table->columns($column->getName(), $column);
224 75
            if ($column->getIsPrimaryKey()) {
225 52
                $table->primaryKey($column->getName());
226
            }
227
        }
228
229 75
        $pk = $table->getPrimaryKey();
230 75
        if (count($pk) === 1 && !strncasecmp($table->getColumn($pk[0])->getDbType(), 'int', 3)) {
231 52
            $table->sequenceName('');
232 52
            $table->getColumn($pk[0])->autoIncrement(true);
233
        }
234
235 75
        return true;
236
    }
237
238
    /**
239
     * Collects the foreign key column details for the given table.
240
     *
241
     * @param TableSchema $table the table metadata
242
     */
243 75
    protected function findConstraints($table)
244
    {
245 75
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

245
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
246 75
        $keys = $this->getDb()->createCommand($sql)->queryAll();
247
248 75
        foreach ($keys as $key) {
249 5
            $id = (int) $key['id'];
250 5
            $fk = $table->getForeignKeys();
251 5
            if (!isset($fk[$id])) {
252 5
                $table->foreignKey($id, ([$key['table'], $key['from'] => $key['to']]));
253
            } else {
254
                /** composite FK */
255 5
                $table->compositeFK($id, $key['from'], $key['to']);
256
            }
257
        }
258 75
    }
259
260
    /**
261
     * Returns all unique indexes for the given table.
262
     *
263
     * Each array element is of the following structure:
264
     *
265
     * ```php
266
     * [
267
     *     'IndexName1' => ['col1' [, ...]],
268
     *     'IndexName2' => ['col2' [, ...]],
269
     * ]
270
     * ```
271
     *
272
     * @param TableSchema $table the table metadata
273
     *
274
     * @return array all unique indexes for the given table.
275
     */
276 1
    public function findUniqueIndexes($table)
277
    {
278 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

278
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
279 1
        $indexes = $this->getDb()->createCommand($sql)->queryAll();
280 1
        $uniqueIndexes = [];
281
282 1
        foreach ($indexes as $index) {
283 1
            $indexName = $index['name'];
284 1
            $indexInfo = $this->getDb()->createCommand(
285 1
                'PRAGMA index_info(' . $this->quoteValue($index['name']) . ')'
286 1
            )->queryAll();
287
288 1
            if ($index['unique']) {
289 1
                $uniqueIndexes[$indexName] = [];
290 1
                foreach ($indexInfo as $row) {
291 1
                    $uniqueIndexes[$indexName][] = $row['name'];
292
                }
293
            }
294
        }
295
296 1
        return $uniqueIndexes;
297
    }
298
299
    /**
300
     * Loads the column information into a {@see ColumnSchema} object.
301
     *
302
     * @param array $info column information
303
     *
304
     * @return ColumnSchema the column schema object
305
     */
306 75
    protected function loadColumnSchema($info): ColumnSchema
307
    {
308 75
        $column = $this->createColumnSchema();
309 75
        $column->name($info['name']);
310 75
        $column->allowNull(!$info['notnull']);
311 75
        $column->isPrimaryKey($info['pk'] != 0);
312 75
        $column->dbType(strtolower($info['type']));
313 75
        $column->unsigned(strpos($column->getDbType(), 'unsigned') !== false);
314 75
        $column->type(self::TYPE_STRING);
315
316 75
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
317 75
            $type = strtolower($matches[1]);
318
319 75
            if (isset($this->typeMap[$type])) {
320 75
                $column->type($this->typeMap[$type]);
321
            }
322
323 75
            if (!empty($matches[2])) {
324 72
                $values = explode(',', $matches[2]);
325 72
                $column->precision((int) $values[0]);
326 72
                $column->size((int) $values[0]);
327 72
                if (isset($values[1])) {
328 25
                    $column->scale((int) $values[1]);
329
                }
330 72
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
331 21
                    $column->type('boolean');
332 72
                } elseif ($type === 'bit') {
333
                    if ($column->getSize() > 32) {
334
                        $column->type('bigint');
335
                    } elseif ($column->getSize() === 32) {
336
                        $column->type('integer');
337
                    }
338
                }
339
            }
340
        }
341
342 75
        $column->phpType($this->getColumnPhpType($column));
343
344 75
        if (!$column->getIsPrimaryKey()) {
345 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
346 73
                $column->defaultValue(null);
347 60
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
348 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
349
            } else {
350 60
                $value = trim($info['dflt_value'], "'\"");
351 60
                $column->defaultValue($column->phpTypecast($value));
352
            }
353
        }
354
355 75
        return $column;
356
    }
357
358
    /**
359
     * Sets the isolation level of the current transaction.
360
     *
361
     * @param string $level The transaction isolation level to use for this transaction. This can be either
362
     * {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
363
     *
364
     * @throws NotSupportedException when unsupported isolation levels are used. SQLite only supports SERIALIZABLE and
365
     * READ UNCOMMITTED.
366
     *
367
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
368
     */
369 3
    public function setTransactionIsolationLevel($level): void
370
    {
371
        switch ($level) {
372 3
            case Transaction::SERIALIZABLE:
373 1
                $this->getDb()->createCommand('PRAGMA read_uncommitted = False;')->execute();
374 1
                break;
375 3
            case Transaction::READ_UNCOMMITTED:
376 3
                $this->getDb()->createCommand('PRAGMA read_uncommitted = True;')->execute();
377 3
                break;
378
            default:
379
                throw new NotSupportedException(
380
                    get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
381
                );
382
        }
383 3
    }
384
385
    /**
386
     * Returns table columns info.
387
     *
388
     * @param string $tableName table name
389
     *
390
     * @return array
391
     */
392 28
    private function loadTableColumnsInfo($tableName): array
393
    {
394 28
        $tableColumns = $this->getDb()->createCommand(
395 28
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
396 28
        )->queryAll();
397
398 28
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
399
400 28
        return ArrayHelper::index($tableColumns, 'cid');
401
    }
402
403
    /**
404
     * Loads multiple types of constraints and returns the specified ones.
405
     *
406
     * @param string $tableName table name.
407
     * @param string $returnType return type:
408
     * - primaryKey
409
     * - indexes
410
     * - uniques
411
     *
412
     * @return mixed constraints.
413
     */
414 52
    private function loadTableConstraints($tableName, $returnType)
415
    {
416 52
        $indexes = $this->getDb()->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
417 52
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
418 52
        $tableColumns = null;
419
420 52
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
421
            /*
422
             * SQLite may not have an "origin" column in INDEX_LIST
423
             * See https://www.sqlite.org/src/info/2743846cdba572f6
424
             */
425
            $tableColumns = $this->loadTableColumnsInfo($tableName);
426
        }
427
428
        $result = [
429 52
            'primaryKey' => null,
430
            'indexes' => [],
431
            'uniques' => [],
432
        ];
433
434 52
        foreach ($indexes as $index) {
435 42
            $columns = $this->getDb()->createCommand(
436 42
                'PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')'
437 42
            )->queryAll();
438
439 42
            $columns = $this->normalizePdoRowKeyCase($columns, true);
440
441 42
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
442
443 42
            if ($tableColumns !== null) {
444
                // SQLite may not have an "origin" column in INDEX_LIST
445
                $index['origin'] = 'c';
446
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
447
                    $index['origin'] = 'pk';
448
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
449
                    $index['origin'] = 'u';
450
                }
451
            }
452
453 42
            $ic = new IndexConstraint();
454
455 42
            $ic->setIsPrimary($index['origin'] === 'pk');
456 42
            $ic->setIsUnique((bool) $index['unique']);
457 42
            $ic->setName($index['name']);
458 42
            $ic->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
459
460 42
            $result['indexes'][] = $ic;
461
462 42
            if ($index['origin'] === 'u') {
463 41
                $ct = new Constraint();
464
465 41
                $ct->setName($index['name']);
466 41
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
467
468 41
                $result['uniques'][] = $ct;
469 25
            } elseif ($index['origin'] === 'pk') {
470 24
                $ct = new Constraint();
471
472 24
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
473
474 24
                $result['primaryKey'] = $ct;
475
            }
476
        }
477
478 52
        if ($result['primaryKey'] === null) {
479
            /*
480
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
481
             * See https://www.sqlite.org/lang_createtable.html#primkeyconst
482
             */
483
484 28
            if ($tableColumns === null) {
485 28
                $tableColumns = $this->loadTableColumnsInfo($tableName);
486
            }
487
488 28
            foreach ($tableColumns as $tableColumn) {
489 28
                if ($tableColumn['pk'] > 0) {
490 18
                    $ct = new Constraint();
491 18
                    $ct->setColumnNames([$tableColumn['name']]);
492
493 18
                    $result['primaryKey'] = $ct;
494 18
                    break;
495
                }
496
            }
497
        }
498
499 52
        foreach ($result as $type => $data) {
500 52
            $this->setTableMetadata($tableName, $type, $data);
501
        }
502
503 52
        return $result[$returnType];
504
    }
505
506
    /**
507
     * Return whether the specified identifier is a SQLite system identifier.
508
     *
509
     * @param string $identifier
510
     *
511
     * @return bool
512
     *
513
     * {@see https://www.sqlite.org/src/artifact/74108007d286232f}
514
     */
515
    private function isSystemIdentifier($identifier): bool
516
    {
517
        return strncmp($identifier, 'sqlite_', 7) === 0;
518
    }
519
}
520