Completed
Push — master ( 7da46b...c54c56 )
by José
13s
created

SQLiteAdapter::databaseVersionAtLeast()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Cake\Database\Connection;
32
use Cake\Database\Driver\Sqlite as SqliteDriver;
33
use Phinx\Db\Table\Column;
34
use Phinx\Db\Table\ForeignKey;
35
use Phinx\Db\Table\Index;
36
use Phinx\Db\Table\Table;
37
use Phinx\Db\Util\AlterInstructions;
38
use Phinx\Util\Literal;
39
use Phinx\Util\Expression;
40
41
/**
42
 * Phinx SQLite Adapter.
43
 *
44
 * @author Rob Morgan <[email protected]>
45
 * @author Richard McIntyre <[email protected]>
46
 */
47
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
48
{
49
    // list of supported Phinx column types with their SQL equivalents
50
    // some types have an affinity appended to ensure they do not receive NUMERIC affinity
51
    protected static $supportedColumnTypes = [
52
        self::PHINX_TYPE_BIG_INTEGER => 'biginteger',
53
        self::PHINX_TYPE_BINARY => 'binary_blob',
54
        self::PHINX_TYPE_BLOB => 'blob',
55
        self::PHINX_TYPE_BOOLEAN => 'boolean_integer',
56 42
        self::PHINX_TYPE_CHAR => 'char',
57
        self::PHINX_TYPE_DATE => 'date_text',
58 42
        self::PHINX_TYPE_DATETIME => 'datetime_text',
59 42
        self::PHINX_TYPE_DOUBLE => 'double',
60
        self::PHINX_TYPE_FLOAT => 'float',
61
        self::PHINX_TYPE_INTEGER => 'integer',
62
        self::PHINX_TYPE_JSON => 'json_text',
63
        self::PHINX_TYPE_JSONB => 'jsonb_text',
64
        self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger',
65 42
        self::PHINX_TYPE_STRING => 'varchar',
66 42
        self::PHINX_TYPE_TEXT => 'text',
67
        self::PHINX_TYPE_TIME => 'time_text',
68
        self::PHINX_TYPE_UUID => 'uuid_text',
69 42
        self::PHINX_TYPE_TIMESTAMP => 'timestamp_text',
70
        self::PHINX_TYPE_VARBINARY => 'varbinary_blob'
71
    ];
72 42
73 42
    // list of aliases of supported column types
74 42
    protected static $supportedColumnTypeAliases = [
75 42
        'varchar' => self::PHINX_TYPE_STRING,
76
        'tinyint' => self::PHINX_TYPE_SMALL_INTEGER,
77
        'tinyinteger' => self::PHINX_TYPE_SMALL_INTEGER,
78
        'smallint' => self::PHINX_TYPE_SMALL_INTEGER,
79 42
        'int' => self::PHINX_TYPE_INTEGER,
80 42
        'mediumint' => self::PHINX_TYPE_INTEGER,
81
        'mediuminteger' => self::PHINX_TYPE_INTEGER,
82
        'bigint' => self::PHINX_TYPE_BIG_INTEGER,
83
        'tinytext' => self::PHINX_TYPE_TEXT,
84
        'mediumtext' => self::PHINX_TYPE_TEXT,
85
        'longtext' => self::PHINX_TYPE_TEXT,
86
        'tinyblob' => self::PHINX_TYPE_BLOB,
87 42
        'mediumblob' => self::PHINX_TYPE_BLOB,
88 42
        'longblob' => self::PHINX_TYPE_BLOB,
89 42
        'real' => self::PHINX_TYPE_FLOAT,
90
    ];
91
92
    // list of known but unsupported Phinx column types
93
    protected static $unsupportedColumnTypes = [
94 48
        self::PHINX_TYPE_BIT,
95
        self::PHINX_TYPE_CIDR,
96 48
        self::PHINX_TYPE_DECIMAL,
97 48
        self::PHINX_TYPE_ENUM,
98
        self::PHINX_TYPE_FILESTREAM,
99
        self::PHINX_TYPE_GEOMETRY,
100
        self::PHINX_TYPE_INET,
101
        self::PHINX_TYPE_INTERVAL,
102
        self::PHINX_TYPE_LINESTRING,
103
        self::PHINX_TYPE_MACADDR,
104
        self::PHINX_TYPE_POINT,
105
        self::PHINX_TYPE_POLYGON,
106
        self::PHINX_TYPE_SET
107
    ];
108
109
    protected $definitionsWithLimits = [
110 1
        'CHAR',
111
        'CHARACTER',
112 1
        'VARCHAR',
113 1
        'VARYING CHARACTER',
114
        'NCHAR',
115
        'NATIVE CHARACTER',
116
        'NVARCHAR'
117
    ];
118
119
    protected $suffix = '.sqlite3';
120
121
    /** Indicates whether the database library version is at least the specified version
122
     *
123
     * @param string $ver The version to check against e.g. '3.28.0'
124
     * @return boolean
125
     */
126
    public function databaseVersionAtLeast($ver)
127
    {
128
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
129
        return version_compare($actual, $ver, '>=');
130
    }
131
132
    /**
133
     * {@inheritdoc}
134 43
     */
135
    public function connect()
136 43
    {
137
        if ($this->connection === null) {
138
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
139
                // @codeCoverageIgnoreStart
140
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
141
                // @codeCoverageIgnoreEnd
142 44
            }
143
144 44
            $db = null;
145
            $options = $this->getOptions();
146
147
            // use a memory database if the option was specified
148
            if (!empty($options['memory'])) {
149
                $dsn = 'sqlite::memory:';
150 42
            } else {
151
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
152 42
            }
153 42
154 42
            try {
155 12
                $db = new \PDO($dsn);
156 42
            } catch (\PDOException $exception) {
157
                throw new \InvalidArgumentException(sprintf(
158 42
                    'There was a problem connecting to the database: %s',
159
                    $exception->getMessage()
160
                ));
161
            }
162
163
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
164 42
            $this->setConnection($db);
165
        }
166
    }
