Completed
Pull Request — master (#1847)
by
unknown
01:32
created

SQLiteAdapter::getDeclaringIndexSql()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 9
cp 0.7778
rs 9.8333
c 0
b 0
f 0
cc 4
nc 3
nop 2
crap 4.1755
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
    protected $suffix = '.sqlite3';
124
125
    /**
126
     * Indicates whether the database library version is at least the specified version
127
     *
128
     * @param string $ver The version to check against e.g. '3.28.0'
129
     *
130
     * @return bool
131
     */
132
    public function databaseVersionAtLeast($ver)
133
    {
134 43
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
135
136 43
        return version_compare($actual, $ver, '>=');
137
    }
138
139
    /**
140
     * {@inheritDoc}
141
     *
142 44
     * @throws \RuntimeException
143
     * @throws \InvalidArgumentException
144 44
     *
145
     * @return void
146
     */
147
    public function connect()
148
    {
149
        if ($this->connection === null) {
150 42
            if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) {
151
                // @codeCoverageIgnoreStart
152 42
                throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
153 42
                // @codeCoverageIgnoreEnd
154 42
            }
155 12
156 42
            $options = $this->getOptions();
157
158 42
            // use a memory database if the option was specified
159
            if (!empty($options['memory']) || $options['name'] === static::MEMORY) {
160
                $dsn = 'sqlite:' . static::MEMORY;
161
            } else {
162
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
163
            }
164 42
165
            $driverOptions = [];
166
167 42
            // use custom data fetch mode
168 42
            if (!empty($options['fetch_mode'])) {
169 42
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
170 35
            }
171 35
172 35
            $db = $this->createPdoConnection($dsn, null, null, $driverOptions);
173 35
174
            $this->setConnection($db);
175 35
        }
176 42
    }
177
178 1
    /**
179 1
     * @inheritDoc
180 1
     */
181 1
    public function setOptions(array $options)
182
    {
183 1
        parent::setOptions($options);
184 1
185
        if (isset($options['suffix'])) {
186
            $this->suffix = $options['suffix'];
187 42
        }
188 42
        //don't "fix" the file extension if it is blank, some people
189 42
        //might want a SQLITE db file with absolutely no extension.
190 42
        if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
191 42
            $this->suffix = '.' . $this->suffix;
192
        }
193
194 42
        return $this;
195 42
    }
196 42
197 42
    /**
198 42
     * @inheritDoc
199 42
     */
200
    public function disconnect()
201
    {
202 1
        $this->connection = null;
203 1
    }
204 1
205
    /**
206 1
     * @inheritDoc
207 1
     */
208 1
    public function hasTransactions()
209 1
    {
210 1
        return true;
211 1
    }
212 42
213 42
    /**
214 37
     * @inheritDoc
215
     */
216
    public function beginTransaction()
217
    {
218 42
        $this->getConnection()->beginTransaction();
219 42
    }
220 1
221 1
    /**
222 1
     * @inheritDoc
223 1
     */
224
    public function commitTransaction()
225 42
    {
226
        $this->getConnection()->commit();
227 42
    }
228
229 42
    /**
230 6
     * @inheritDoc
231 42
     */
232 42
    public function rollbackTransaction()
233
    {
234
        $this->getConnection()->rollBack();
235
    }
236
237 1
    /**
238
     * @inheritDoc
239 1
     */
240 1
    public function quoteTableName($tableName)
241
    {
242
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
243
    }
244
245 1
    /**
246
     * @inheritDoc
247 1
     */
248 1
    public function quoteColumnName($columnName)
249
    {
250
        return '`' . str_replace('`', '``', $columnName) . '`';
251
    }
252
253 1
    /**
254
     * @param string $tableName Table name
255 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
256 1
     *
257 1
     * @return array
258 1
     */
259
    protected function getSchemaName($tableName, $quoted = false)
260 1
    {
261 1
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
262
            $table = $match[1];
263
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
264
            $result = ['schema' => $schema, 'table' => $table];
265
        } else {
266 1
            $result = ['schema' => '', 'table' => $tableName];
267
        }
268 1
269 1
        if ($quoted) {
270
            $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : '';
271 1
            $result['table'] = $this->quoteColumnName($result['table']);
272 1
        }
273 1
274 1
        return $result;
275 1
    }
276 1
277
    /**
278 1
     * Retrieves information about a given table from one of the SQLite pragmas
279 1
     *
280 1
     * @param string $tableName The table to query
281
     * @param string $pragma The pragma to query
282 1
     *
283 1
     * @return array
284 1
     */
285
    protected function getTableInfo($tableName, $pragma = 'table_info')
286 1
    {
287 1
        $info = $this->getSchemaName($tableName, true);
288
289 1
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
290
    }
291
292
    /**
293
     * Searches through all available schemata to find a table and returns an array
294
     * containing the bare schema name and whether the table exists at all.
295 8
     * If no schema was specified and the table does not exist the "main" schema is returned
296
     *
297 8
     * @param string $tableName The name of the table to find
298 8
     *
299 8
     * @return array
300 7
     */
301
    protected function resolveTable($tableName)
302 8
    {
303
        $info = $this->getSchemaName($tableName);
304 8
        if ($info['schema'] === '') {
305
            // if no schema is specified we search all schemata
306
            $rows = $this->fetchAll('PRAGMA database_list;');
307
            // the temp schema is always first to be searched
308
            $schemata = ['temp'];
309
            foreach ($rows as $row) {
310 4
                if (strtolower($row['name']) !== 'temp') {
311
                    $schemata[] = $row['name'];
312 4
                }
313 4
            }
314 4
            $defaultSchema = 'main';
315 4
        } else {
316 4
            // otherwise we search just the specified schema
317 4
            $schemata = (array)$info['schema'];
318
            $defaultSchema = $info['schema'];
319 4
        }
320 4
321
        $table = strtolower($info['table']);
322
        foreach ($schemata as $schema) {
323
            if (strtolower($schema) === 'temp') {
324
                $master = 'sqlite_temp_master';
325 2
            } else {
326
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
327 2
            }
328
            try {
329 2
                $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table)));
