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

SQLiteAdapter::getPrimaryKey()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 11
cts 11
cp 1
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Cake\Database\Connection;
32
use Cake\Database\Driver\Sqlite as SqliteDriver;
33
use Phinx\Db\Table\Column;
34
use Phinx\Db\Table\ForeignKey;
35
use Phinx\Db\Table\Index;
36
use Phinx\Db\Table\Table;
37
use Phinx\Db\Util\AlterInstructions;
38
use Phinx\Util\Literal;
39
use Phinx\Util\Expression;
40
41
/**
42
 * Phinx SQLite Adapter.
43
 *
44
 * @author Rob Morgan <[email protected]>
45
 * @author Richard McIntyre <[email protected]>
46
 */
47
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
48
{
49
    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
     * Searches through all available schemata to find a table and returns an array
220 1
     * containing the bare schema name and whether the table exists at all.
221 1
     * If no schema was specified and the table does not exist the "main" schema is returned
222 1
     *
223 1
     * @param string $tableName The name of the table to find
224
     * @return array
225 42
     */
226
    protected function resolveTable($tableName)
227 42
    {
228
        $info = $this->getSchemaName($tableName);
229 42
        if ($info['schema'] === '') {
230 6
            // if no schema is specified we search all schemata
231 42
            $rows = $this->fetchAll('PRAGMA database_list;');
232 42
            // the temp schema is always first to be searched
233
            $schemata = ['temp'];
234
            foreach ($rows as $row) {
235
                if (strtolower($row['name']) !== 'temp') {
236
                    $schemata[] = $row['name'];
237 1
                }
238
            }
239 1
            $default = 'main';
240 1
        } else {
241
            // otherwise we search just the specified schema
242
            $schemata = (array)$info['schema'];
243
            $default = $info['schema'];
244
        }
245 1
246
        $table = strtolower($info['table']);
247 1
        foreach ($schemata as $schema) {
248 1
            if ($schema === 'temp') {
249
                $master = 'sqlite_temp_master';
250
            } else {
251
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
252
            }
253 1
            try {
254
                $rows = $this->fetchAll(sprintf('SELECT name FROM %s WHERE type=\'table\' AND lower(name) = %s', $master, $this->quoteString($table)));
255 1
            } catch (\PDOException $e) {
256 1
                // an exception can occur if the schema part of the table refers to a database which is not attached
257 1
                return ['schema' => $default, 'exists' => false];
258 1
            }
259
260 1
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
261 1
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
262
            foreach ($rows as $row) {
263
                if (strtolower($row['name']) === $table) {
264
                    return ['schema' => $schema, 'exists' => true];
265
                }
266 1
            }
267
        }
268 1
269 1
        return ['schema' => $default, 'exists' => false];
270
    }
271 1
272 1
    /**
273 1
     * Retrieves information about a given table from one of the SQLite pragmas
274 1
     *
275 1
     * @param string $tableName The table to query
276 1
     * @param string $pragma The pragma to query
277
     * @return array
278 1
     */
279 1
    protected function getTableInfo($tableName, $pragma = 'table_info')
280 1
    {
281
        $info = $this->getSchemaName($tableName, true);
282 1
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
283 1
    }
284 1
285
    /**
286 1
     * {@inheritdoc}
287 1
     */
288
    public function hasTable($tableName)
289 1
    {
290
        return $this->resolveTable($tableName)['exists'];
291
    }
292
293
    /**
294
     * {@inheritdoc}
295 8
     */
296
    public function createTable(Table $table, array $columns = [], array $indexes = [])
297 8
    {
298 8
        // Add the default primary key
299 8
        $options = $table->getOptions();
300 7 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...
301
            $options['id'] = 'id';
302 8
        }
303
304 8
        if (isset($options['id']) && is_string($options['id'])) {
305
            // Handle id => "field_name" to support AUTO_INCREMENT
306
            $column = new Column();
307
            $column->setName($options['id'])
308
                   ->setType('integer')
309
                   ->setIdentity(true);
310 4
311
            array_unshift($columns, $column);
312 4
        }
313 4
314 4
        $sql = 'CREATE TABLE ';
315 4
        $sql .= $this->quoteTableName($table->getName()) . ' (';
316 4
        foreach ($columns as $column) {
317 4
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
318
319 4
            if (isset($options['primary_key']) && $column->getIdentity()) {
320 4
                //remove column from the primary key array as it is already defined as an autoincrement
321
                //primary id
322
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
323
                if ($identityColumnIndex !== false) {
324
                    unset($options['primary_key'][$identityColumnIndex]);
325 2
326
                    if (empty($options['primary_key'])) {
327 2
                        //The last primary key has been removed
328
                        unset($options['primary_key']);
329 2
                    }
330
                }
331 2
            }
332 2
        }
333 2
334 2
        // set the primary key(s)
335 2 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...
336 2
            $sql = rtrim($sql);
337
            $sql .= ' PRIMARY KEY (';
338 2
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
339 2
                $sql .= $this->quoteColumnName($options['primary_key']);
340 2
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
341 2
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
342 2
            }
343 2
            $sql .= ')';
344 2
        } else {
345 2
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
346 2
        }
347
348 2
        $sql = rtrim($sql) . ');';
349 1
        // execute the sql
350
        $this->execute($sql);
351 1
352
        foreach ($indexes as $index) {
353
            $this->addIndex($table, $index);
354 1
        }
355
    }
356 1
357 1
    /**
358 1
     * {@inheritdoc}
359
     */
360 1
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
361 1
    {
362
        $instructions = new AlterInstructions();
363
364 1
        // Drop the existing primary key
365 1
        $primaryKey = $this->getPrimaryKey($table->getName());
366 1
        if (!empty($primaryKey)) {
367 1
            $instructions->merge(
368 1
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
369
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
370 1
            );
371
        }
372 1
373
        // Add the primary key(s)
374 1
        if (!empty($newColumns)) {
375 1
            if (!is_string($newColumns)) {
376
                throw new \InvalidArgumentException(sprintf(
377
                    "Invalid value for primary key: %s",
378
                    json_encode($newColumns)
379
                ));
380 6
            }
381
382
            $instructions->merge(
383
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
384 6
            );
385
        }
386 6
387
        return $instructions;
388 6
    }
389 6
390 6
    /**
391 6
     * {@inheritdoc}
392 6
     */
393 6
    protected function getChangeCommentInstructions(Table $table, $newComment)
394
    {
395 6
        throw new \BadMethodCallException('SQLite does not have table comments');
396 6
    }
397 6
398 6
    /**
399 6
     * {@inheritdoc}
400 6
     */
401 6 View Code Duplication
    protected function getRenameTableInstructions($tableName, $newTableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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