167 42
168 42
    /**
169 42
     * {@inheritdoc}
170 35
     */
171 35
    public function setOptions(array $options)
172 35
    {
173 35
        parent::setOptions($options);
174
175 35
        if (isset($options['suffix'])) {
176 42
            $this->suffix = $options['suffix'];
177
        }
178 1
        //don't "fix" the file extension if it is blank, some people
179 1
        //might want a SQLITE db file with absolutely no extension.
180 1
        if (strlen($this->suffix) && substr($this->suffix, 0, 1) !== '.') {
181 1
            $this->suffix = '.' . $this->suffix;
182
        }
183 1
184 1
        return $this;
185
    }
186
187 42
    /**
188 42
     * {@inheritdoc}
189 42
     */
190 42
    public function disconnect()
191 42
    {
192
        $this->connection = null;
193
    }
194 42
195 42
    /**
196 42
     * {@inheritdoc}
197 42
     */
198 42
    public function hasTransactions()
199 42
    {
200
        return true;
201
    }
202 1
203 1
    /**
204 1
     * {@inheritdoc}
205
     */
206 1
    public function beginTransaction()
207 1
    {
208 1
        $this->getConnection()->beginTransaction();
209 1
    }
210 1
211 1
    /**
212 42
     * {@inheritdoc}
213 42
     */
214 37
    public function commitTransaction()
215
    {
216
        $this->getConnection()->commit();
217
    }
218 42
219 42
    /**
220 1
     * {@inheritdoc}
221 1
     */
222 1
    public function rollbackTransaction()
223 1
    {
224
        $this->getConnection()->rollBack();
225 42
    }
226
227 42
    /**
228
     * {@inheritdoc}
229 42
     */
230 6
    public function quoteTableName($tableName)
231 42
    {
232 42
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
233
    }
234
235
    /**
236
     * {@inheritdoc}
237 1
     */
238
    public function quoteColumnName($columnName)
239 1
    {
240 1
        return '`' . str_replace('`', '``', $columnName) . '`';
241
    }
242
243
    /**
244
     * @param string $tableName Table name
245 1
     * @param boolean $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
246
     * @return array
247 1
     */
248 1
    protected function getSchemaName($tableName, $quoted = false)
249
    {
250
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
251
            $table = $match[1];
252
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
253 1
            $result = ['schema' => $schema, 'table' => $table];
254
        } else {
255 1
            $result = ['schema' => '', 'table' => $tableName];
256 1
        }
257 1
258 1
        if ($quoted) {
259
            $result['schema'] = strlen($result['schema']) ? $this->quoteColumnName($result['schema']) . '.' : '';
260 1
            $result['table'] = $this->quoteColumnName($result['table']);
261 1
        }
262
263
        return $result;
264
    }
265
266 1
    /**
267
     * Retrieves information about a given table from one of the SQLite pragmas
268 1
     *
269 1
     * @param string $tableName The table to query
270
     * @param string $pragma The pragma to query
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
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
277
    }
278 1
279 1
    /**
280 1
     * {@inheritdoc}
281
     */
282 1
    public function hasTable($tableName)
283 1
    {
284 1
        $info = $this->getSchemaName($tableName);
285
        if ($info['schema'] === '') {
286 1
            // if no schema is specified we search all schemata
287 1
            $rows = $this->fetchAll('PRAGMA database_list;');
288
            $schemata = [];
289 1
            foreach ($rows as $row) {
290
                $schemata[] = $row['name'];
291
            }
292
        } else {
293
            // otherwise we search just the specified schema
294
            $schemata = (array)$info['schema'];
295 8
        }
296
297 8
        $table = strtolower($info['table']);
298 8
        foreach ($schemata as $schema) {
299 8
            if (strtolower($schema) === 'temp') {
300 7
                $master = 'sqlite_temp_master';
301
            } else {
302 8
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
303
            }
304 8
            try {
305
                $rows = $this->fetchAll(sprintf('SELECT name FROM %s WHERE type=\'table\' AND lower(name) = %s', $master, $this->quoteString($table)));
306
            } catch (\PDOException $e) {
307
                // an exception can occur if the schema part of the table refers to a database which is not attached
308
                return false;
309
            }
310 4
311
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
312 4
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
313 4
            foreach ($rows as $row) {
314 4
                if (strtolower($row['name']) === $table) {
315 4
                    return true;
316 4
                }
317 4
            }
318
        }
319 4
320 4
        return false;
321
    }
322
323
    /**
324
     * {@inheritdoc}
325 2
     */
326
    public function createTable(Table $table, array $columns = [], array $indexes = [])
327 2
    {
328
        // Add the default primary key
329 2
        $options = $table->getOptions();
330 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...
331 2
            $options['id'] = 'id';
332 2
        }
333 2
334 2
        if (isset($options['id']) && is_string($options['id'])) {
335 2
            // Handle id => "field_name" to support AUTO_INCREMENT
336 2
            $column = new Column();
337
            $column->setName($options['id'])
338 2
                   ->setType('integer')
339 2
                   ->setIdentity(true);
340 2
341 2
            array_unshift($columns, $column);
342 2
        }
343 2
344 2
        $sql = 'CREATE TABLE ';
