Completed
Push — master ( 16c525...becd49 )
by José
01:24 queued 50s
created

SQLiteAdapter::getPrimaryKey()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 2
cts 2
cp 1
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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
     * Searches through all available schemata to find a table and returns an array
281
     * containing the bare schema name and whether the table exists at all.
282 1
     * If no schema was specified and the table does not exist the "main" schema is returned
283 1
     *
284 1
     * @param string $tableName The name of the table to find
285
     * @return array
286 1
     */
287 1
    protected function resolveTable($tableName)
288
    {
289 1
        $info = $this->getSchemaName($tableName);
290
        if ($info['schema'] === '') {
291
            // if no schema is specified we search all schemata
292
            $rows = $this->fetchAll('PRAGMA database_list;');
293
            // the temp schema is always first to be searched
294
            $schemata = ['temp'];
295 8
            foreach ($rows as $row) {
296
                if (strtolower($row['name']) !== 'temp') {
297 8
                    $schemata[] = $row['name'];
298 8
                }
299 8
            }
300 7
            $defaultSchema = 'main';
301
        } else {
302 8
            // otherwise we search just the specified schema
303
            $schemata = (array)$info['schema'];
304 8
            $defaultSchema = $info['schema'];
305
        }
306
307
        $table = strtolower($info['table']);
308
        foreach ($schemata as $schema) {
309
            if (strtolower($schema) === 'temp') {
310 4
                $master = 'sqlite_temp_master';
311
            } else {
312 4
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
313 4
            }
314 4
            try {
315 4
                $rows = $this->fetchAll(sprintf('SELECT name FROM %s WHERE type=\'table\' AND lower(name) = %s', $master, $this->quoteString($table)));
316 4
            } catch (\PDOException $e) {
317 4
                // an exception can occur if the schema part of the table refers to a database which is not attached
318
                break;
319 4
            }
320 4
321
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
322
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
323
            foreach ($rows as $row) {
324
                if (strtolower($row['name']) === $table) {
325 2
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
326
                }
327 2
            }
328
        }
329 2
330
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
331 2
    }
332 2
333 2
    /**
334 2
     * {@inheritdoc}
335 2
     */
336 2
    public function hasTable($tableName)
337
    {
338 2
        return $this->resolveTable($tableName)['exists'];
339 2
    }
340 2
341 2
    /**
342 2
     * {@inheritdoc}
343 2
     */
344 2
    public function createTable(Table $table, array $columns = [], array $indexes = [])
345 2
    {
346 2
        // Add the default primary key
347
        $options = $table->getOptions();
348 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...
349 1
            $options['id'] = 'id';
350
        }
351 1
352
        if (isset($options['id']) && is_string($options['id'])) {
353
            // Handle id => "field_name" to support AUTO_INCREMENT
354 1
            $column = new Column();
355
            $column->setName($options['id'])
356 1
                   ->setType('integer')
357 1
                   ->setIdentity(true);
358 1
359
            array_unshift($columns, $column);
360 1
        }
361 1
362
        $sql = 'CREATE TABLE ';
363
        $sql .= $this->quoteTableName($table->getName()) . ' (';
364 1
        foreach ($columns as $column) {
365 1
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
366 1
367 1
            if (isset($options['primary_key']) && $column->getIdentity()) {
368 1
                //remove column from the primary key array as it is already defined as an autoincrement
369
                //primary id
370 1
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
371
                if ($identityColumnIndex !== false) {
372 1
                    unset($options['primary_key'][$identityColumnIndex]);
373
374 1
                    if (empty($options['primary_key'])) {
375 1
                        //The last primary key has been removed
376
                        unset($options['primary_key']);
377
                    }
378
                }
379
            }
380 6
        }
381
382
        // set the primary key(s)
383 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...
384 6
            $sql = rtrim($sql);
385
            $sql .= ' PRIMARY KEY (';
386 6
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
387
                $sql .= $this->quoteColumnName($options['primary_key']);
388 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
389 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
390 6
            }
391 6
            $sql .= ')';
392 6
        } else {
393 6
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
394
        }
395 6
396 6
        $sql = rtrim($sql) . ');';
397 6
        // execute the sql
398 6
        $this->execute($sql);
399 6
400 6
        foreach ($indexes as $index) {
401 6
            $this->addIndex($table, $index);
402 6
        }
403 6
    }
404
405 6
    /**
406
     * {@inheritdoc}
407
     */
