SQLiteAdapter   F
last analyzed

Complexity

Total Complexity 216

Size/Duplication

Total Lines 1641
Duplicated Lines 0 %

Test Coverage

Coverage 86.84%

Importance

Changes 13
Bugs 1 Features 1
Metric Value
wmc 216
eloc 706
c 13
b 1
f 1
dl 0
loc 1641
ccs 541
cts 623
cp 0.8684
rs 1.894

61 Methods

Rating   Name   Duplication   Size   Complexity  
A databaseVersionAtLeast() 0 5 1
A quoteTableName() 0 3 1
A getDropTableInstructions() 0 6 1
A getSchemaName() 0 16 4
A getSqlType() 0 14 4
B calculateNewTableColumns() 0 35 6
A beginTransaction() 0 3 1
A getCommentDefinition() 0 7 2
A getDeclaringIndexSql() 0 12 4
A getPrimaryKey() 0 13 3
B resolveTable() 0 44 9
A beginAlterByCopyTable() 0 26 1
A truncateTable() 0 17 2
A hasForeignKey() 0 22 5
A resolveIndex() 0 14 3
B resolveIdentity() 0 33 8
A rollbackTransaction() 0 3 1
A getColumns() 0 25 2
A dropDatabase() 0 9 3
C createTable() 0 61 15
A copyDataToNewTable() 0 10 1
A getDeclaringSql() 0 27 4
A getDropForeignKeyInstructions() 0 3 1
A hasIndex() 0 3 1
A getDropColumnInstructions() 0 27 2
A getAddPrimaryKeyInstructions() 0 36 4
A getRenameTableInstructions() 0 10 1
A getDropIndexByNameInstructions() 0 24 4
A hasColumn() 0 10 3
A getDropIndexByColumnsInstructions() 0 16 3
A getAddColumnInstructions() 0 40 2
A commitTransaction() 0 3 1
A getAddIndexInstructions() 0 15 2
A hasTransactions() 0 3 1
A hasPrimaryKey() 0 14 4
A createDatabase() 0 3 1
A setOptions() 0 14 4
A hasDatabase() 0 3 1
A getChangeColumnInstructions() 0 24 1
A getRenameColumnInstructions() 0 22 1
A getIndexSqlDefinition() 0 19 4
A getAddForeignKeyInstructions() 0 43 3
C getPhinxType() 0 40 12
A getColumnTypes() 0 3 1
A getForeignKeys() 0 14 3
A getIndexes() 0 16 3
A copyAndDropTmpTable() 0 21 1
A hasTable() 0 3 2
A quoteColumnName() 0 3 1
A getDecoratedConnection() 0 21 4
C parseDefaultValue() 0 61 13
A disconnect() 0 3 1
A hasIndexByName() 0 12 3
B getColumnSqlDefinition() 0 27 10
A getDropPrimaryKeyInstructions() 0 22 2
A getForeignKeySqlDefinition() 0 24 6
A getDropForeignKeyByColumnsInstructions() 0 39 4
A getChangeCommentInstructions() 0 3 1
A getChangePrimaryKeyInstructions() 0 28 4
D connect() 0 48 19
A getTableInfo() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like SQLiteAdapter 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 SQLiteAdapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use BadMethodCallException;
11
use Cake\Database\Connection;
12
use Cake\Database\Driver\Sqlite as SqliteDriver;
13
use InvalidArgumentException;
14
use PDO;
15
use PDOException;
16
use Phinx\Db\Table\Column;
17
use Phinx\Db\Table\ForeignKey;
18
use Phinx\Db\Table\Index;
19
use Phinx\Db\Table\Table;
20
use Phinx\Db\Util\AlterInstructions;
21
use Phinx\Util\Expression;
22
use Phinx\Util\Literal;
23
use RuntimeException;
24
25
/**
26
 * Phinx SQLite Adapter.
27
 *
28
 * @author Rob Morgan <[email protected]>
29
 * @author Richard McIntyre <[email protected]>
30
 */
