Completed
Pull Request — master (#1842)
by
unknown
02:55 queued 01:19
created

SQLiteAdapter::getRenameTableInstructions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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
     * @return string
747
     */
748 1
    protected function getDeclaringIndexSql($tableName, $indexName)
749
    {
750
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'");
751
752
        $sql = '';
753
        foreach ($rows as $table) {
754 1
            if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) {
755
                $sql = $table['sql'] . '; ';
756 1
            }
757 1
        }
758 1
759 1
        return $sql;
760 1
    }
761 1
762 1
    /**
763
     * Copies all the data from a tmp table to another table
764 1
     *
765
     * @param string $tableName The table name to copy the data to
766 1
     * @param string $tmpTableName The tmp table name where the data is stored
767 1
     * @param string[] $writeColumns The list of columns in the target table
768 1
     * @param string[] $selectColumns The list of columns in the tmp table
769 1
     *
770 1
     * @return void
771
     */
772 1
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
773
    {
774 1
        $sql = sprintf(
775 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
776 1
            $this->quoteTableName($tableName),
777
            implode(', ', $writeColumns),
778
            implode(', ', $selectColumns),
779
            $this->quoteTableName($tmpTableName)
780
        );
781
        $this->execute($sql);
782
    }
783
784
    /**
785
     * Modifies the passed instructions to copy all data from the table into
786
     * the provided tmp table and then drops the table and rename tmp table.
787
     *
788
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
789
     * @param string $tableName The table name to copy the data to
790
     *
791
     * @return \Phinx\Db\Util\AlterInstructions
792
     */
793
    protected function copyAndDropTmpTable($instructions, $tableName)
794
    {
795
        $instructions->addPostStep(function ($state) use ($tableName) {
796
            $this->copyDataToNewTable(
797
                $state['tmpTableName'],
798
                $tableName,
799
                $state['writeColumns'],
800
                $state['selectColumns']
801
            );
802
803
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
804
            $this->execute(sprintf(
805
                'ALTER TABLE %s RENAME TO %s',
806
                $this->quoteTableName($state['tmpTableName']),
807
                $this->quoteTableName($tableName)
808
            ));
809
810 43
            return $state;
811
        });
812
813 43
        return $instructions;
814 42
    }
815
816 43
    /**
817
     * Returns the columns and type to use when copying a table to another in the process
818
     * of altering a table
819 43
     *
820 1
     * @param string $tableName The table to modify
821
     * @param string|false $columnName The column name that is about to change
822 43
     * @param string|false $newColumnName Optionally the new name for the column
823 38
     *
824
     * @throws \InvalidArgumentException
825 43
     *
826 42
     * @return array
827
     */
828 43
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
829 2
    {
830
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
831 43
        $selectColumns = [];
832 1
        $writeColumns = [];
833
        $columnType = null;
834 43
        $found = false;
835 1
836
        foreach ($columns as $column) {
837 43
            $selectName = $column['name'];
838 42
            $writeName = $selectName;
839
840 43
            if ($selectName === $columnName) {
841 1
                $writeName = $newColumnName;
842
                $found = true;
843 43
                $columnType = $column['type'];
844 1
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
845
            }
846 43
847 43
            $selectColumns[] = $selectName;
848 1
            $writeColumns[] = $writeName;
849
        }
850 43
851 42
        $selectColumns = array_filter($selectColumns, 'strlen');
852
        $writeColumns = array_filter($writeColumns, 'strlen');
853 5
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
854
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
855 5
856 4
        if ($columnName && !$found) {
857
            throw new InvalidArgumentException(sprintf(
858
                'The specified column doesn\'t exist: ' . $columnName
859
            ));
860 1
        }
861 1
862
        return compact('writeColumns', 'selectColumns', 'columnType');
863
    }
864 1
865
    /**
866
     * Returns the initial instructions to alter a table using the
867 1
     * create-copy-drop strategy
868
     *
869 1
     * @param string $tableName The table to modify
870 1
     *
871 1
     * @return \Phinx\Db\Util\AlterInstructions
872
     */
873
    protected function beginAlterByCopyTable($tableName)