408
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
409
    {
410
        $instructions = new AlterInstructions();
411 6
412
        // Drop the existing primary key
413 6
        $primaryKey = $this->getPrimaryKey($table->getName());
414 6
        if (!empty($primaryKey)) {
415 6
            $instructions->merge(
416 6
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
417
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
418 6
            );
419
        }
420 6
421
        // Add the primary key(s)
422 6
        if (!empty($newColumns)) {
423 6
            if (!is_string($newColumns)) {
424 6
                throw new \InvalidArgumentException(sprintf(
425 6
                    "Invalid value for primary key: %s",
426 6
                    json_encode($newColumns)
427
                ));
428 6
            }
429
430 6
            $instructions->merge(
431 6
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
432 6
            );
433
        }
434
435
        return $instructions;
436
    }
437 2
438
    /**
439
     * {@inheritdoc}
440 2
     */
441
    protected function getChangeCommentInstructions(Table $table, $newComment)
442 2
    {
443
        throw new \BadMethodCallException('SQLite does not have table comments');
444 2
    }
445 2
446 2
    /**
447 2
     * {@inheritdoc}
448 2
     */
449 2 View Code Duplication
    protected function getRenameTableInstructions($tableName, $newTableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
450
    {
451 2
        $sql = sprintf(
452 2
            'ALTER TABLE %s RENAME TO %s',
453 2
            $this->quoteTableName($tableName),
454 2
            $this->quoteTableName($newTableName)
455 2
        );
456 2
457 2
        return new AlterInstructions([], [$sql]);
458 2
    }
459 2
460
    /**
461 2
     * {@inheritdoc}
462
     */
463 2
    protected function getDropTableInstructions($tableName)
464
    {
465
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
466
467
        return new AlterInstructions([], [$sql]);
468
    }
469 2
470
    /**
471 2
     * {@inheritdoc}
472 2
     */
473 2
    public function truncateTable($tableName)
474
    {
475 2
        $info = $this->resolveTable($tableName);
476
        // first try deleting the rows
477 2
        $this->execute(sprintf(
478 2
            'DELETE FROM %s.%s',
479 2
            $this->quoteColumnName($info['schema']),
480
            $this->quoteColumnName($info['table'])
481 2
        ));
482
483 2
        // assuming no error occurred, reset the autoincrement (if any)
484 2
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
485 2
            $this->execute(sprintf(
486 2
                'DELETE FROM %s.%s where name  = %s',
487 2
                $this->quoteColumnName($info['schema']),
488
                'sqlite_sequence',
489 2
                $this->quoteString($info['table'])
490
            ));
491 2
        }
492 2
    }
493 2
494
    /**
495
     * Parses a default-value expression to yield either a Literal representing
496
     * a string value, a string representing an expression, or some other scalar
497
     *
498
     * @param mixed $v The default-value expression to interpret
499
     * @param string $t The Phinx type of the column
500
     * @return mixed
501 9
     */
502
    protected function parseDefaultValue($v, $t)
503 9
    {
504 9
        if (is_null($v)) {
505
            return null;
506 9
        }
507 9
508 9
        // split the input into tokens
509 9
        $trimChars = " \t\n\r\0\x0B";
510 9
        $pattern = <<<PCRE_PATTERN
511 9
            /
512 9
                '(?:[^']|'')*'|                 # String literal
513 9
                "(?:[^"]|"")*"|                 # Standard identifier