330
            } catch (PDOException $e) {
331 2
                // an exception can occur if the schema part of the table refers to a database which is not attached
332 2
                break;
333 2
            }
334 2
335 2
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
336 2
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
337
            foreach ($rows as $row) {
338 2
                if (strtolower($row['name']) === $table) {
339 2
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
340 2
                }
341 2
            }
342 2
        }
343 2
344 2
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
345 2
    }
346 2
347
    /**
348 2
     * @inheritDoc
349 1
     */
350
    public function hasTable($tableName)
351 1
    {
352
        return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
353
    }
354 1
355
    /**
356 1
     * @inheritDoc
357 1
     */
358 1
    public function createTable(Table $table, array $columns = [], array $indexes = [])
359
    {
360 1
        // Add the default primary key
361 1
        $options = $table->getOptions();
362
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
363
            $options['id'] = 'id';
364 1
        }
365 1
366 1
        if (isset($options['id']) && is_string($options['id'])) {
367 1
            // Handle id => "field_name" to support AUTO_INCREMENT
368 1
            $column = new Column();
369
            $column->setName($options['id'])
370 1
                   ->setType('integer')
371
                   ->setIdentity(true);
372 1
373
            array_unshift($columns, $column);
374 1
        }
375 1
376
        $sql = 'CREATE TABLE ';
377
        $sql .= $this->quoteTableName($table->getName()) . ' (';
378
        foreach ($columns as $column) {
379
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
380 6
381
            if (isset($options['primary_key']) && $column->getIdentity()) {
382
                //remove column from the primary key array as it is already defined as an autoincrement
383
                //primary id
384 6
                $identityColumnIndex = array_search($column->getName(), $options['primary_key'], true);
385
                if ($identityColumnIndex !== false) {
386 6
                    unset($options['primary_key'][$identityColumnIndex]);
387
388 6
                    if (empty($options['primary_key'])) {
389 6
                        //The last primary key has been removed
390 6
                        unset($options['primary_key']);
391 6
                    }
392 6
                }
393 6
            }
394
        }
395 6
396 6
        // set the primary key(s)
397 6
        if (isset($options['primary_key'])) {
398 6
            $sql = rtrim($sql);
399 6
            $sql .= ' PRIMARY KEY (';
400 6
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
401 6
                $sql .= $this->quoteColumnName($options['primary_key']);
402 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
403 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
404
            }
405 6
            $sql .= ')';
406
        } else {
407
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
408
        }
409
410
        $sql = rtrim($sql) . ');';
411 6
        // execute the sql
412
        $this->execute($sql);
413 6
414 6
        foreach ($indexes as $index) {
415 6
            $this->addIndex($table, $index);
416 6
        }
417
418 6
        $this->addCreatedTable($table->getName());
419
    }
420 6
421
    /**
422 6
     * {@inheritDoc}
423 6
     *
424 6
     * @throws \InvalidArgumentException
425 6
     */
426 6
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
427
    {
428 6
        $instructions = new AlterInstructions();
429
430 6
        // Drop the existing primary key
431 6
        $primaryKey = $this->getPrimaryKey($table->getName());
432 6
        if (!empty($primaryKey)) {
433
            $instructions->merge(
434
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
435
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
436
            );
437 2
        }
438
439
        // Add the primary key(s)
440 2
        if (!empty($newColumns)) {
441
            if (!is_string($newColumns)) {
442 2
                throw new InvalidArgumentException(sprintf(
443
                    'Invalid value for primary key: %s',
444 2
                    json_encode($newColumns)
445 2
                ));
446 2
            }
447 2
448 2
            $instructions->merge(
449 2
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
450
            );
451 2
        }
452 2
453 2
        return $instructions;
454 2
    }
455 2
456 2
    /**
457 2
     * @inheritDoc
458 2
     *
459 2
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
460
     *
461 2
     * @throws \BadMethodCallException
462
     */
463 2
    protected function getChangeCommentInstructions(Table $table, $newComment)
464
    {
465
        throw new BadMethodCallException('SQLite does not have table comments');
466
    }
467
468
    /**
469 2
     * @inheritDoc
470
     */
471 2
    protected function getRenameTableInstructions($tableName, $newTableName)
472 2
    {
473 2
        $this->updateCreatedTableName($tableName, $newTableName);
474
        $sql = sprintf(
475 2
            'ALTER TABLE %s RENAME TO %s',
476
            $this->quoteTableName($tableName),
477 2
            $this->quoteTableName($newTableName)
478 2
        );
479 2
480
        return new AlterInstructions([], [$sql]);
481 2
    }
482
483 2
    /**
484 2
     * @inheritDoc
485 2
     */
486 2
    protected function getDropTableInstructions($tableName)
487 2
    {
488
        $this->removeCreatedTable($tableName);
489 2
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
490
491 2
        return new AlterInstructions([], [$sql]);
492 2
    }
493 2
494
    /**
495
     * @inheritDoc
496
     */
497
    public function truncateTable($tableName)