874
    {
875
        $instructions = new AlterInstructions();
876
        $instructions->addPostStep(function ($state) use ($tableName) {
877
            $tmpTableName = "tmp_{$tableName}";
878
            $createSQL = $this->getDeclaringSql($tableName);
879
880 3
            // Table name in SQLite can be hilarious inside declaring SQL:
881
            // - tableName
882 3
            // - `tableName`
883 1
            // - "tableName"
884
            // - [this is a valid table name too!]
885 2
            // - etc.
886 2
            // Just remove all characters before first "(" and build them again
887 2
            $createSQL = preg_replace(
888 2
                "/^CREATE TABLE .* \(/Ui",
889 1
                '',
890 1
                $createSQL
891 2
            );
892
893
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
894 2
895 2
            return compact('createSQL', 'tmpTableName') + $state;
896 1
        });
897 1
898
        return $instructions;
899
    }
900 1
901 2
    /**
902
     * @inheritDoc
903
     */
904
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
905
    {
906
        $instructions = $this->beginAlterByCopyTable($tableName);
907
908
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
909
            $sql = str_replace(
910 2
                $this->quoteColumnName($columnName),
911
                $this->quoteColumnName($newColumnName),
912
                $state['createSQL']
913
            );
914
            $this->execute($sql);
915
916 2
            return $state;
917 1
        });
918
919
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
920 1
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
921 1
922 2
            return $newState + $state;
923 1
        });
924 1
925 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
926 2
    }
927
928
    /**
929
     * @inheritDoc
930
     */
931
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
932
    {
933 2
        $instructions = $this->beginAlterByCopyTable($tableName);
934
935
        $newColumnName = $newColumn->getName();
936 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
937 1
            $sql = preg_replace(
938
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
939 1
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
940
                $state['createSQL'],
941
                1
942
            );
943
            $this->execute($sql);
944
945
            return $state;
946 48
        });
947
948 48
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
949 48
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
950
951
            return $newState + $state;
952
        });
953
954 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
955
    }
956 2
957
    /**
958
     * @inheritDoc
959
     */
960
    protected function getDropColumnInstructions($tableName, $columnName)
961
    {
962 48
        $instructions = $this->beginAlterByCopyTable($tableName);
963
964 48
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
965 47
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
966 47
967 48
            return $newState + $state;
968
        });
969
970
        $instructions->addPostStep(function ($state) use ($columnName) {
971
            $sql = preg_replace(
972
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
973
                '',
974
                $state['createSQL']
975 42
            );
976
977 42
            if (substr($sql, -2) === ', ') {
978 8
                $sql = substr($sql, 0, -2) . ')';
979 42
            }
980 42
981 42
            $this->execute($sql);
982 42
983
            return $state;
984
        });
985
986
        return $this->copyAndDropTmpTable($instructions, $tableName);
987
    }
988
989
    /**
990
     * Get an array of indexes from a particular table.
991 42
     *
992
     * @param string $tableName Table name
993 42
     *
994 42
     * @return array
995 42
     */
996 42
    protected function getIndexes($tableName)
997
    {
998
        $indexes = [];
999 42
        $schema = $this->getSchemaName($tableName, true)['schema'];
1000 42
        $indexList = $this->getTableInfo($tableName, 'index_list');
1001 42
1002 42
        foreach ($indexList as $index) {
1003 42
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
1004 4
            $cols = [];
1005 4
            foreach ($indexData as $indexItem) {
1006
                $cols[] = $indexItem['name'];
1007 42
            }
1008
            $indexes[$index['name']] = $cols;
1009 42
        }
1010 42
1011 42
        return $indexes;
1012
    }
1013 42
1014
    /**
1015
     * Finds the names of a table's indexes matching the supplied columns
1016
     *
1017 42
     * @param string $tableName The table to which the index belongs
1018
     * @param string|string[] $columns The columns of the index
1019 42
     *
1020
     * @return array
1021
     */
1022
    protected function resolveIndex($tableName, $columns)
