Completed
Push — master ( d748e1...415000 )
by Mark
18s queued 13s
created

SQLiteAdapter::getColumnSqlDefinition()   B

Complexity

Conditions 11
Paths 64

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 0
cts 0
cp 0
rs 7.3166
c 0
b 0
f 0
cc 11
nc 64
nop 1
crap 132

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use BadMethodCallException;
11
use Cake\Database\Connection;
12
use Cake\Database\Driver\Sqlite as SqliteDriver;
13
use InvalidArgumentException;
14
use PDO;
15
use PDOException;
16
use Phinx\Db\Table\Column;
17
use Phinx\Db\Table\ForeignKey;
18
use Phinx\Db\Table\Index;
19
use Phinx\Db\Table\Table;
20
use Phinx\Db\Util\AlterInstructions;
21
use Phinx\Util\Expression;
22
use Phinx\Util\Literal;
23
use RuntimeException;
24
25
/**
26
 * Phinx SQLite Adapter.
27
 *
28
 * @author Rob Morgan <[email protected]>
29
 * @author Richard McIntyre <[email protected]>
30
 */
31
class SQLiteAdapter extends PdoAdapter
32
{
33
    // list of supported Phinx column types with their SQL equivalents
34
    // some types have an affinity appended to ensure they do not receive NUMERIC affinity
35
    protected static $supportedColumnTypes = [
36
        self::PHINX_TYPE_BIG_INTEGER => 'biginteger',
37
        self::PHINX_TYPE_BINARY => 'binary_blob',
38
        self::PHINX_TYPE_BLOB => 'blob',
39
        self::PHINX_TYPE_BOOLEAN => 'boolean_integer',
40
        self::PHINX_TYPE_CHAR => 'char',
41
        self::PHINX_TYPE_DATE => 'date_text',
42
        self::PHINX_TYPE_DATETIME => 'datetime_text',
43
        self::PHINX_TYPE_DOUBLE => 'double',
44
        self::PHINX_TYPE_FLOAT => 'float',
45
        self::PHINX_TYPE_INTEGER => 'integer',
46
        self::PHINX_TYPE_JSON => 'json_text',
47
        self::PHINX_TYPE_JSONB => 'jsonb_text',
48
        self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger',
49
        self::PHINX_TYPE_STRING => 'varchar',
50
        self::PHINX_TYPE_TEXT => 'text',
51
        self::PHINX_TYPE_TIME => 'time_text',
52
        self::PHINX_TYPE_UUID => 'uuid_text',
53
        self::PHINX_TYPE_TIMESTAMP => 'timestamp_text',
54
        self::PHINX_TYPE_VARBINARY => 'varbinary_blob',
55
    ];
56 42
57
    // list of aliases of supported column types
58 42
    protected static $supportedColumnTypeAliases = [
59 42
        'varchar' => self::PHINX_TYPE_STRING,
60
        'tinyint' => self::PHINX_TYPE_SMALL_INTEGER,
61
        'tinyinteger' => self::PHINX_TYPE_SMALL_INTEGER,
62
        'smallint' => self::PHINX_TYPE_SMALL_INTEGER,
63
        'int' => self::PHINX_TYPE_INTEGER,
64
        'mediumint' => self::PHINX_TYPE_INTEGER,
65 42
        'mediuminteger' => self::PHINX_TYPE_INTEGER,
66 42
        'bigint' => self::PHINX_TYPE_BIG_INTEGER,
67
        'tinytext' => self::PHINX_TYPE_TEXT,
68
        'mediumtext' => self::PHINX_TYPE_TEXT,
69 42
        'longtext' => self::PHINX_TYPE_TEXT,
70
        'tinyblob' => self::PHINX_TYPE_BLOB,
71
        'mediumblob' => self::PHINX_TYPE_BLOB,
72 42
        'longblob' => self::PHINX_TYPE_BLOB,
73 42
        'real' => self::PHINX_TYPE_FLOAT,
74 42
    ];
75 42
76
    // list of known but unsupported Phinx column types
77
    protected static $unsupportedColumnTypes = [
78
        self::PHINX_TYPE_BIT,
79 42
        self::PHINX_TYPE_CIDR,
80 42
        self::PHINX_TYPE_DECIMAL,
81
        self::PHINX_TYPE_ENUM,
82
        self::PHINX_TYPE_FILESTREAM,
83
        self::PHINX_TYPE_GEOMETRY,
84
        self::PHINX_TYPE_INET,
85
        self::PHINX_TYPE_INTERVAL,
86
        self::PHINX_TYPE_LINESTRING,
87 42
        self::PHINX_TYPE_MACADDR,
88 42
        self::PHINX_TYPE_POINT,
89 42
        self::PHINX_TYPE_POLYGON,
90
        self::PHINX_TYPE_SET,
91
    ];
92
93
    protected $definitionsWithLimits = [
94 48
        'CHAR',
95
        'CHARACTER',
96 48
        'VARCHAR',
97 48
        'VARYING CHARACTER',
98
        'NCHAR',
99
        'NATIVE CHARACTER',
100
        'NVARCHAR',
101
    ];
102
103
    protected $suffix = '.sqlite3';
104
105
    /**
106
     * Indicates whether the database library version is at least the specified version
107
     *
108
     * @param string $ver The version to check against e.g. '3.28.0'
109
     *
110 1
     * @return bool
111
     */
112 1
    public function databaseVersionAtLeast($ver)
113 1
    {
114
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
115
116
        return version_compare($actual, $ver, '>=');
117
    }
118
119
    /**
120
     * {@inheritDoc}
121
     *
122
     * @throws \RuntimeException
123
     * @throws \InvalidArgumentException
124
     *
125
     * @return void
126
     */
127
    public function connect()
128
    {
129
        if ($this->connection === null) {
130
            if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) {
131
                // @codeCoverageIgnoreStart
132
                throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
133
                // @codeCoverageIgnoreEnd
134 43
            }
135
136 43
            $options = $this->getOptions();
137
138
            // use a memory database if the option was specified
139
            if (!empty($options['memory'])) {
140
                $dsn = 'sqlite::memory:';
141
            } else {
142 44
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
143
            }
144 44
145
            $driverOptions = [];
146
147
            // use custom data fetch mode
148 View Code Duplication
            if (!empty($options['fetch_mode'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
150 42
            }
151
152 42
            $db = $this->createPdoConnection($dsn, null, null, $driverOptions);
153 42
154 42
            $this->setConnection($db);
155 12
        }
156 42
    }
157
158 42
    /**
159
     * @inheritDoc
160
     */
161
    public function setOptions(array $options)
162
    {
163
        parent::setOptions($options);
164 42
165
        if (isset($options['suffix'])) {
166
            $this->suffix = $options['suffix'];
167 42
        }
168 42
        //don't "fix" the file extension if it is blank, some people
169 42
        //might want a SQLITE db file with absolutely no extension.
170 35
        if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
171 35
            $this->suffix = '.' . $this->suffix;
172 35
        }
173 35
174
        return $this;
175 35
    }
176 42
177
    /**
178 1
     * {@inheritDoc}
179 1
     *
180 1
     * @return void
181 1
     */
182
    public function disconnect()
183 1
    {
184 1
        $this->connection = null;
185
    }
186
187 42
    /**
188 42
     * @inheritDoc
189 42
     */
190 42
    public function hasTransactions()
191 42
    {
192
        return true;
193
    }
194 42
195 42
    /**
196 42
     * {@inheritDoc}
197 42
     *
198 42
     * @return void
199 42
     */
200
    public function beginTransaction()
201
    {
202 1
        $this->getConnection()->beginTransaction();
203 1
    }
204 1
205
    /**
206 1
     * {@inheritDoc}
207 1
     *
208 1
     * @return void
209 1
     */
210 1
    public function commitTransaction()
211 1
    {
212 42
        $this->getConnection()->commit();
213 42
    }
214 37
215
    /**
216
     * {@inheritDoc}
217
     *
218 42
     * @return void
219 42
     */
220 1
    public function rollbackTransaction()
221 1
    {
222 1
        $this->getConnection()->rollBack();
223 1
    }
224
225 42
    /**
226
     * @inheritDoc
227 42
     */
228
    public function quoteTableName($tableName)
229 42
    {
230 6
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
231 42
    }
232 42
233
    /**
234
     * @inheritDoc
235
     */
236
    public function quoteColumnName($columnName)
237 1
    {
238
        return '`' . str_replace('`', '``', $columnName) . '`';
239 1
    }
240 1
241
    /**
242
     * @param string $tableName Table name
243
     * @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
244
     *
245 1
     * @return array
246
     */
247 1
    protected function getSchemaName($tableName, $quoted = false)
248 1
    {
249
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
250
            $table = $match[1];
251
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
252
            $result = ['schema' => $schema, 'table' => $table];
253 1
        } else {
254
            $result = ['schema' => '', 'table' => $tableName];
255 1
        }
256 1
257 1
        if ($quoted) {
258 1
            $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : '';
259
            $result['table'] = $this->quoteColumnName($result['table']);
260 1
        }
261 1
262
        return $result;
263
    }