31
class SQLiteAdapter extends PdoAdapter
32
{
33
    public const MEMORY = ':memory:';
34
35
    /**
36
     * List of supported Phinx column types with their SQL equivalents
37
     * some types have an affinity appended to ensure they do not receive NUMERIC affinity
38
     *
39
     * @var string[]
40
     */
41
    protected static $supportedColumnTypes = [
42
        self::PHINX_TYPE_BIG_INTEGER => 'biginteger',
43
        self::PHINX_TYPE_BINARY => 'binary_blob',
44
        self::PHINX_TYPE_BINARYUUID => 'binary_blob',
45
        self::PHINX_TYPE_BLOB => 'blob',
46
        self::PHINX_TYPE_BOOLEAN => 'boolean_integer',
47
        self::PHINX_TYPE_CHAR => 'char',
48
        self::PHINX_TYPE_DATE => 'date_text',
49
        self::PHINX_TYPE_DATETIME => 'datetime_text',
50
        self::PHINX_TYPE_DECIMAL => 'decimal',
51
        self::PHINX_TYPE_DOUBLE => 'double',
52
        self::PHINX_TYPE_FLOAT => 'float',
53
        self::PHINX_TYPE_INTEGER => 'integer',
54
        self::PHINX_TYPE_JSON => 'json_text',
55
        self::PHINX_TYPE_JSONB => 'jsonb_text',
56 42
        self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger',
57
        self::PHINX_TYPE_STRING => 'varchar',
58 42
        self::PHINX_TYPE_TEXT => 'text',
59 42
        self::PHINX_TYPE_TIME => 'time_text',
60
        self::PHINX_TYPE_TIMESTAMP => 'timestamp_text',
61
        self::PHINX_TYPE_TINY_INTEGER => 'tinyinteger',
62
        self::PHINX_TYPE_UUID => 'uuid_text',
63
        self::PHINX_TYPE_VARBINARY => 'varbinary_blob',
64
    ];
65 42
66 42
    /**
67
     * List of aliases of supported column types
68
     *
69 42
     * @var string[]
70
     */
71
    protected static $supportedColumnTypeAliases = [
72 42
        'varchar' => self::PHINX_TYPE_STRING,
73 42
        'tinyint' => self::PHINX_TYPE_TINY_INTEGER,
74 42
        'tinyinteger' => self::PHINX_TYPE_TINY_INTEGER,
75 42
        'smallint' => self::PHINX_TYPE_SMALL_INTEGER,
76
        'int' => self::PHINX_TYPE_INTEGER,
77
        'mediumint' => self::PHINX_TYPE_INTEGER,
78
        'mediuminteger' => self::PHINX_TYPE_INTEGER,
79 42
        'bigint' => self::PHINX_TYPE_BIG_INTEGER,
80 42
        'tinytext' => self::PHINX_TYPE_TEXT,
81
        'mediumtext' => self::PHINX_TYPE_TEXT,
82
        'longtext' => self::PHINX_TYPE_TEXT,
83
        'tinyblob' => self::PHINX_TYPE_BLOB,
84
        'mediumblob' => self::PHINX_TYPE_BLOB,
85
        'longblob' => self::PHINX_TYPE_BLOB,
86
        'real' => self::PHINX_TYPE_FLOAT,
87 42
    ];
88 42
89 42
    /**
90
     * List of known but unsupported Phinx column types
91
     *
92
     * @var string[]
93
     */
94 48
    protected static $unsupportedColumnTypes = [
95
        self::PHINX_TYPE_BIT,
96 48
        self::PHINX_TYPE_CIDR,
97 48
        self::PHINX_TYPE_ENUM,
98
        self::PHINX_TYPE_FILESTREAM,
99
        self::PHINX_TYPE_GEOMETRY,
100
        self::PHINX_TYPE_INET,
101
        self::PHINX_TYPE_INTERVAL,
102
        self::PHINX_TYPE_LINESTRING,
103
        self::PHINX_TYPE_MACADDR,
104
        self::PHINX_TYPE_POINT,
105
        self::PHINX_TYPE_POLYGON,
106
        self::PHINX_TYPE_SET,
107
    ];
108
109
    /**
110 1
     * @var string[]
111
     */
112 1
    protected $definitionsWithLimits = [
113 1
        'CHAR',
114
        'CHARACTER',
115
        'VARCHAR',
116
        'VARYING CHARACTER',
117
        'NCHAR',
118
        'NATIVE CHARACTER',
119
        'NVARCHAR',
120
    ];
121
122
    /**
123
     * @var string
124
     */
125
    protected $suffix = '.sqlite3';
126
127
    /**
128
     * Indicates whether the database library version is at least the specified version
129
     *
130
     * @param string $ver The version to check against e.g. '3.28.0'
131
     * @return bool
132
     */
133
    public function databaseVersionAtLeast($ver)
134 43
    {
135
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
136 43
137
        return version_compare($actual, $ver, '>=');
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare($actual, $ver, '>=') also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
138
    }
139
140
    /**
141
     * {@inheritDoc}
142 44
     *
143
     * @throws \RuntimeException
144 44
     * @throws \InvalidArgumentException
145
     * @return void
146
     */
147
    public function connect()
148
    {
149
        if ($this->connection === null) {
150 42
            if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) {
151
                // @codeCoverageIgnoreStart
152 42
                throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
153 42
                // @codeCoverageIgnoreEnd
154 42
            }
155 12
156 42
            $options = $this->getOptions();
157
158 42
            if (PHP_VERSION_ID < 80100 && (!empty($options['mode']) || !empty($options['cache']))) {
159
                throw new RuntimeException('SQLite URI support requires PHP 8.1.');
160
            } elseif ((!empty($options['mode']) || !empty($options['cache'])) && !empty($options['memory'])) {
161
                throw new RuntimeException('Memory must not be set when cache or mode are.');
162
            } elseif (PHP_VERSION_ID >= 80100 && (!empty($options['mode']) || !empty($options['cache']))) {
163
                $params = [];
164 42
                if (!empty($options['cache'])) {
165
                    $params[] = 'cache=' . $options['cache'];
166
                }
167 42
                if (!empty($options['mode'])) {
168 42
                    $params[] = 'mode=' . $options['mode'];
169 42
                }
170 35
                $dsn = 'sqlite:file:' . ($options['name'] ?? '') . '?' . implode('&', $params);
171 35
            } else {
172 35
                // use a memory database if the option was specified
173 35
                if (!empty($options['memory']) || $options['name'] === static::MEMORY) {
174
                    $dsn = 'sqlite:' . static::MEMORY;
175 35
                } else {
176 42
                    $dsn = 'sqlite:' . $options['name'] . $this->suffix;
177
                }
178 1
            }
179 1
180 1
            $driverOptions = [];
181 1
182
            // use custom data fetch mode
183 1
            if (!empty($options['fetch_mode'])) {
184 1
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
185
            }
186
187 42
            // pass \PDO::ATTR_PERSISTENT to driver options instead of useless setting it after instantiation
188 42
            if (isset($options['attr_persistent'])) {
189 42
                $driverOptions[PDO::ATTR_PERSISTENT] = $options['attr_persistent'];
190 42
            }
191 42
192
            $db = $this->createPdoConnection($dsn, null, null, $driverOptions);
193
194 42
            $this->setConnection($db);
195 42
        }
196 42
    }
197 42
198 42
    /**
199 42
     * @inheritDoc
200
     */
201
    public function setOptions(array $options)
202 1
    {
203 1
        parent::setOptions($options);
204 1
205
        if (isset($options['suffix'])) {
206 1
            $this->suffix = $options['suffix'];
207 1
        }
208 1
        //don't "fix" the file extension if it is blank, some people
209 1
        //might want a SQLITE db file with absolutely no extension.
210 1
        if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
211 1
            $this->suffix = '.' . $this->suffix;
212 42
        }
213 42
214 37
        return $this;
215
    }
216
217
    /**
218 42
     * @inheritDoc
219 42
     */
220 1
    public function disconnect()
221 1
    {
222 1
        $this->connection = null;
223 1
    }
224
225 42
    /**
226
     * @inheritDoc
227 42
     */
228
    public function hasTransactions()
229 42
    {
230 6
        return true;
231 42
    }
232 42
233
    /**
234
     * @inheritDoc
235
     */
236
    public function beginTransaction()
237 1
    {
238
        $this->getConnection()->beginTransaction();
239 1
    }
240 1
241
    /**
242
     * @inheritDoc
243
     */
244
    public function commitTransaction()
245 1
    {
246
        $this->getConnection()->commit();
247 1
    }
248 1
249
    /**
250
     * @inheritDoc
251
     */
252
    public function rollbackTransaction()
253 1
    {
254
        $this->getConnection()->rollBack();
255 1
    }
256 1
257 1
    /**
258 1
     * @inheritDoc
259
     */
260 1
    public function quoteTableName($tableName)
261 1
    {
262
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
263
    }
264
265
    /**
266 1
     * @inheritDoc
267
     */
268 1
    public function quoteColumnName($columnName)
269 1
    {
270
        return '`' . str_replace('`', '``', $columnName) . '`';
271 1
    }
272 1
273 1
    /**
274 1
     * @param string $tableName Table name
275 1
     * @param bool $quoted Whether to return the schema name and table name escaped and quoted. If quoted, the schema (if any) will also be appended with a dot
276 1
     * @return array
277
     */
278 1
    protected function getSchemaName($tableName, $quoted = false)
279 1
    {
280 1
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
281
            $table = $match[1];
282 1
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
283 1
            $result = ['schema' => $schema, 'table' => $table];
284 1
        } else {
285
            $result = ['schema' => '', 'table' => $tableName];
286 1
        }
287 1
288
        if ($quoted) {
289 1
            $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : '';
290
            $result['table'] = $this->quoteColumnName($result['table']);
291
        }
292
293
        return $result;
294
    }
