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