345 2
        $sql .= $this->quoteTableName($table->getName()) . ' (';
346 2
        foreach ($columns as $column) {
347
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
348 2
349 1
            if (isset($options['primary_key']) && $column->getIdentity()) {
350
                //remove column from the primary key array as it is already defined as an autoincrement
351 1
                //primary id
352
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
353
                if ($identityColumnIndex !== false) {
354 1
                    unset($options['primary_key'][$identityColumnIndex]);
355
356 1
                    if (empty($options['primary_key'])) {
357 1
                        //The last primary key has been removed
358 1
                        unset($options['primary_key']);
359
                    }
360 1
                }
361 1
            }
362
        }
363
364 1
        // set the primary key(s)
365 1 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...
366 1
            $sql = rtrim($sql);
367 1
            $sql .= ' PRIMARY KEY (';
368 1
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
369
                $sql .= $this->quoteColumnName($options['primary_key']);
370 1
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
371
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
372 1
            }
373
            $sql .= ')';
374 1
        } else {
375 1
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
376
        }
377
378
        $sql = rtrim($sql) . ');';
379
        // execute the sql
380 6
        $this->execute($sql);
381
382
        foreach ($indexes as $index) {
383
            $this->addIndex($table, $index);
384 6
        }
385
    }
386 6
387
    /**
388 6
     * {@inheritdoc}
389 6
     */
390 6
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
391 6
    {
392 6
        $instructions = new AlterInstructions();
393 6
394
        // Drop the existing primary key
395 6
        $primaryKey = $this->getPrimaryKey($table->getName());
396 6
        if (!empty($primaryKey)) {
397 6
            $instructions->merge(
398 6
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
399 6
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
400 6
            );
401 6
        }
402 6
403 6
        // Add the primary key(s)
404
        if (!empty($newColumns)) {
405 6
            if (!is_string($newColumns)) {
406
                throw new \InvalidArgumentException(sprintf(
407
                    "Invalid value for primary key: %s",
408
                    json_encode($newColumns)
409
                ));
410
            }
411 6
412
            $instructions->merge(
413 6
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
414 6
            );
415 6
        }
416 6
417
        return $instructions;
418 6
    }
419
420 6
    /**
421
     * {@inheritdoc}
422 6
     */
423 6
    protected function getChangeCommentInstructions(Table $table, $newComment)
424 6
    {
425 6
        throw new \BadMethodCallException('SQLite does not have table comments');
426 6
    }
427
428 6
    /**
429
     * {@inheritdoc}
430 6
     */
431 6 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...
432 6
    {
433
        $sql = sprintf(
434
            'ALTER TABLE %s RENAME TO %s',
435
            $this->quoteTableName($tableName),
436
            $this->quoteTableName($newTableName)
437 2
        );
438
439
        return new AlterInstructions([], [$sql]);
440 2
    }
441
442 2
    /**
443
     * {@inheritdoc}
444 2
     */
445 2
    protected function getDropTableInstructions($tableName)
446 2
    {
447 2
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
448 2
449 2
        return new AlterInstructions([], [$sql]);
450
    }
451 2
452 2
    /**
453 2
     * {@inheritdoc}
454 2
     */
455 2
    public function truncateTable($tableName)
456 2
    {
457 2
        $sql = sprintf(
458 2
            'DELETE FROM %s',
459 2
            $this->quoteTableName($tableName)
460
        );
461 2
462
        $this->execute($sql);
463 2
    }
464
465
    /**
466
     * Parses a default-value expression to yield either a Literal representing
467
     * a string value, a string representing an expression, or some other scalar
468
     *
469 2
     * @param mixed $v The default-value expression to interpret
470
     * @param string $t The Phinx type of the column
471 2
     * @return mixed
472 2
     */
473 2
    protected function parseDefaultValue($v, $t)
474
    {
475 2
        if (is_null($v)) {
476
            return null;
477 2
        }
478 2
479 2
        // split the input into tokens
480
        $trimChars = " \t\n\r\0\x0B";
481 2
        $pattern = <<<PCRE_PATTERN
482
            /
483 2
                '(?:[^']|'')*'|                 # String literal
484 2
                "(?:[^"]|"")*"|                 # Standard identifier