264
265
    /**
266 1
     * Retrieves information about a given table from one of the SQLite pragmas
267
     *
268 1
     * @param string $tableName The table to query
269 1
     * @param string $pragma The pragma to query
270
     *
271 1
     * @return array
272 1
     */
273 1
    protected function getTableInfo($tableName, $pragma = 'table_info')
274 1
    {
275 1
        $info = $this->getSchemaName($tableName, true);
276 1
277
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
278 1
    }
279 1
280 1
    /**
281
     * Searches through all available schemata to find a table and returns an array
282 1
     * containing the bare schema name and whether the table exists at all.
283 1
     * If no schema was specified and the table does not exist the "main" schema is returned
284 1
     *
285
     * @param string $tableName The name of the table to find
286 1
     *
287 1
     * @return array
288
     */
289 1
    protected function resolveTable($tableName)
290
    {
291
        $info = $this->getSchemaName($tableName);
292
        if ($info['schema'] === '') {
293
            // if no schema is specified we search all schemata
294
            $rows = $this->fetchAll('PRAGMA database_list;');
295 8
            // the temp schema is always first to be searched
296
            $schemata = ['temp'];
297 8
            foreach ($rows as $row) {
298 8
                if (strtolower($row['name']) !== 'temp') {
299 8
                    $schemata[] = $row['name'];
300 7
                }
301
            }
302 8
            $defaultSchema = 'main';
303
        } else {
304 8
            // otherwise we search just the specified schema
305
            $schemata = (array)$info['schema'];
306
            $defaultSchema = $info['schema'];
307
        }
308
309
        $table = strtolower($info['table']);
310 4
        foreach ($schemata as $schema) {
311
            if (strtolower($schema) === 'temp') {
312 4
                $master = 'sqlite_temp_master';
313 4
            } else {
314 4
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
315 4
            }
316 4
            try {
317 4
                $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table)));
318
            } catch (PDOException $e) {
319 4
                // an exception can occur if the schema part of the table refers to a database which is not attached
320 4
                break;
321
            }
322
323
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
324
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
325 2
            foreach ($rows as $row) {
326
                if (strtolower($row['name']) === $table) {
327 2
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
328
                }
329 2
            }
330
        }
331 2
332 2
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
333 2
    }
334 2
335 2
    /**
336 2
     * @inheritDoc
337
     */
338 2
    public function hasTable($tableName)
339 2
    {
340 2
        return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
341 2
    }
342 2
343 2
    /**
344 2
     * {@inheritDoc}
345 2
     *
346 2
     * @return void
347
     */
348 2
    public function createTable(Table $table, array $columns = [], array $indexes = [])
349 1
    {
350
        // Add the default primary key
351 1
        $options = $table->getOptions();
352 View Code Duplication
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
353
            $options['id'] = 'id';
354 1
        }
355
356 1
        if (isset($options['id']) && is_string($options['id'])) {
357 1
            // Handle id => "field_name" to support AUTO_INCREMENT
358 1
            $column = new Column();
359
            $column->setName($options['id'])
360 1
                   ->setType('integer')
361 1
                   ->setIdentity(true);
362
363
            array_unshift($columns, $column);
364 1
        }