1023
    {
1024
        $columns = array_map('strtolower', (array)$columns);
1025
        $indexes = $this->getIndexes($tableName);
1026
        $matches = [];
1027
1028 42
        foreach ($indexes as $name => $index) {
1029
            $indexCols = array_map('strtolower', $index);
1030 42
            if ($columns == $indexCols) {
1031 2
                $matches[] = $name;
1032
            }
1033 42
        }
1034
1035
        return $matches;
1036
    }
1037
1038
    /**
1039
     * @inheritDoc
1040
     */
1041
    public function hasIndex($tableName, $columns)
1042 8
    {
1043
        return (bool)$this->resolveIndex($tableName, $columns);
1044 8
    }
1045 2
1046 2
    /**
1047 6
     * @inheritDoc
1048
     */
1049 8
    public function hasIndexByName($tableName, $indexName)
1050 3
    {
1051 3
        $indexName = strtolower($indexName);
1052 6
        $indexes = $this->getIndexes($tableName);
1053 6
1054 6
        foreach (array_keys($indexes) as $index) {
1055 6
            if ($indexName === strtolower($index)) {
1056 6
                return true;
1057
            }
1058 8
        }
1059 8
1060
        return false;
1061
    }
1062
1063
    /**
1064
     * @inheritDoc
1065 47
     */
1066
    protected function getAddIndexInstructions(Table $table, Index $index)
1067 47
    {
1068
        $indexColumnArray = [];
1069
        foreach ($index->getColumns() as $column) {
1070
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1071
        }
1072
        $indexColumns = implode(',', $indexColumnArray);
1073
        $sql = sprintf(
1074
            'CREATE %s ON %s (%s)',
1075
            $this->getIndexSqlDefinition($table, $index),
1076 5
            $this->quoteTableName($table->getName()),
1077
            $indexColumns
1078 5
        );
1079 5
1080
        return new AlterInstructions([], [$sql]);
1081
    }
1082 5
1083 5
    /**
1084 5
     * @inheritDoc
1085 5
     */
1086 5
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1087 5
    {
1088 5
        $instructions = new AlterInstructions();
1089 5
        $indexNames = $this->resolveIndex($tableName, $columns);
1090 5
        $schema = $this->getSchemaName($tableName, true)['schema'];
1091 5
        foreach ($indexNames as $indexName) {
1092 5
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1093 1
                $instructions->addPostStep(sprintf(
1094 1
                    'DROP INDEX %s%s',
1095 5
                    $schema,
1096 1
                    $this->quoteColumnName($indexName)
1097 1
                ));
1098
            }
1099 5
        }
1100
1101
        return $instructions;
1102
    }
1103
1104
    /**
1105
     * @inheritDoc
1106
     */
1107
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1108
    {
1109
        $instructions = new AlterInstructions();
1110
        $indexName = strtolower($indexName);
1111
        $indexes = $this->getIndexes($tableName);
1112
1113
        $found = false;
1114
        foreach (array_keys($indexes) as $index) {
1115
            if ($indexName === strtolower($index)) {
1116
                $found = true;
1117
                break;
1118
            }
1119
        }
1120
1121
        if ($found) {
1122
            $schema = $this->getSchemaName($tableName, true)['schema'];
1123
                $instructions->addPostStep(sprintf(
1124
                    'DROP INDEX %s%s',
1125
                    $schema,
1126
                    $this->quoteColumnName($indexName)
1127
                ));
1128
        }
1129
1130
        return $instructions;
1131
    }
1132
1133
    /**
1134
     * {@inheritDoc}
1135
     *
1136
     * @throws \InvalidArgumentException
1137
     */
1138
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1139
    {
1140
        if ($constraint !== null) {
1141
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1142
        }
1143
1144
        $columns = array_map('strtolower', (array)$columns);
1145
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1146
1147
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1148
            return false;
1149
        }
1150
1151
        return true;
1152
    }
1153
1154
    /**
1155
     * Get the primary key from a particular table.
1156
     *
1157
     * @param string $tableName Table name
1158
     *
1159
     * @return string[]
1160
     */
1161
    protected function getPrimaryKey($tableName)
1162
    {
1163
        $primaryKey = [];
1164
1165
        $rows = $this->getTableInfo($tableName);
1166
1167
        foreach ($rows as $row) {
1168
            if ($row['pk'] > 0) {
1169
                $primaryKey[$row['pk'] - 1] = $row['name'];
1170
            }
1171
        }
1172
1173
        return $primaryKey;
1174
    }
