Completed
Pull Request — master (#1657)
by mark
04:09
created

SQLiteAdapter::createDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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