514 9
                `(?:[^`]|``)*`|                 # MySQL identifier
515 9
                \[[^\]]*\]|                     # SQL Server identifier
516
                --[^\r\n]*|                     # Single-line comment
517
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
518
                [^\/\-]+|                       # Non-special characters
519
                .                               # Any other single character
520
            /sx
521 9
PCRE_PATTERN;
522
        preg_match_all($pattern, $v, $matches);
523 9
        // strip out any comment tokens
524 4
        $matches = array_map(function ($v) {
525 4
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
526
        }, $matches[0]);
527 9
        // reconstitute the string, trimming whitespace as well as parentheses
528 9
        $vClean = trim(implode('', $matches));
529
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
530 9
531 9
        // match the string against one of several patterns
532 9
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
533 9
            // magic date or time
534
            return strtoupper($vBare);
535 8
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
536
            // string literal
537 8
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
538
            return Literal::from($str);
539
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
540
            $int = (int)$vBare;
541
            // integer literal
542
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int == 0 || $int == 1)) {
543 1
                return (bool)$int;
544
            } else {
545 1
                return $int;
546
            }
547 1
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
548 1
            // float literal
549 1
            return (float)$vBare;
550
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
551
            // hexadecimal literal
552
            return hexdec(substr($vBare, 2));
553
        } elseif (preg_match('/^null$/i', $vBare)) {
554
            // null literal
555
            return null;
556
        } elseif (preg_match('/^true|false$/i', $vBare)) {
557
            // boolean literal
558
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
559 8
        } else {
560
            // any other expression: return the expression with parentheses, but without comments
561 8
            return Expression::from($vClean);
562 8
        }
563 8
    }
564 8
565 8
    /**
566 8
     * Returns the name of the specified table's identity column, or null if the table has no identity
567 8
     *
568 8
     * 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
569 8
     *
570 8
     * @param string $tableName The name of the table
571
     * @return string|null
572 8
     */
573 8
    protected function resolveIdentity($tableName)
574 8
    {
575
        $result = null;
576
        // make sure the table has only one primary key column which is of type integer
577
        foreach ($this->getTableInfo($tableName) as $col) {
578
            $type = strtolower($col['type']);
579 1
            if ($col['pk'] > 1) {
580
                // the table has a composite primary key
581 1
                return null;
582 1
            } elseif ($col['pk'] == 0) {
583 1
                // the column is not a primary key column and is thus not relevant
584
                continue;
585 1
            } elseif ($type !== 'integer') {
586 1
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
587
                return null;
588 1
            } else {
589 1
                // the column is a candidate for a row ID alias
590 1
                $result = $col['name'];
591 1
            }
592 1
        }
593 1
        // if there is no suitable PK column, stop now
594 1
        if (is_null($result)) {
595 1
            return null;
596 1
        }
597 1
        // make sure the table does not have a PK-origin autoindex
598
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
599
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
600
            if ($idx['origin'] === 'pk') {
601
                return null;
602
            }
603
        }
604
        return $result;
605 1
    }
606
607 1
    /**
608
     * {@inheritdoc}
609 1
     */
610 1
    public function getColumns($tableName)
611 1
    {
612 1
        $columns = [];
613 1
614 1
        $rows = $this->getTableInfo($tableName);
615 1
        $identity = $this->resolveIdentity($tableName);
616 1
617 1
        foreach ($rows as $columnInfo) {
618
            $column = new Column();
619
            $type = $this->getPhinxType($columnInfo['type']);
620
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
621
            
622
            $column->setName($columnInfo['name'])
623
                   ->setNull($columnInfo['notnull'] !== '1')
624
                   ->setDefault($default)
625 5
                   ->setType($type['name'])
626
                   ->setLimit($type['limit'])
627 5
                   ->setScale($type['scale'])
628
                   ->setIdentity($columnInfo['name'] === $identity);
629
630 5
            $columns[] = $column;
631
        }
632 5
633 5
        return $columns;
634 5
    }
635
636 1
    /**
637
     * {@inheritdoc}
638
     */
639 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...
640
    {
641
        $rows = $this->getTableInfo($tableName);
642
        foreach ($rows as $column) {
643
            if (strcasecmp($column['name'], $columnName) === 0) {
644
                return true;
645 5
            }
646
        }
647 5
648 5
        return false;
649
    }
650
651
    /**
652
     * {@inheritdoc}
653
     */
654 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...
655
    {
656
        $alter = sprintf(
657
            'ALTER TABLE %s ADD COLUMN %s %s',
658
            $this->quoteTableName($table->getName()),
659
            $this->quoteColumnName($column->getName()),
660
            $this->getColumnSqlDefinition($column)
661 5
        );
662
663 5
        return new AlterInstructions([], [$alter]);
664 5
    }
665 5
666 5
    /**
667 5
     * Returns the original CREATE statement for the give table
668 5
     *
669 5
     * @param string $tableName The table name to get the create statement for
670 5
     * @return string
671 5
     */
672 5
    protected function getDeclaringSql($tableName)
673 5
    {
674
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
675
676
        $sql = '';
677
        foreach ($rows as $table) {
678
            if ($table['tbl_name'] === $tableName) {
679 4
                $sql = $table['sql'];
680
            }
681
        }
682 4
683
        return $sql;
684 4
    }
685 4
686
    /**
687 4
     * Copies all the data from a tmp table to another table
688 4
     *
689 4
     * @param string $tableName The table name to copy the data to
690 4
     * @param string $tmpTableName The tmp table name where the data is stored
691 4
     * @param string[] $writeColumns The list of columns in the target table
692 4
     * @param string[] $selectColumns The list of columns in the tmp table
693
     * @return void
694 4
     */
695 4
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
696 4
    {
697 4
        $sql = sprintf(
698 4
            'INSERT INTO %s(%s) SELECT %s FROM %s',
699
            $this->quoteTableName($tableName),
700 4
            implode(', ', $writeColumns),
701
            implode(', ', $selectColumns),
702 4
            $this->quoteTableName($tmpTableName)
703 4
        );
704
        $this->execute($sql);
705 4
    }
706 4
707 4
    /**
708 4
     * Modifies the passed instructions to copy all data from the tmp table into
709 4
     * the provided table and then drops the tmp table.
710 4
     *
711 4
     * @param AlterInstructions $instructions The instructions to modify
712
     * @param string $tableName The table name to copy the data to
713 4
     * @return AlterInstructions
714 4
     */
715 4
    protected function copyAndDropTmpTable($instructions, $tableName)
716
    {
717
        $instructions->addPostStep(function ($state) use ($tableName) {
718
            $this->copyDataToNewTable(
719
                $tableName,
720 1
                $state['tmpTableName'],
721
                $state['writeColumns'],
722
                $state['selectColumns']
723 1
            );
724
725
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
726
727 1
            return $state;
728
        });
729 1
730
        return $instructions;
731 1
    }
732 1
733 1
    /**
734 1
     * Returns the columns and type to use when copying a table to another in the process
735 1
     * of altering a table
736 1
     *
737
     * @param string $tableName The table to modify
738 1
     * @param string $columnName The column name that is about to change
739 1
     * @param string|false $newColumnName Optionally the new name for the column
740 1
     * @return AlterInstructions
741 1
     */
742 1
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
743 1
    {
744 1
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
745
        $selectColumns = [];
746 1
        $writeColumns = [];
747
        $columnType = null;
748 1
        $found = false;
749
750
        foreach ($columns as $column) {
751
            $selectName = $column['name'];
752
            $writeName = $selectName;
753
754 1
            if ($selectName == $columnName) {
755
                $writeName = $newColumnName;
756 1
                $found = true;
757 1
                $columnType = $column['type'];
758 1
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
759 1
            }
760 1
761 1
            $selectColumns[] = $selectName;
762 1
            $writeColumns[] = $writeName;
763
        }
764 1
765
        $selectColumns = array_filter($selectColumns, 'strlen');
766 1
        $writeColumns = array_filter($writeColumns, 'strlen');
767 1
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
768 1
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
769 1
770 1
        if (!$found) {
771
            throw new \InvalidArgumentException(sprintf(
772 1
                'The specified column doesn\'t exist: ' . $columnName
773
            ));
774 1
        }
775 1
776 1
        return compact('writeColumns', 'selectColumns', 'columnType');
777
    }
778
779
    /**
780
     * Returns the initial instructions to alter a table using the
781
     * rename-alter-copy strategy
782
     *
783
     * @param string $tableName The table to modify
784
     * @return AlterInstructions
785
     */
786
    protected function beginAlterByCopyTable($tableName)
787
    {
788
        $instructions = new AlterInstructions();
789
        $instructions->addPostStep(function ($state) use ($tableName) {
790
            $createSQL = $this->getDeclaringSql($tableName);
791
792
            $tmpTableName = 'tmp_' . $tableName;
793
            $this->execute(
794
                sprintf(
795
                    'ALTER TABLE %s RENAME TO %s',
796
                    $this->quoteTableName($tableName),
797
                    $this->quoteTableName($tmpTableName)
798
                )
799
            );
800
801
            return compact('createSQL', 'tmpTableName') + $state;
802
        });
803
804
        return $instructions;
805
    }
806
807
    /**
808
     * {@inheritdoc}
809
     */
810 43
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
811
    {
812
        $instructions = $this->beginAlterByCopyTable($tableName);
813 43 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...
814 42
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
815
816 43
            return $newState + $state;
817
        });
818
819 43
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
820 1
            $sql = str_replace(
821
                $this->quoteColumnName($columnName),
822 43
                $this->quoteColumnName($newColumnName),
823 38
                $state['createSQL']
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 getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
837 43
    {
838 42
        $instructions = $this->beginAlterByCopyTable($tableName);
839
840 43
        $newColumnName = $newColumn->getName();
841 1 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...
842
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
843 43
844 1
            return $newState + $state;
845
        });
846 43
847 43
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
848 1
            $sql = preg_replace(
849
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
850 43
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
851 42
                $state['createSQL'],
852
                1
853 5
            );
854
            $this->execute($sql);
855 5
856 4
            return $state;
857
        });
858
859
        return $this->copyAndDropTmpTable($instructions, $tableName);
860 1
    }
861 1
862
    /**
863
     * {@inheritdoc}
864 1
     */
865
    protected function getDropColumnInstructions($tableName, $columnName)
866
    {
867 1
        $instructions = $this->beginAlterByCopyTable($tableName);
868
869 1 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...
870 1
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
871 1
872
            return $newState + $state;
873
        });
874
875
        $instructions->addPostStep(function ($state) use ($columnName) {
876
            $sql = preg_replace(
877
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
878
                "",
879
                $state['createSQL']
880 3
            );
881
882 3
            if (substr($sql, -2) === ', ') {
883 1
                $sql = substr($sql, 0, -2) . ')';
884
            }
885 2
886 2
            $this->execute($sql);
887 2
888 2
            return $state;
889 1
        });
890 1
891 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
892
    }
893
894 2
    /**
895 2
     * Get an array of indexes from a particular table.
896 1
     *
897 1
     * @param string $tableName Table Name
898
     * @return array
899
     */
900 1
    protected function getIndexes($tableName)
901 2
    {
902
        $indexes = [];
903
        $schema = $this->getSchemaName($tableName, true)['schema'];
904
        $indexList = $this->getTableInfo($tableName, 'index_list');
905
906
        foreach ($indexList as $index) {
907
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
908
            $cols = [];
909
            foreach ($indexData as $indexItem) {
910 2
                $cols[] = $indexItem['name'];
911
            }
912
            $indexes[$index['name']] = $cols;
913
        }
914
915
        return $indexes;
916 2
    }
917 1
918
    /**
919
     * Finds the names of a table's indexes matching the supplied columns
920 1
     *
921 1
     * @param string $tableName The table to which the index belongs
922 2
     * @param string|string[] $columns The columns of the index
923 1
     * @return array
924 1
     */
925 2
    protected function resolveIndex($tableName, $columns)
926 2
    {
927
        $columns = array_map('strtolower', (array)$columns);
928
        $indexes = $this->getIndexes($tableName);
929
        $matches = [];
930
931
        foreach ($indexes as $name => $index) {
932
            $indexCols = array_map('strtolower', $index);
933 2
            if ($columns == $indexCols) {
934
                $matches[] = $name;
935
            }
936 1
        }
937 1
938
        return $matches;
939 1
    }
940
941
    /**
942
     * {@inheritdoc}
943
     */
944
    public function hasIndex($tableName, $columns)
945
    {
946 48
        return (bool)$this->resolveIndex($tableName, $columns);
947
    }
948 48
949 48
    /**
950
     * {@inheritdoc}
951
     */
952
    public function hasIndexByName($tableName, $indexName)
953
    {
954 2
        $indexName = strtolower($indexName);
955
        $indexes = $this->getIndexes($tableName);
956 2
957
        foreach (array_keys($indexes) as $index) {
958
            if ($indexName === strtolower($index)) {
959
                return true;
960
            }
961
        }
962 48
963
        return false;
964 48
    }
965 47
966 47
    /**
967 48
     * {@inheritdoc}
968
     */
969
    protected function getAddIndexInstructions(Table $table, Index $index)
970
    {
971
        $indexColumnArray = [];
972
        foreach ($index->getColumns() as $column) {
973
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
974
        }
975 42
        $indexColumns = implode(',', $indexColumnArray);
976
        $sql = sprintf(
977 42
            'CREATE %s ON %s (%s)',
978 8
            $this->getIndexSqlDefinition($table, $index),
979 42
            $this->quoteTableName($table->getName()),
980 42
            $indexColumns
981 42
        );
982 42
983
        return new AlterInstructions([], [$sql]);
984
    }
985
986
    /**
987
     * {@inheritdoc}
988
     */
989
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
990
    {
991 42
        $instructions = new AlterInstructions();
992
        $indexNames = $this->resolveIndex($tableName, $columns);
993 42
        $schema = $this->getSchemaName($tableName, true)['schema'];
994 42
        foreach ($indexNames as $indexName) {
995 42
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
996 42
                $instructions->addPostStep(sprintf(
997
                    'DROP INDEX %s%s',
998
                    $schema,
999 42
                    $this->quoteColumnName($indexName)
1000 42
                ));
1001 42
            }
1002 42
        }
1003 42
1004 4
        return $instructions;
1005 4
    }
1006
1007 42
    /**
1008
     * {@inheritdoc}
1009 42
     */
1010 42
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1011 42
    {
1012
        $instructions = new AlterInstructions();
1013 42
        $indexName = strtolower($indexName);
1014
        $indexes = $this->getIndexes($tableName);
1015
1016
        $found = false;
1017 42
        foreach (array_keys($indexes) as $index) {
1018
            if ($indexName === strtolower($index)) {
1019 42
                $found = true;
1020
                break;
1021
            }
1022
        }
1023
1024
        if ($found) {
1025
            $schema = $this->getSchemaName($tableName, true)['schema'];
1026
                $instructions->addPostStep(sprintf(
1027
                    'DROP INDEX %s%s',
1028 42
                    $schema,
1029
                    $this->quoteColumnName($indexName)
1030 42
                ));
1031 2
        }
1032
1033 42
        return $instructions;
1034
    }
1035
1036
    /**
1037
     * {@inheritdoc}
1038
     */
1039
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1040
    {
1041
        if (!is_null($constraint)) {
1042 8
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
1043
        }
1044 8
1045 2
        $columns = array_map('strtolower', (array)$columns);
1046 2
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1047 6
1048
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1049 8
            return false;
1050 3
        }
1051 3
        
1052 6
        return true;
1053 6
    }
1054 6
1055 6
    /**
1056 6
     * Get the primary key from a particular table.
1057
     *
1058 8
     * @param string $tableName Table Name
1059 8
     * @return string[]
1060
     */
1061
    protected function getPrimaryKey($tableName)
1062
    {
1063
        $primaryKey = [];
1064
1065 47
        $rows = $this->getTableInfo($tableName);
1066
1067 47
        foreach ($rows as $row) {
1068
            if ($row['pk'] > 0) {
1069
                $primaryKey[$row['pk'] - 1] = $row['name'];
1070
            }
1071
        }
1072
1073
        return $primaryKey;
1074
    }
1075
1076 5
    /**
1077
     * {@inheritdoc}
1078 5
     */
1079 5
    public function hasForeignKey($tableName, $columns, $constraint = null)
1080
    {
1081
        if (!is_null($constraint)) {
1082 5
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
1083 5
        }
1084 5
1085 5
        $columns = array_map('strtolower', (array)$columns);
1086 5
        $foreignKeys = $this->getForeignKeys($tableName);
1087 5
1088 5
        foreach ($foreignKeys as $key) {
1089 5
            $key = array_map('strtolower', $key);
1090 5
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1091 5
                continue;
1092 5
            }
1093 1
            return true;
1094 1
        }
1095 5
1096 1
        return false;
1097 1
    }
1098
1099 5
    /**
1100
     * Get an array of foreign keys from a particular table.
1101
     *
1102
     * @param string $tableName Table Name
1103
     * @return array
1104
     */
1105
    protected function getForeignKeys($tableName)
1106
    {
1107
        $foreignKeys = [];
1108
1109
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1110
1111
        foreach ($rows as $row) {
1112
            if (!isset($foreignKeys[$row['id']])) {
1113
                $foreignKeys[$row['id']] = [];
1114
            }
1115
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1116
        }
1117
1118
        return $foreignKeys;
1119
    }
1120
1121
    /**
1122
     * @param Table $table The Table
1123
     * @param string $column Column Name
1124
     * @return AlterInstructions
1125
     */
1126
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1127
    {
1128
        $instructions = $this->beginAlterByCopyTable($table->getName());
1129
1130
        $tableName = $table->getName();
1131
        $instructions->addPostStep(function ($state) use ($column) {
1132
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1133
1134
            $sql = $state['createSQL'];
1135
1136
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1137
                if (isset($matches[2])) {
1138
                    if ($matches[2] === 'INTEGER') {
1139
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1140
                    } else {
1141
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1142
                    }
1143
1144
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1145
                }
1146
            }
1147
1148
            $this->execute($sql);
1149
1150
            return $state;
1151
        });
1152
1153 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...
1154
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1155
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1156
            $selectColumns = $writeColumns = $names;
1157
1158
            return compact('selectColumns', 'writeColumns') + $state;
1159
        });
1160
1161
        return $this->copyAndDropTmpTable($instructions, $tableName);
1162
    }
1163
1164
    /**
1165
     * @param Table $table Table
1166
     * @param string $column Column Name
1167
     * @return AlterInstructions
1168
     */
1169
    protected function getDropPrimaryKeyInstructions($table, $column)
1170
    {
1171
        $instructions = $this->beginAlterByCopyTable($table->getName());
1172
1173
        $instructions->addPostStep(function ($state) use ($column) {
1174
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1175
1176
            return $newState + $state;
1177
        });
1178
1179
        $instructions->addPostStep(function ($state) {
1180
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1181
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1182
1183
            if ($sql) {
1184
                $this->execute($sql);
1185
            }
1186
1187
            return $state;
1188
        });
1189
1190
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1191
    }
1192
1193
    /**
1194
     * {@inheritdoc}
1195
     */
1196
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1197
    {
1198
        $instructions = $this->beginAlterByCopyTable($table->getName());
1199
1200
        $tableName = $table->getName();
1201
        $instructions->addPostStep(function ($state) use ($foreignKey) {
1202
            $this->execute('pragma foreign_keys = ON');
1203
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
1204
            $this->execute($sql);
1205
1206
            return $state;
1207
        });
1208
1209 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...
1210
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1211
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1212
            $selectColumns = $writeColumns = $names;
1213
1214
            return compact('selectColumns', 'writeColumns') + $state;
1215
        });
1216
1217
        return $this->copyAndDropTmpTable($instructions, $tableName);
1218
    }
1219
1220
    /**
1221
     * {@inheritdoc}
1222
     */
1223
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1224
    {
1225
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
1226
    }
1227
1228
    /**
1229
     * {@inheritdoc}
1230
     */
1231
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1232
    {
1233
        $instructions = $this->beginAlterByCopyTable($tableName);
1234
1235
        $instructions->addPostStep(function ($state) use ($columns) {
1236
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1237
1238
            $selectColumns = $newState['selectColumns'];
1239
            $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...
1240
            $diff = array_diff($columns, $selectColumns);
1241
1242
            if (!empty($diff)) {
1243
                throw new \InvalidArgumentException(sprintf(
1244
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1245
                ));
1246
            }
1247
1248
            return $newState + $state;
1249
        });
1250
1251
        $instructions->addPostStep(function ($state) use ($columns) {
1252
            $sql = '';
1253
1254
            foreach ($columns as $columnName) {
1255
                $search = sprintf(
1256
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1257
                    $this->quoteColumnName($columnName)
1258
                );
1259
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1260
            }
1261
1262
            if ($sql) {
1263
                $this->execute($sql);
1264
            }
1265
1266
            return $state;
1267
        });
1268
1269
        return $this->copyAndDropTmpTable($instructions, $tableName);
1270
    }
1271
1272
    /**
1273
     * {@inheritdoc}
1274
     */
1275
    public function getSqlType($type, $limit = null)
1276
    {
1277
        $typeLC = strtolower($type);
1278
        if ($type instanceof Literal) {
1279
            $name = $type;
1280
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1281
            $name = self::$supportedColumnTypes[$typeLC];
1282
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1283
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1284
        } else {
1285
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1286
        }
1287
        return ['name' => $name, 'limit' => $limit];
1288
    }
1289
1290
    /**
1291
     * Returns Phinx type by SQL type
1292
     *
1293
     * @param string|null $sqlTypeDef SQL type
1294
     * @return array
1295
     */
1296
    public function getPhinxType($sqlTypeDef)
1297
    {
1298
        $limit = null;
1299
        $scale = null;
1300
        if (is_null($sqlTypeDef)) {
1301
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1302
            $name = null;
1303
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1304
            // doesn't match the pattern of a type we'd know about
1305
            $name = Literal::from($sqlTypeDef);
1306
        } else {
1307
            // possibly a known type
1308
            $type = $match[1];
1309
            $typeLC = strtolower($type);
1310
            $affinity = isset($match[2]) ? $match[2] : '';
1311
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1312
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1313
            if (isset(self::$supportedColumnTypes[$typeLC])) {
1314
                // the type is an explicitly supported type
1315
                $name = $typeLC;
1316 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...
1317
                // the type is a MySQL-style boolean
1318
                $name = static::PHINX_TYPE_BOOLEAN;
1319
                $limit = null;
1320
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1321
                // the type is an alias for a supported type
1322
                $name = self::$supportedColumnTypeAliases[$typeLC];
1323
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes)) {
1324
                // unsupported but known types are passed through lowercased, and without appended affinity
1325
                $name = Literal::from($typeLC);
1326
            } else {
1327
                // unknown types are passed through as-is
1328
                $name = Literal::from($type . $affinity);
1329
            }
1330
        }
1331
1332
        return [
1333
            'name' => $name,
1334
            'limit' => $limit,
1335
            'scale' => $scale
1336
        ];
1337
    }
1338
1339
    /**
1340
     * {@inheritdoc}
1341
     */
1342
    public function createDatabase($name, $options = [])
1343
    {
1344
        touch($name . $this->suffix);
1345
    }
1346
1347
    /**
1348
     * {@inheritdoc}
1349
     */
1350
    public function hasDatabase($name)
1351
    {
1352
        return is_file($name . $this->suffix);
1353
    }
1354
1355
    /**
1356
     * {@inheritdoc}
1357
     */
1358
    public function dropDatabase($name)
1359
    {
1360
        if ($this->getOption('memory')) {
1361
            $this->disconnect();
1362
            $this->connect();
1363
        }
1364
        if (file_exists($name . $this->suffix)) {
1365
            unlink($name . $this->suffix);
1366
        }
1367
    }
1368
1369
    /**
1370
     * Gets the SQLite Column Definition for a Column object.
1371
     *
1372
     * @param \Phinx\Db\Table\Column $column Column
1373
     * @return string
1374
     */
1375
    protected function getColumnSqlDefinition(Column $column)
1376
    {
1377
        $isLiteralType = $column->getType() instanceof Literal;
1378
        if ($isLiteralType) {
1379
            $def = (string)$column->getType();
1380
        } else {
1381
            $sqlType = $this->getSqlType($column->getType());
1382
            $def = strtoupper($sqlType['name']);
1383
1384
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
1385
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1386
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1387
            }
1388
        }
1389
        if ($column->getPrecision() && $column->getScale()) {
1390
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1391
        }
1392
1393
        $default = $column->getDefault();
1394
1395
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
1396
        $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...
1397
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1398
1399
        if ($column->getUpdate()) {
1400
            $def .= ' ON UPDATE ' . $column->getUpdate();
1401
        }
1402
1403
        $def .= $this->getCommentDefinition($column);
1404
1405
        return $def;
1406
    }
1407
1408
    /**
1409
     * Gets the comment Definition for a Column object.
1410
     *
1411
     * @param \Phinx\Db\Table\Column $column Column
1412
     * @return string
1413
     */
1414
    protected function getCommentDefinition(Column $column)
1415
    {
1416
        if ($column->getComment()) {
1417
            return ' /* ' . $column->getComment() . ' */ ';
1418
        }
1419
1420
        return '';
1421
    }
1422
1423
    /**
1424
     * Gets the SQLite Index Definition for an Index object.
1425
     *
1426
     * @param \Phinx\Db\Table\Table $table Table
1427
     * @param \Phinx\Db\Table\Index $index Index
1428
     * @return string
1429
     */
1430
    protected function getIndexSqlDefinition(Table $table, Index $index)
1431
    {
1432
        if ($index->getType() === Index::UNIQUE) {
1433
            $def = 'UNIQUE INDEX';
1434
        } else {
1435
            $def = 'INDEX';
1436
        }
1437
        if (is_string($index->getName())) {
1438
            $indexName = $index->getName();
1439
        } else {
1440
            $indexName = $table->getName() . '_';
1441
            foreach ($index->getColumns() as $column) {
1442
                $indexName .= $column . '_';
1443
            }
1444
            $indexName .= 'index';
1445
        }
1446
        $def .= ' `' . $indexName . '`';
1447
1448
        return $def;
1449
    }
1450
1451
    /**
1452
     * {@inheritdoc}
1453
     */
1454
    public function getColumnTypes()
1455
    {
1456
        return array_keys(self::$supportedColumnTypes);
1457
    }
1458
1459
    /**
1460
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1461
     *
1462
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1463
     * @return string
1464
     */
1465 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...
1466
    {
1467
        $def = '';
1468
        if ($foreignKey->getConstraint()) {
1469
            $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...
1470
        } else {
1471
            $columnNames = [];
1472
            foreach ($foreignKey->getColumns() as $column) {
1473
                $columnNames[] = $this->quoteColumnName($column);
1474
            }
1475
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1476
            $refColumnNames = [];
1477
            foreach ($foreignKey->getReferencedColumns() as $column) {
1478
                $refColumnNames[] = $this->quoteColumnName($column);
1479
            }
1480
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1481
            if ($foreignKey->getOnDelete()) {
1482
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1483
            }
1484
            if ($foreignKey->getOnUpdate()) {
1485
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1486
            }
1487
        }
1488
1489
        return $def;
1490
    }
1491
1492
    /**
1493
     * {@inheritDoc}
1494
     *
1495
     */
1496
    public function getDecoratedConnection()
1497
    {
1498
        $options = $this->getOptions();
1499
        $options['quoteIdentifiers'] = true;
1500
        $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...
1501
1502
        if (!empty($options['name'])) {
1503
            $options['database'] = $options['name'];
1504
1505
            if (file_exists($options['name'] . $this->suffix)) {
1506
                $options['database'] = $options['name'] . $this->suffix;
1507
            }
1508
        }
1509
1510
        $driver = new SqliteDriver($options);
1511
        if (method_exists($driver, 'setConnection')) {
1512
            $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...
1513
        } else {
1514
            $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...
1515
        }
1516
1517
        return new Connection(['driver' => $driver] + $options);
1518
    }
1519
}
1520