295 8
296
    /**
297 8
     * Retrieves information about a given table from one of the SQLite pragmas
298 8
     *
299 8
     * @param string $tableName The table to query
300 7
     * @param string $pragma The pragma to query
301
     * @return array
302 8
     */
303
    protected function getTableInfo($tableName, $pragma = 'table_info')
304 8
    {
305
        $info = $this->getSchemaName($tableName, true);
306
307
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
308
    }
309
310 4
    /**
311
     * Searches through all available schemata to find a table and returns an array
312 4
     * containing the bare schema name and whether the table exists at all.
313 4
     * If no schema was specified and the table does not exist the "main" schema is returned
314 4
     *
315 4
     * @param string $tableName The name of the table to find
316 4
     * @return array
317 4
     */
318
    protected function resolveTable($tableName)
319 4
    {
320 4
        $info = $this->getSchemaName($tableName);
321
        if ($info['schema'] === '') {
322
            // if no schema is specified we search all schemata
323
            $rows = $this->fetchAll('PRAGMA database_list;');
324
            // the temp schema is always first to be searched
325 2
            $schemata = ['temp'];
326
            foreach ($rows as $row) {
327 2
                if (strtolower($row['name']) !== 'temp') {
328
                    $schemata[] = $row['name'];
329 2
                }
330
            }
331 2
            $defaultSchema = 'main';
332 2
        } else {
333 2
            // otherwise we search just the specified schema
334 2
            $schemata = (array)$info['schema'];
335 2
            $defaultSchema = $info['schema'];
336 2
        }
337
338 2
        $table = strtolower($info['table']);
339 2
        foreach ($schemata as $schema) {
340 2
            if (strtolower($schema) === 'temp') {
341 2
                $master = 'sqlite_temp_master';
342 2
            } else {
343 2
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
344 2
            }
345 2
            try {
346 2
                $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table)));
347
            } catch (PDOException $e) {
348 2
                // an exception can occur if the schema part of the table refers to a database which is not attached
349 1
                break;
350
            }
351 1
352
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
353
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
354 1
            foreach ($rows as $row) {
355
                if (strtolower($row['name']) === $table) {
356 1
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
357 1
                }
358 1
            }
359
        }
360 1
361 1
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
362
    }
363
364 1
    /**
365 1
     * @inheritDoc
366 1
     */
367 1
    public function hasTable($tableName)
368 1
    {
369
        return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
370 1
    }
371
372 1
    /**
373
     * @inheritDoc
374 1
     */
375 1
    public function createTable(Table $table, array $columns = [], array $indexes = [])
376
    {
377
        // Add the default primary key
378
        $options = $table->getOptions();
379
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
380 6
            $options['id'] = 'id';
381
        }
382
383
        if (isset($options['id']) && is_string($options['id'])) {
384 6
            // Handle id => "field_name" to support AUTO_INCREMENT
385
            $column = new Column();
386 6
            $column->setName($options['id'])
387
                   ->setType('integer')
388 6
                   ->setIdentity(true);
389 6
390 6
            array_unshift($columns, $column);
391 6
        }
392 6
393 6
        $sql = 'CREATE TABLE ';
394
        $sql .= $this->quoteTableName($table->getName()) . ' (';
395 6
        foreach ($columns as $column) {
396 6
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
397 6
398 6
            if (isset($options['primary_key']) && $column->getIdentity()) {
399 6
                //remove column from the primary key array as it is already defined as an autoincrement
400 6
                //primary id
401 6
                $identityColumnIndex = array_search($column->getName(), $options['primary_key'], true);
402 6
                if ($identityColumnIndex !== false) {
403 6
                    unset($options['primary_key'][$identityColumnIndex]);
404
405 6
                    if (empty($options['primary_key'])) {
406
                        //The last primary key has been removed
407
                        unset($options['primary_key']);
408
                    }
409
                }
410
            }
411 6
        }
412
413 6
        // set the primary key(s)
414 6
        if (isset($options['primary_key'])) {
415 6
            $sql = rtrim($sql);
416 6
            $sql .= ' PRIMARY KEY (';
417
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
418 6
                $sql .= $this->quoteColumnName($options['primary_key']);
419
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
420 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
421
            }
422 6
            $sql .= ')';
423 6
        } else {
424 6
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
425 6
        }
426 6
427
        $sql = rtrim($sql) . ');';
428 6
        // execute the sql
429
        $this->execute($sql);
430 6
431 6
        foreach ($indexes as $index) {
432 6
            $this->addIndex($table, $index);
433
        }
434
435
        $this->addCreatedTable($table->getName());
436
    }
437 2
438
    /**
439
     * {@inheritDoc}
440 2
     *
441
     * @throws \InvalidArgumentException
442 2
     */
443
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
444 2
    {
445 2
        $instructions = new AlterInstructions();
446 2
447 2
        // Drop the existing primary key
448 2
        $primaryKey = $this->getPrimaryKey($table->getName());
449 2
        if (!empty($primaryKey)) {
450
            $instructions->merge(
451 2
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
452 2
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
453 2
            );
454 2
        }
455 2
456 2
        // Add the primary key(s)
457 2
        if (!empty($newColumns)) {
458 2
            if (!is_string($newColumns)) {
459 2
                throw new InvalidArgumentException(sprintf(
460
                    'Invalid value for primary key: %s',
461 2
                    json_encode($newColumns)
462
                ));
463 2
            }
464
465
            $instructions->merge(
466
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
467
            );
468
        }
469 2
470
        return $instructions;
471 2
    }
472 2
473 2
    /**
474
     * {@inheritDoc}
475 2
     *
476
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
477 2
     *
478 2
     * @throws \BadMethodCallException
479 2
     */
480
    protected function getChangeCommentInstructions(Table $table, $newComment)
481 2
    {
482
        throw new BadMethodCallException('SQLite does not have table comments');
483 2
    }
484 2
485 2
    /**
486 2
     * @inheritDoc
487 2
     */
488
    protected function getRenameTableInstructions($tableName, $newTableName)
489 2
    {
490
        $this->updateCreatedTableName($tableName, $newTableName);
491 2
        $sql = sprintf(
492 2
            'ALTER TABLE %s RENAME TO %s',
493 2
            $this->quoteTableName($tableName),
494
            $this->quoteTableName($newTableName)
495
        );
496
497
        return new AlterInstructions([], [$sql]);
498
    }
499
500
    /**
501 9
     * @inheritDoc
502
     */
503 9
    protected function getDropTableInstructions($tableName)
504 9
    {
505
        $this->removeCreatedTable($tableName);
506 9
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
507 9
508 9
        return new AlterInstructions([], [$sql]);
509 9
    }
510 9
511 9
    /**
512 9
     * @inheritDoc
513 9
     */
514 9
    public function truncateTable($tableName)