365 1
366 1
        $sql = 'CREATE TABLE ';
367 1
        $sql .= $this->quoteTableName($table->getName()) . ' (';
368 1
        foreach ($columns as $column) {
369
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
370 1
371
            if (isset($options['primary_key']) && $column->getIdentity()) {
372 1
                //remove column from the primary key array as it is already defined as an autoincrement
373
                //primary id
374 1
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
375 1
                if ($identityColumnIndex !== false) {
376
                    unset($options['primary_key'][$identityColumnIndex]);
377
378
                    if (empty($options['primary_key'])) {
379
                        //The last primary key has been removed
380 6
                        unset($options['primary_key']);
381
                    }
382
                }
383
            }
384 6
        }
385
386 6
        // set the primary key(s)
387 View Code Duplication
        if (isset($options['primary_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
388 6
            $sql = rtrim($sql);
389 6
            $sql .= ' PRIMARY KEY (';
390 6
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
391 6
                $sql .= $this->quoteColumnName($options['primary_key']);
392 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
393 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
394
            }
395 6
            $sql .= ')';
396 6
        } else {
397 6
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
398 6
        }
399 6
400 6
        $sql = rtrim($sql) . ');';
401 6
        // execute the sql
402 6
        $this->execute($sql);
403 6
404
        foreach ($indexes as $index) {
405 6
            $this->addIndex($table, $index);
406
        }
407
408
        $this->addCreatedTable($table->getName());
409
    }
410
411 6
    /**
412
     * {@inheritDoc}
413 6
     *
414 6
     * @throws \InvalidArgumentException
415 6
     */
416 6
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
417
    {
418 6
        $instructions = new AlterInstructions();
419
420 6
        // Drop the existing primary key
421
        $primaryKey = $this->getPrimaryKey($table->getName());
422 6
        if (!empty($primaryKey)) {
423 6
            $instructions->merge(
424 6
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
425 6
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
426 6
            );
427
        }
428 6
429
        // Add the primary key(s)
430 6
        if (!empty($newColumns)) {
431 6
            if (!is_string($newColumns)) {
432 6
                throw new InvalidArgumentException(sprintf(
433
                    'Invalid value for primary key: %s',
434
                    json_encode($newColumns)
435
                ));
436
            }
437 2
438
            $instructions->merge(
439
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
440 2
            );
441
        }
442 2
443
        return $instructions;
444 2
    }
445 2
446 2
    /**
447 2
     * {@inheritDoc}
448 2
     *
449 2
     * @throws \BadMethodCallException
450
     *
451 2
     * @return void
452 2
     */
453 2
    protected function getChangeCommentInstructions(Table $table, $newComment)
454 2
    {
455 2
        throw new BadMethodCallException('SQLite does not have table comments');
456 2
    }
457 2
458 2
    /**
459 2
     * @inheritDoc
460
     */
461 2 View Code Duplication
    protected function getRenameTableInstructions($tableName, $newTableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
462
    {
463 2
        $this->updateCreatedTableName($tableName, $newTableName);
464
        $sql = sprintf(
465
            'ALTER TABLE %s RENAME TO %s',
466
            $this->quoteTableName($tableName),
467
            $this->quoteTableName($newTableName)
468
        );
469 2
470
        return new AlterInstructions([], [$sql]);
471 2
    }
472 2
473 2
    /**
474
     * @inheritDoc
475 2
     */
476 View Code Duplication
    protected function getDropTableInstructions($tableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477 2
    {
478 2
        $this->removeCreatedTable($tableName);
479 2
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
480
481 2
        return new AlterInstructions([], [$sql]);
482
    }
483 2
484 2
    /**
485 2
     * {@inheritDoc}
486 2
     *
487 2
     * @return void
488
     */
489 2
    public function truncateTable($tableName)
490
    {
491 2
        $info = $this->resolveTable($tableName);
492 2
        // first try deleting the rows
493 2
        $this->execute(sprintf(
494
            'DELETE FROM %s.%s',
495
            $this->quoteColumnName($info['schema']),
496
            $this->quoteColumnName($info['table'])
497
        ));
498
499
        // assuming no error occurred, reset the autoincrement (if any)
500
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
501 9
            $this->execute(sprintf(
502
                'DELETE FROM %s.%s where name  = %s',
503 9
                $this->quoteColumnName($info['schema']),
504 9
                'sqlite_sequence',
505
                $this->quoteString($info['table'])
506 9
            ));
507 9
        }
508 9
    }
509 9
510 9
    /**
511 9
     * Parses a default-value expression to yield either a Literal representing
512 9
     * a string value, a string representing an expression, or some other scalar
513 9
     *
514 9
     * @param mixed $v The default-value expression to interpret
515 9
     * @param string $t The Phinx type of the column
516
     *
517
     * @return mixed
518
     */
519
    protected function parseDefaultValue($v, $t)
520
    {
521 9
        if ($v === null) {
522
            return null;
523 9
        }
524 4
525 4
        // split the input into tokens
526
        $trimChars = " \t\n\r\0\x0B";
527 9
        $pattern = <<<PCRE_PATTERN
528 9
            /
529
                '(?:[^']|'')*'|                 # String literal
530 9
                "(?:[^"]|"")*"|                 # Standard identifier
