Passed
Push — master ( acdd85...1e8bfd )
by mark
02:54 queued 12s
created

SQLiteAdapter::connect()   D

Complexity

Conditions 18
Paths 16

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 18

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 25
c 2
b 0
f 1
dl 0
loc 43
ccs 26
cts 26
cp 1
rs 4.8666
cc 18
nc 16
nop 0
crap 18

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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