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

SQLiteAdapter::getSchemaName()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 12
cts 12
cp 1
rs 9.7
c 0
b 0
f 0
cc 4
nc 6
nop 2
crap 4
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
    protected $definitionsWithLimits = [
50
        'CHAR',
51
        'CHARACTER',
52
        'VARCHAR',
53
        'VARYING CHARACTER',
54
        'NCHAR',
55
        'NATIVE CHARACTER',
56 42
        'NVARCHAR'
57
    ];
58 42
59 42
    protected $suffix = '.sqlite3';
60
61
    /** Indicates whether the database library version is at least the specified version
62
     *
63
     * @param string $ver The version to check against e.g. '3.28.0'
64
     * @return boolean
65 42
     */
66 42
    public function databaseVersionAtLeast($ver)
67
    {
68
        $ver = array_map('intval', explode('.', $ver));
69 42
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
70
        $actual = array_map('intval', explode('.', $actual));
71
        $actual = array_pad($actual, sizeof($ver), 0);
72 42
73 42
        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...
74 42
            if ($actual[$a] < $ver[$a]) {
75 42
                return false;
76
            } elseif ($actual[$a] > $ver[$a]) {
77
                return true;
78
            }
79 42
        }
80 42
81
        return true;
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 42
    public function connect()
88 42
    {
89 42
        if ($this->connection === null) {
90
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
91
                // @codeCoverageIgnoreStart
92
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
93
                // @codeCoverageIgnoreEnd
94 48
            }
95
96 48
            $db = null;
97 48
            $options = $this->getOptions();
98
99
            // use a memory database if the option was specified
100
            if (!empty($options['memory'])) {
101
                $dsn = 'sqlite::memory:';
102
            } else {
103
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
104
            }
105
106
            try {
107
                $db = new \PDO($dsn);
108
            } catch (\PDOException $exception) {
109
                throw new \InvalidArgumentException(sprintf(
110 1
                    'There was a problem connecting to the database: %s',
111
                    $exception->getMessage()
112 1
                ));
113 1
            }
114
115
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
116
            $this->setConnection($db);
117
        }
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function setOptions(array $options)
124
    {
125
        parent::setOptions($options);
126
127
        if (isset($options['suffix'])) {
128
            $this->suffix = $options['suffix'];
129
        }
130
        //don't "fix" the file extension if it is blank, some people
131
        //might want a SQLITE db file with absolutely no extension.
132
        if (strlen($this->suffix) && substr($this->suffix, 0, 1) !== '.') {
133
            $this->suffix = '.' . $this->suffix;
134 43
        }
135
136 43
        return $this;
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 44
    public function disconnect()
143
    {
144 44
        $this->connection = null;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 42
    public function hasTransactions()
151
    {
152 42
        return true;
153 42
    }
154 42
155 12
    /**
156 42
     * {@inheritdoc}
157
     */
158 42
    public function beginTransaction()
159
    {
160
        $this->getConnection()->beginTransaction();
161
    }
162
163
    /**
164 42
     * {@inheritdoc}
165
     */
166
    public function commitTransaction()
167 42
    {
168 42
        $this->getConnection()->commit();
169 42
    }
170 35
171 35
    /**
172 35
     * {@inheritdoc}
173 35
     */
174
    public function rollbackTransaction()
175 35
    {
176 42
        $this->getConnection()->rollBack();
177
    }
178 1
179 1
    /**
180 1
     * {@inheritdoc}
181 1
     */
182
    public function quoteTableName($tableName)
183 1
    {
184 1
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
185
    }
186
187 42
    /**
188 42
     * {@inheritdoc}
189 42
     */
190 42
    public function quoteColumnName($columnName)
191 42
    {
192
        return '`' . str_replace('`', '``', $columnName) . '`';
193
    }
194 42
195 42
    /**
196 42
     * @param string $tableName Table name
197 42
     * @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
198 42
     * @return array
199 42
     */
200
    protected function getSchemaName($tableName, $quoted = false)
201
    {
202 1
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
203 1
            $table = $match[1];
204 1
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
205
            $result = ['schema' => $schema, 'table' => $table];
206 1
        } else {
207 1
            $result = ['schema' => '', 'table' => $tableName];
208 1
        }
209 1
210 1
        if ($quoted) {
211 1
            $result['schema'] = strlen($result['schema']) ? $this->quoteColumnName($result['schema']) . '.' : '';
212 42
            $result['table'] = $this->quoteColumnName($result['table']);
213 42
        }
214 37
215
        return $result;
216
    }
217
218 42
    /**
219 42
     * Retrieves information about a given table from one of the SQLite pragmas
220 1
     *
221 1
     * @param string $tableName The table to query
222 1
     * @param string $pragma The pragma to query
223 1
     * @return array
224
     */
225 42
    protected function getTableInfo($tableName, $pragma = 'table_info')
226
    {
227 42
        $info = $this->getSchemaName($tableName, true);
228
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
229 42
    }
230 6
231 42
    /**
232 42
     * {@inheritdoc}
233
     */
234
    public function hasTable($tableName)
235
    {
236
        $info = $this->getSchemaName($tableName);
237 1
        if ($info['schema'] === '') {
238
            // if no schema is specified we search all schemata
239 1
            $rows = $this->fetchAll('PRAGMA database_list;');
240 1
            $schemata = [];
241
            foreach ($rows as $row) {
242
                $schemata[] = $row['name'];
243
            }
244
        } else {
245 1
            // otherwise we search just the specified schema
246
            $schemata = (array)$info['schema'];
247 1
        }
248 1
249
        $table = strtolower($info['table']);
250
        foreach ($schemata as $schema) {
251
            if (strtolower($schema) === 'temp') {
252
                $master = 'sqlite_temp_master';
253 1
            } else {
254
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
255 1
            }
256 1
            try {
257 1
                $rows = $this->fetchAll(sprintf('SELECT name FROM %s WHERE type=\'table\' AND lower(name) = %s', $master, $this->quoteString($table)));
258 1
            } catch (\PDOException $e) {
259
                // an exception can occur if the schema part of the table refers to a database which is not attached
260 1
                return false;
261 1
            }
262
263
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
264
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
265
            foreach ($rows as $row) {
266 1
                if (strtolower($row['name']) === $table) {
267
                    return true;
268 1
                }
269 1
            }
270
        }
271 1
272 1
        return false;
273 1
    }
274 1
275 1
    /**
276 1
     * {@inheritdoc}
277
     */
278 1
    public function createTable(Table $table, array $columns = [], array $indexes = [])
279 1
    {
280 1
        // Add the default primary key
281
        $options = $table->getOptions();
282 1 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...
283 1
            $options['id'] = 'id';
284 1
        }
285
286 1
        if (isset($options['id']) && is_string($options['id'])) {
287 1
            // Handle id => "field_name" to support AUTO_INCREMENT
288
            $column = new Column();
289 1
            $column->setName($options['id'])
290
                   ->setType('integer')
291
                   ->setIdentity(true);
292
293
            array_unshift($columns, $column);
294
        }
295 8
296
        $sql = 'CREATE TABLE ';
297 8
        $sql .= $this->quoteTableName($table->getName()) . ' (';
298 8
        foreach ($columns as $column) {
299 8
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
300 7
301
            if (isset($options['primary_key']) && $column->getIdentity()) {
302 8
                //remove column from the primary key array as it is already defined as an autoincrement
303
                //primary id
304 8
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
305
                if ($identityColumnIndex !== false) {
306
                    unset($options['primary_key'][$identityColumnIndex]);
307
308
                    if (empty($options['primary_key'])) {
309
                        //The last primary key has been removed
310 4
                        unset($options['primary_key']);
311
                    }
312 4
                }
313 4
            }
314 4
        }
315 4
316 4
        // set the primary key(s)
317 4 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...
318
            $sql = rtrim($sql);
319 4
            $sql .= ' PRIMARY KEY (';
320 4
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
321
                $sql .= $this->quoteColumnName($options['primary_key']);
322
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
323
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
324
            }
