Passed
Pull Request — master (#1928)
by Corey
03:03
created

SQLiteAdapter::getForeignKeySqlDefinition()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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