1175
1176
    /**
1177
     * {@inheritDoc}
1178
     *
1179
     * @throws \InvalidArgumentException
1180
     */
1181
    public function hasForeignKey($tableName, $columns, $constraint = null)
1182
    {
1183
        if ($constraint !== null) {
1184
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1185
        }
1186
1187
        $columns = array_map('strtolower', (array)$columns);
1188
        $foreignKeys = $this->getForeignKeys($tableName);
1189
1190
        foreach ($foreignKeys as $key) {
1191
            $key = array_map('strtolower', $key);
1192
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1193
                continue;
1194
            }
1195
1196
            return true;
1197
        }
1198
1199
        return false;
1200
    }
1201
1202
    /**
1203
     * Get an array of foreign keys from a particular table.
1204
     *
1205
     * @param string $tableName Table name
1206
     *
1207
     * @return array
1208
     */
1209
    protected function getForeignKeys($tableName)
1210
    {
1211
        $foreignKeys = [];
1212
1213
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1214
1215
        foreach ($rows as $row) {
1216
            if (!isset($foreignKeys[$row['id']])) {
1217
                $foreignKeys[$row['id']] = [];
1218
            }
1219
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1220
        }
1221
1222
        return $foreignKeys;
1223
    }
1224
1225
    /**
1226
     * @param \Phinx\Db\Table\Table $table The Table
1227
     * @param string $column Column Name
1228
     *
1229
     * @return \Phinx\Db\Util\AlterInstructions
1230
     */
1231
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1232
    {
1233
        $instructions = $this->beginAlterByCopyTable($table->getName());
1234
1235
        $tableName = $table->getName();
1236
        $instructions->addPostStep(function ($state) use ($column) {
1237
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1238
1239
            $sql = $state['createSQL'];
1240
1241
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1242
                if (isset($matches[2])) {
1243
                    if ($matches[2] === 'INTEGER') {
1244
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1245
                    } else {
1246
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1247
                    }
1248
1249
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1250
                }
1251
            }
1252
1253
            $this->execute($sql);
1254
1255
            return $state;
1256
        });
1257
1258
        $instructions->addPostStep(function ($state) {
1259
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1260
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1261
            $selectColumns = $writeColumns = $names;
1262
1263
            return compact('selectColumns', 'writeColumns') + $state;
1264
        });
1265
1266
        return $this->copyAndDropTmpTable($instructions, $tableName);
1267
    }
1268
1269
    /**
1270
     * @param \Phinx\Db\Table\Table $table Table
1271
     * @param string $column Column Name
1272
     *
1273
     * @return \Phinx\Db\Util\AlterInstructions
1274
     */
1275
    protected function getDropPrimaryKeyInstructions($table, $column)
1276
    {
1277
        $instructions = $this->beginAlterByCopyTable($table->getName());
1278
1279
        $instructions->addPostStep(function ($state) {
1280
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1281
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1282
1283
            if ($sql) {
1284
                $this->execute($sql);
1285
            }
1286
1287
            return $state;
1288
        });
1289
1290
        $instructions->addPostStep(function ($state) use ($column) {
1291
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1292
1293
            return $newState + $state;
1294
        });
1295
1296
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1297
    }
1298
1299
    /**
1300
     * @inheritDoc
1301
     */
1302
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1303
    {
1304
        $instructions = $this->beginAlterByCopyTable($table->getName());
1305
1306
        $tableName = $table->getName();
1307
        $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
1308
            $this->execute('pragma foreign_keys = ON');
1309
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1310
1311
            //Delete indexes from original table and recreate them in temporary table
1312
            $schema = $this->getSchemaName($tableName, true)['schema'];
1313
            $tmpTableName = $state['tmpTableName'];
1314
            $indexes = $this->getIndexes($tableName);
1315
            foreach (array_keys($indexes) as $indexName) {
1316
                $sql .= sprintf(
1317
                    'DROP INDEX %s%s; ',
1318
                    $schema,
1319
                    $this->quoteColumnName($indexName)
1320
                );
1321
                $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName);
1322
                $sql .= preg_replace(
1323
                    "/\b${tableName}\b/",
1324
                    $tmpTableName,
1325
                    $createIndexSQL
1326
                );
1327
            }