498
    {
499
        $info = $this->resolveTable($tableName);
500
        // first try deleting the rows
501 9
        $this->execute(sprintf(
502
            'DELETE FROM %s.%s',
503 9
            $this->quoteColumnName($info['schema']),
504 9
            $this->quoteColumnName($info['table'])
505
        ));
506 9
507 9
        // assuming no error occurred, reset the autoincrement (if any)
508 9
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
509 9
            $this->execute(sprintf(
510 9
                'DELETE FROM %s.%s where name  = %s',
511 9
                $this->quoteColumnName($info['schema']),
512 9
                'sqlite_sequence',
513 9
                $this->quoteString($info['table'])
514 9
            ));
515 9
        }
516
    }
517
518
    /**
519
     * Parses a default-value expression to yield either a Literal representing
520
     * a string value, a string representing an expression, or some other scalar
521 9
     *
522
     * @param mixed $v The default-value expression to interpret
523 9
     * @param string $t The Phinx type of the column
524 4
     *
525 4
     * @return mixed
526
     */
527 9
    protected function parseDefaultValue($v, $t)
528 9
    {
529
        if ($v === null) {
530 9
            return null;
531 9
        }
532 9
533 9
        // split the input into tokens
534
        $trimChars = " \t\n\r\0\x0B";
535 8
        $pattern = <<<PCRE_PATTERN
536
            /
537 8
                '(?:[^']|'')*'|                 # String literal
538
                "(?:[^"]|"")*"|                 # Standard identifier