515 9
    {
516
        $info = $this->resolveTable($tableName);
517
        // first try deleting the rows
518
        $this->execute(sprintf(
519
            'DELETE FROM %s.%s',
520
            $this->quoteColumnName($info['schema']),
521 9
            $this->quoteColumnName($info['table'])
522
        ));
523 9
524 4
        // assuming no error occurred, reset the autoincrement (if any)
525 4
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
526
            $this->execute(sprintf(
527 9
                'DELETE FROM %s.%s where name  = %s',
528 9
                $this->quoteColumnName($info['schema']),
529
                'sqlite_sequence',
530 9
                $this->quoteString($info['table'])
531 9
            ));
532 9
        }
533 9
    }
534
535 8
    /**
536
     * Parses a default-value expression to yield either a Literal representing
537 8
     * a string value, a string representing an expression, or some other scalar
538
     *
539
     * @param mixed $v The default-value expression to interpret
540
     * @param string $t The Phinx type of the column
541
     * @return mixed
542
     */
543 1
    protected function parseDefaultValue($v, $t)
544
    {
545 1
        if ($v === null) {
546
            return null;
547 1
        }
548 1
549 1
        // split the input into tokens
550
        $trimChars = " \t\n\r\0\x0B";
551
        $pattern = <<<PCRE_PATTERN
552
            /
553
                '(?:[^']|'')*'|                 # String literal
554
                "(?:[^"]|"")*"|                 # Standard identifier