531 9
                `(?:[^`]|``)*`|                 # MySQL identifier
532 9
                \[[^\]]*\]|                     # SQL Server identifier
533 9
                --[^\r\n]*|                     # Single-line comment
534
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
535 8
                [^\/\-]+|                       # Non-special characters
536
                .                               # Any other single character
537 8
            /sx
538
PCRE_PATTERN;
539
        preg_match_all($pattern, $v, $matches);
540
        // strip out any comment tokens
541
        $matches = array_map(function ($v) {
542
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
543 1
        }, $matches[0]);
544
        // reconstitute the string, trimming whitespace as well as parentheses
545 1
        $vClean = trim(implode('', $matches));
546
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
547 1
548 1
        // match the string against one of several patterns
549 1
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
550
            // magic date or time
551
            return strtoupper($vBare);
552
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
553
            // string literal
554
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
555
556
            return Literal::from($str);
557
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
558
            $int = (int)$vBare;
559 8
            // integer literal
560
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int == 0 || $int == 1)) {
561 8
                return (bool)$int;
562 8
            } else {
563 8
                return $int;
564 8
            }
565 8
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
566 8
            // float literal
567 8
            return (float)$vBare;
568 8
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
569 8
            // hexadecimal literal
570 8
            return hexdec(substr($vBare, 2));
571
        } elseif (preg_match('/^null$/i', $vBare)) {
572 8
            // null literal
573 8
            return null;
574 8
        } elseif (preg_match('/^true|false$/i', $vBare)) {
575
            // boolean literal
576
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
577
        } else {
578
            // any other expression: return the expression with parentheses, but without comments
579 1
            return Expression::from($vClean);
580
        }
581 1
    }
582 1
583 1
    /**
584
     * Returns the name of the specified table's identity column, or null if the table has no identity
585 1
     *
586 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
587
     *
588 1
     * @param string $tableName The name of the table
589 1
     *
590 1
     * @return string|null
591 1
     */
592 1
    protected function resolveIdentity($tableName)
593 1
    {
594 1
        $result = null;
595 1
        // make sure the table has only one primary key column which is of type integer
596 1
        foreach ($this->getTableInfo($tableName) as $col) {
597 1
            $type = strtolower($col['type']);
598
            if ($col['pk'] > 1) {
599
                // the table has a composite primary key
600
                return null;
601
            } elseif ($col['pk'] == 0) {
602
                // the column is not a primary key column and is thus not relevant
603
                continue;
604
            } elseif ($type !== 'integer') {
605 1
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
606
                return null;
607 1
            } else {
608
                // the column is a candidate for a row ID alias
609 1
                $result = $col['name'];
610 1
            }
611 1
        }
612 1
        // if there is no suitable PK column, stop now
613 1
        if ($result === null) {
614 1
            return null;
615 1
        }
616 1
        // make sure the table does not have a PK-origin autoindex
617 1
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
618
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
619
            if ($idx['origin'] === 'pk') {
620
                return null;
621
            }
622
        }
623
624
        return $result;
625 5
    }
626
627 5
    /**
628
     * @inheritDoc
629
     */
630 5
    public function getColumns($tableName)
631
    {
632 5
        $columns = [];
633 5
634 5
        $rows = $this->getTableInfo($tableName);
635
        $identity = $this->resolveIdentity($tableName);
636 1
637
        foreach ($rows as $columnInfo) {
638
            $column = new Column();
639
            $type = $this->getPhinxType($columnInfo['type']);
640
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
641
642
            $column->setName($columnInfo['name'])
643
                   ->setNull($columnInfo['notnull'] !== '1')
644
                   ->setDefault($default)
645 5
                   ->setType($type['name'])
646
                   ->setLimit($type['limit'])
647 5
                   ->setScale($type['scale'])
648 5
                   ->setIdentity($columnInfo['name'] === $identity);
649
650
            $columns[] = $column;
651
        }
652
653
        return $columns;
654
    }
655
656
    /**
657
     * @inheritDoc
658
     */
659 View Code Duplication
    public function hasColumn($tableName, $columnName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
660
    {
661 5
        $rows = $this->getTableInfo($tableName);
662
        foreach ($rows as $column) {
663 5
            if (strcasecmp($column['name'], $columnName) === 0) {
664 5
                return true;
665 5
            }
666 5
        }
667 5
668 5
        return false;
669 5
    }
670 5
671 5
    /**
672 5
     * @inheritDoc
673 5
     */
674
    protected function getAddColumnInstructions(Table $table, Column $column)
675
    {
676
        $tableName = $table->getName();
677
678
        $instructions = $this->beginAlterByCopyTable($tableName);
679 4
680 View Code Duplication
        $instructions->addPostStep(function ($state) use ($column) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
681
            $sql = $state['createSQL'];
0 ignored issues
show
Unused Code introduced by
$sql is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
682 4
            $sql = preg_replace(
683
                "/\)$/",
684 4
                sprintf(', %s %s$1)', $this->quoteColumnName($column->getName()), $this->getColumnSqlDefinition($column)),
685 4
                $state['createSQL'],
686
                1
687 4
            );
688 4
            $this->execute($sql);
689 4
690 4
            return $state;
691 4
        });
692 4
693
        $instructions->addPostStep(function ($state) use ($tableName) {
694 4
            $newState = $this->calculateNewTableColumns($tableName, false, false);
695 4
696 4
            return $newState + $state;
697 4
        });
698 4
699
        return $this->copyAndDropTmpTable($instructions, $tableName);
700 4
    }
701
702 4
    /**
703 4
     * Returns the original CREATE statement for the give table
704
     *
705 4
     * @param string $tableName The table name to get the create statement for
706 4
     *
707 4
     * @return string
708 4
     */
709 4
    protected function getDeclaringSql($tableName)
710 4
    {
711 4
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'");
712
713 4
        $sql = '';
714 4
        foreach ($rows as $table) {
715 4
            if ($table['tbl_name'] === $tableName) {
716
                $sql = $table['sql'];
717
            }
718
        }
719
720 1
        return $sql;
721
    }
722
723 1
    /**
724
     * Copies all the data from a tmp table to another table
725
     *
726
     * @param string $tableName The table name to copy the data to
727 1
     * @param string $tmpTableName The tmp table name where the data is stored
728
     * @param string[] $writeColumns The list of columns in the target table
729 1
     * @param string[] $selectColumns The list of columns in the tmp table
730
     *
731 1
     * @return void
732 1
     */
733 1
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
734 1
    {
735 1
        $sql = sprintf(
736 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
737
            $this->quoteTableName($tableName),
738 1
            implode(', ', $writeColumns),
739 1
            implode(', ', $selectColumns),
740 1
            $this->quoteTableName($tmpTableName)
741 1
        );
742 1
        $this->execute($sql);
743 1
    }
744 1
745
    /**
746 1
     * Modifies the passed instructions to copy all data from the table into
747
     * the provided tmp table and then drops the table and rename tmp table.
748 1
     *
749
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
750
     * @param string $tableName The table name to copy the data to
751
     *
752
     * @return \Phinx\Db\Util\AlterInstructions
753
     */
754 1
    protected function copyAndDropTmpTable($instructions, $tableName)
755
    {
756 1
        $instructions->addPostStep(function ($state) use ($tableName) {
757 1
            $this->copyDataToNewTable(
758 1
                $state['tmpTableName'],
759 1
                $tableName,
760 1
                $state['writeColumns'],
761 1
                $state['selectColumns']
762 1
            );
763
764 1
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
765
            $this->execute(sprintf(
766 1
                'ALTER TABLE %s RENAME TO %s',
767 1
                $this->quoteTableName($state['tmpTableName']),
768 1
                $this->quoteTableName($tableName)
769 1
            ));
770 1
771
            return $state;
772 1
        });
773
774 1
        return $instructions;
775 1
    }
776 1
777
    /**
778
     * Returns the columns and type to use when copying a table to another in the process
779
     * of altering a table
780
     *
781
     * @param string $tableName The table to modify
782
     * @param string|false $columnName The column name that is about to change
783
     * @param string|false $newColumnName Optionally the new name for the column
784
     *
785
     * @throws \InvalidArgumentException
786
     *
787
     * @return \Phinx\Db\Util\AlterInstructions
788
     */
789
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
790
    {
791
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
792
        $selectColumns = [];
793
        $writeColumns = [];
794
        $columnType = null;
795
        $found = false;
796
797
        foreach ($columns as $column) {
798
            $selectName = $column['name'];
799
            $writeName = $selectName;
800
801
            if ($selectName == $columnName) {
802
                $writeName = $newColumnName;
803
                $found = true;
804
                $columnType = $column['type'];
805
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
806
            }
807
808
            $selectColumns[] = $selectName;
809
            $writeColumns[] = $writeName;
810 43
        }
811
812
        $selectColumns = array_filter($selectColumns, 'strlen');
813 43
        $writeColumns = array_filter($writeColumns, 'strlen');
814 42
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
815
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
816 43
817
        if ($columnName && !$found) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columnName of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
818
            throw new InvalidArgumentException(sprintf(
819 43
                'The specified column doesn\'t exist: ' . $columnName
820 1
            ));
821
        }
822 43
823 38
        return compact('writeColumns', 'selectColumns', 'columnType');
824
    }
825 43
826 42
    /**
827
     * Returns the initial instructions to alter a table using the
828 43
     * create-copy-drop strategy
829 2
     *
830
     * @param string $tableName The table to modify
831 43
     *
832 1
     * @return \Phinx\Db\Util\AlterInstructions
833
     */
834 43
    protected function beginAlterByCopyTable($tableName)
835 1
    {
836
        $instructions = new AlterInstructions();
837 43
        $instructions->addPostStep(function ($state) use ($tableName) {
838 42
            $tmpTableName = "tmp_{$tableName}";
839
            $createSQL = $this->getDeclaringSql($tableName);
840 43
841 1
            // Table name in SQLite can be hilarious inside declaring SQL:
842
            // - tableName
843 43
            // - `tableName`
844 1
            // - "tableName"
845
            // - [this is a valid table name too!]
846 43
            // - etc.
847 43
            // Just remove all characters before first "(" and build them again
848 1
            $createSQL = preg_replace(
849
                "/^CREATE TABLE .* \(/Ui",
850 43
                '',
851 42
                $createSQL
852
            );
853 5
854
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
855 5
856 4
            return compact('createSQL', 'tmpTableName') + $state;
857
        });
858
859
        return $instructions;
860 1
    }
861 1
862
    /**
863
     * @inheritDoc
864 1
     */
865
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
866
    {
867 1
        $instructions = $this->beginAlterByCopyTable($tableName);
868
869 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
870 1
            $sql = str_replace(
871 1
                $this->quoteColumnName($columnName),
872
                $this->quoteColumnName($newColumnName),
873
                $state['createSQL']
874
            );
875
            $this->execute($sql);
876
877
            return $state;
878
        });
879
880 3
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
881
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
882 3
883 1
            return $newState + $state;
884
        });
885 2
886 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
887 2
    }
888 2
889 1
    /**
890 1
     * @inheritDoc
891 2
     */
892
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
893
    {
894 2
        $instructions = $this->beginAlterByCopyTable($tableName);
895 2
896 1
        $newColumnName = $newColumn->getName();
897 1 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
898
            $sql = preg_replace(
899
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
900 1
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
901 2
                $state['createSQL'],
902
                1
903
            );
904
            $this->execute($sql);
905
906
            return $state;
907
        });
908
909
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
910 2
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
911
912
            return $newState + $state;
913
        });
914
915
        return $this->copyAndDropTmpTable($instructions, $tableName);
916 2
    }
917 1
918
    /**
919
     * @inheritDoc
920 1
     */
921 1
    protected function getDropColumnInstructions($tableName, $columnName)
922 2
    {
923 1
        $instructions = $this->beginAlterByCopyTable($tableName);
924 1
925 2
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
926 2
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
927
928
            return $newState + $state;
929
        });
930
931
        $instructions->addPostStep(function ($state) use ($columnName) {
932
            $sql = preg_replace(
933 2
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
934
                '',
935
                $state['createSQL']
936 1
            );
937 1
938
            if (substr($sql, -2) === ', ') {
939 1
                $sql = substr($sql, 0, -2) . ')';
940
            }
941
942
            $this->execute($sql);
943
944
            return $state;
945
        });
946 48
947
        return $this->copyAndDropTmpTable($instructions, $tableName);
948 48
    }
949 48
950
    /**
951
     * Get an array of indexes from a particular table.
952
     *
953
     * @param string $tableName Table Name
954 2
     *
955
     * @return array
956 2
     */
957
    protected function getIndexes($tableName)
958
    {
959
        $indexes = [];
960
        $schema = $this->getSchemaName($tableName, true)['schema'];
961
        $indexList = $this->getTableInfo($tableName, 'index_list');
962 48
963
        foreach ($indexList as $index) {
964 48
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
965 47
            $cols = [];
966 47
            foreach ($indexData as $indexItem) {
967 48
                $cols[] = $indexItem['name'];
968
            }
969
            $indexes[$index['name']] = $cols;
970
        }
971
972
        return $indexes;
973
    }
974
975 42
    /**
976
     * Finds the names of a table's indexes matching the supplied columns
977 42
     *
978 8
     * @param string $tableName The table to which the index belongs
979 42
     * @param string|string[] $columns The columns of the index
980 42
     *
981 42
     * @return array
982 42
     */
983
    protected function resolveIndex($tableName, $columns)
984
    {
985
        $columns = array_map('strtolower', (array)$columns);
986
        $indexes = $this->getIndexes($tableName);
987
        $matches = [];
988
989
        foreach ($indexes as $name => $index) {
990
            $indexCols = array_map('strtolower', $index);
991 42
            if ($columns == $indexCols) {
992
                $matches[] = $name;
993 42
            }
994 42
        }
995 42
996 42
        return $matches;
997
    }
998
999 42
    /**
1000 42
     * @inheritDoc
1001 42
     */
1002 42
    public function hasIndex($tableName, $columns)
1003 42
    {
1004 4
        return (bool)$this->resolveIndex($tableName, $columns);
1005 4
    }
1006
1007 42
    /**
1008
     * @inheritDoc
1009 42
     */
1010 42
    public function hasIndexByName($tableName, $indexName)
1011 42
    {
1012
        $indexName = strtolower($indexName);
1013 42
        $indexes = $this->getIndexes($tableName);
1014
1015
        foreach (array_keys($indexes) as $index) {
1016
            if ($indexName === strtolower($index)) {
1017 42
                return true;
1018
            }
1019 42
        }
1020
1021
        return false;
1022
    }
1023
1024
    /**
1025
     * @inheritDoc
1026
     */
1027
    protected function getAddIndexInstructions(Table $table, Index $index)
1028 42
    {
1029
        $indexColumnArray = [];
1030 42
        foreach ($index->getColumns() as $column) {
1031 2
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1032
        }
1033 42
        $indexColumns = implode(',', $indexColumnArray);
1034
        $sql = sprintf(
1035
            'CREATE %s ON %s (%s)',
1036
            $this->getIndexSqlDefinition($table, $index),
1037
            $this->quoteTableName($table->getName()),
1038
            $indexColumns
1039
        );
1040
1041
        return new AlterInstructions([], [$sql]);
1042 8
    }
1043
1044 8
    /**
1045 2
     * @inheritDoc
1046 2
     */
1047 6
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1048
    {
1049 8
        $instructions = new AlterInstructions();
1050 3
        $indexNames = $this->resolveIndex($tableName, $columns);
1051 3
        $schema = $this->getSchemaName($tableName, true)['schema'];
1052 6
        foreach ($indexNames as $indexName) {
1053 6
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1054 6
                $instructions->addPostStep(sprintf(
1055 6
                    'DROP INDEX %s%s',
1056 6
                    $schema,
1057
                    $this->quoteColumnName($indexName)
1058 8
                ));
1059 8
            }
1060
        }
1061
1062
        return $instructions;
1063
    }
1064
1065 47
    /**
1066
     * @inheritDoc
1067 47
     */
1068
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1069
    {
1070
        $instructions = new AlterInstructions();
1071
        $indexName = strtolower($indexName);
1072
        $indexes = $this->getIndexes($tableName);
1073
1074
        $found = false;
1075
        foreach (array_keys($indexes) as $index) {
1076 5
            if ($indexName === strtolower($index)) {
1077
                $found = true;
1078 5
                break;
1079 5
            }
1080
        }
1081
1082 5
        if ($found) {
1083 5
            $schema = $this->getSchemaName($tableName, true)['schema'];
1084 5
                $instructions->addPostStep(sprintf(
1085 5
                    'DROP INDEX %s%s',
1086 5
                    $schema,
1087 5
                    $this->quoteColumnName($indexName)
1088 5
                ));
1089 5
        }
1090 5
1091 5
        return $instructions;
1092 5
    }
1093 1
1094 1
    /**
1095 5
     * {@inheritDoc}
1096 1
     *
1097 1
     * @throws \InvalidArgumentException
1098
     */
1099 5
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1100
    {
1101
        if ($constraint !== null) {
1102
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1103
        }
1104
1105
        $columns = array_map('strtolower', (array)$columns);
1106
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1107
1108
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1109
            return false;
1110
        }
1111
1112
        return true;
1113
    }
1114
1115
    /**
1116
     * Get the primary key from a particular table.
1117
     *
1118
     * @param string $tableName Table Name
1119
     *
1120
     * @return string[]
1121
     */
1122
    protected function getPrimaryKey($tableName)
1123
    {
1124
        $primaryKey = [];
1125
1126
        $rows = $this->getTableInfo($tableName);
1127
1128
        foreach ($rows as $row) {
1129
            if ($row['pk'] > 0) {
1130
                $primaryKey[$row['pk'] - 1] = $row['name'];
1131
            }
1132
        }
1133
1134
        return $primaryKey;
1135
    }
1136
1137
    /**
1138
     * {@inheritDoc}
1139
     *
1140
     * @throws \InvalidArgumentException
1141
     */
1142
    public function hasForeignKey($tableName, $columns, $constraint = null)
1143
    {
1144
        if ($constraint !== null) {
1145
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1146
        }
1147
1148
        $columns = array_map('strtolower', (array)$columns);
1149
        $foreignKeys = $this->getForeignKeys($tableName);
1150
1151
        foreach ($foreignKeys as $key) {
1152
            $key = array_map('strtolower', $key);
1153
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1154
                continue;
1155
            }
1156
1157
            return true;
1158
        }
1159
1160
        return false;
1161
    }
1162
1163
    /**
1164
     * Get an array of foreign keys from a particular table.
1165
     *
1166
     * @param string $tableName Table Name
1167
     *
1168
     * @return array
1169
     */
1170
    protected function getForeignKeys($tableName)
1171
    {
1172
        $foreignKeys = [];
1173
1174
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1175
1176
        foreach ($rows as $row) {
1177
            if (!isset($foreignKeys[$row['id']])) {
1178
                $foreignKeys[$row['id']] = [];
1179
            }
1180
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1181
        }
1182
1183
        return $foreignKeys;
1184
    }
1185
1186
    /**
1187
     * @param \Phinx\Db\Table\Table $table The Table
1188
     * @param string $column Column Name
1189
     *
1190
     * @return \Phinx\Db\Util\AlterInstructions
1191
     */
1192
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1193
    {
1194
        $instructions = $this->beginAlterByCopyTable($table->getName());
1195
1196
        $tableName = $table->getName();
1197
        $instructions->addPostStep(function ($state) use ($column) {
1198
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1199
1200
            $sql = $state['createSQL'];
1201
1202
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1203
                if (isset($matches[2])) {
1204
                    if ($matches[2] === 'INTEGER') {
1205
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1206
                    } else {
1207
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1208
                    }
1209
1210
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1211
                }
1212
            }
1213
1214
            $this->execute($sql);
1215
1216
            return $state;
1217
        });
1218
1219 View Code Duplication
        $instructions->addPostStep(function ($state) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1220
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1221
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1222
            $selectColumns = $writeColumns = $names;
1223
1224
            return compact('selectColumns', 'writeColumns') + $state;
1225
        });
1226
1227
        return $this->copyAndDropTmpTable($instructions, $tableName);
1228
    }
1229
1230
    /**
1231
     * @param \Phinx\Db\Table\Table $table Table
1232
     * @param string $column Column Name
1233
     *
1234
     * @return \Phinx\Db\Util\AlterInstructions
1235
     */
1236
    protected function getDropPrimaryKeyInstructions($table, $column)
1237
    {
1238
        $instructions = $this->beginAlterByCopyTable($table->getName());
1239
1240
        $instructions->addPostStep(function ($state) {
1241
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1242
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1243
1244
            if ($sql) {
1245
                $this->execute($sql);
1246
            }
1247
1248
            return $state;
1249
        });
1250
1251
        $instructions->addPostStep(function ($state) use ($column) {
1252
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1253
1254
            return $newState + $state;
1255
        });
1256
1257
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1258
    }
1259
1260
    /**
1261
     * @inheritDoc
1262
     */
1263
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1264
    {
1265
        $instructions = $this->beginAlterByCopyTable($table->getName());
1266
1267
        $tableName = $table->getName();
1268
        $instructions->addPostStep(function ($state) use ($foreignKey) {
1269
            $this->execute('pragma foreign_keys = ON');
1270
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
1271
            $this->execute($sql);
1272
1273
            return $state;
1274
        });
1275
1276 View Code Duplication
        $instructions->addPostStep(function ($state) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1277
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1278
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1279
            $selectColumns = $writeColumns = $names;
1280
1281
            return compact('selectColumns', 'writeColumns') + $state;
1282
        });
1283
1284
        return $this->copyAndDropTmpTable($instructions, $tableName);
1285
    }
1286
1287
    /**
1288
     * {@inheritDoc}
1289
     *
1290
     * @throws \BadMethodCallException
1291
     *
1292
     * @return void
1293
     */
1294
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1295
    {
1296
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1297
    }
1298
1299
    /**
1300
     * {@inheritDoc}
1301
     *
1302
     * @throws \InvalidArgumentException
1303
     */
1304
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1305
    {
1306
        $instructions = $this->beginAlterByCopyTable($tableName);
1307
1308
        $instructions->addPostStep(function ($state) use ($columns) {
1309
            $sql = '';
1310
1311
            foreach ($columns as $columnName) {
1312
                $search = sprintf(
1313
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1314
                    $this->quoteColumnName($columnName)
1315
                );
1316
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1317
            }
1318
1319
            if ($sql) {
1320
                $this->execute($sql);
1321
            }
1322
1323
            return $state;
1324
        });
1325
1326
        $instructions->addPostStep(function ($state) use ($columns) {
1327
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1328
1329
            $selectColumns = $newState['selectColumns'];
1330
            $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...
1331
            $diff = array_diff($columns, $selectColumns);
1332
1333
            if (!empty($diff)) {
1334
                throw new InvalidArgumentException(sprintf(
1335
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1336
                ));
1337
            }
1338
1339
            return $newState + $state;
1340
        });
1341
1342
        return $this->copyAndDropTmpTable($instructions, $tableName);
1343
    }
1344
1345
    /**
1346
     * {@inheritDoc}
1347
     *
1348
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1349
     */
1350
    public function getSqlType($type, $limit = null)
1351
    {
1352
        $typeLC = strtolower($type);
1353
        if ($type instanceof Literal) {
1354
            $name = $type;
1355
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1356
            $name = self::$supportedColumnTypes[$typeLC];
1357
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1358
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1359
        } else {
1360
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1361
        }
1362
1363
        return ['name' => $name, 'limit' => $limit];
1364
    }
1365
1366
    /**
1367
     * Returns Phinx type by SQL type
1368
     *
1369
     * @param string|null $sqlTypeDef SQL type
1370
     *
1371
     * @return array
1372
     */
1373
    public function getPhinxType($sqlTypeDef)
1374
    {
1375
        $limit = null;
1376
        $scale = null;
1377
        if ($sqlTypeDef === null) {
1378
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1379
            $name = null;
1380
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1381
            // doesn't match the pattern of a type we'd know about
1382
            $name = Literal::from($sqlTypeDef);
1383
        } else {
1384
            // possibly a known type
1385
            $type = $match[1];
1386
            $typeLC = strtolower($type);
1387
            $affinity = isset($match[2]) ? $match[2] : '';
1388
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1389
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1390
            if (isset(self::$supportedColumnTypes[$typeLC])) {
1391
                // the type is an explicitly supported type
1392
                $name = $typeLC;
1393 View Code Duplication
            } elseif ($typeLC === 'tinyint' && $limit == 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1394
                // the type is a MySQL-style boolean
1395
                $name = static::PHINX_TYPE_BOOLEAN;
1396
                $limit = null;
1397
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1398
                // the type is an alias for a supported type
1399
                $name = self::$supportedColumnTypeAliases[$typeLC];
1400
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1401
                // unsupported but known types are passed through lowercased, and without appended affinity
1402
                $name = Literal::from($typeLC);
1403
            } else {
1404
                // unknown types are passed through as-is
1405
                $name = Literal::from($type . $affinity);
1406
            }
1407
        }
1408
1409
        return [
1410
            'name' => $name,
1411
            'limit' => $limit,
1412
            'scale' => $scale,
1413
        ];
1414
    }
1415
1416
    /**
1417
     * {@inheritDoc}
1418
     *
1419
     * @return void
1420
     */
1421
    public function createDatabase($name, $options = [])
1422
    {
1423
        touch($name . $this->suffix);
1424
    }
1425
1426
    /**
1427
     * @inheritDoc
1428
     */
1429
    public function hasDatabase($name)
1430
    {
1431
        return is_file($name . $this->suffix);
1432
    }
1433
1434
    /**
1435
     * {@inheritDoc}
1436
     *
1437
     * @return void
1438
     */
1439
    public function dropDatabase($name)
1440
    {
1441
        if ($this->getOption('memory')) {
1442
            $this->disconnect();
1443
            $this->connect();
1444
        }
1445
        if (file_exists($name . $this->suffix)) {
1446
            unlink($name . $this->suffix);
1447
        }
1448
    }
1449
1450
    /**
1451
     * Gets the SQLite Column Definition for a Column object.
1452
     *
1453
     * @param \Phinx\Db\Table\Column $column Column
1454
     *
1455
     * @return string
1456
     */
1457
    protected function getColumnSqlDefinition(Column $column)
1458
    {
1459
        $isLiteralType = $column->getType() instanceof Literal;
1460
        if ($isLiteralType) {
1461
            $def = (string)$column->getType();
1462
        } else {
1463
            $sqlType = $this->getSqlType($column->getType());
1464
            $def = strtoupper($sqlType['name']);
1465
1466
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
1467
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1468
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1469
            }
1470
        }
1471
        if ($column->getPrecision() && $column->getScale()) {
1472
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1473
        }
1474
1475
        $default = $column->getDefault();
1476
1477
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1478
        $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...
1479
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1480
1481
        if ($column->getUpdate()) {
1482
            $def .= ' ON UPDATE ' . $column->getUpdate();
1483
        }
1484
1485
        $def .= $this->getCommentDefinition($column);
1486
1487
        return $def;
1488
    }
1489
1490
    /**
1491
     * Gets the comment Definition for a Column object.
1492
     *
1493
     * @param \Phinx\Db\Table\Column $column Column
1494
     *
1495
     * @return string
1496
     */
1497
    protected function getCommentDefinition(Column $column)
1498
    {
1499
        if ($column->getComment()) {
1500
            return ' /* ' . $column->getComment() . ' */ ';
1501
        }
1502
1503
        return '';
1504
    }
1505
1506
    /**
1507
     * Gets the SQLite Index Definition for an Index object.
1508
     *
1509
     * @param \Phinx\Db\Table\Table $table Table
1510
     * @param \Phinx\Db\Table\Index $index Index
1511
     *
1512
     * @return string
1513
     */
1514
    protected function getIndexSqlDefinition(Table $table, Index $index)
1515
    {
1516
        if ($index->getType() === Index::UNIQUE) {
1517
            $def = 'UNIQUE INDEX';
1518
        } else {
1519
            $def = 'INDEX';
1520
        }
1521
        if (is_string($index->getName())) {
1522
            $indexName = $index->getName();
1523
        } else {
1524
            $indexName = $table->getName() . '_';
1525
            foreach ($index->getColumns() as $column) {
1526
                $indexName .= $column . '_';
1527
            }
1528
            $indexName .= 'index';
1529
        }
1530
        $def .= ' `' . $indexName . '`';
1531
1532
        return $def;
1533
    }
1534
1535
    /**
1536
     * @inheritDoc
1537
     */
1538
    public function getColumnTypes()
1539
    {
1540
        return array_keys(self::$supportedColumnTypes);
1541
    }
1542
1543
    /**
1544
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1545
     *
1546
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1547
     *
1548
     * @return string
1549
     */
1550 View Code Duplication
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1551
    {
1552
        $def = '';
1553
        if ($foreignKey->getConstraint()) {
1554
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
0 ignored issues
show
Bug introduced by
It seems like $foreignKey->getConstraint() targeting Phinx\Db\Table\ForeignKey::getConstraint() can also be of type boolean; however, Phinx\Db\Adapter\SQLiteAdapter::quoteColumnName() does only seem to accept string, 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...
1555
        } else {
1556
            $columnNames = [];
1557
            foreach ($foreignKey->getColumns() as $column) {
1558
                $columnNames[] = $this->quoteColumnName($column);
1559
            }
1560
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1561
            $refColumnNames = [];
1562
            foreach ($foreignKey->getReferencedColumns() as $column) {
1563
                $refColumnNames[] = $this->quoteColumnName($column);
1564
            }
1565
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1566
            if ($foreignKey->getOnDelete()) {
1567
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1568
            }
1569
            if ($foreignKey->getOnUpdate()) {
1570
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1571
            }
1572
        }
1573
1574
        return $def;
1575
    }
1576
1577
    /**
1578
     * @inheritDoc
1579
     */
1580
    public function getDecoratedConnection()
1581
    {
1582
        $options = $this->getOptions();
1583
        $options['quoteIdentifiers'] = true;
1584
        $database = ':memory:';
0 ignored issues
show
Unused Code introduced by
$database is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1585
1586
        if (!empty($options['name'])) {
1587
            $options['database'] = $options['name'];
1588
1589
            if (file_exists($options['name'] . $this->suffix)) {
1590
                $options['database'] = $options['name'] . $this->suffix;
1591
            }
1592
        }
1593
1594
        $driver = new SqliteDriver($options);
1595
        $driver->setConnection($this->connection);
0 ignored issues
show
Bug introduced by
It seems like $this->connection can be null; however, setConnection() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1596
1597
        return new Connection(['driver' => $driver] + $options);
1598
    }
1599
}
1600