539
                `(?:[^`]|``)*`|                 # MySQL identifier
540
                \[[^\]]*\]|                     # SQL Server identifier
541
                --[^\r\n]*|                     # Single-line comment
542
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
543 1
                [^\/\-]+|                       # Non-special characters
544
                .                               # Any other single character
545 1
            /sx
546
PCRE_PATTERN;
547 1
        preg_match_all($pattern, $v, $matches);
548 1
        // strip out any comment tokens
549 1
        $matches = array_map(function ($v) {
550
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
551
        }, $matches[0]);
552
        // reconstitute the string, trimming whitespace as well as parentheses
553
        $vClean = trim(implode('', $matches));
554
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
555
556
        // match the string against one of several patterns
557
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
558
            // magic date or time
559 8
            return strtoupper($vBare);
560
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
561 8
            // string literal
562 8
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
563 8
564 8
            return Literal::from($str);
565 8
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
566 8
            $int = (int)$vBare;
567 8
            // integer literal
568 8
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int === 0 || $int === 1)) {
569 8
                return (bool)$int;
570 8
            } else {
571
                return $int;
572 8
            }
573 8
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
574 8
            // float literal
575
            return (float)$vBare;
576
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
577
            // hexadecimal literal
578
            return hexdec(substr($vBare, 2));
579 1
        } elseif (preg_match('/^null$/i', $vBare)) {
580
            // null literal
581 1
            return null;
582 1
        } elseif (preg_match('/^true|false$/i', $vBare)) {
583 1
            // boolean literal
584
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
585 1
        } else {
586 1
            // any other expression: return the expression with parentheses, but without comments
587
            return Expression::from($vClean);
588 1
        }
589 1
    }
590 1
591 1
    /**
592 1
     * Returns the name of the specified table's identity column, or null if the table has no identity
593 1
     *
594 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
595 1
     *
596 1
     * @param string $tableName The name of the table
597 1
     *
598
     * @return string|null
599
     */
600
    protected function resolveIdentity($tableName)
601
    {
602
        $result = null;
603
        // make sure the table has only one primary key column which is of type integer
604
        foreach ($this->getTableInfo($tableName) as $col) {
605 1
            $type = strtolower($col['type']);
606
            if ($col['pk'] > 1) {
607 1
                // the table has a composite primary key
608
                return null;
609 1
            } elseif ($col['pk'] == 0) {
610 1
                // the column is not a primary key column and is thus not relevant
611 1
                continue;
612 1
            } elseif ($type !== 'integer') {
613 1
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
614 1
                return null;
615 1
            } else {
616 1
                // the column is a candidate for a row ID alias
617 1
                $result = $col['name'];
618
            }
619
        }
620
        // if there is no suitable PK column, stop now
621
        if ($result === null) {
622
            return null;
623
        }
624
        // make sure the table does not have a PK-origin autoindex
625 5
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
626
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
627 5
            if ($idx['origin'] === 'pk') {
628
                return null;
629
            }
630 5
        }
631
632 5
        return $result;
633 5
    }
634 5
635
    /**
636 1
     * @inheritDoc
637
     */
638
    public function getColumns($tableName)
639
    {
640
        $columns = [];
641
642
        $rows = $this->getTableInfo($tableName);
643
        $identity = $this->resolveIdentity($tableName);
644
645 5
        foreach ($rows as $columnInfo) {
646
            $column = new Column();
647 5
            $type = $this->getPhinxType($columnInfo['type']);
648 5
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
0 ignored issues
show
Bug introduced by
It seems like $type['name'] can also be of type null or object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\SQLiteA...er::parseDefaultValue() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
649
650
            $column->setName($columnInfo['name'])
651
                   ->setNull($columnInfo['notnull'] !== '1')
652
                   ->setDefault($default)
653
                   ->setType($type['name'])
0 ignored issues
show
Bug introduced by
It seems like $type['name'] can also be of type null; however, Phinx\Db\Table\Column::setType() does only seem to accept string|object<Phinx\Util\Literal>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
654
                   ->setLimit($type['limit'])
655
                   ->setScale($type['scale'])
656
                   ->setIdentity($columnInfo['name'] === $identity);
657
658
            $columns[] = $column;
659
        }
660
661 5
        return $columns;
662
    }
663 5
664 5
    /**
665 5
     * @inheritDoc
666 5
     */
667 5
    public function hasColumn($tableName, $columnName)
668 5
    {
669 5
        $rows = $this->getTableInfo($tableName);
670 5
        foreach ($rows as $column) {
671 5
            if (strcasecmp($column['name'], $columnName) === 0) {
672 5
                return true;
673 5
            }
674
        }
675
676
        return false;
677
    }
678
679 4
    /**
680
     * @inheritDoc
681
     */
682 4
    protected function getAddColumnInstructions(Table $table, Column $column)
683
    {
684 4
        $tableName = $table->getName();
685 4
686
        $instructions = $this->beginAlterByCopyTable($tableName);
687 4
688 4
        $instructions->addPostStep(function ($state) use ($tableName, $column) {
689 4
            // we use the final column to anchor our regex to insert the new column,
690 4
            // as the alternative is unwinding all possible table constraints which
691 4
            // gets messy quickly with CHECK constraints.
692 4
            $columns = $this->getColumns($tableName);
693
            $finalColumnName = end($columns)->getName();
694 4
            $sql = preg_replace(
695 4
                sprintf(
696 4
                    "/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/",
697 4
                    $this->quoteColumnName($finalColumnName)
698 4
                ),
699
                sprintf(
700 4
                    '$1, %s %s$2',
701
                    $this->quoteColumnName($column->getName()),
702 4
                    $this->getColumnSqlDefinition($column)
703 4
                ),
704
                $state['createSQL'],
705 4
                1
706 4
            );
707 4
            $this->execute($sql);
708 4
709 4
            return $state;
710 4
        });
711 4
712
        $instructions->addPostStep(function ($state) use ($tableName) {
713 4
            $newState = $this->calculateNewTableColumns($tableName, false, false);
714 4
715 4
            return $newState + $state;
716
        });
717
718
        return $this->copyAndDropTmpTable($instructions, $tableName);
719
    }
720 1
721
    /**
722
     * Returns the original CREATE statement for the give table
723 1
     *
724
     * @param string $tableName The table name to get the create statement for
725
     *
726
     * @return string
727 1
     */
728
    protected function getDeclaringSql($tableName)
729 1
    {
730
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'");
731 1
732 1
        $sql = '';
733 1
        foreach ($rows as $table) {
734 1
            if ($table['tbl_name'] === $tableName) {
735 1
                $sql = $table['sql'];
736 1
            }
737
        }
738 1
739 1
        return $sql;
740 1
    }
741 1
742 1
    /**
743 1
     * Returns the original CREATE statement for the give index
744 1
     *
745
     * @param string $tableName The table name to get the create statement for
746 1
     *
747
     * @return string
748 1
     */
749
    protected function getDeclaringIndexSql($tableName, $indexName)
750
    {
751
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'");
752
753
        $sql = '';
754 1
        foreach ($rows as $table) {
755
            if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) {
756 1
                $sql = $table['sql'] . '; ';
757 1
            }
758 1
        }
759 1
760 1
        return $sql;
761 1
    }
762 1
763
    /**
764 1
     * Copies all the data from a tmp table to another table
765
     *
766 1
     * @param string $tableName The table name to copy the data to
767 1
     * @param string $tmpTableName The tmp table name where the data is stored
768 1
     * @param string[] $writeColumns The list of columns in the target table
769 1
     * @param string[] $selectColumns The list of columns in the tmp table
770 1
     *
771
     * @return void
772 1
     */
773
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
774 1
    {
775 1
        $sql = sprintf(
776 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
777
            $this->quoteTableName($tableName),
778
            implode(', ', $writeColumns),
779
            implode(', ', $selectColumns),
780
            $this->quoteTableName($tmpTableName)
781
        );
782
        $this->execute($sql);
783
    }
784
785
    /**
786
     * Modifies the passed instructions to copy all data from the table into
787
     * the provided tmp table and then drops the table and rename tmp table.
788
     *
789
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
790
     * @param string $tableName The table name to copy the data to
791
     *
792
     * @return \Phinx\Db\Util\AlterInstructions
793
     */
794
    protected function copyAndDropTmpTable($instructions, $tableName)
795
    {
796
        $instructions->addPostStep(function ($state) use ($tableName) {
797
            $this->copyDataToNewTable(
798
                $state['tmpTableName'],
799
                $tableName,
800
                $state['writeColumns'],
801
                $state['selectColumns']
802
            );
803
804
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
805
            $this->execute(sprintf(
806
                'ALTER TABLE %s RENAME TO %s',
807
                $this->quoteTableName($state['tmpTableName']),
808
                $this->quoteTableName($tableName)
809
            ));
810 43
811
            return $state;
812
        });
813 43
814 42
        return $instructions;
815
    }
816 43
817
    /**
818
     * Returns the columns and type to use when copying a table to another in the process
819 43
     * of altering a table
820 1
     *
821
     * @param string $tableName The table to modify
822 43
     * @param string|false $columnName The column name that is about to change
823 38
     * @param string|false $newColumnName Optionally the new name for the column
824
     *
825 43
     * @throws \InvalidArgumentException
826 42
     *
827
     * @return array
828 43
     */
829 2
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
830
    {
831 43
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
832 1
        $selectColumns = [];
833
        $writeColumns = [];
834 43
        $columnType = null;
835 1
        $found = false;
836
837 43
        foreach ($columns as $column) {
838 42
            $selectName = $column['name'];
839
            $writeName = $selectName;
840 43
841 1
            if ($selectName === $columnName) {
842
                $writeName = $newColumnName;
843 43
                $found = true;
844 1
                $columnType = $column['type'];
845
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
846 43
            }
847 43
848 1
            $selectColumns[] = $selectName;
849
            $writeColumns[] = $writeName;
850 43
        }
851 42
852
        $selectColumns = array_filter($selectColumns, 'strlen');
853 5
        $writeColumns = array_filter($writeColumns, 'strlen');
854
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
855 5
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
856 4
857
        if ($columnName && !$found) {
858
            throw new InvalidArgumentException(sprintf(
859
                'The specified column doesn\'t exist: ' . $columnName
860 1
            ));
861 1
        }
862
863
        return compact('writeColumns', 'selectColumns', 'columnType');
864 1
    }
865
866
    /**
867 1
     * Returns the initial instructions to alter a table using the
868
     * create-copy-drop strategy
869 1
     *
870 1
     * @param string $tableName The table to modify
871 1
     *
872
     * @return \Phinx\Db\Util\AlterInstructions
873
     */
874
    protected function beginAlterByCopyTable($tableName)
875
    {
876
        $instructions = new AlterInstructions();
877
        $instructions->addPostStep(function ($state) use ($tableName) {
878
            $tmpTableName = "tmp_{$tableName}";
879
            $createSQL = $this->getDeclaringSql($tableName);
880 3
881
            // Table name in SQLite can be hilarious inside declaring SQL:
882 3
            // - tableName
883 1
            // - `tableName`
884
            // - "tableName"
885 2
            // - [this is a valid table name too!]
886 2
            // - etc.
887 2
            // Just remove all characters before first "(" and build them again
888 2
            $createSQL = preg_replace(
889 1
                "/^CREATE TABLE .* \(/Ui",
890 1
                '',
891 2
                $createSQL
892
            );
893
894 2
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
895 2
896 1
            return compact('createSQL', 'tmpTableName') + $state;
897 1
        });
898
899
        return $instructions;
900 1
    }
901 2
902
    /**
903
     * @inheritDoc
904
     */
905
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
906
    {
907
        $instructions = $this->beginAlterByCopyTable($tableName);
908
909
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
910 2
            $sql = str_replace(
911
                $this->quoteColumnName($columnName),
912
                $this->quoteColumnName($newColumnName),
913
                $state['createSQL']
914
            );
915
            $this->execute($sql);
916 2
917 1
            return $state;
918
        });
919
920 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
921 1
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
922 2
923 1
            return $newState + $state;
924 1
        });
925 2
926 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
927
    }
928
929
    /**
930
     * @inheritDoc
931
     */
932
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
933 2
    {
934
        $instructions = $this->beginAlterByCopyTable($tableName);
935
936 1
        $newColumnName = $newColumn->getName();
937 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
938
            $sql = preg_replace(
939 1
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
940
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
941
                $state['createSQL'],
942
                1
943
            );
944
            $this->execute($sql);
945
946 48
            return $state;
947
        });
948 48
949 48
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
950
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
951
952
            return $newState + $state;
953
        });
954 2
955
        return $this->copyAndDropTmpTable($instructions, $tableName);
956 2
    }
957
958
    /**
959
     * @inheritDoc
960
     */
961
    protected function getDropColumnInstructions($tableName, $columnName)
962 48
    {
963
        $instructions = $this->beginAlterByCopyTable($tableName);
964 48
965 47
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
966 47
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
967 48
968
            return $newState + $state;
969
        });
970
971
        $instructions->addPostStep(function ($state) use ($columnName) {
972
            $sql = preg_replace(
973
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
974
                '',
975 42
                $state['createSQL']
976
            );
977 42
978 8
            if (substr($sql, -2) === ', ') {
979 42
                $sql = substr($sql, 0, -2) . ')';
980 42
            }
981 42
982 42
            $this->execute($sql);
983
984
            return $state;
985
        });
986
987
        return $this->copyAndDropTmpTable($instructions, $tableName);
988
    }
989
990
    /**
991 42
     * Get an array of indexes from a particular table.
992
     *
993 42
     * @param string $tableName Table name
994 42
     *
995 42
     * @return array
996 42
     */
997
    protected function getIndexes($tableName)
998
    {
999 42
        $indexes = [];
1000 42
        $schema = $this->getSchemaName($tableName, true)['schema'];
1001 42
        $indexList = $this->getTableInfo($tableName, 'index_list');
1002 42
1003 42
        foreach ($indexList as $index) {
1004 4
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
1005 4
            $cols = [];
1006
            foreach ($indexData as $indexItem) {
1007 42
                $cols[] = $indexItem['name'];
1008
            }
1009 42
            $indexes[$index['name']] = $cols;
1010 42
        }
1011 42
1012
        return $indexes;
1013 42
    }
1014
1015
    /**
1016
     * Finds the names of a table's indexes matching the supplied columns
1017 42
     *
1018
     * @param string $tableName The table to which the index belongs
1019 42
     * @param string|string[] $columns The columns of the index
1020
     *
1021
     * @return array
1022
     */
1023
    protected function resolveIndex($tableName, $columns)
1024
    {
1025
        $columns = array_map('strtolower', (array)$columns);
1026
        $indexes = $this->getIndexes($tableName);
1027
        $matches = [];
1028 42
1029
        foreach ($indexes as $name => $index) {
1030 42
            $indexCols = array_map('strtolower', $index);
1031 2
            if ($columns == $indexCols) {
1032
                $matches[] = $name;
1033 42
            }
1034
        }
1035
1036
        return $matches;
1037
    }
1038
1039
    /**
1040
     * @inheritDoc
1041
     */
1042 8
    public function hasIndex($tableName, $columns)
1043
    {
1044 8
        return (bool)$this->resolveIndex($tableName, $columns);
1045 2
    }
1046 2
1047 6
    /**
1048
     * @inheritDoc
1049 8
     */
1050 3
    public function hasIndexByName($tableName, $indexName)
1051 3
    {
1052 6
        $indexName = strtolower($indexName);
1053 6
        $indexes = $this->getIndexes($tableName);
1054 6
1055 6
        foreach (array_keys($indexes) as $index) {
1056 6
            if ($indexName === strtolower($index)) {
1057
                return true;
1058 8
            }
1059 8
        }
1060
1061
        return false;
1062
    }
1063
1064
    /**
1065 47
     * @inheritDoc
1066
     */
1067 47
    protected function getAddIndexInstructions(Table $table, Index $index)
1068
    {
1069
        $indexColumnArray = [];
1070
        foreach ($index->getColumns() as $column) {
1071
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1072
        }
1073
        $indexColumns = implode(',', $indexColumnArray);
1074
        $sql = sprintf(
1075
            'CREATE %s ON %s (%s)',
1076 5
            $this->getIndexSqlDefinition($table, $index),
1077
            $this->quoteTableName($table->getName()),
1078 5
            $indexColumns
1079 5
        );
1080
1081
        return new AlterInstructions([], [$sql]);
1082 5
    }
1083 5
1084 5
    /**
1085 5
     * @inheritDoc
1086 5
     */
1087 5
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1088 5
    {
1089 5
        $instructions = new AlterInstructions();
1090 5
        $indexNames = $this->resolveIndex($tableName, $columns);
1091 5
        $schema = $this->getSchemaName($tableName, true)['schema'];
1092 5
        foreach ($indexNames as $indexName) {
1093 1
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1094 1
                $instructions->addPostStep(sprintf(
1095 5
                    'DROP INDEX %s%s',
1096 1
                    $schema,
1097 1
                    $this->quoteColumnName($indexName)
1098
                ));
1099 5
            }
1100
        }
1101
1102
        return $instructions;
1103
    }
1104
1105
    /**
1106
     * @inheritDoc
1107
     */
1108
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1109
    {
1110
        $instructions = new AlterInstructions();
1111
        $indexName = strtolower($indexName);
1112
        $indexes = $this->getIndexes($tableName);
1113
1114
        $found = false;
1115
        foreach (array_keys($indexes) as $index) {
1116
            if ($indexName === strtolower($index)) {
1117
                $found = true;
1118
                break;
1119
            }
1120
        }
1121
1122
        if ($found) {
1123
            $schema = $this->getSchemaName($tableName, true)['schema'];
1124
                $instructions->addPostStep(sprintf(
1125
                    'DROP INDEX %s%s',
1126
                    $schema,
1127
                    $this->quoteColumnName($indexName)
1128
                ));
1129
        }
1130
1131
        return $instructions;
1132
    }
1133
1134
    /**
1135
     * {@inheritDoc}
1136
     *
1137
     * @throws \InvalidArgumentException
1138
     */
1139
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1140
    {
1141
        if ($constraint !== null) {
1142
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1143
        }
1144
1145
        $columns = array_map('strtolower', (array)$columns);
1146
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1147
1148
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1149
            return false;
1150
        }
1151
1152
        return true;
1153
    }
1154
1155
    /**
1156
     * Get the primary key from a particular table.
1157
     *
1158
     * @param string $tableName Table name
1159
     *
1160
     * @return string[]
1161
     */
1162
    protected function getPrimaryKey($tableName)
1163
    {
1164
        $primaryKey = [];
1165
1166
        $rows = $this->getTableInfo($tableName);
1167
1168
        foreach ($rows as $row) {
1169
            if ($row['pk'] > 0) {
1170
                $primaryKey[$row['pk'] - 1] = $row['name'];
1171
            }
1172
        }
1173
1174
        return $primaryKey;
1175
    }
1176
1177
    /**
1178
     * {@inheritDoc}
1179
     *
1180
     * @throws \InvalidArgumentException
1181
     */
1182
    public function hasForeignKey($tableName, $columns, $constraint = null)
1183
    {
1184
        if ($constraint !== null) {
1185
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1186
        }
1187
1188
        $columns = array_map('strtolower', (array)$columns);
1189
        $foreignKeys = $this->getForeignKeys($tableName);
1190
1191
        foreach ($foreignKeys as $key) {
1192
            $key = array_map('strtolower', $key);
1193
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1194
                continue;
1195
            }
1196
1197
            return true;
1198
        }
1199
1200
        return false;
1201
    }
1202
1203
    /**
1204
     * Get an array of foreign keys from a particular table.
1205
     *
1206
     * @param string $tableName Table name
1207
     *
1208
     * @return array
1209
     */
1210
    protected function getForeignKeys($tableName)
1211
    {
1212
        $foreignKeys = [];
1213
1214
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1215
1216
        foreach ($rows as $row) {
1217
            if (!isset($foreignKeys[$row['id']])) {
1218
                $foreignKeys[$row['id']] = [];
1219
            }
1220
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1221
        }
1222
1223
        return $foreignKeys;
1224
    }
1225
1226
    /**
1227
     * @param \Phinx\Db\Table\Table $table The Table
1228
     * @param string $column Column Name
1229
     *
1230
     * @return \Phinx\Db\Util\AlterInstructions
1231
     */
1232
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1233
    {
1234
        $instructions = $this->beginAlterByCopyTable($table->getName());
1235
1236
        $tableName = $table->getName();
1237
        $instructions->addPostStep(function ($state) use ($column) {
1238
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1239
1240
            $sql = $state['createSQL'];
1241
1242
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1243
                if (isset($matches[2])) {
1244
                    if ($matches[2] === 'INTEGER') {
1245
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1246
                    } else {
1247
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1248
                    }
1249
1250
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1251
                }
1252
            }
1253
1254
            $this->execute($sql);
1255
1256
            return $state;
1257
        });
1258
1259
        $instructions->addPostStep(function ($state) {
1260
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1261
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1262
            $selectColumns = $writeColumns = $names;
1263
1264
            return compact('selectColumns', 'writeColumns') + $state;
1265
        });
1266
1267
        return $this->copyAndDropTmpTable($instructions, $tableName);
1268
    }
1269
1270
    /**
1271
     * @param \Phinx\Db\Table\Table $table Table
1272
     * @param string $column Column Name
1273
     *
1274
     * @return \Phinx\Db\Util\AlterInstructions
1275
     */
1276
    protected function getDropPrimaryKeyInstructions($table, $column)
1277
    {
1278
        $instructions = $this->beginAlterByCopyTable($table->getName());
1279
1280
        $instructions->addPostStep(function ($state) {
1281
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1282
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1283
1284
            if ($sql) {
1285
                $this->execute($sql);
1286
            }
1287
1288
            return $state;
1289
        });
1290
1291
        $instructions->addPostStep(function ($state) use ($column) {
1292
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1293
1294
            return $newState + $state;
1295
        });
1296
1297
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1298
    }
1299
1300
    /**
1301
     * @inheritDoc
1302
     */
1303
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1304
    {
1305
        $instructions = $this->beginAlterByCopyTable($table->getName());
1306
1307
        $tableName = $table->getName();
1308
        $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
1309
            $this->execute('pragma foreign_keys = ON');
1310
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1311
1312
            //Delete indexes from original table and recreate them in temporary table
1313
            $schema = $this->getSchemaName($tableName, true)['schema'];
1314
            $tmpTableName = $state['tmpTableName'];
1315
            $indexes = $this->getIndexes($tableName);
1316
            foreach (array_keys($indexes) as $indexName) {
1317
                $sql .= sprintf(
1318
                    'DROP INDEX %s%s; ',
1319
                    $schema,
1320
                    $this->quoteColumnName($indexName)
1321
                );
1322
                $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName);
1323
                $sql .= preg_replace(
1324
                    "/\b${tableName}\b/",
1325
                    $tmpTableName,
1326
                    $createIndexSQL
1327
                );
1328
            }
1329
1330
            $this->execute($sql);
1331
1332
            return $state;
1333
        });
1334
1335
        $instructions->addPostStep(function ($state) {
1336
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1337
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1338
            $selectColumns = $writeColumns = $names;
1339
1340
            return compact('selectColumns', 'writeColumns') + $state;
1341
        });
1342
1343
        return $this->copyAndDropTmpTable($instructions, $tableName);
1344
    }
1345
1346
    /**
1347
     * @inheritDoc
1348
     *
1349
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
1350
     *
1351
     * @throws \BadMethodCallException
1352
     */
1353
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1354
    {
1355
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1356
    }
1357
1358
    /**
1359
     * {@inheritDoc}
1360
     *
1361
     * @throws \InvalidArgumentException
1362
     */
1363
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1364
    {
1365
        $instructions = $this->beginAlterByCopyTable($tableName);
1366
1367
        $instructions->addPostStep(function ($state) use ($columns) {
1368
            $sql = '';
1369
1370
            foreach ($columns as $columnName) {
1371
                $search = sprintf(
1372
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1373
                    $this->quoteColumnName($columnName)
1374
                );
1375
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1376
            }
1377
1378
            if ($sql) {
1379
                $this->execute($sql);
1380
            }
1381
1382
            return $state;
1383
        });
1384
1385
        $instructions->addPostStep(function ($state) use ($columns) {
1386
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1387
1388
            $selectColumns = $newState['selectColumns'];
1389
            $columns = array_map([$this, 'quoteColumnName'], $columns);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $columns, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
1390
            $diff = array_diff($columns, $selectColumns);
1391
1392
            if (!empty($diff)) {
1393
                throw new InvalidArgumentException(sprintf(
1394
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1395
                ));
1396
            }
1397
1398
            return $newState + $state;
1399
        });
1400
1401
        return $this->copyAndDropTmpTable($instructions, $tableName);
1402
    }
1403
1404
    /**
1405
     * {@inheritDoc}
1406
     *
1407
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1408
     */
1409
    public function getSqlType($type, $limit = null)
1410
    {
1411
        $typeLC = strtolower($type);
1412
        if ($type instanceof Literal) {
1413
            $name = $type;
1414
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1415
            $name = self::$supportedColumnTypes[$typeLC];
1416
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1417
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1418
        } else {
1419
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1420
        }
1421
1422
        return ['name' => $name, 'limit' => $limit];
1423
    }
1424
1425
    /**
1426
     * Returns Phinx type by SQL type
1427
     *
1428
     * @param string|null $sqlTypeDef SQL Type definition
1429
     *
1430
     * @return array
1431
     */
1432
    public function getPhinxType($sqlTypeDef)
1433
    {
1434
        $limit = null;
1435
        $scale = null;
1436
        if ($sqlTypeDef === null) {
1437
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1438
            $name = null;
1439
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1440
            // doesn't match the pattern of a type we'd know about
1441
            $name = Literal::from($sqlTypeDef);
1442
        } else {
1443
            // possibly a known type
1444
            $type = $match[1];
1445
            $typeLC = strtolower($type);
1446
            $affinity = $match[2] ?? '';
1447
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1448
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1449
            if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) {
1450
                // the type is a MySQL-style boolean
1451
                $name = static::PHINX_TYPE_BOOLEAN;
1452
                $limit = null;
1453
            } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1454
                // the type is an explicitly supported type
1455
                $name = $typeLC;
1456
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1457
                // the type is an alias for a supported type
1458
                $name = self::$supportedColumnTypeAliases[$typeLC];
1459
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1460
                // unsupported but known types are passed through lowercased, and without appended affinity
1461
                $name = Literal::from($typeLC);
1462
            } else {
1463
                // unknown types are passed through as-is
1464
                $name = Literal::from($type . $affinity);
1465
            }
1466
        }
1467
1468
        return [
1469
            'name' => $name,
1470
            'limit' => $limit,
1471
            'scale' => $scale,
1472
        ];
1473
    }
1474
1475
    /**
1476
     * @inheritDoc
1477
     */
1478
    public function createDatabase($name, $options = [])
1479
    {
1480
        touch($name . $this->suffix);
1481
    }
1482
1483
    /**
1484
     * @inheritDoc
1485
     */
1486
    public function hasDatabase($name)
1487
    {
1488
        return is_file($name . $this->suffix);
1489
    }
1490
1491
    /**
1492
     * @inheritDoc
1493
     */
1494
    public function dropDatabase($name)
1495
    {
1496
        if ($this->getOption('memory')) {
1497
            $this->disconnect();
1498
            $this->connect();
1499
        }
1500
        if (file_exists($name . $this->suffix)) {
1501
            unlink($name . $this->suffix);
1502
        }
1503
    }
1504
1505
    /**
1506
     * Gets the SQLite Column Definition for a Column object.
1507
     *
1508
     * @param \Phinx\Db\Table\Column $column Column
1509
     *
1510
     * @return string
1511
     */
1512
    protected function getColumnSqlDefinition(Column $column)
1513
    {
1514
        $isLiteralType = $column->getType() instanceof Literal;
1515
        if ($isLiteralType) {
1516
            $def = (string)$column->getType();
1517
        } else {
1518
            $sqlType = $this->getSqlType($column->getType());
1519
            $def = strtoupper($sqlType['name']);
1520
1521
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true);
1522
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1523
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1524
            }
1525
        }
1526
        if ($column->getPrecision() && $column->getScale()) {
1527
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1528
        }
1529
1530
        $default = $column->getDefault();
1531
1532
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1533
        $def .= $this->getDefaultValueDefinition($default, $column->getType());
0 ignored issues
show
Bug introduced by
It seems like $column->getType() targeting Phinx\Db\Table\Column::getType() can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\PdoAdap...efaultValueDefinition() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1534
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1535
1536
        $def .= $this->getCommentDefinition($column);
1537
1538
        return $def;
1539
    }
1540
1541
    /**
1542
     * Gets the comment Definition for a Column object.
1543
     *
1544
     * @param \Phinx\Db\Table\Column $column Column
1545
     *
1546
     * @return string
1547
     */
1548
    protected function getCommentDefinition(Column $column)
1549
    {
1550
        if ($column->getComment()) {
1551
            return ' /* ' . $column->getComment() . ' */ ';
1552
        }
1553
1554
        return '';
1555
    }
1556
1557
    /**
1558
     * Gets the SQLite Index Definition for an Index object.
1559
     *
1560
     * @param \Phinx\Db\Table\Table $table Table
1561
     * @param \Phinx\Db\Table\Index $index Index
1562
     *
1563
     * @return string
1564
     */
1565
    protected function getIndexSqlDefinition(Table $table, Index $index)
1566
    {
1567
        if ($index->getType() === Index::UNIQUE) {
1568
            $def = 'UNIQUE INDEX';
1569
        } else {
1570
            $def = 'INDEX';
1571
        }
1572
        if (is_string($index->getName())) {
1573
            $indexName = $index->getName();
1574
        } else {
1575
            $indexName = $table->getName() . '_';
1576
            foreach ($index->getColumns() as $column) {
1577
                $indexName .= $column . '_';
1578
            }
1579
            $indexName .= 'index';
1580
        }
1581
        $def .= ' `' . $indexName . '`';
1582
1583
        return $def;
1584
    }
1585
1586
    /**
1587
     * @inheritDoc
1588
     */
1589
    public function getColumnTypes()
1590
    {
1591
        return array_keys(static::$supportedColumnTypes);
1592
    }
1593
1594
    /**
1595
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1596
     *
1597
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1598
     *
1599
     * @return string
1600
     */
1601
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1602
    {
1603
        $def = '';
1604
        if ($foreignKey->getConstraint()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foreignKey->getConstraint() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1605
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1606
        }
1607
        $columnNames = [];
1608
        foreach ($foreignKey->getColumns() as $column) {
1609
            $columnNames[] = $this->quoteColumnName($column);
1610
        }
1611
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1612
        $refColumnNames = [];
1613
        foreach ($foreignKey->getReferencedColumns() as $column) {
1614
            $refColumnNames[] = $this->quoteColumnName($column);
1615
        }
1616
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1617
        if ($foreignKey->getOnDelete()) {
1618
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1619
        }
1620
        if ($foreignKey->getOnUpdate()) {
1621
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1622
        }
1623
1624
        return $def;
1625
    }
1626
1627
    /**
1628
     * @inheritDoc
1629
     */
1630
    public function getDecoratedConnection()
1631
    {
1632
        $options = $this->getOptions();
1633
        $options['quoteIdentifiers'] = true;
1634
1635
        if (!empty($options['name'])) {
1636
            $options['database'] = $options['name'];
1637
1638
            if (file_exists($options['name'] . $this->suffix)) {
1639
                $options['database'] = $options['name'] . $this->suffix;
1640
            }
1641
        }
1642
1643
        if ($this->connection === null) {
1644
            throw new RuntimeException('You need to connect first.');
1645
        }
1646
1647
        $driver = new SqliteDriver($options);
1648
        $driver->setConnection($this->connection);
1649
1650
        return new Connection(['driver' => $driver] + $options);
1651
    }
1652
}
1653