485 2
                `(?:[^`]|``)*`|                 # MySQL identifier
486 2
                \[[^\]]*\]|                     # SQL Server identifier
487 2
                --[^\r\n]*|                     # Single-line comment
488
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
489 2
                [^\/\-]+|                       # Non-special characters
490
                .                               # Any other single character
491 2
            /sx
492 2
PCRE_PATTERN;
493 2
        preg_match_all($pattern, $v, $matches);
494
        // strip out any comment tokens
495
        $matches = array_map(function ($v) {
496
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
497
        }, $matches[0]);
498
        // reconstitute the string, trimming whitespace as well as parentheses
499
        $vClean = trim(implode('', $matches));
500
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
501 9
502
        // match the string against one of several patterns
503 9
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
504 9
            // magic date or time
505
            return strtoupper($vBare);
506 9
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
507 9
            // string literal
508 9
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
509 9
            return Literal::from($str);
510 9
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
511 9
            $int = (int)$vBare;
512 9
            // integer literal
513 9
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int == 0 || $int == 1)) {
514 9
                return (bool)$int;
515 9
            } else {
516
                return $int;
517
            }
518
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
519
            // float literal
520
            return (float)$vBare;
521 9
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
522
            // hexadecimal literal
523 9
            return hexdec(substr($vBare, 2));
524 4
        } elseif (preg_match('/^null$/i', $vBare)) {
525 4
            // null literal
526
            return null;
527 9
        } elseif (preg_match('/^true|false$/i', $vBare)) {
528 9
            // boolean literal
529
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
530 9
        } else {
531 9
            // any other expression: return the expression with parentheses, but without comments
532 9
            return Expression::from($vClean);
533 9
        }
534
    }
535 8
536
    /**
537 8
     * Returns the name of the specified table's identity column, or null if the table has no identity
538
     *
539
     * 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
540
     *
541
     * @param string $tableName The name of the table
542
     * @return string|null
543 1
     */
544
    protected function resolveIdentity($tableName)
545 1
    {
546
        $result = null;
547 1
        // make sure the table has only one primary key column which is of type integer
548 1
        foreach ($this->getTableInfo($tableName) as $col) {
549 1
            $type = strtolower($col['type']);
550
            if ($col['pk'] > 1) {
551
                // the table has a composite primary key
552
                return null;
553
            } elseif ($col['pk'] == 0) {
554
                // the column is not a primary key column and is thus not relevant
555
                continue;
556
            } elseif ($type !== 'integer') {
557
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
558
                return null;
559 8
            } else {
560
                // the column is a candidate for a row ID alias
561 8
                $result = $col['name'];
562 8
            }
563 8
        }
564 8
        // if there is no suitable PK column, stop now
565 8
        if (is_null($result)) {
566 8
            return null;
567 8
        }
568 8
        // make sure the table does not have a PK-origin autoindex
569 8
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
570 8
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
571
            if ($idx['origin'] === 'pk') {
572 8
                return null;
573 8
            }
574 8
        }
575
        return $result;
576
    }
577
578
    /**
579 1
     * {@inheritdoc}
580
     */
581 1
    public function getColumns($tableName)
582 1
    {
583 1
        $columns = [];
584
585 1
        $rows = $this->getTableInfo($tableName);
586 1
        $identity = $this->resolveIdentity($tableName);
587
588 1
        foreach ($rows as $columnInfo) {
589 1
            $column = new Column();
590 1
            $type = $this->getPhinxType($columnInfo['type']);
591 1
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
592 1
            
593 1
            $column->setName($columnInfo['name'])
594 1
                   ->setNull($columnInfo['notnull'] !== '1')
595 1
                   ->setDefault($default)
596 1
                   ->setType($type['name'])
597 1
                   ->setLimit($type['limit'])
598
                   ->setScale($type['scale'])
599
                   ->setIdentity($columnInfo['name'] === $identity);
600
601
            $columns[] = $column;
602
        }
603
604
        return $columns;
605 1
    }
606
607 1
    /**
608
     * {@inheritdoc}
609 1
     */
610 1 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...
611 1
    {
612 1
        $rows = $this->getTableInfo($tableName);
613 1
        foreach ($rows as $column) {
614 1
            if (strcasecmp($column['name'], $columnName) === 0) {
615 1
                return true;
616 1
            }
617 1
        }
618
619
        return false;
620
    }
621
622
    /**
623
     * {@inheritdoc}
624
     */
625 5 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...
626
    {
627 5
        $alter = sprintf(
628
            'ALTER TABLE %s ADD COLUMN %s %s',
629
            $this->quoteTableName($table->getName()),
630 5
            $this->quoteColumnName($column->getName()),
631
            $this->getColumnSqlDefinition($column)
632 5
        );
633 5
634 5
        return new AlterInstructions([], [$alter]);
635
    }
636 1
637
    /**
638
     * Returns the original CREATE statement for the give table
639
     *
640
     * @param string $tableName The table name to get the create statement for
641
     * @return string
642
     */
643
    protected function getDeclaringSql($tableName)
644
    {
645 5
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
646
647 5
        $sql = '';
648 5
        foreach ($rows as $table) {
649
            if ($table['tbl_name'] === $tableName) {
650
                $sql = $table['sql'];
651
            }
652
        }
653
654
        return $sql;
655
    }
656
657
    /**
658
     * Copies all the data from a tmp table to another table
659
     *
660
     * @param string $tableName The table name to copy the data to
661 5
     * @param string $tmpTableName The tmp table name where the data is stored
662
     * @param string[] $writeColumns The list of columns in the target table
663 5
     * @param string[] $selectColumns The list of columns in the tmp table
664 5
     * @return void
665 5
     */
666 5
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
667 5
    {
668 5
        $sql = sprintf(
669 5
            'INSERT INTO %s(%s) SELECT %s FROM %s',
670 5
            $this->quoteTableName($tableName),
671 5
            implode(', ', $writeColumns),
672 5
            implode(', ', $selectColumns),
673 5
            $this->quoteTableName($tmpTableName)
674
        );
675
        $this->execute($sql);
676
    }
677
678
    /**
679 4
     * Modifies the passed instructions to copy all data from the tmp table into
680
     * the provided table and then drops the tmp table.
681
     *
682 4
     * @param AlterInstructions $instructions The instructions to modify
683
     * @param string $tableName The table name to copy the data to
684 4
     * @return AlterInstructions
685 4
     */
686
    protected function copyAndDropTmpTable($instructions, $tableName)
687 4
    {
688 4
        $instructions->addPostStep(function ($state) use ($tableName) {
689 4
            $this->copyDataToNewTable(
690 4
                $tableName,
691 4
                $state['tmpTableName'],
692 4
                $state['writeColumns'],
693
                $state['selectColumns']
694 4
            );
695 4
696 4
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
697 4
698 4
            return $state;
699
        });
700 4
701
        return $instructions;
702 4
    }
703 4
704
    /**
705 4
     * Returns the columns and type to use when copying a table to another in the process
706 4
     * of altering a table
707 4
     *
708 4
     * @param string $tableName The table to modify
709 4
     * @param string $columnName The column name that is about to change
710 4
     * @param string|false $newColumnName Optionally the new name for the column
711 4
     * @return AlterInstructions
712
     */
713 4
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
714 4
    {
715 4
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
716
        $selectColumns = [];
717
        $writeColumns = [];
718
        $columnType = null;
719
        $found = false;
720 1
721
        foreach ($columns as $column) {
722
            $selectName = $column['name'];
723 1
            $writeName = $selectName;
724
725
            if ($selectName == $columnName) {
726
                $writeName = $newColumnName;
727 1
                $found = true;
728
                $columnType = $column['type'];
729 1
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
730
            }
731 1
732 1
            $selectColumns[] = $selectName;
733 1
            $writeColumns[] = $writeName;
734 1
        }
735 1
736 1
        $selectColumns = array_filter($selectColumns, 'strlen');
737
        $writeColumns = array_filter($writeColumns, 'strlen');
738 1
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
739 1
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
740 1
741 1
        if (!$found) {
742 1
            throw new \InvalidArgumentException(sprintf(
743 1
                'The specified column doesn\'t exist: ' . $columnName
744 1
            ));
745
        }
746 1
747
        return compact('writeColumns', 'selectColumns', 'columnType');
748 1
    }
749
750
    /**
751
     * Returns the initial instructions to alter a table using the
752
     * rename-alter-copy strategy
753
     *
754 1
     * @param string $tableName The table to modify
755
     * @return AlterInstructions
756 1
     */
757 1
    protected function beginAlterByCopyTable($tableName)
758 1
    {
759 1
        $instructions = new AlterInstructions();
760 1
        $instructions->addPostStep(function ($state) use ($tableName) {
761 1
            $createSQL = $this->getDeclaringSql($tableName);
762 1
763
            $tmpTableName = 'tmp_' . $tableName;
764 1
            $this->execute(
765
                sprintf(
766 1
                    'ALTER TABLE %s RENAME TO %s',
767 1
                    $this->quoteTableName($tableName),
768 1
                    $this->quoteTableName($tmpTableName)
769 1
                )
770 1
            );
771
772 1
            return compact('createSQL', 'tmpTableName') + $state;
773
        });
774 1
775 1
        return $instructions;
776 1
    }
777
778
    /**
779
     * {@inheritdoc}
780
     */
781
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
782
    {
783
        $instructions = $this->beginAlterByCopyTable($tableName);
784 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
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...
785
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
786
787
            return $newState + $state;
788
        });
789
790
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
791
            $sql = str_replace(
792
                $this->quoteColumnName($columnName),
793
                $this->quoteColumnName($newColumnName),
794
                $state['createSQL']
795
            );
796
            $this->execute($sql);
797
798
            return $state;
799
        });
800
801
        return $this->copyAndDropTmpTable($instructions, $tableName);
802
    }
803
804
    /**
805
     * {@inheritdoc}
806
     */
807
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
808
    {
809
        $instructions = $this->beginAlterByCopyTable($tableName);
810 43
811
        $newColumnName = $newColumn->getName();
812 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
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...
813 43
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
814 42
815
            return $newState + $state;
816 43
        });
817
818
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
819 43
            $sql = preg_replace(
820 1
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
821
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
822 43
                $state['createSQL'],
823 38
                1
824
            );
825 43
            $this->execute($sql);
826 42
827
            return $state;
828 43
        });
829 2
830
        return $this->copyAndDropTmpTable($instructions, $tableName);
831 43
    }
832 1
833
    /**
834 43
     * {@inheritdoc}
835 1
     */
836
    protected function getDropColumnInstructions($tableName, $columnName)
837 43
    {
838 42
        $instructions = $this->beginAlterByCopyTable($tableName);
839
840 43 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName) {
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...
841 1
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
842
843 43
            return $newState + $state;
844 1
        });
845
846 43
        $instructions->addPostStep(function ($state) use ($columnName) {
847 43
            $sql = preg_replace(
848 1
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
849
                "",
850 43
                $state['createSQL']
851 42
            );
852
853 5
            if (substr($sql, -2) === ', ') {
854
                $sql = substr($sql, 0, -2) . ')';
855 5
            }
856 4
857
            $this->execute($sql);
858
859
            return $state;
860 1
        });
861 1
862
        return $this->copyAndDropTmpTable($instructions, $tableName);
863
    }
864 1
865
    /**
866
     * Get an array of indexes from a particular table.
867 1
     *
868
     * @param string $tableName Table Name
869 1
     * @return array
870 1
     */
871 1
    protected function getIndexes($tableName)
872
    {
873
        $indexes = [];
874
        $schema = $this->getSchemaName($tableName, true)['schema'];
875
        $indexList = $this->getTableInfo($tableName, 'index_list');
876
877
        foreach ($indexList as $index) {
878
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
879
            $cols = [];
880 3
            foreach ($indexData as $indexItem) {
881
                $cols[] = $indexItem['name'];
882 3
            }
883 1
            $indexes[$index['name']] = $cols;
884
        }
885 2
886 2
        return $indexes;
887 2
    }
888 2
889 1
    /**
890 1
     * Finds the names of a table's indexes matching the supplied columns
891 2
     *
892
     * @param string $tableName The table to which the index belongs
893
     * @param string|string[] $columns The columns of the index
894 2
     * @return array
895 2
     */
896 1
    protected function resolveIndex($tableName, $columns)
897 1
    {
898
        $columns = array_map('strtolower', (array)$columns);
899
        $indexes = $this->getIndexes($tableName);
900 1
        $matches = [];
901 2
902
        foreach ($indexes as $name => $index) {
903
            $indexCols = array_map('strtolower', $index);
904
            if ($columns == $indexCols) {
905
                $matches[] = $name;
906
            }
907
        }
908
909
        return $matches;
910 2
    }
911
912
    /**
913
     * {@inheritdoc}
914
     */
915
    public function hasIndex($tableName, $columns)
916 2
    {
917 1
        return (bool)$this->resolveIndex($tableName, $columns);
918
    }
919
920 1
    /**
921 1
     * {@inheritdoc}
922 2
     */
923 1
    public function hasIndexByName($tableName, $indexName)
924 1
    {
925 2
        $indexName = strtolower($indexName);
926 2
        $indexes = $this->getIndexes($tableName);
927
928
        foreach (array_keys($indexes) as $index) {
929
            if ($indexName === strtolower($index)) {
930
                return true;
931
            }
932
        }
933 2
934
        return false;
935
    }
936 1
937 1
    /**
938
     * {@inheritdoc}
939 1
     */
940
    protected function getAddIndexInstructions(Table $table, Index $index)
941
    {
942
        $indexColumnArray = [];
943
        foreach ($index->getColumns() as $column) {
944
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
945
        }
946 48
        $indexColumns = implode(',', $indexColumnArray);
947
        $sql = sprintf(
948 48
            'CREATE %s ON %s (%s)',
949 48
            $this->getIndexSqlDefinition($table, $index),
950
            $this->quoteTableName($table->getName()),
951
            $indexColumns
952
        );
953
954 2
        return new AlterInstructions([], [$sql]);
955
    }
956 2
957
    /**
958
     * {@inheritdoc}
959
     */
960
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
961
    {
962 48
        $instructions = new AlterInstructions();
963
        $indexNames = $this->resolveIndex($tableName, $columns);
964 48
        $schema = $this->getSchemaName($tableName, true)['schema'];
965 47
        foreach ($indexNames as $indexName) {
966 47
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
967 48
                $instructions->addPostStep(sprintf(
968
                    'DROP INDEX %s%s',
969
                    $schema,
970
                    $this->quoteColumnName($indexName)
971
                ));
972
            }
973
        }
974
975 42
        return $instructions;
976
    }
977 42
978 8
    /**
979 42
     * {@inheritdoc}
980 42
     */
981 42
    protected function getDropIndexByNameInstructions($tableName, $indexName)
982 42
    {
983
        $instructions = new AlterInstructions();
984
        $indexName = strtolower($indexName);
985
        $indexes = $this->getIndexes($tableName);
986
987
        $found = false;
988
        foreach (array_keys($indexes) as $index) {
989
            if ($indexName === strtolower($index)) {
990
                $found = true;
991 42
                break;
992
            }
993 42
        }
994 42
995 42
        if ($found) {
996 42
            $schema = $this->getSchemaName($tableName, true)['schema'];
997
                $instructions->addPostStep(sprintf(
998
                    'DROP INDEX %s%s',
999 42
                    $schema,
1000 42
                    $this->quoteColumnName($indexName)
1001 42
                ));
1002 42
        }
1003 42
1004 4
        return $instructions;
1005 4
    }
1006
1007 42
    /**
1008
     * {@inheritdoc}
1009 42
     */
1010 42
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1011 42
    {
1012
        if (!is_null($constraint)) {
1013 42
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
1014
        }
1015
1016
        $columns = array_map('strtolower', (array)$columns);
1017 42
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1018
1019 42
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1020
            return false;
1021
        }
1022
        
1023
        return true;
1024
    }
1025
1026
    /**
1027
     * Get the primary key from a particular table.
1028 42
     *
1029
     * @param string $tableName Table Name
1030 42
     * @return string[]
1031 2
     */
1032
    protected function getPrimaryKey($tableName)
1033 42
    {
1034
        $primaryKey = [];
1035
1036
        $rows = $this->getTableInfo($tableName);
1037
1038
        foreach ($rows as $row) {
1039
            if ($row['pk'] > 0) {
1040
                $primaryKey[$row['pk'] - 1] = $row['name'];
1041
            }
1042 8
        }
1043
1044 8
        return $primaryKey;
1045 2
    }
1046 2
1047 6
    /**
1048
     * {@inheritdoc}
1049 8
     */
1050 3
    public function hasForeignKey($tableName, $columns, $constraint = null)
1051 3
    {
1052 6
        if (!is_null($constraint)) {
1053 6
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
1054 6
        }
1055 6
1056 6
        $columns = array_map('strtolower', (array)$columns);
1057
        $foreignKeys = $this->getForeignKeys($tableName);
1058 8
1059 8
        foreach ($foreignKeys as $key) {
1060
            $key = array_map('strtolower', $key);
1061
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1062
                continue;
1063
            }
1064
            return true;
1065 47
        }
1066
1067 47
        return false;
1068
    }
1069
1070
    /**
1071
     * Get an array of foreign keys from a particular table.
1072
     *
1073
     * @param string $tableName Table Name
1074
     * @return array
1075
     */
1076 5
    protected function getForeignKeys($tableName)
1077
    {
1078 5
        $foreignKeys = [];
1079 5
1080
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1081
1082 5
        foreach ($rows as $row) {
1083 5
            if (!isset($foreignKeys[$row['id']])) {
1084 5
                $foreignKeys[$row['id']] = [];
1085 5
            }
1086 5
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1087 5
        }
1088 5
1089 5
        return $foreignKeys;
1090 5
    }
1091 5
1092 5
    /**
1093 1
     * @param Table $table The Table
1094 1
     * @param string $column Column Name
1095 5
     * @return AlterInstructions
1096 1
     */
1097 1
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1098
    {
1099 5
        $instructions = $this->beginAlterByCopyTable($table->getName());
1100
1101
        $tableName = $table->getName();
1102
        $instructions->addPostStep(function ($state) use ($column) {
1103
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1104
1105
            $sql = $state['createSQL'];
1106
1107
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1108
                if (isset($matches[2])) {
1109
                    if ($matches[2] === 'INTEGER') {
1110
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1111
                    } else {
1112
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1113
                    }
1114
1115
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1116
                }
1117
            }
1118
1119
            $this->execute($sql);
1120
1121
            return $state;
1122
        });
1123
1124 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...
1125
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1126
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1127
            $selectColumns = $writeColumns = $names;
1128
1129
            return compact('selectColumns', 'writeColumns') + $state;
1130
        });
1131
1132
        return $this->copyAndDropTmpTable($instructions, $tableName);
1133
    }
1134
1135
    /**
1136
     * @param Table $table Table
1137
     * @param string $column Column Name
1138
     * @return AlterInstructions
1139
     */
1140
    protected function getDropPrimaryKeyInstructions($table, $column)
1141
    {
1142
        $instructions = $this->beginAlterByCopyTable($table->getName());
1143
1144
        $instructions->addPostStep(function ($state) use ($column) {
1145
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1146
1147
            return $newState + $state;
1148
        });
1149
1150
        $instructions->addPostStep(function ($state) {
1151
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1152
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1153
1154
            if ($sql) {
1155
                $this->execute($sql);
1156
            }
1157
1158
            return $state;
1159
        });
1160
1161
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1162
    }
1163
1164
    /**
1165
     * {@inheritdoc}
1166
     */
1167
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1168
    {
1169
        $instructions = $this->beginAlterByCopyTable($table->getName());
1170
1171
        $tableName = $table->getName();
1172
        $instructions->addPostStep(function ($state) use ($foreignKey) {
1173
            $this->execute('pragma foreign_keys = ON');
1174
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
1175
            $this->execute($sql);
1176
1177
            return $state;
1178
        });
1179
1180 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...
1181
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1182
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1183
            $selectColumns = $writeColumns = $names;
1184
1185
            return compact('selectColumns', 'writeColumns') + $state;
1186
        });
1187
1188
        return $this->copyAndDropTmpTable($instructions, $tableName);
1189
    }
1190
1191
    /**
1192
     * {@inheritdoc}
1193
     */
1194
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1195
    {
1196
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
1197
    }
1198
1199
    /**
1200
     * {@inheritdoc}
1201
     */
1202
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1203
    {
1204
        $instructions = $this->beginAlterByCopyTable($tableName);
1205
1206
        $instructions->addPostStep(function ($state) use ($columns) {
1207
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1208
1209
            $selectColumns = $newState['selectColumns'];
1210
            $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...
1211
            $diff = array_diff($columns, $selectColumns);
1212
1213
            if (!empty($diff)) {
1214
                throw new \InvalidArgumentException(sprintf(
1215
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1216
                ));
1217
            }
1218
1219
            return $newState + $state;
1220
        });
1221
1222
        $instructions->addPostStep(function ($state) use ($columns) {
1223
            $sql = '';
1224
1225
            foreach ($columns as $columnName) {
1226
                $search = sprintf(
1227
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1228
                    $this->quoteColumnName($columnName)
1229
                );
1230
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1231
            }
1232
1233
            if ($sql) {
1234
                $this->execute($sql);
1235
            }
1236
1237
            return $state;
1238
        });
1239
1240
        return $this->copyAndDropTmpTable($instructions, $tableName);
1241
    }
1242
1243
    /**
1244
     * {@inheritdoc}
1245
     */
1246
    public function getSqlType($type, $limit = null)
1247
    {
1248
        $typeLC = strtolower($type);
1249
        if ($type instanceof Literal) {
1250
            $name = $type;
1251
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1252
            $name = self::$supportedColumnTypes[$typeLC];
1253
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1254
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1255
        } else {
1256
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1257
        }
1258
        return ['name' => $name, 'limit' => $limit];
1259
    }
1260
1261
    /**
1262
     * Returns Phinx type by SQL type
1263
     *
1264
     * @param string|null $sqlTypeDef SQL type
1265
     * @return array
1266
     */
1267
    public function getPhinxType($sqlTypeDef)
1268
    {
1269
        $limit = null;
1270
        $scale = null;
1271
        if (is_null($sqlTypeDef)) {
1272
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1273
            $name = null;
1274
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1275
            // doesn't match the pattern of a type we'd know about
1276
            $name = Literal::from($sqlTypeDef);
1277
        } else {
1278
            // possibly a known type
1279
            $type = $match[1];
1280
            $typeLC = strtolower($type);
1281
            $affinity = isset($match[2]) ? $match[2] : '';
1282
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1283
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1284
            if (isset(self::$supportedColumnTypes[$typeLC])) {
1285
                // the type is an explicitly supported type
1286
                $name = $typeLC;
1287 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...
1288
                // the type is a MySQL-style boolean
1289
                $name = static::PHINX_TYPE_BOOLEAN;
1290
                $limit = null;
1291
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1292
                // the type is an alias for a supported type
1293
                $name = self::$supportedColumnTypeAliases[$typeLC];
1294
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1295
                // unsupported but known types are passed through lowercased, and without appended affinity
1296
                $name = Literal::from($typeLC);
1297
            } else {
1298
                // unknown types are passed through as-is
1299
                $name = Literal::from($type . $affinity);
1300
            }
1301
        }
1302
1303
        return [
1304
            'name' => $name,
1305
            'limit' => $limit,
1306
            'scale' => $scale
1307
        ];
1308
    }
1309
1310
    /**
1311
     * {@inheritdoc}
1312
     */
1313
    public function createDatabase($name, $options = [])
1314
    {
1315
        touch($name . $this->suffix);
1316
    }
1317
1318
    /**
1319
     * {@inheritdoc}
1320
     */
1321
    public function hasDatabase($name)
1322
    {
1323
        return is_file($name . $this->suffix);
1324
    }
1325
1326
    /**
1327
     * {@inheritdoc}
1328
     */
1329
    public function dropDatabase($name)
1330
    {
1331
        if ($this->getOption('memory')) {
1332
            $this->disconnect();
1333
            $this->connect();
1334
        }
1335
        if (file_exists($name . $this->suffix)) {
1336
            unlink($name . $this->suffix);
1337
        }
1338
    }
1339
1340
    /**
1341
     * Gets the SQLite Column Definition for a Column object.
1342
     *
1343
     * @param \Phinx\Db\Table\Column $column Column
1344
     * @return string
1345
     */
1346
    protected function getColumnSqlDefinition(Column $column)
1347
    {
1348
        $isLiteralType = $column->getType() instanceof Literal;
1349
        if ($isLiteralType) {
1350
            $def = (string)$column->getType();
1351
        } else {
1352
            $sqlType = $this->getSqlType($column->getType());
1353
            $def = strtoupper($sqlType['name']);
1354
1355
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
1356
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1357
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1358
            }
1359
        }
1360
        if ($column->getPrecision() && $column->getScale()) {
1361
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1362
        }
1363
1364
        $default = $column->getDefault();
1365
1366
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
1367
        $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...
1368
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1369
1370
        if ($column->getUpdate()) {
1371
            $def .= ' ON UPDATE ' . $column->getUpdate();
1372
        }
1373
1374
        $def .= $this->getCommentDefinition($column);
1375
1376
        return $def;
1377
    }
1378
1379
    /**
1380
     * Gets the comment Definition for a Column object.
1381
     *
1382
     * @param \Phinx\Db\Table\Column $column Column
1383
     * @return string
1384
     */
1385
    protected function getCommentDefinition(Column $column)
1386
    {
1387
        if ($column->getComment()) {
1388
            return ' /* ' . $column->getComment() . ' */ ';
1389
        }
1390
1391
        return '';
1392
    }
1393
1394
    /**
1395
     * Gets the SQLite Index Definition for an Index object.
1396
     *
1397
     * @param \Phinx\Db\Table\Table $table Table
1398
     * @param \Phinx\Db\Table\Index $index Index
1399
     * @return string
1400
     */
1401
    protected function getIndexSqlDefinition(Table $table, Index $index)
1402
    {
1403
        if ($index->getType() === Index::UNIQUE) {
1404
            $def = 'UNIQUE INDEX';
1405
        } else {
1406
            $def = 'INDEX';
1407
        }
1408
        if (is_string($index->getName())) {
1409
            $indexName = $index->getName();
1410
        } else {
1411
            $indexName = $table->getName() . '_';
1412
            foreach ($index->getColumns() as $column) {
1413
                $indexName .= $column . '_';
1414
            }
1415
            $indexName .= 'index';
1416
        }
1417
        $def .= ' `' . $indexName . '`';
1418
1419
        return $def;
1420
    }
1421
1422
    /**
1423
     * {@inheritdoc}
1424
     */
1425
    public function getColumnTypes()
1426
    {
1427
        return array_keys(self::$supportedColumnTypes);
1428
    }
1429
1430
    /**
1431
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1432
     *
1433
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1434
     * @return string
1435
     */
1436 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...
1437
    {
1438
        $def = '';
1439
        if ($foreignKey->getConstraint()) {
1440
            $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...
1441
        } else {
1442
            $columnNames = [];
1443
            foreach ($foreignKey->getColumns() as $column) {
1444
                $columnNames[] = $this->quoteColumnName($column);
1445
            }
1446
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1447
            $refColumnNames = [];
1448
            foreach ($foreignKey->getReferencedColumns() as $column) {
1449
                $refColumnNames[] = $this->quoteColumnName($column);
1450
            }
1451
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1452
            if ($foreignKey->getOnDelete()) {
1453
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1454
            }
1455
            if ($foreignKey->getOnUpdate()) {
1456
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1457
            }
1458
        }
1459
1460
        return $def;
1461
    }
1462
1463
    /**
1464
     * {@inheritDoc}
1465
     *
1466
     */
1467
    public function getDecoratedConnection()
1468
    {
1469
        $options = $this->getOptions();
1470
        $options['quoteIdentifiers'] = true;
1471
        $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...
1472
1473
        if (!empty($options['name'])) {
1474
            $options['database'] = $options['name'];
1475
1476
            if (file_exists($options['name'] . $this->suffix)) {
1477
                $options['database'] = $options['name'] . $this->suffix;
1478
            }
1479
        }
1480
1481
        $driver = new SqliteDriver($options);
1482
        if (method_exists($driver, 'setConnection')) {
1483
            $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...
1484
        } else {
1485
            $driver->connection($this->connection);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Database\Driver::connection() has been deprecated with message: 3.6.0 Use getConnection()/setConnection() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1486
        }
1487
1488
        return new Connection(['driver' => $driver] + $options);
1489
    }
1490
}
1491