555
                `(?:[^`]|``)*`|                 # MySQL identifier
556
                \[[^\]]*\]|                     # SQL Server identifier
557
                --[^\r\n]*|                     # Single-line comment
558
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
559 8
                [^\/\-]+|                       # Non-special characters
560
                .                               # Any other single character
561 8
            /sx
562 8
PCRE_PATTERN;
563 8
        preg_match_all($pattern, $v, $matches);
564 8
        // strip out any comment tokens
565 8
        $matches = array_map(function ($v) {
566 8
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
567 8
        }, $matches[0]);
568 8
        // reconstitute the string, trimming whitespace as well as parentheses
569 8
        $vClean = trim(implode('', $matches));
570 8
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
571
572 8
        // match the string against one of several patterns
573 8
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
574 8
            // magic date or time
575
            return strtoupper($vBare);
576
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
577
            // string literal
578
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
579 1
580
            return Literal::from($str);
581 1
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
582 1
            $int = (int)$vBare;
583 1
            // integer literal
584
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int === 0 || $int === 1)) {
585 1
                return (bool)$int;
586 1
            } else {
587
                return $int;
588 1
            }
589 1
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
590 1
            // float literal
591 1
            return (float)$vBare;
592 1
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
593 1
            // hexadecimal literal
594 1
            return hexdec(substr($vBare, 2));
595 1
        } elseif (preg_match('/^null$/i', $vBare)) {
596 1
            // null literal
597 1
            return null;
598
        } elseif (preg_match('/^true|false$/i', $vBare)) {
599
            // boolean literal
600
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
601
        } else {
602
            // any other expression: return the expression with parentheses, but without comments
603
            return Expression::from($vClean);
604
        }
605 1
    }
606
607 1
    /**
608
     * Returns the name of the specified table's identity column, or null if the table has no identity
609 1
     *
610 1
     * The process of finding an identity column is somewhat convoluted as SQLite has no direct way of querying whether a given column is an alias for the table's row ID
611 1
     *
612 1
     * @param string $tableName The name of the table
613 1
     * @return string|null
614 1
     */
615 1
    protected function resolveIdentity($tableName)
616 1
    {
617 1
        $result = null;
618
        // make sure the table has only one primary key column which is of type integer
619
        foreach ($this->getTableInfo($tableName) as $col) {
620
            $type = strtolower($col['type']);
621
            if ($col['pk'] > 1) {
622
                // the table has a composite primary key
623
                return null;
624
            } elseif ($col['pk'] == 0) {
625 5
                // the column is not a primary key column and is thus not relevant
626
                continue;
627 5
            } elseif ($type !== 'integer') {
628
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
629
                return null;
630 5
            } else {
631
                // the column is a candidate for a row ID alias
632 5
                $result = $col['name'];
633 5
            }
634 5
        }
635
        // if there is no suitable PK column, stop now
636 1
        if ($result === null) {
637
            return null;
638
        }
639
        // make sure the table does not have a PK-origin autoindex
640
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
641
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
642
            if ($idx['origin'] === 'pk') {
643
                return null;
644
            }
645 5
        }
646
647 5
        return $result;
648 5
    }
649
650
    /**
651
     * @inheritDoc
652
     */
653
    public function getColumns($tableName)
654
    {
655
        $columns = [];
656
657
        $rows = $this->getTableInfo($tableName);
658
        $identity = $this->resolveIdentity($tableName);
659
660
        foreach ($rows as $columnInfo) {
661 5
            $column = new Column();
662
            $type = $this->getPhinxType($columnInfo['type']);
663 5
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
664 5
665 5
            $column->setName($columnInfo['name'])
666 5
                // SQLite on PHP 8.1 returns int for notnull, older versions return a string
667 5
                   ->setNull((int)$columnInfo['notnull'] !== 1)
668 5
                   ->setDefault($default)
669 5
                   ->setType($type['name'])
670 5
                   ->setLimit($type['limit'])
671 5
                   ->setScale($type['scale'])
672 5
                   ->setIdentity($columnInfo['name'] === $identity);
673 5
674
            $columns[] = $column;
675
        }
676
677
        return $columns;
678
    }
679 4
680
    /**
681
     * @inheritDoc
682 4
     */
683
    public function hasColumn($tableName, $columnName)
684 4
    {
685 4
        $rows = $this->getTableInfo($tableName);
686
        foreach ($rows as $column) {
687 4
            if (strcasecmp($column['name'], $columnName) === 0) {
688 4
                return true;
689 4
            }
690 4
        }
691 4
692 4
        return false;
693
    }
694 4
695 4
    /**
696 4
     * @inheritDoc
697 4
     */
698 4
    protected function getAddColumnInstructions(Table $table, Column $column)
699
    {
700 4
        $tableName = $table->getName();
701
702 4
        $instructions = $this->beginAlterByCopyTable($tableName);
703 4
704
        $instructions->addPostStep(function ($state) use ($tableName, $column) {
705 4
            // we use the final column to anchor our regex to insert the new column,
706 4
            // as the alternative is unwinding all possible table constraints which
707 4
            // gets messy quickly with CHECK constraints.
708 4
            $columns = $this->getColumns($tableName);
709 4
            if (!$columns) {
710 4
                return $state;
711 4
            }
712
            $finalColumnName = end($columns)->getName();
713 4
            $sql = preg_replace(
714 4
                sprintf(
715 4
                    "/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/",
716
                    $this->quoteColumnName($finalColumnName)
717
                ),
718
                sprintf(
719
                    '$1, %s %s$2',
720 1
                    $this->quoteColumnName($column->getName()),
721
                    $this->getColumnSqlDefinition($column)
722
                ),
723 1
                $state['createSQL'],
724
                1
725
            );
726
            $this->execute($sql);
727 1
728
            return $state;
729 1
        });
730
731 1
        $instructions->addPostStep(function ($state) use ($tableName) {
732 1
            $newState = $this->calculateNewTableColumns($tableName, false, false);
733 1
734 1
            return $newState + $state;
735 1
        });
736 1
737
        return $this->copyAndDropTmpTable($instructions, $tableName);
738 1
    }
739 1
740 1
    /**
741 1
     * Returns the original CREATE statement for the give table
742 1
     *
743 1
     * @param string $tableName The table name to get the create statement for
744 1
     * @return string
745
     */
746 1
    protected function getDeclaringSql($tableName)
747
    {
748 1
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'");
749
750
        $sql = '';
751
        foreach ($rows as $table) {
752
            if ($table['tbl_name'] === $tableName) {
753
                $sql = $table['sql'];
754 1
            }
755
        }
756 1
757 1
        $columnsInfo = $this->getTableInfo($tableName);
758 1
759 1
        foreach ($columnsInfo as $column) {
760 1
            $columnName = $column['name'];
761 1
            $columnNamePattern = "\"$columnName\"|`$columnName`|\\[$columnName\\]|$columnName";
762 1
            $columnNamePattern = "#([\(,]+\\s*)($columnNamePattern)(\\s)#iU";
763
764 1
            $sql = preg_replace($columnNamePattern, "$1`$columnName`$3", $sql);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $1 seems to be never defined.
Loading history...
765
        }
766 1
767 1
        $tableNamePattern = "\"$tableName\"|`$tableName`|\\[$tableName\\]|$tableName";
768 1
        $tableNamePattern = "#^(CREATE TABLE)\s*($tableNamePattern)\s*(\()#Ui";
769 1
770 1
        $sql = preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1);
771
772 1
        return $sql;
773
    }
774 1
775 1
    /**
776 1
     * Returns the original CREATE statement for the give index
777
     *
778
     * @param string $tableName The table name to get the create statement for
779
     * @param string $indexName The table index
780
     * @return string
781
     */
782
    protected function getDeclaringIndexSql($tableName, $indexName)
783
    {
784
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'");
785
786
        $sql = '';
787
        foreach ($rows as $table) {
788
            if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) {
789
                $sql = $table['sql'] . '; ';
790
            }
791
        }
792
793
        return $sql;
794
    }
795
796
    /**
797
     * Copies all the data from a tmp table to another table
798
     *
799
     * @param string $tableName The table name to copy the data to
800
     * @param string $tmpTableName The tmp table name where the data is stored
801
     * @param string[] $writeColumns The list of columns in the target table
802
     * @param string[] $selectColumns The list of columns in the tmp table
803
     * @return void
804
     */
805
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
806
    {
807
        $sql = sprintf(
808
            'INSERT INTO %s(%s) SELECT %s FROM %s',
809
            $this->quoteTableName($tableName),
810 43
            implode(', ', $writeColumns),
811
            implode(', ', $selectColumns),
812
            $this->quoteTableName($tmpTableName)
813 43
        );
814 42
        $this->execute($sql);
815
    }
816 43
817
    /**
818
     * Modifies the passed instructions to copy all data from the table into
819 43
     * the provided tmp table and then drops the table and rename tmp table.
820 1
     *
821
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
822 43
     * @param string $tableName The table name to copy the data to
823 38
     * @return \Phinx\Db\Util\AlterInstructions
824
     */
825 43
    protected function copyAndDropTmpTable($instructions, $tableName)
826 42
    {
827
        $instructions->addPostStep(function ($state) use ($tableName) {
828 43
            $this->copyDataToNewTable(
829 2
                $state['tmpTableName'],
830
                $tableName,
831 43
                $state['writeColumns'],
832 1
                $state['selectColumns']
833
            );
834 43
835 1
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
836
            $this->execute(sprintf(
837 43
                'ALTER TABLE %s RENAME TO %s',
838 42
                $this->quoteTableName($state['tmpTableName']),
839
                $this->quoteTableName($tableName)
840 43
            ));
841 1
842
            return $state;
843 43
        });
844 1
845
        return $instructions;
846 43
    }
847 43
848 1
    /**
849
     * Returns the columns and type to use when copying a table to another in the process
850 43
     * of altering a table
851 42
     *
852
     * @param string $tableName The table to modify
853 5
     * @param string|false $columnName The column name that is about to change
854
     * @param string|false $newColumnName Optionally the new name for the column
855 5
     * @throws \InvalidArgumentException
856 4
     * @return array
857
     */
858
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
859
    {
860 1
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
861 1
        $selectColumns = [];
862
        $writeColumns = [];
863
        $columnType = null;
864 1
        $found = false;
865
866
        foreach ($columns as $column) {
867 1
            $selectName = $column['name'];
868
            $writeName = $selectName;
869 1
870 1
            if ($selectName === $columnName) {
871 1
                $writeName = $newColumnName;
872
                $found = true;
873
                $columnType = $column['type'];
874
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
875
            }
876
877
            $selectColumns[] = $selectName;
878
            $writeColumns[] = $writeName;
879
        }
880 3
881
        $selectColumns = array_filter($selectColumns, 'strlen');
882 3
        $writeColumns = array_filter($writeColumns, 'strlen');
883 1
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
884
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
885 2
886 2
        if ($columnName && !$found) {
887 2
            throw new InvalidArgumentException(sprintf(
888 2
                'The specified column doesn\'t exist: ' . $columnName
889 1
            ));
890 1
        }
891 2
892
        return compact('writeColumns', 'selectColumns', 'columnType');
893
    }
894 2
895 2
    /**
896 1
     * Returns the initial instructions to alter a table using the
897 1
     * create-copy-drop strategy
898
     *
899
     * @param string $tableName The table to modify
900 1
     * @return \Phinx\Db\Util\AlterInstructions
901 2
     */
902
    protected function beginAlterByCopyTable($tableName)
903
    {
904
        $instructions = new AlterInstructions();
905
        $instructions->addPostStep(function ($state) use ($tableName) {
906
            $tmpTableName = "tmp_{$tableName}";
907
            $createSQL = $this->getDeclaringSql($tableName);
908
909
            // Table name in SQLite can be hilarious inside declaring SQL:
910 2
            // - tableName
911
            // - `tableName`
912
            // - "tableName"
913
            // - [this is a valid table name too!]
914
            // - etc.
915
            // Just remove all characters before first "(" and build them again
916 2
            $createSQL = preg_replace(
917 1
                "/^CREATE TABLE .* \(/Ui",
918
                '',
919
                $createSQL
920 1
            );
921 1
922 2
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
923 1
924 1
            return compact('createSQL', 'tmpTableName') + $state;
925 2
        });
926 2
927
        return $instructions;
928
    }
929
930
    /**
931
     * @inheritDoc
932
     */
933 2
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
934
    {
935
        $instructions = $this->beginAlterByCopyTable($tableName);
936 1
937 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
938
            $sql = str_replace(
939 1
                $this->quoteColumnName($columnName),
940
                $this->quoteColumnName($newColumnName),
941
                $state['createSQL']
942
            );
943
            $this->execute($sql);
944
945
            return $state;
946 48
        });
947
948 48
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
949 48
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
950
951
            return $newState + $state;
952
        });
953
954 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
955
    }
956 2
957
    /**
958
     * @inheritDoc
959
     */
960
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
961
    {
962 48
        $instructions = $this->beginAlterByCopyTable($tableName);
963
964 48
        $newColumnName = $newColumn->getName();
965 47
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
966 47
            $sql = preg_replace(
967 48
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
968
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
969
                $state['createSQL'],
970
                1
971
            );
972
            $this->execute($sql);
973
974
            return $state;
975 42
        });
976
977 42
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
978 8
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
979 42
980 42
            return $newState + $state;
981 42
        });
982 42
983
        return $this->copyAndDropTmpTable($instructions, $tableName);
984
    }
985
986
    /**
987
     * @inheritDoc
988
     */
989
    protected function getDropColumnInstructions($tableName, $columnName)
990
    {
991 42
        $instructions = $this->beginAlterByCopyTable($tableName);
992
993 42
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
994 42
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
995 42
996 42
            return $newState + $state;
997
        });
998
999 42
        $instructions->addPostStep(function ($state) use ($columnName) {
1000 42
            $sql = preg_replace(
1001 42
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
1002 42
                '',
1003 42
                $state['createSQL']
1004 4
            );
1005 4
1006
            if (substr($sql, -2) === ', ') {
1007 42
                $sql = substr($sql, 0, -2) . ')';
1008
            }
1009 42
1010 42
            $this->execute($sql);
1011 42
1012
            return $state;
1013 42
        });
1014
1015
        return $this->copyAndDropTmpTable($instructions, $tableName);
1016
    }
1017 42
1018
    /**
1019 42
     * Get an array of indexes from a particular table.
1020
     *
1021
     * @param string $tableName Table name
1022
     * @return array
1023
     */
1024
    protected function getIndexes($tableName)
1025
    {
1026
        $indexes = [];
1027
        $schema = $this->getSchemaName($tableName, true)['schema'];
1028 42
        $indexList = $this->getTableInfo($tableName, 'index_list');
1029
1030 42
        foreach ($indexList as $index) {
1031 2
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
1032
            $cols = [];
1033 42
            foreach ($indexData as $indexItem) {
1034
                $cols[] = $indexItem['name'];
1035
            }
1036
            $indexes[$index['name']] = $cols;
1037
        }
1038
1039
        return $indexes;
1040
    }
1041
1042 8
    /**
1043
     * Finds the names of a table's indexes matching the supplied columns
1044 8
     *
1045 2
     * @param string $tableName The table to which the index belongs
1046 2
     * @param string|string[] $columns The columns of the index
1047 6
     * @return array
1048
     */
1049 8
    protected function resolveIndex($tableName, $columns)
1050 3
    {
1051 3
        $columns = array_map('strtolower', (array)$columns);
1052 6
        $indexes = $this->getIndexes($tableName);
1053 6
        $matches = [];
1054 6
1055 6
        foreach ($indexes as $name => $index) {
1056 6
            $indexCols = array_map('strtolower', $index);
1057
            if ($columns == $indexCols) {
1058 8
                $matches[] = $name;
1059 8
            }
1060
        }
1061
1062
        return $matches;
1063
    }
1064
1065 47
    /**
1066
     * @inheritDoc
1067 47
     */
1068
    public function hasIndex($tableName, $columns)
1069
    {
1070
        return (bool)$this->resolveIndex($tableName, $columns);
1071
    }
1072
1073
    /**
1074
     * @inheritDoc
1075
     */
1076 5
    public function hasIndexByName($tableName, $indexName)
1077
    {
1078 5
        $indexName = strtolower($indexName);
1079 5
        $indexes = $this->getIndexes($tableName);
1080
1081
        foreach (array_keys($indexes) as $index) {
1082 5
            if ($indexName === strtolower($index)) {
1083 5
                return true;
1084 5
            }
1085 5
        }
1086 5
1087 5
        return false;
1088 5
    }
1089 5
1090 5
    /**
1091 5
     * @inheritDoc
1092 5
     */
1093 1
    protected function getAddIndexInstructions(Table $table, Index $index)
1094 1
    {
1095 5
        $indexColumnArray = [];
1096 1
        foreach ($index->getColumns() as $column) {
1097 1
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1098
        }
1099 5
        $indexColumns = implode(',', $indexColumnArray);
1100
        $sql = sprintf(
1101
            'CREATE %s ON %s (%s)',
1102
            $this->getIndexSqlDefinition($table, $index),
1103
            $this->quoteTableName($table->getName()),
1104
            $indexColumns
1105
        );
1106
1107
        return new AlterInstructions([], [$sql]);
1108
    }
1109
1110
    /**
1111
     * @inheritDoc
1112
     */
1113
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1114
    {
1115
        $instructions = new AlterInstructions();
1116
        $indexNames = $this->resolveIndex($tableName, $columns);
1117
        $schema = $this->getSchemaName($tableName, true)['schema'];
1118
        foreach ($indexNames as $indexName) {
1119
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1120
                $instructions->addPostStep(sprintf(
1121
                    'DROP INDEX %s%s',
1122
                    $schema,
1123
                    $this->quoteColumnName($indexName)
1124
                ));
1125
            }
1126
        }
1127
1128
        return $instructions;
1129
    }
1130
1131
    /**
1132
     * @inheritDoc
1133
     */
1134
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1135
    {
1136
        $instructions = new AlterInstructions();
1137
        $indexName = strtolower($indexName);
1138
        $indexes = $this->getIndexes($tableName);
1139
1140
        $found = false;
1141
        foreach (array_keys($indexes) as $index) {
1142
            if ($indexName === strtolower($index)) {
1143
                $found = true;
1144
                break;
1145
            }
1146
        }
1147
1148
        if ($found) {
1149
            $schema = $this->getSchemaName($tableName, true)['schema'];
1150
                $instructions->addPostStep(sprintf(
1151
                    'DROP INDEX %s%s',
1152
                    $schema,
1153
                    $this->quoteColumnName($indexName)
1154
                ));
1155
        }
1156
1157
        return $instructions;
1158
    }
1159
1160
    /**
1161
     * {@inheritDoc}
1162
     *
1163
     * @throws \InvalidArgumentException
1164
     */
1165
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1166
    {
1167
        if ($constraint !== null) {
1168
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1169
        }
1170
1171
        $columns = array_map('strtolower', (array)$columns);
1172
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1173
1174
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1175
            return false;
1176
        }
1177
1178
        return true;
1179
    }
1180
1181
    /**
1182
     * Get the primary key from a particular table.
1183
     *
1184
     * @param string $tableName Table name
1185
     * @return string[]
1186
     */
1187
    protected function getPrimaryKey($tableName)
1188
    {
1189
        $primaryKey = [];
1190
1191
        $rows = $this->getTableInfo($tableName);
1192
1193
        foreach ($rows as $row) {
1194
            if ($row['pk'] > 0) {
1195
                $primaryKey[$row['pk'] - 1] = $row['name'];
1196
            }
1197
        }
1198
1199
        return $primaryKey;
1200
    }
1201
1202
    /**
1203
     * @inheritDoc
1204
     */
1205
    public function hasForeignKey($tableName, $columns, $constraint = null)
1206
    {
1207
        if ($constraint !== null) {
1208
            return preg_match(
1209
                "/,?\sCONSTRAINT\s" . preg_quote($this->quoteColumnName($constraint)) . ' FOREIGN KEY/',
1210
                $this->getDeclaringSql($tableName)
1211
            ) === 1;
1212
        }
1213
1214
        $columns = array_map('strtolower', (array)$columns);
1215
        $foreignKeys = $this->getForeignKeys($tableName);
1216
1217
        foreach ($foreignKeys as $key) {
1218
            $key = array_map('strtolower', $key);
1219
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1220
                continue;
1221
            }
1222
1223
            return true;
1224
        }
1225
1226
        return false;
1227
    }
1228
1229
    /**
1230
     * Get an array of foreign keys from a particular table.
1231
     *
1232
     * @param string $tableName Table name
1233
     * @return array
1234
     */
1235
    protected function getForeignKeys($tableName)
1236
    {
1237
        $foreignKeys = [];
1238
1239
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1240
1241
        foreach ($rows as $row) {
1242
            if (!isset($foreignKeys[$row['id']])) {
1243
                $foreignKeys[$row['id']] = [];
1244
            }
1245
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1246
        }
1247
1248
        return $foreignKeys;
1249
    }
1250
1251
    /**
1252
     * @param \Phinx\Db\Table\Table $table The Table
1253
     * @param string $column Column Name
1254
     * @return \Phinx\Db\Util\AlterInstructions
1255
     */
1256
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1257
    {
1258
        $instructions = $this->beginAlterByCopyTable($table->getName());
1259
1260
        $tableName = $table->getName();
1261
        $instructions->addPostStep(function ($state) use ($column) {
1262
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1263
1264
            $sql = $state['createSQL'];
1265
1266
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1267
                if (isset($matches[2])) {
1268
                    if ($matches[2] === 'INTEGER') {
1269
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1270
                    } else {
1271
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1272
                    }
1273
1274
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1275
                }
1276
            }
1277
1278
            $this->execute($sql);
1279
1280
            return $state;
1281
        });
1282
1283
        $instructions->addPostStep(function ($state) {
1284
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1285
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1286
            $selectColumns = $writeColumns = $names;
1287
1288
            return compact('selectColumns', 'writeColumns') + $state;
1289
        });
1290
1291
        return $this->copyAndDropTmpTable($instructions, $tableName);
1292
    }
1293
1294
    /**
1295
     * @param \Phinx\Db\Table\Table $table Table
1296
     * @param string $column Column Name
1297
     * @return \Phinx\Db\Util\AlterInstructions
1298
     */
1299
    protected function getDropPrimaryKeyInstructions($table, $column)
1300
    {
1301
        $instructions = $this->beginAlterByCopyTable($table->getName());
1302
1303
        $instructions->addPostStep(function ($state) {
1304
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1305
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1306
1307
            if ($sql) {
1308
                $this->execute($sql);
1309
            }
1310
1311
            return $state;
1312
        });
1313
1314
        $instructions->addPostStep(function ($state) use ($column) {
1315
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1316
1317
            return $newState + $state;
1318
        });
1319
1320
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1321
    }
1322
1323
    /**
1324
     * @inheritDoc
1325
     */
1326
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1327
    {
1328
        $instructions = $this->beginAlterByCopyTable($table->getName());
1329
1330
        $tableName = $table->getName();
1331
        $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
1332
            $this->execute('pragma foreign_keys = ON');
1333
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1334
1335
            //Delete indexes from original table and recreate them in temporary table
1336
            $schema = $this->getSchemaName($tableName, true)['schema'];
1337
            $tmpTableName = $state['tmpTableName'];
1338
            $indexes = $this->getIndexes($tableName);
1339
            foreach (array_keys($indexes) as $indexName) {
1340
                if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1341
                    $sql .= sprintf(
1342
                        'DROP INDEX %s%s; ',
1343
                        $schema,
1344
                        $this->quoteColumnName($indexName)
1345
                    );
1346
                    $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName);
1347
                    $sql .= preg_replace(
1348
                        "/\b${tableName}\b/",
1349
                        $tmpTableName,
1350
                        $createIndexSQL
1351
                    );
1352
                }
1353
            }
1354
1355
            $this->execute($sql);
1356
1357
            return $state;
1358
        });
1359
1360
        $instructions->addPostStep(function ($state) {
1361
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1362
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1363
            $selectColumns = $writeColumns = $names;
1364
1365
            return compact('selectColumns', 'writeColumns') + $state;
1366
        });
1367
1368
        return $this->copyAndDropTmpTable($instructions, $tableName);
1369
    }
1370
1371
    /**
1372
     * {@inheritDoc}
1373
     *
1374
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
1375
     *
1376
     * @throws \BadMethodCallException
1377
     */
1378
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1379
    {
1380
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1381
    }
1382
1383
    /**
1384
     * {@inheritDoc}
1385
     *
1386
     * @throws \InvalidArgumentException
1387
     */
1388
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1389
    {
1390
        $instructions = $this->beginAlterByCopyTable($tableName);
1391
1392
        $instructions->addPostStep(function ($state) use ($columns) {
1393
            $sql = '';
1394
1395
            foreach ($columns as $columnName) {
1396
                $search = sprintf(
1397
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1398
                    $this->quoteColumnName($columnName)
1399
                );
1400
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1401
            }
1402
1403
            if ($sql) {
1404
                $this->execute($sql);
1405
            }
1406
1407
            return $state;
1408
        });
1409
1410
        $instructions->addPostStep(function ($state) use ($columns) {
1411
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1412
1413
            $selectColumns = $newState['selectColumns'];
1414
            $columns = array_map([$this, 'quoteColumnName'], $columns);
1415
            $diff = array_diff($columns, $selectColumns);
1416
1417
            if (!empty($diff)) {
1418
                throw new InvalidArgumentException(sprintf(
1419
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1420
                ));
1421
            }
1422
1423
            return $newState + $state;
1424
        });
1425
1426
        return $this->copyAndDropTmpTable($instructions, $tableName);
1427
    }
1428
1429
    /**
1430
     * {@inheritDoc}
1431
     *
1432
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1433
     */
1434
    public function getSqlType($type, $limit = null)
1435
    {
1436
        $typeLC = strtolower($type);
1437
        if ($type instanceof Literal) {
1438
            $name = $type;
1439
        } elseif (isset(static::$supportedColumnTypes[$typeLC])) {
1440
            $name = static::$supportedColumnTypes[$typeLC];
1441
        } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) {
1442
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1443
        } else {
1444
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1445
        }
1446
1447
        return ['name' => $name, 'limit' => $limit];
1448
    }
1449
1450
    /**
1451
     * Returns Phinx type by SQL type
1452
     *
1453
     * @param string|null $sqlTypeDef SQL Type definition
1454
     * @return array
1455
     */
1456
    public function getPhinxType($sqlTypeDef)
1457
    {
1458
        $limit = null;
1459
        $scale = null;
1460
        if ($sqlTypeDef === null) {
1461
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1462
            $name = null;
1463
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1464
            // doesn't match the pattern of a type we'd know about
1465
            $name = Literal::from($sqlTypeDef);
1466
        } else {
1467
            // possibly a known type
1468
            $type = $match[1];
1469
            $typeLC = strtolower($type);
1470
            $affinity = $match[2] ?? '';
1471
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1472
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1473
            if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) {
1474
                // the type is a MySQL-style boolean
1475
                $name = static::PHINX_TYPE_BOOLEAN;
1476
                $limit = null;
1477
            } elseif (isset(static::$supportedColumnTypes[$typeLC])) {
1478
                // the type is an explicitly supported type
1479
                $name = $typeLC;
1480
            } elseif (isset(static::$supportedColumnTypeAliases[$typeLC])) {
1481
                // the type is an alias for a supported type
1482
                $name = static::$supportedColumnTypeAliases[$typeLC];
1483
            } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) {
1484
                // unsupported but known types are passed through lowercased, and without appended affinity
1485
                $name = Literal::from($typeLC);
1486
            } else {
1487
                // unknown types are passed through as-is
1488
                $name = Literal::from($type . $affinity);
1489
            }
1490
        }
1491
1492
        return [
1493
            'name' => $name,
1494
            'limit' => $limit,
1495
            'scale' => $scale,
1496
        ];
1497
    }
1498
1499
    /**
1500
     * @inheritDoc
1501
     */
1502
    public function createDatabase($name, $options = [])
1503
    {
1504
        touch($name . $this->suffix);
1505
    }
1506
1507
    /**
1508
     * @inheritDoc
1509
     */
1510
    public function hasDatabase($name)
1511
    {
1512
        return is_file($name . $this->suffix);
1513
    }
1514
1515
    /**
1516
     * @inheritDoc
1517
     */
1518
    public function dropDatabase($name)
1519
    {
1520
        $this->createdTables = [];
1521
        if ($this->getOption('memory')) {
1522
            $this->disconnect();
1523
            $this->connect();
1524
        }
1525
        if (file_exists($name . $this->suffix)) {
1526
            unlink($name . $this->suffix);
1527
        }
1528
    }
1529
1530
    /**
1531
     * Gets the SQLite Column Definition for a Column object.
1532
     *
1533
     * @param \Phinx\Db\Table\Column $column Column
1534
     * @return string
1535
     */
1536
    protected function getColumnSqlDefinition(Column $column)
1537
    {
1538
        $isLiteralType = $column->getType() instanceof Literal;
1539
        if ($isLiteralType) {
1540
            $def = (string)$column->getType();
1541
        } else {
1542
            $sqlType = $this->getSqlType($column->getType());
1543
            $def = strtoupper($sqlType['name']);
1544
1545
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true);
1546
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1547
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1548
            }
1549
        }
1550
        if ($column->getPrecision() && $column->getScale()) {
1551
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1552
        }
1553
1554
        $default = $column->getDefault();
1555
1556
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1557
        $def .= $this->getDefaultValueDefinition($default, $column->getType());
1558
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1559
1560
        $def .= $this->getCommentDefinition($column);
1561
1562
        return $def;
1563
    }
1564
1565
    /**
1566
     * Gets the comment Definition for a Column object.
1567
     *
1568
     * @param \Phinx\Db\Table\Column $column Column
1569
     * @return string
1570
     */
1571
    protected function getCommentDefinition(Column $column)
1572
    {
1573
        if ($column->getComment()) {
1574
            return ' /* ' . $column->getComment() . ' */ ';
1575
        }
1576
1577
        return '';
1578
    }
1579
1580
    /**
1581
     * Gets the SQLite Index Definition for an Index object.
1582
     *
1583
     * @param \Phinx\Db\Table\Table $table Table
1584
     * @param \Phinx\Db\Table\Index $index Index
1585
     * @return string
1586
     */
1587
    protected function getIndexSqlDefinition(Table $table, Index $index)
1588
    {
1589
        if ($index->getType() === Index::UNIQUE) {
1590
            $def = 'UNIQUE INDEX';
1591
        } else {
1592
            $def = 'INDEX';
1593
        }
1594
        if (is_string($index->getName())) {
1595
            $indexName = $index->getName();
1596
        } else {
1597
            $indexName = $table->getName() . '_';
1598
            foreach ($index->getColumns() as $column) {
1599
                $indexName .= $column . '_';
1600
            }
1601
            $indexName .= 'index';
1602
        }
1603
        $def .= ' `' . $indexName . '`';
1604
1605
        return $def;
1606
    }
1607
1608
    /**
1609
     * @inheritDoc
1610
     */
1611
    public function getColumnTypes()
1612
    {
1613
        return array_keys(static::$supportedColumnTypes);
1614
    }
1615
1616
    /**
1617
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1618
     *
1619
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1620
     * @return string
1621
     */
1622
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1623
    {
1624
        $def = '';
1625
        if ($foreignKey->getConstraint()) {
1626
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1627
        }
1628
        $columnNames = [];
1629
        foreach ($foreignKey->getColumns() as $column) {
1630
            $columnNames[] = $this->quoteColumnName($column);
1631
        }
1632
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1633
        $refColumnNames = [];
1634
        foreach ($foreignKey->getReferencedColumns() as $column) {
1635
            $refColumnNames[] = $this->quoteColumnName($column);
1636
        }
1637
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1638
        if ($foreignKey->getOnDelete()) {
1639
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1640
        }
1641
        if ($foreignKey->getOnUpdate()) {
1642
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1643
        }
1644
1645
        return $def;
1646
    }
1647
1648
    /**
1649
     * @inheritDoc
1650
     */
1651
    public function getDecoratedConnection()
1652
    {
1653
        $options = $this->getOptions();
1654
        $options['quoteIdentifiers'] = true;
1655
1656
        if (!empty($options['name'])) {
1657
            $options['database'] = $options['name'];
1658
1659
            if (file_exists($options['name'] . $this->suffix)) {
1660
                $options['database'] = $options['name'] . $this->suffix;
1661
            }
1662
        }
1663
1664
        if ($this->connection === null) {
1665
            throw new RuntimeException('You need to connect first.');
1666
        }
1667
1668
        $driver = new SqliteDriver($options);
1669
        $driver->setConnection($this->connection);
1670
1671
        return new Connection(['driver' => $driver] + $options);
1672
    }
1673
}
1674