Completed
Pull Request — master (#1554)
by
unknown
01:35
created

SQLiteAdapter::copyAndDropTmpTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 13
cts 13
cp 1
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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
        $ver = array_map('intval', explode('.', $ver));
129
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
130
        $actual = array_map('intval', explode('.', $actual));
131
        $actual = array_pad($actual, sizeof($ver), 0);
132
133
        for ($a = 0; $a < sizeof($ver); $a++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function sizeof() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

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