1328
1329
            $this->execute($sql);
1330
1331
            return $state;
1332
        });
1333
1334
        $instructions->addPostStep(function ($state) {
1335
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1336
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1337
            $selectColumns = $writeColumns = $names;
1338
1339
            return compact('selectColumns', 'writeColumns') + $state;
1340
        });
1341
1342
        return $this->copyAndDropTmpTable($instructions, $tableName);
1343
    }
1344
1345
    /**
1346
     * @inheritDoc
1347
     *
1348
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
1349
     *
1350
     * @throws \BadMethodCallException
1351
     */
1352
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1353
    {
1354
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1355
    }
1356
1357
    /**
1358
     * {@inheritDoc}
1359
     *
1360
     * @throws \InvalidArgumentException
1361
     */
1362
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1363
    {
1364
        $instructions = $this->beginAlterByCopyTable($tableName);
1365
1366
        $instructions->addPostStep(function ($state) use ($columns) {
1367
            $sql = '';
1368
1369
            foreach ($columns as $columnName) {
1370
                $search = sprintf(
1371
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1372
                    $this->quoteColumnName($columnName)
1373
                );
1374
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1375
            }
1376
1377
            if ($sql) {
1378
                $this->execute($sql);
1379
            }
1380
1381
            return $state;
1382
        });
1383
1384
        $instructions->addPostStep(function ($state) use ($columns) {
1385
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1386
1387
            $selectColumns = $newState['selectColumns'];
1388
            $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...
1389
            $diff = array_diff($columns, $selectColumns);
1390
1391
            if (!empty($diff)) {
1392
                throw new InvalidArgumentException(sprintf(
1393
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1394
                ));
1395
            }
1396
1397
            return $newState + $state;
1398
        });
1399
1400
        return $this->copyAndDropTmpTable($instructions, $tableName);
1401
    }
1402
1403
    /**
1404
     * {@inheritDoc}
1405
     *
1406
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1407
     */
1408
    public function getSqlType($type, $limit = null)
1409
    {
1410
        $typeLC = strtolower($type);
1411
        if ($type instanceof Literal) {
1412
            $name = $type;
1413
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1414
            $name = self::$supportedColumnTypes[$typeLC];
1415
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1416
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1417
        } else {
1418
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1419
        }
1420
1421
        return ['name' => $name, 'limit' => $limit];
1422
    }
1423
1424
    /**
1425
     * Returns Phinx type by SQL type
1426
     *
1427
     * @param string|null $sqlTypeDef SQL Type definition
1428
     *
1429
     * @return array
1430
     */
1431
    public function getPhinxType($sqlTypeDef)
1432
    {
1433
        $limit = null;
1434
        $scale = null;
1435
        if ($sqlTypeDef === null) {
1436
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1437
            $name = null;
1438
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1439
            // doesn't match the pattern of a type we'd know about
1440
            $name = Literal::from($sqlTypeDef);
1441
        } else {
1442
            // possibly a known type
1443
            $type = $match[1];
1444
            $typeLC = strtolower($type);
1445
            $affinity = $match[2] ?? '';
1446
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1447
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1448
            if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) {
1449
                // the type is a MySQL-style boolean
1450
                $name = static::PHINX_TYPE_BOOLEAN;
1451
                $limit = null;
1452
            } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1453
                // the type is an explicitly supported type
1454
                $name = $typeLC;
1455
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1456
                // the type is an alias for a supported type
1457
                $name = self::$supportedColumnTypeAliases[$typeLC];
1458
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1459
                // unsupported but known types are passed through lowercased, and without appended affinity
1460
                $name = Literal::from($typeLC);
1461
            } else {
1462
                // unknown types are passed through as-is
1463
                $name = Literal::from($type . $affinity);
1464
            }
1465
        }
1466
1467
        return [
1468
            'name' => $name,
1469
            'limit' => $limit,
1470
            'scale' => $scale,
1471
        ];
1472
    }