325 2
            $sql .= ')';
326
        } else {
327 2
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
328
        }
329 2
330
        $sql = rtrim($sql) . ');';
331 2
        // execute the sql
332 2
        $this->execute($sql);
333 2
334 2
        foreach ($indexes as $index) {
335 2
            $this->addIndex($table, $index);
336 2
        }
337
    }
338 2
339 2
    /**
340 2
     * {@inheritdoc}
341 2
     */
342 2
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
343 2
    {
344 2
        $instructions = new AlterInstructions();
345 2
346 2
        // Drop the existing primary key
347
        $primaryKey = $this->getPrimaryKey($table->getName());
348 2
        if (!empty($primaryKey)) {
349 1
            $instructions->merge(
350
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
351 1
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
352
            );
353
        }
354 1
355
        // Add the primary key(s)
356 1
        if (!empty($newColumns)) {
357 1
            if (!is_string($newColumns)) {
358 1
                throw new \InvalidArgumentException(sprintf(
359
                    "Invalid value for primary key: %s",
360 1
                    json_encode($newColumns)
361 1
                ));
362
            }
363
364 1
            $instructions->merge(
365 1
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
366 1
            );
367 1
        }
368 1
369
        return $instructions;
370 1
    }
371
372 1
    /**
373
     * {@inheritdoc}
374 1
     */
375 1
    protected function getChangeCommentInstructions(Table $table, $newComment)
376
    {
377
        throw new \BadMethodCallException('SQLite does not have table comments');
378
    }
379
380 6
    /**
381
     * {@inheritdoc}
382
     */
383 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...
384 6
    {
385
        $sql = sprintf(
386 6
            'ALTER TABLE %s RENAME TO %s',
387
            $this->quoteTableName($tableName),
388 6
            $this->quoteTableName($newTableName)
389 6
        );
390 6
391 6
        return new AlterInstructions([], [$sql]);
392 6
    }
393 6
394
    /**
395 6
     * {@inheritdoc}
396 6
     */
397 6
    protected function getDropTableInstructions($tableName)
398 6
    {
399 6
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
400 6
401 6
        return new AlterInstructions([], [$sql]);
402 6
    }
403 6
404
    /**
405 6
     * {@inheritdoc}
406
     */
407
    public function truncateTable($tableName)
408
    {
409
        $sql = sprintf(
410
            'DELETE FROM %s',
411 6
            $this->quoteTableName($tableName)
412
        );
413 6
414 6
        $this->execute($sql);
415 6
    }
416 6
417
    /**
418 6
     * Parses a default-value expression to yield either a Literal representing
419
     * a string value, a string representing an expression, or some other scalar
420 6
     *
421
     * @param mixed $v The default-value expression to interpret
422 6
     * @param string $t The Phinx type of the column
423 6
     * @return mixed
424 6
     */
425 6
    protected function parseDefaultValue($v, $t)
