Passed
Pull Request — master (#1946)
by Oliver
04:50 queued 01:14
created

SQLiteAdapter::createTable()   D

Complexity

Conditions 16
Paths 240

Size

Total Lines 65
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 16.0276

Importance

Changes 0
Metric Value
eloc 36
c 0
b 0
f 0
dl 0
loc 65
ccs 40
cts 42
cp 0.9524
rs 4.2333
cc 16
nc 240
nop 3
crap 16.0276

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