1473
1474
    /**
1475
     * @inheritDoc
1476
     */
1477
    public function createDatabase($name, $options = [])
1478
    {
1479
        touch($name . $this->suffix);
1480
    }
1481
1482
    /**
1483
     * @inheritDoc
1484
     */
1485
    public function hasDatabase($name)
1486
    {
1487
        return is_file($name . $this->suffix);
1488
    }
1489
1490
    /**
1491
     * @inheritDoc
1492
     */
1493
    public function dropDatabase($name)
1494
    {
1495
        if ($this->getOption('memory')) {
1496
            $this->disconnect();
1497
            $this->connect();
1498
        }
1499
        if (file_exists($name . $this->suffix)) {
1500
            unlink($name . $this->suffix);
1501
        }
1502
    }
1503
1504
    /**
1505
     * Gets the SQLite Column Definition for a Column object.
1506
     *
1507
     * @param \Phinx\Db\Table\Column $column Column
1508
     *
1509
     * @return string
1510
     */
1511
    protected function getColumnSqlDefinition(Column $column)
1512
    {
1513
        $isLiteralType = $column->getType() instanceof Literal;
1514
        if ($isLiteralType) {
1515
            $def = (string)$column->getType();
1516
        } else {
1517
            $sqlType = $this->getSqlType($column->getType());
1518
            $def = strtoupper($sqlType['name']);
1519
1520
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true);
1521
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1522
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1523
            }
1524
        }
1525
        if ($column->getPrecision() && $column->getScale()) {
1526
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1527
        }
1528
1529
        $default = $column->getDefault();
1530
1531
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1532
        $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...
1533
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1534
1535
        $def .= $this->getCommentDefinition($column);
1536
1537
        return $def;
1538
    }
1539
1540
    /**
1541
     * Gets the comment Definition for a Column object.
1542
     *
1543
     * @param \Phinx\Db\Table\Column $column Column
1544
     *
1545
     * @return string
1546
     */
1547
    protected function getCommentDefinition(Column $column)
1548
    {
1549
        if ($column->getComment()) {
1550
            return ' /* ' . $column->getComment() . ' */ ';
1551
        }
1552
1553
        return '';
1554
    }
1555
1556
    /**
1557
     * Gets the SQLite Index Definition for an Index object.
1558
     *
1559
     * @param \Phinx\Db\Table\Table $table Table
1560
     * @param \Phinx\Db\Table\Index $index Index
1561
     *
1562
     * @return string
1563
     */
1564
    protected function getIndexSqlDefinition(Table $table, Index $index)
1565
    {
1566
        if ($index->getType() === Index::UNIQUE) {
1567
            $def = 'UNIQUE INDEX';
1568
        } else {
1569
            $def = 'INDEX';
1570
        }
1571
        if (is_string($index->getName())) {
1572
            $indexName = $index->getName();
1573
        } else {
1574
            $indexName = $table->getName() . '_';
1575
            foreach ($index->getColumns() as $column) {
1576
                $indexName .= $column . '_';
1577
            }
1578
            $indexName .= 'index';
1579
        }
1580
        $def .= ' `' . $indexName . '`';
1581
1582
        return $def;
1583
    }
1584
1585
    /**
1586
     * @inheritDoc
1587
     */
1588
    public function getColumnTypes()
1589
    {
1590
        return array_keys(static::$supportedColumnTypes);
1591
    }
1592
1593
    /**
1594
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1595
     *
1596
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1597
     *
1598
     * @return string
1599
     */
1600
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1601
    {
1602
        $def = '';
1603
        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...
1604
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1605
        } else {
1606
            $columnNames = [];
1607
            foreach ($foreignKey->getColumns() as $column) {
1608
                $columnNames[] = $this->quoteColumnName($column);
1609
            }
1610
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1611
            $refColumnNames = [];
1612
            foreach ($foreignKey->getReferencedColumns() as $column) {
1613
                $refColumnNames[] = $this->quoteColumnName($column);
1614
            }
1615
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1616
            if ($foreignKey->getOnDelete()) {
1617
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1618
            }
1619
            if ($foreignKey->getOnUpdate()) {
1620
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1621
            }
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