426 6
    {
427
        if (is_null($v)) {
428 6
            return null;
429
        }
430 6
431 6
        // split the input into tokens
432 6
        $trimChars = " \t\n\r\0\x0B";
433
        $pattern = <<<PCRE_PATTERN
434
            /
435
                '(?:[^']|'')*'|                 # String literal
436
                "(?:[^"]|"")*"|                 # Standard identifier
437 2
                `(?:[^`]|``)*`|                 # MySQL identifier
438
                \[[^\]]*\]|                     # SQL Server identifier
439
                --[^\r\n]*|                     # Single-line comment
440 2
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
441
                [^\/\-]+|                       # Non-special characters
442 2
                .                               # Any other single character
443
            /sx
444 2
PCRE_PATTERN;
445 2
        preg_match_all($pattern, $v, $matches);
446 2
        // strip out any comment tokens
447 2
        $matches = array_map(function ($v) {
448 2
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
449 2
        }, $matches[0]);
450
        // reconstitute the string, trimming whitespace as well as parentheses
451 2
        $vClean = trim(implode('', $matches));
452 2
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
453 2
454 2
        // match the string against one of several patterns
455 2
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
456 2
            // magic date or time
457 2
            return strtoupper($vBare);
458 2
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
459 2
            // string literal
460
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
461 2
            return Literal::from($str);
462
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
463 2
            $int = (int)$vBare;
464
            // integer literal
465
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int == 0 || $int == 1)) {
466
                return (bool)$int;
467
            } else {
468
                return $int;
469 2
            }
470
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
471 2
            // float literal
472 2
            return (float)$vBare;
473 2
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
474
            // hexadecimal literal
475 2
            return hexdec(substr($vBare, 2));
476
        } elseif (preg_match('/^null$/i', $vBare)) {
477 2
            // null literal
478 2
            return null;
479 2
        } elseif (preg_match('/^true|false$/i', $vBare)) {
480
            // boolean literal
481 2
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
482
        } else {
483 2
            // any other expression: return the expression with parentheses, but without comments
484 2
            return Expression::from($vClean);
485 2
        }
486 2
    }
487 2
488
    /**
489 2
     * Returns the name of the specified table's identity column, or null if the table has no identity
490
     *
491 2
     * 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
492 2
     *
493 2
     * @param string $tableName The name of the table
494
     * @return string|null
495
     */
496
    protected function resolveIdentity($tableName)
497
    {
498
        $result = null;
499
        // make sure the table has only one primary key column which is of type integer
500
        foreach ($this->getTableInfo($tableName) as $col) {
501 9
            $type = strtolower($col['type']);
502
            if ($col['pk'] > 1) {
503 9
                // the table has a composite primary key
504 9
                return null;
505
            } elseif ($col['pk'] == 0) {
506 9
                // the column is not a primary key column and is thus not relevant
507 9
                continue;
508 9
            } elseif ($type !== 'integer') {
509 9
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
510 9
                return null;
511 9
            } else {
512 9
                // the column is a candidate for a row ID alias
513 9
                $result = $col['name'];
514 9
            }
515 9
        }
516
        // if there is no suitable PK column, stop now
517
        if (is_null($result)) {
518
            return null;
519
        }
520
        // make sure the table does not have a PK-origin autoindex
521 9
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
522
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
523 9
            if ($idx['origin'] === 'pk') {
524 4
                return null;
525 4
            }
526
        }
527 9
        return $result;
528 9
    }
529
530 9
    /**
531 9
     * {@inheritdoc}
532 9
     */
533 9
    public function getColumns($tableName)
534
    {
535 8
        $columns = [];
536
537 8
        $rows = $this->getTableInfo($tableName);
538
        $identity = $this->resolveIdentity($tableName);
539
540
        foreach ($rows as $columnInfo) {
541
            $column = new Column();
542
            $type = $this->getPhinxType(strtolower($columnInfo['type']));  // FIXME: this should not be lowercased, but the current implementation of getPhinxType requires it
543 1
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
0 ignored issues
show
Bug introduced by
It seems like $type['name'] can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\SQLiteA...er::parseDefaultValue() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
544
            
545 1
            $column->setName($columnInfo['name'])
546
                   ->setNull($columnInfo['notnull'] !== '1')
547 1
                   ->setDefault($default)
548 1
                   ->setType($type['name'])
549 1
                   ->setLimit($type['limit'])
550
                   ->setScale($type['scale'])
551
                   ->setIdentity($columnInfo['name'] === $identity);
552
553
            $columns[] = $column;
554
        }
555
556
        return $columns;
557
    }
558
559 8
    /**
560
     * {@inheritdoc}
561 8
     */
562 8 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...
563 8
    {
564 8
        $rows = $this->getTableInfo($tableName);
565 8
        foreach ($rows as $column) {
566 8
            if (strcasecmp($column['name'], $columnName) === 0) {
567 8
                return true;
568 8
            }
569 8
        }
570 8
571
        return false;
572 8
    }
573 8
574 8
    /**
575
     * {@inheritdoc}
576
     */
577 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...
578
    {
579 1
        $alter = sprintf(
580
            'ALTER TABLE %s ADD COLUMN %s %s',
581 1
            $this->quoteTableName($table->getName()),
582 1
            $this->quoteColumnName($column->getName()),
583 1
            $this->getColumnSqlDefinition($column)
584
        );
585 1
586 1
        return new AlterInstructions([], [$alter]);
587
    }
588 1
589 1
    /**
590 1
     * Returns the original CREATE statement for the give table
591 1
     *
592 1
     * @param string $tableName The table name to get the create statement for
593 1
     * @return string
594 1
     */
595 1
    protected function getDeclaringSql($tableName)
596 1
    {
597 1
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
598
599
        $sql = '';
600
        foreach ($rows as $table) {
601
            if ($table['tbl_name'] === $tableName) {
602
                $sql = $table['sql'];
603
            }
604
        }
605 1
606
        return $sql;
607 1
    }
608
609 1
    /**
610 1
     * Copies all the data from a tmp table to another table
611 1
     *
612 1
     * @param string $tableName The table name to copy the data to
613 1
     * @param string $tmpTableName The tmp table name where the data is stored
614 1
     * @param string[] $writeColumns The list of columns in the target table
615 1
     * @param string[] $selectColumns The list of columns in the tmp table
616 1
     * @return void
617 1
     */
618
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
619
    {
620
        $sql = sprintf(
621
            'INSERT INTO %s(%s) SELECT %s FROM %s',
622
            $this->quoteTableName($tableName),
623
            implode(', ', $writeColumns),
624
            implode(', ', $selectColumns),
625 5
            $this->quoteTableName($tmpTableName)
626
        );
627 5
        $this->execute($sql);
628
    }
629
630 5
    /**
631
     * Modifies the passed instructions to copy all data from the tmp table into
632 5
     * the provided table and then drops the tmp table.
633 5
     *
634 5
     * @param AlterInstructions $instructions The instructions to modify
635
     * @param string $tableName The table name to copy the data to
636 1
     * @return AlterInstructions
637
     */
638
    protected function copyAndDropTmpTable($instructions, $tableName)
639
    {
640
        $instructions->addPostStep(function ($state) use ($tableName) {
641
            $this->copyDataToNewTable(
642
                $tableName,
643
                $state['tmpTableName'],
644
                $state['writeColumns'],
645 5
                $state['selectColumns']
646
            );
647 5
648 5
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
649
650
            return $state;
651
        });
652
653
        return $instructions;
654
    }
655
656
    /**
657
     * Returns the columns and type to use when copying a table to another in the process
658
     * of altering a table
659
     *
660
     * @param string $tableName The table to modify
661 5
     * @param string $columnName The column name that is about to change
662
     * @param string|false $newColumnName Optionally the new name for the column
663 5
     * @return AlterInstructions
664 5
     */
665 5
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
666 5
    {
667 5
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
668 5
        $selectColumns = [];
669 5
        $writeColumns = [];
670 5
        $columnType = null;
671 5
        $found = false;
672 5
673 5
        foreach ($columns as $column) {
674
            $selectName = $column['name'];
675
            $writeName = $selectName;
676
677
            if ($selectName == $columnName) {
678
                $writeName = $newColumnName;
679 4
                $found = true;
680
                $columnType = $column['type'];
681
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
682 4
            }
683
684 4
            $selectColumns[] = $selectName;
685 4
            $writeColumns[] = $writeName;
686
        }
687 4
688 4
        $selectColumns = array_filter($selectColumns, 'strlen');
689 4
        $writeColumns = array_filter($writeColumns, 'strlen');
690 4
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
691 4
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
692 4
693
        if (!$found) {
694 4
            throw new \InvalidArgumentException(sprintf(
695 4
                'The specified column doesn\'t exist: ' . $columnName
696 4
            ));
697 4
        }
698 4
699
        return compact('writeColumns', 'selectColumns', 'columnType');
700 4
    }
701
702 4
    /**
703 4
     * Returns the initial instructions to alter a table using the
704
     * rename-alter-copy strategy
705 4
     *
706 4
     * @param string $tableName The table to modify
707 4
     * @return AlterInstructions
708 4
     */
709 4
    protected function beginAlterByCopyTable($tableName)
710 4
    {
711 4
        $instructions = new AlterInstructions();
712
        $instructions->addPostStep(function ($state) use ($tableName) {
713 4
            $createSQL = $this->getDeclaringSql($tableName);
714 4
715 4
            $tmpTableName = 'tmp_' . $tableName;
716
            $this->execute(
717
                sprintf(
718
                    'ALTER TABLE %s RENAME TO %s',
719
                    $this->quoteTableName($tableName),
720 1
                    $this->quoteTableName($tmpTableName)
721
                )
722
            );
723 1
724
            return compact('createSQL', 'tmpTableName') + $state;
725
        });
726
727 1
        return $instructions;
728
    }
729 1
730
    /**
731 1
     * {@inheritdoc}
732 1
     */
733 1
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
734 1
    {
735 1
        $instructions = $this->beginAlterByCopyTable($tableName);
736 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...
737
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
738 1
739 1
            return $newState + $state;
740 1
        });
741 1
742 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
743 1
            $sql = str_replace(
744 1
                $this->quoteColumnName($columnName),
745
                $this->quoteColumnName($newColumnName),
746 1
                $state['createSQL']
747
            );
748 1
            $this->execute($sql);
749
750
            return $state;
751
        });
752
753
        return $this->copyAndDropTmpTable($instructions, $tableName);
754 1
    }
755
756 1
    /**
757 1
     * {@inheritdoc}
758 1
     */
759 1
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
760 1
    {
761 1
        $instructions = $this->beginAlterByCopyTable($tableName);
762 1
763
        $newColumnName = $newColumn->getName();
764 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...
765
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
766 1
767 1
            return $newState + $state;
768 1
        });
769 1
770 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
771
            $sql = preg_replace(
772 1
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
773
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
774 1
                $state['createSQL'],
775 1
                1
776 1
            );
777
            $this->execute($sql);
778
779
            return $state;
780
        });
781
782
        return $this->copyAndDropTmpTable($instructions, $tableName);
783
    }
784
785
    /**
786
     * {@inheritdoc}
787
     */
788
    protected function getDropColumnInstructions($tableName, $columnName)
789
    {
790
        $instructions = $this->beginAlterByCopyTable($tableName);
791
792 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...
793
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
794
795
            return $newState + $state;
796
        });
797
798
        $instructions->addPostStep(function ($state) use ($columnName) {
799
            $sql = preg_replace(
800
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
801
                "",
802
                $state['createSQL']
803
            );
804
805
            if (substr($sql, -2) === ', ') {
806
                $sql = substr($sql, 0, -2) . ')';
807
            }
808
809
            $this->execute($sql);
810 43
811
            return $state;
812
        });
813 43
814 42
        return $this->copyAndDropTmpTable($instructions, $tableName);
815
    }
816 43
817
    /**
818
     * Get an array of indexes from a particular table.
819 43
     *
820 1
     * @param string $tableName Table Name
821
     * @return array
822 43
     */
823 38
    protected function getIndexes($tableName)
824
    {
825 43
        $indexes = [];
826 42
        $schema = $this->getSchemaName($tableName, true)['schema'];
827
        $indexList = $this->getTableInfo($tableName, 'index_list');
828 43
829 2
        foreach ($indexList as $index) {
830
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
831 43
            $cols = [];
832 1
            foreach ($indexData as $indexItem) {
833
                $cols[] = $indexItem['name'];
834 43
            }
835 1
            $indexes[$index['name']] = $cols;
836
        }
837 43
838 42
        return $indexes;
839
    }
840 43
841 1
    /**
842
     * Finds the names of a table's indexes matching the supplied columns
843 43
     *
844 1
     * @param string $tableName The table to which the index belongs
845
     * @param string|string[] $columns The columns of the index
846 43
     * @return array
847 43
     */
848 1
    protected function resolveIndex($tableName, $columns)
849
    {
850 43
        $columns = array_map('strtolower', (array)$columns);
851 42
        $indexes = $this->getIndexes($tableName);
852
        $matches = [];
853 5
854
        foreach ($indexes as $name => $index) {
855 5
            $indexCols = array_map('strtolower', $index);
856 4
            if ($columns == $indexCols) {
857
                $matches[] = $name;
858
            }
859
        }
860 1
861 1
        return $matches;
862
    }
863
864 1
    /**
865
     * {@inheritdoc}
866
     */
867 1
    public function hasIndex($tableName, $columns)
868
    {
869 1
        return (bool)$this->resolveIndex($tableName, $columns);
870 1
    }
871 1
872
    /**
873
     * {@inheritdoc}
874
     */
875
    public function hasIndexByName($tableName, $indexName)
876
    {
877
        $indexName = strtolower($indexName);
878
        $indexes = $this->getIndexes($tableName);
879
880 3
        foreach (array_keys($indexes) as $index) {
881
            if ($indexName === strtolower($index)) {
882 3
                return true;
883 1
            }
884
        }
885 2
886 2
        return false;
887 2
    }
888 2
889 1
    /**
890 1
     * {@inheritdoc}
891 2
     */
892
    protected function getAddIndexInstructions(Table $table, Index $index)
893
    {
894 2
        $indexColumnArray = [];
895 2
        foreach ($index->getColumns() as $column) {
896 1
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
897 1
        }
898
        $indexColumns = implode(',', $indexColumnArray);
899
        $sql = sprintf(
900 1
            'CREATE %s ON %s (%s)',
901 2
            $this->getIndexSqlDefinition($table, $index),
902
            $this->quoteTableName($table->getName()),
903
            $indexColumns
904
        );
905
906
        return new AlterInstructions([], [$sql]);
907
    }
908
909
    /**
910 2
     * {@inheritdoc}
911
     */
912
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
913
    {
914
        $instructions = new AlterInstructions();
915
        $indexNames = $this->resolveIndex($tableName, $columns);
916 2
        $schema = $this->getSchemaName($tableName, true)['schema'];
917 1
        foreach ($indexNames as $indexName) {
918
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
919
                $instructions->addPostStep(sprintf(
920 1
                    'DROP INDEX %s%s',
921 1
                    $schema,
922 2
                    $this->quoteColumnName($indexName)
923 1
                ));
924 1
            }
925 2
        }
926 2
927
        return $instructions;
928
    }
929
930
    /**
931
     * {@inheritdoc}
932
     */
933 2
    protected function getDropIndexByNameInstructions($tableName, $indexName)
934
    {
935
        $instructions = new AlterInstructions();
936 1
        $indexName = strtolower($indexName);
937 1
        $indexes = $this->getIndexes($tableName);
938
939 1
        $found = false;
940
        foreach (array_keys($indexes) as $index) {
941
            if ($indexName === strtolower($index)) {
942
                $found = true;
943
                break;
944
            }
945
        }
946 48
947
        if ($found) {
948 48
            $schema = $this->getSchemaName($tableName, true)['schema'];
949 48
                $instructions->addPostStep(sprintf(
950
                    'DROP INDEX %s%s',
951
                    $schema,
952
                    $this->quoteColumnName($indexName)
953
                ));
954 2
        }
955
956 2
        return $instructions;
957
    }
958
959
    /**
960
     * {@inheritdoc}
961
     */
962 48
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
963
    {
964 48
        if (!is_null($constraint)) {
965 47
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
966 47
        }
967 48
968
        $columns = array_map('strtolower', (array)$columns);
969
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
970
971
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
972
            return false;
973
        }
974
        
975 42
        return true;
976
    }
977 42
978 8
    /**
979 42
     * Get the primary key from a particular table.
980 42
     *
981 42
     * @param string $tableName Table Name
982 42
     * @return string[]
983
     */
984
    protected function getPrimaryKey($tableName)
985
    {
986
        $primaryKey = [];
987
988
        $rows = $this->getTableInfo($tableName);
989
990
        foreach ($rows as $row) {
991 42
            if ($row['pk'] > 0) {
992
                $primaryKey[$row['pk'] - 1] = $row['name'];
993 42
            }
994 42
        }
995 42
996 42
        return $primaryKey;
997
    }
998
999 42
    /**
1000 42
     * {@inheritdoc}
1001 42
     */
1002 42
    public function hasForeignKey($tableName, $columns, $constraint = null)
1003 42
    {
1004 4
        if (!is_null($constraint)) {
1005 4
            throw new \InvalidArgumentException('SQLite does not support named constraints.');
1006
        }
1007 42
1008
        $columns = array_map('strtolower', (array)$columns);
1009 42
        $foreignKeys = $this->getForeignKeys($tableName);
1010 42
1011 42
        foreach ($foreignKeys as $key) {
1012
            $key = array_map('strtolower', $key);
1013 42
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1014
                continue;
1015
            }
1016
            return true;
1017 42
        }
1018
1019 42
        return false;
1020
    }
1021
1022
    /**
1023
     * Get an array of foreign keys from a particular table.
1024
     *
1025
     * @param string $tableName Table Name
1026
     * @return array
1027
     */
1028 42
    protected function getForeignKeys($tableName)
1029
    {
1030 42
        $foreignKeys = [];
1031 2
1032
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1033 42
1034
        foreach ($rows as $row) {
1035
            if (!isset($foreignKeys[$row['id']])) {
1036
                $foreignKeys[$row['id']] = [];
1037
            }
1038
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1039
        }
1040
1041
        return $foreignKeys;
1042 8
    }
1043
1044 8
    /**
1045 2
     * @param Table $table The Table
1046 2
     * @param string $column Column Name
1047 6
     * @return AlterInstructions
1048
     */
1049 8
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1050 3
    {
1051 3
        $instructions = $this->beginAlterByCopyTable($table->getName());
1052 6
1053 6
        $tableName = $table->getName();
1054 6
        $instructions->addPostStep(function ($state) use ($column) {
1055 6
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1056 6
1057
            $sql = $state['createSQL'];
1058 8
1059 8
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1060
                if (isset($matches[2])) {
1061
                    if ($matches[2] === 'INTEGER') {
1062
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1063
                    } else {
1064
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1065 47
                    }
1066
1067 47
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1068
                }
1069
            }
1070
1071
            $this->execute($sql);
1072
1073
            return $state;
1074
        });
1075
1076 5 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...
1077
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1078 5
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1079 5
            $selectColumns = $writeColumns = $names;
1080
1081
            return compact('selectColumns', 'writeColumns') + $state;
1082 5
        });
1083 5
1084 5
        return $this->copyAndDropTmpTable($instructions, $tableName);
1085 5
    }
1086 5
1087 5
    /**
1088 5
     * @param Table $table Table
1089 5
     * @param string $column Column Name
1090 5
     * @return AlterInstructions
1091 5
     */
1092 5
    protected function getDropPrimaryKeyInstructions($table, $column)
1093 1
    {
1094 1
        $instructions = $this->beginAlterByCopyTable($table->getName());
1095 5
1096 1
        $instructions->addPostStep(function ($state) use ($column) {
1097 1
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1098
1099 5
            return $newState + $state;
1100
        });
1101
1102
        $instructions->addPostStep(function ($state) {
1103
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1104
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1105
1106
            if ($sql) {
1107
                $this->execute($sql);
1108
            }
1109
1110
            return $state;
1111
        });
1112
1113
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1114
    }
1115
1116
    /**
1117
     * {@inheritdoc}
1118
     */
1119
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1120
    {
1121
        $instructions = $this->beginAlterByCopyTable($table->getName());
1122
1123
        $tableName = $table->getName();
1124
        $instructions->addPostStep(function ($state) use ($foreignKey) {
1125
            $this->execute('pragma foreign_keys = ON');
1126
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
1127
            $this->execute($sql);
1128
1129
            return $state;
1130
        });
1131
1132 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...
1133
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1134
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1135
            $selectColumns = $writeColumns = $names;
1136
1137
            return compact('selectColumns', 'writeColumns') + $state;
1138
        });
1139
1140
        return $this->copyAndDropTmpTable($instructions, $tableName);
1141
    }
1142
1143
    /**
1144
     * {@inheritdoc}
1145
     */
1146
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1147
    {
1148
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
1149
    }
1150
1151
    /**
1152
     * {@inheritdoc}
1153
     */
1154
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1155
    {
1156
        $instructions = $this->beginAlterByCopyTable($tableName);
1157
1158
        $instructions->addPostStep(function ($state) use ($columns) {
1159
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1160
1161
            $selectColumns = $newState['selectColumns'];
1162
            $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...
1163
            $diff = array_diff($columns, $selectColumns);
1164
1165
            if (!empty($diff)) {
1166
                throw new \InvalidArgumentException(sprintf(
1167
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1168
                ));
1169
            }
1170
1171
            return $newState + $state;
1172
        });
1173
1174
        $instructions->addPostStep(function ($state) use ($columns) {
1175
            $sql = '';
1176
1177
            foreach ($columns as $columnName) {
1178
                $search = sprintf(
1179
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1180
                    $this->quoteColumnName($columnName)
1181
                );
1182
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1183
            }
1184
1185
            if ($sql) {
1186
                $this->execute($sql);
1187
            }
1188
1189
            return $state;
1190
        });
1191
1192
        return $this->copyAndDropTmpTable($instructions, $tableName);
1193
    }
1194
1195
    /**
1196
     * {@inheritdoc}
1197
     */
1198
    public function getSqlType($type, $limit = null)
1199
    {
1200
        switch ($type) {
1201
            case static::PHINX_TYPE_TEXT:
1202
            case static::PHINX_TYPE_INTEGER:
1203
            case static::PHINX_TYPE_FLOAT:
1204
            case static::PHINX_TYPE_DOUBLE:
1205
            case static::PHINX_TYPE_DECIMAL:
1206
            case static::PHINX_TYPE_DATETIME:
1207
            case static::PHINX_TYPE_TIME:
1208
            case static::PHINX_TYPE_DATE:
1209
            case static::PHINX_TYPE_BLOB:
1210
            case static::PHINX_TYPE_BOOLEAN:
1211
            case static::PHINX_TYPE_ENUM:
1212
                return ['name' => $type];
1213
            case static::PHINX_TYPE_STRING:
1214
                return ['name' => 'varchar', 'limit' => 255];
1215
            case static::PHINX_TYPE_CHAR:
1216
                return ['name' => 'char', 'limit' => 255];
1217
            case static::PHINX_TYPE_SMALL_INTEGER:
1218
                return ['name' => 'smallint'];
1219
            case static::PHINX_TYPE_BIG_INTEGER:
1220
                return ['name' => 'bigint'];
1221
            case static::PHINX_TYPE_TIMESTAMP:
1222
                return ['name' => 'datetime'];
1223
            case static::PHINX_TYPE_BINARY:
1224
                return ['name' => 'blob'];
1225
            case static::PHINX_TYPE_UUID:
1226
                return ['name' => 'char', 'limit' => 36];
1227
            case static::PHINX_TYPE_JSON:
1228
            case static::PHINX_TYPE_JSONB:
1229
                return ['name' => 'text'];
1230
            // Geospatial database types
1231
            // No specific data types exist in SQLite, instead all geospatial
1232
            // functionality is handled in the client. See also: SpatiaLite.
1233
            case static::PHINX_TYPE_GEOMETRY:
1234
            case static::PHINX_TYPE_POLYGON:
1235
                return ['name' => 'text'];
1236
            case static::PHINX_TYPE_LINESTRING:
1237
                return ['name' => 'varchar', 'limit' => 255];
1238
            case static::PHINX_TYPE_POINT:
1239
                return ['name' => 'float'];
1240
            default:
1241
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1242
        }
1243
    }
1244
1245
    /**
1246
     * Returns Phinx type by SQL type
1247
     *
1248
     * @param string $sqlTypeDef SQL type
1249
     * @throws UnsupportedColumnTypeException
1250
     * @return array
1251
     */
1252
    public function getPhinxType($sqlTypeDef)
1253
    {
1254
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*$/', $sqlTypeDef, $matches)) {
1255
            throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by SQLite.');
1256
        } else {
1257
            $limit = null;
1258
            $scale = null;
1259
            $type = $matches[1];
1260
            if (count($matches) > 2) {
1261
                $limit = $matches[3] ?: null;
1262
            }
1263
            if (count($matches) > 4) {
1264
                $scale = $matches[5];
1265
            }
1266
            switch ($type) {
1267
                case 'varchar':
1268
                    $type = static::PHINX_TYPE_STRING;
1269
                    if ($limit === 255) {
1270
                        $limit = null;
1271
                    }
1272
                    break;
1273 View Code Duplication
                case 'char':
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...
1274
                    $type = static::PHINX_TYPE_CHAR;
1275
                    if ($limit === 255) {
1276
                        $limit = null;
1277
                    }
1278
                    if ($limit === 36) {
1279
                        $type = static::PHINX_TYPE_UUID;
1280
                    }
1281
                    break;
1282
                case 'smallint':
1283
                    $type = static::PHINX_TYPE_SMALL_INTEGER;
1284
                    if ($limit === 11) {
1285
                        $limit = null;
1286
                    }
1287
                    break;
1288
                case 'int':
1289
                    $type = static::PHINX_TYPE_INTEGER;
1290
                    if ($limit === 11) {
1291
                        $limit = null;
1292
                    }
1293
                    break;
1294
                case 'bigint':
1295
                    if ($limit === 11) {
1296
                        $limit = null;
1297
                    }
1298
                    $type = static::PHINX_TYPE_BIG_INTEGER;
1299
                    break;
1300
                case 'blob':
1301
                    $type = static::PHINX_TYPE_BINARY;
1302
                    break;
1303
            }
1304 View Code Duplication
            if ($type === 'tinyint') {
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...
1305
                if ($matches[3] === 1) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $matches[3] (string) and 1 (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1306
                    $type = static::PHINX_TYPE_BOOLEAN;
1307
                    $limit = null;
1308
                }
1309
            }
1310
1311
            try {
1312
                // Call this to check if parsed type is supported.
1313
                $this->getSqlType($type);
1314
            } catch (UnsupportedColumnTypeException $e) {
1315
                $type = Literal::from($type);
1316
            }
1317
1318
            return [
1319
                'name' => $type,
1320
                'limit' => $limit,
1321
                'scale' => $scale
1322
            ];
1323
        }
1324
    }
1325
1326
    /**
1327
     * {@inheritdoc}
1328
     */
1329
    public function createDatabase($name, $options = [])
1330
    {
1331
        touch($name . $this->suffix);
1332
    }
1333
1334
    /**
1335
     * {@inheritdoc}
1336
     */
1337
    public function hasDatabase($name)
1338
    {
1339
        return is_file($name . $this->suffix);
1340
    }
1341
1342
    /**
1343
     * {@inheritdoc}
1344
     */
1345
    public function dropDatabase($name)
1346
    {
1347
        if ($this->getOption('memory')) {
1348
            $this->disconnect();
1349
            $this->connect();
1350
        }
1351
        if (file_exists($name . $this->suffix)) {
1352
            unlink($name . $this->suffix);
1353
        }
1354
    }
1355
1356
    /**
1357
     * Gets the SQLite Column Definition for a Column object.
1358
     *
1359
     * @param \Phinx\Db\Table\Column $column Column
1360
     * @return string
1361
     */
1362
    protected function getColumnSqlDefinition(Column $column)
1363
    {
1364
        $isLiteralType = $column->getType() instanceof Literal;
1365
        if ($isLiteralType) {
1366
            $def = (string)$column->getType();
1367
        } else {
1368
            $sqlType = $this->getSqlType($column->getType());
1369
            $def = strtoupper($sqlType['name']);
1370
1371
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
1372
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1373
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1374
            }
1375
        }
1376
        if ($column->getPrecision() && $column->getScale()) {
1377
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1378
        }
1379 View Code Duplication
        if (($values = $column->getValues()) && is_array($values)) {
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...
1380
            $def .= " CHECK({$this->quoteColumnName($column->getName())} IN ('" . implode("', '", $values) . "'))";
1381
        }
1382
1383
        $default = $column->getDefault();
1384
1385
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
1386
        $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...
1387
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1388
1389
        if ($column->getUpdate()) {
1390
            $def .= ' ON UPDATE ' . $column->getUpdate();
1391
        }
1392
1393
        $def .= $this->getCommentDefinition($column);
1394
1395
        return $def;
1396
    }
1397
1398
    /**
1399
     * Gets the comment Definition for a Column object.
1400
     *
1401
     * @param \Phinx\Db\Table\Column $column Column
1402
     * @return string
1403
     */
1404
    protected function getCommentDefinition(Column $column)
1405
    {
1406
        if ($column->getComment()) {
1407
            return ' /* ' . $column->getComment() . ' */ ';
1408
        }
1409
1410
        return '';
1411
    }
1412
1413
    /**
1414
     * Gets the SQLite Index Definition for an Index object.
1415
     *
1416
     * @param \Phinx\Db\Table\Table $table Table
1417
     * @param \Phinx\Db\Table\Index $index Index
1418
     * @return string
1419
     */
1420
    protected function getIndexSqlDefinition(Table $table, Index $index)
1421
    {
1422
        if ($index->getType() === Index::UNIQUE) {
1423
            $def = 'UNIQUE INDEX';
1424
        } else {
1425
            $def = 'INDEX';
1426
        }
1427
        if (is_string($index->getName())) {
1428
            $indexName = $index->getName();
1429
        } else {
1430
            $indexName = $table->getName() . '_';
1431
            foreach ($index->getColumns() as $column) {
1432
                $indexName .= $column . '_';
1433
            }
1434
            $indexName .= 'index';
1435
        }
1436
        $def .= ' `' . $indexName . '`';
1437
1438
        return $def;
1439
    }
1440
1441
    /**
1442
     * {@inheritdoc}
1443
     */
1444
    public function getColumnTypes()
1445
    {
1446
        return array_merge(parent::getColumnTypes(), ['enum', 'json', 'jsonb']);
1447
    }
1448
1449
    /**
1450
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1451
     *
1452
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1453
     * @return string
1454
     */
1455 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...
1456
    {
1457
        $def = '';
1458
        if ($foreignKey->getConstraint()) {
1459
            $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...
1460
        } else {
1461
            $columnNames = [];
1462
            foreach ($foreignKey->getColumns() as $column) {
1463
                $columnNames[] = $this->quoteColumnName($column);
1464
            }
1465
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1466
            $refColumnNames = [];
1467
            foreach ($foreignKey->getReferencedColumns() as $column) {
1468
                $refColumnNames[] = $this->quoteColumnName($column);
1469
            }
1470
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1471
            if ($foreignKey->getOnDelete()) {
1472
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1473
            }
1474
            if ($foreignKey->getOnUpdate()) {
1475
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1476
            }
1477
        }
1478
1479
        return $def;
1480
    }
1481
1482
    /**
1483
     * {@inheritDoc}
1484
     *
1485
     */
1486
    public function getDecoratedConnection()
1487
    {
1488
        $options = $this->getOptions();
1489
        $options['quoteIdentifiers'] = true;
1490
        $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...
1491
1492
        if (!empty($options['name'])) {
1493
            $options['database'] = $options['name'];
1494
1495
            if (file_exists($options['name'] . $this->suffix)) {
1496
                $options['database'] = $options['name'] . $this->suffix;
1497
            }
1498
        }
1499
1500
        $driver = new SqliteDriver($options);
1501
        if (method_exists($driver, 'setConnection')) {
1502
            $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...
1503
        } else {
1504
            $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...
1505
        }
1506
1507
        return new Connection(['driver' => $driver] + $options);
1508
    }
1509
}
1510