Completed
Pull Request — master (#1371)
by
unknown
19:24 queued 08:02
created

SQLiteAdapter::calculateNewTableColumns()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 25
cts 25
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 24
nc 8
nop 3
crap 5
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
40
/**
41
 * Phinx SQLite Adapter.
42
 *
43
 * @author Rob Morgan <[email protected]>
44
 * @author Richard McIntyre <[email protected]>
45
 */
46
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
47
{
48
    protected $definitionsWithLimits = [
49
        'CHARACTER',
50
        'VARCHAR',
51
        'VARYING CHARACTER',
52
        'NCHAR',
53
        'NATIVE CHARACTER',
54
        'NVARCHAR'
55
    ];
56 42
57
    protected $suffix = '.sqlite3';
58 42
59 42
    /**
60
     * {@inheritdoc}
61
     */
62
    public function connect()
63
    {
64
        if ($this->connection === null) {
65 42
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
66 42
                // @codeCoverageIgnoreStart
67
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
68
                // @codeCoverageIgnoreEnd
69 42
            }
70
71
            $db = null;
72 42
            $options = $this->getOptions();
73 42
74 42
            // if port is specified use it, otherwise use the MySQL default
75 42
            if (isset($options['memory'])) {
76
                $dsn = 'sqlite::memory:';
77
            } else {
78
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
79 42
            }
80 42
81
            try {
82
                $db = new \PDO($dsn);
83
            } catch (\PDOException $exception) {
84
                throw new \InvalidArgumentException(sprintf(
85
                    'There was a problem connecting to the database: %s',
86
                    $exception->getMessage()
87 42
                ));
88 42
            }
89 42
90
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
91
            $this->setConnection($db);
92
        }
93
    }
94 48
95
    /**
96 48
     * {@inheritdoc}
97 48
     */
98
    public function setOptions(array $options)
99
    {
100
        parent::setOptions($options);
101
102
        if (isset($options['suffix'])) {
103
            $this->suffix = $options['suffix'];
104
        }
105
        //don't "fix" the file extension if it is blank, some people
106
        //might want a SQLITE db file with absolutely no extension.
107
        if (strlen($this->suffix) && substr($this->suffix, 0, 1) !== '.') {
108
            $this->suffix = '.' . $this->suffix;
109
        }
110 1
111
        return $this;
112 1
    }
113 1
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function disconnect()
118
    {
119
        $this->connection = null;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function hasTransactions()
126
    {
127
        return true;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function beginTransaction()
134 43
    {
135
        $this->getConnection()->beginTransaction();
136 43
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function commitTransaction()
142 44
    {
143
        $this->getConnection()->commit();
144 44
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function rollbackTransaction()
150 42
    {
151
        $this->getConnection()->rollBack();
152 42
    }
153 42
154 42
    /**
155 12
     * {@inheritdoc}
156 42
     */
157
    public function quoteTableName($tableName)
158 42
    {
159
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
160
    }
161
162
    /**
163
     * {@inheritdoc}
164 42
     */
165
    public function quoteColumnName($columnName)
166
    {
167 42
        return '`' . str_replace('`', '``', $columnName) . '`';
168 42
    }
169 42
170 35
    /**
171 35
     * {@inheritdoc}
172 35
     */
173 35
    public function hasTable($tableName)
174
    {
175 35
        $tables = [];
176 42
        $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName));
177
        foreach ($rows as $row) {
178 1
            $tables[] = strtolower($row[0]);
179 1
        }
180 1
181 1
        return in_array(strtolower($tableName), $tables);
182
    }
183 1
184 1
    /**
185
     * {@inheritdoc}
186
     */
187 42
    public function createTable(Table $table, array $columns = [], array $indexes = [])
188 42
    {
189 42
        // Add the default primary key
190 42
        $options = $table->getOptions();
191 42
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
192
            $column = new Column();
193
            $column->setName('id')
194 42
                   ->setType('integer')
195 42
                   ->setIdentity(true);
196 42
197 42
            array_unshift($columns, $column);
198 42
        } elseif (isset($options['id']) && is_string($options['id'])) {
199 42
            // Handle id => "field_name" to support AUTO_INCREMENT
200
            $column = new Column();
201
            $column->setName($options['id'])
202 1
                   ->setType('integer')
203 1
                   ->setIdentity(true);
204 1
205
            array_unshift($columns, $column);
206 1
        }
207 1
208 1
        $sql = 'CREATE TABLE ';
209 1
        $sql .= $this->quoteTableName($table->getName()) . ' (';
210 1
        foreach ($columns as $column) {
211 1
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
212 42
213 42
            if (isset($options['primary_key']) && $column->getIdentity()) {
214 37
                //remove column from the primary key array as it is already defined as an autoincrement
215
                //primary id
216
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
217
                if ($identityColumnIndex !== false) {
218 42
                    unset($options['primary_key'][$identityColumnIndex]);
219 42
220 1
                    if (empty($options['primary_key'])) {
221 1
                        //The last primary key has been removed
222 1
                        unset($options['primary_key']);
223 1
                    }
224
                }
225 42
            }
226
        }
227 42
228
        // set the primary key(s)
229 42 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...
230 6
            $sql = rtrim($sql);
231 42
            $sql .= ' PRIMARY KEY (';
232 42
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
233
                $sql .= $this->quoteColumnName($options['primary_key']);
234
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
235
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
236
            }
237 1
            $sql .= ')';
238
        } else {
239 1
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
240 1
        }
241
242
        $sql = rtrim($sql) . ');';
243
        // execute the sql
244
        $this->execute($sql);
245 1
246
        foreach ($indexes as $index) {
247 1
            $this->addIndex($table, $index);
248 1
        }
249
    }
250
251
    /**
252
     * {@inheritdoc}
253 1
     */
254 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...
255 1
    {
256 1
        $sql = sprintf(
257 1
            'ALTER TABLE %s RENAME TO %s',
258 1
            $this->quoteTableName($tableName),
259
            $this->quoteTableName($newTableName)
260 1
        );
261 1
262
        return new AlterInstructions([], [$sql]);
263
    }
264
265
    /**
266 1
     * {@inheritdoc}
267
     */
268 1
    protected function getDropTableInstructions($tableName)
269 1
    {
270
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
271 1
272 1
        return new AlterInstructions([], [$sql]);
273 1
    }
274 1
275 1
    /**
276 1
     * {@inheritdoc}
277
     */
278 1
    public function truncateTable($tableName)
279 1
    {
280 1
        $sql = sprintf(
281
            'DELETE FROM %s',
282 1
            $this->quoteTableName($tableName)
283 1
        );
284 1
285
        $this->execute($sql);
286 1
    }
287 1
288
    /**
289 1
     * {@inheritdoc}
290
     */
291
    public function getColumns($tableName)
292
    {
293
        $columns = [];
294
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
295 8
296
        foreach ($rows as $columnInfo) {
297 8
            $column = new Column();
298 8
            $type = strtolower($columnInfo['type']);
299 8
            $column->setName($columnInfo['name'])
300 7
                   ->setNull($columnInfo['notnull'] !== '1')
301
                   ->setDefault($columnInfo['dflt_value']);
302 8
303
            $phinxType = $this->getPhinxType($type);
304 8
            $column->setType($phinxType['name'])
305
                   ->setLimit($phinxType['limit']);
306
307
            if ($columnInfo['pk'] == 1) {
308
                $column->setIdentity(true);
309
            }
310 4
311
            $columns[] = $column;
312 4
        }
313 4
314 4
        return $columns;
315 4
    }
316 4
317 4
    /**
318
     * {@inheritdoc}
319 4
     */
320 4 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...
321
    {
322
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
323
        foreach ($rows as $column) {
324
            if (strcasecmp($column['name'], $columnName) === 0) {
325 2
                return true;
326
            }
327 2
        }
328
329 2
        return false;
330
    }
331 2
332 2
    /**
333 2
     * {@inheritdoc}
334 2
     */
335 2 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...
336 2
    {
337
        $alter = sprintf(
338 2
            'ADD COLUMN %s %s',
339 2
            $this->quoteColumnName($column->getName()),
340 2
            $this->getColumnSqlDefinition($column)
341 2
        );
342 2
343 2
        return new AlterInstructions([$alter]);
344 2
    }
345 2
346 2
    /**
347
     * Returns the original CREATE statement for the give table
348 2
     *
349 1
     * @param string $tableName The table name to get the create statement for
350
     * @return string
351 1
     */
352
    protected function getDeclaringSql($tableName)
353
    {
354 1
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
355
356 1
        $sql = '';
357 1
        foreach ($rows as $table) {
358 1
            if ($table['tbl_name'] === $tableName) {
359
                $sql = $table['sql'];
360 1
            }
361 1
        }
362
363
        return $sql;
364 1
    }
365 1
366 1
    /**
367 1
     * Copies all the data from a tmp table to another table
368 1
     *
369
     * @param string $tableName The table name to copy the data to
370 1
     * @param string $tmpTableName The tmp table name where the data is stored
371
     * @param string[] $writeColumns The list of columns in the target table
372 1
     * @param string[] $selectColumns The list of columns in the tmp table
373
     * @return void
374 1
     */
375 1
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
376
    {
377
        $sql = sprintf(
378
            'INSERT INTO %s(%s) SELECT %s FROM %s',
379
            $this->quoteTableName($tableName),
380 6
            implode(', ', $writeColumns),
381
            implode(', ', $selectColumns),
382
            $this->quoteTableName($tmpTableName)
383
        );
384 6
        $this->execute($sql);
385
    }
386 6
387
    /**
388 6
     * Modifies the passed instructions to copy all data from the tmp table into
389 6
     * the provided table and then drops the tmp table.
390 6
     *
391 6
     * @param AlterInstructions $instructions The instructions to modify
392 6
     * @param string $tableName The table name to copy the data to
393 6
     * @return AlterInstructions
394
     */
395 6
    protected function copyAndDropTmpTable($instructions, $tableName)
396 6
    {
397 6
        $instructions->addPostStep(function ($state) use ($tableName) {
398 6
            $this->copyDataToNewTable(
399 6
                $tableName,
400 6
                $state['tmpTableName'],
401 6
                $state['writeColumns'],
402 6
                $state['selectColumns']
403 6
            );
404
405 6
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
406
407
            return $state;
408
        });
409
410
        return $instructions;
411 6
    }
412
413 6
    /**
414 6
     * Returns the columns and type to use when copying a table to another in the process
415 6
     * of altering a table
416 6
     *
417
     * @param string $tableName The table to modify
418 6
     * @param string $columnName The column name that is about to change
419
     * @param string|false $newColumnName Optionally the new name for the column
420 6
     * @return AlterInstructions
421
     */
422 6
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
423 6
    {
424 6
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
425 6
        $selectColumns = [];
426 6
        $writeColumns = [];
427
        $columnType = null;
428 6
        $found = false;
429
430 6
        foreach ($columns as $column) {
431 6
            $selectName = $column['name'];
432 6
            $writeName = $selectName;
433
434
            if ($selectName == $columnName) {
435
                $writeName = $newColumnName;
436
                $found = true;
437 2
                $columnType = $column['type'];
438
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
439
            }
440 2
441
            $selectColumns[] = $selectName;
442 2
            $writeColumns[] = $writeName;
443
        }
444 2
445 2
        $selectColumns = array_filter($selectColumns, 'strlen');
446 2
        $writeColumns = array_filter($writeColumns, 'strlen');
447 2
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
448 2
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
449 2
450
        if (!$found) {
451 2
            throw new \InvalidArgumentException(sprintf(
452 2
                'The specified column doesn\'t exist: ' . $columnName
453 2
            ));
454 2
        }
455 2
456 2
        return compact('writeColumns', 'selectColumns', 'columnType');
457 2
    }
458 2
459 2
    /**
460
     * Returns the initial instructions to alter a table using the
461 2
     * rename-alter-copy strategy
462
     *
463 2
     * @param string $tableName The table to modify
464
     * @return AlterInstructions
465
     */
466
    protected function beginAlterByCopyTable($tableName)
467
    {
468
        $instructions = new AlterInstructions();
469 2
        $instructions->addPostStep(function ($state) use ($tableName) {
470
            $createSQL = $this->getDeclaringSql($tableName);
471 2
472 2
            $tmpTableName = 'tmp_' . $tableName;
473 2
            $this->execute(
474
                sprintf(
475 2
                    'ALTER TABLE %s RENAME TO %s',
476
                    $this->quoteTableName($tableName),
477 2
                    $this->quoteTableName($tmpTableName)
478 2
                )
479 2
            );
480
481 2
            return compact('createSQL', 'tmpTableName') + $state;
482
        });
483 2
484 2
        return $instructions;
485 2
    }
486 2
487 2
    /**
488
     * {@inheritdoc}
489 2
     */
490
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
491 2
    {
492 2
        $instructions = $this->beginAlterByCopyTable($tableName);
493 2 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...
494
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
495
496
            return $newState + $state;
497
        });
498
499
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
500
            $sql = str_replace(
501 9
                $this->quoteColumnName($columnName),
502
                $this->quoteColumnName($newColumnName),
503 9
                $state['createSQL']
504 9
            );
505
            $this->execute($sql);
506 9
507 9
            return $state;
508 9
        });
509 9
510 9
        return $this->copyAndDropTmpTable($instructions, $tableName);
511 9
    }
512 9
513 9
    /**
514 9
     * {@inheritdoc}
515 9
     */
516
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
517
    {
518
        $instructions = $this->beginAlterByCopyTable($tableName);
519
520
        $newColumnName = $newColumn->getName();
521 9 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...
522
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
523 9
524 4
            return $newState + $state;
525 4
        });
526
527 9
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
528 9
            $sql = preg_replace(
529
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
530 9
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
531 9
                $state['createSQL'],
532 9
                1
533 9
            );
534
            $this->execute($sql);
535 8
536
            return $state;
537 8
        });
538
539
        return $this->copyAndDropTmpTable($instructions, $tableName);
540
    }
541
542
    /**
543 1
     * {@inheritdoc}
544
     */
545 1
    protected function getDropColumnInstructions($tableName, $columnName)
546
    {
547 1
        $instructions = $this->beginAlterByCopyTable($tableName);
548 1
549 1 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
550
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
551
552
            return $newState + $state;
553
        });
554
555
        $instructions->addPostStep(function ($state) use ($columnName) {
556
            $sql = preg_replace(
557
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
558
                "",
559 8
                $state['createSQL']
560
            );
561 8
562 8
            if (substr($sql, -2) === ', ') {
563 8
                $sql = substr($sql, 0, -2) . ')';
564 8
            }
565 8
566 8
            $this->execute($sql);
567 8
568 8
            return $state;
569 8
        });
570 8
571
        return $this->copyAndDropTmpTable($instructions, $tableName);
572 8
    }
573 8
574 8
    /**
575
     * Get an array of indexes from a particular table.
576
     *
577
     * @param string $tableName Table Name
578
     * @return array
579 1
     */
580
    protected function getIndexes($tableName)
581 1
    {
582 1
        $indexes = [];
583 1
        $rows = $this->fetchAll(sprintf('pragma index_list(%s)', $tableName));
584
585 1
        foreach ($rows as $row) {
586 1
            $indexData = $this->fetchAll(sprintf('pragma index_info(%s)', $row['name']));
587
            if (!isset($indexes[$tableName])) {
588 1
                $indexes[$tableName] = ['index' => $row['name'], 'columns' => []];
589 1
            }
590 1
            foreach ($indexData as $indexItem) {
591 1
                $indexes[$tableName]['columns'][] = strtolower($indexItem['name']);
592 1
            }
593 1
        }
594 1
595 1
        return $indexes;
596 1
    }
597 1
598
    /**
599
     * {@inheritdoc}
600
     */
601 View Code Duplication
    public function hasIndex($tableName, $columns)
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...
602
    {
603
        if (is_string($columns)) {
604
            $columns = [$columns]; // str to array
605 1
        }
606
607 1
        $columns = array_map('strtolower', $columns);
608
        $indexes = $this->getIndexes($tableName);
609 1
610 1
        foreach ($indexes as $index) {
611 1
            $a = array_diff($columns, $index['columns']);
612 1
            if (empty($a)) {
613 1
                return true;
614 1
            }
615 1
        }
616 1
617 1
        return false;
618
    }
619
620
    /**
621
     * {@inheritdoc}
622
     */
623 View Code Duplication
    public function hasIndexByName($tableName, $indexName)
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...
624
    {
625 5
        $indexes = $this->getIndexes($tableName);
626
627 5
        foreach ($indexes as $index) {
628
            if ($indexName === $index['index']) {
629
                return true;
630 5
            }
631
        }
632 5
633 5
        return false;
634 5
    }
635
636 1
    /**
637
     * {@inheritdoc}
638
     */
639
    protected function getAddIndexInstructions(Table $table, Index $index)
640
    {
641
        $indexColumnArray = [];
642
        foreach ($index->getColumns() as $column) {
643
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
644
        }
645 5
        $indexColumns = implode(',', $indexColumnArray);
646
        $sql = sprintf(
647 5
            'CREATE %s ON %s (%s)',
648 5
            $this->getIndexSqlDefinition($table, $index),
649
            $this->quoteTableName($table->getName()),
650
            $indexColumns
651
        );
652
653
        return new AlterInstructions([], [$sql]);
654
    }
655
656
    /**
657
     * {@inheritdoc}
658
     */
659
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
660
    {
661 5
        if (is_string($columns)) {
662
            $columns = [$columns]; // str to array
663 5
        }
664 5
665 5
        $indexes = $this->getIndexes($tableName);
666 5
        $columns = array_map('strtolower', $columns);
667 5
        $instructions = new AlterInstructions();
668 5
669 5
        foreach ($indexes as $index) {
670 5
            $a = array_diff($columns, $index['columns']);
671 5
            if (empty($a)) {
672 5
                $instructions->addPostStep(sprintf(
673 5
                    'DROP INDEX %s',
674
                    $this->quoteColumnName($index['index'])
675
                ));
676
            }
677
        }
678
679 4
        return $instructions;
680
    }
681
682 4
    /**
683
     * {@inheritdoc}
684 4
     */
685 4
    protected function getDropIndexByNameInstructions($tableName, $indexName)
686
    {
687 4
        $indexes = $this->getIndexes($tableName);
688 4
        $instructions = new AlterInstructions();
689 4
690 4
        foreach ($indexes as $index) {
691 4
            if ($indexName === $index['index']) {
692 4
                $instructions->addPostStep(sprintf(
693
                    'DROP INDEX %s',
694 4
                    $this->quoteColumnName($indexName)
695 4
                ));
696 4
            }
697 4
        }
698 4
699
        return $instructions;
700 4
    }
701
702 4
    /**
703 4
     * {@inheritdoc}
704
     */
705 4
    public function hasForeignKey($tableName, $columns, $constraint = null)
706 4
    {
707 4
        if (is_string($columns)) {
708 4
            $columns = [$columns]; // str to array
709 4
        }
710 4
        $foreignKeys = $this->getForeignKeys($tableName);
711 4
712
        return !array_diff($columns, $foreignKeys);
713 4
    }
714 4
715 4
    /**
716
     * Get an array of foreign keys from a particular table.
717
     *
718
     * @param string $tableName Table Name
719
     * @return array
720 1
     */
721
    protected function getForeignKeys($tableName)
722
    {
723 1
        $foreignKeys = [];
724
        $rows = $this->fetchAll(
725
            "SELECT sql, tbl_name
726
              FROM (
727 1
                    SELECT sql sql, type type, tbl_name tbl_name, name name
728
                      FROM sqlite_master
729 1
                     UNION ALL
730
                    SELECT sql, type, tbl_name, name
731 1
                      FROM sqlite_temp_master
732 1
                   )
733 1
             WHERE type != 'meta'
734 1
               AND sql NOTNULL
735 1
               AND name NOT LIKE 'sqlite_%'
736 1
             ORDER BY substr(type, 2, 1), name"
737
        );
738 1
739 1
        foreach ($rows as $row) {
740 1
            if ($row['tbl_name'] === $tableName) {
741 1
                if (strpos($row['sql'], 'REFERENCES') !== false) {
742 1
                    preg_match_all("/\(`([^`]*)`\) REFERENCES/", $row['sql'], $matches);
743 1
                    foreach ($matches[1] as $match) {
744 1
                        $foreignKeys[] = $match;
745
                    }
746 1
                }
747
            }
748 1
        }
749
750
        return $foreignKeys;
751
    }
752
753
    /**
754 1
     * {@inheritdoc}
755
     */
756 1
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
757 1
    {
758 1
        $instructions = $this->beginAlterByCopyTable($table->getName());
759 1
760 1
        $tableName = $table->getName();
761 1
        $instructions->addPostStep(function ($state) use ($foreignKey) {
762 1
            $this->execute('pragma foreign_keys = ON');
763
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
764 1
            $this->execute($sql);
765
766 1
            return $state;
767 1
        });
768 1
769 1
        $instructions->addPostStep(function ($state) {
770 1
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
771
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
772 1
            $selectColumns = $writeColumns = $names;
773
774 1
            return compact('selectColumns', 'writeColumns') + $state;
775 1
        });
776 1
777
        return $this->copyAndDropTmpTable($instructions, $tableName);
778
    }
779
780
    /**
781
     * {@inheritdoc}
782
     */
783
    protected function getDropForeignKeyInstructions($tableName, $constraint)
784
    {
785
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
786
    }
787
788
    /**
789
     * {@inheritdoc}
790
     */
791
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
792
    {
793
        $instructions = $this->beginAlterByCopyTable($tableName);
794
795
        $instructions->addPostStep(function ($state) use ($columns) {
796
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
797
798
            $selectColumns = $newState['selectColumns'];
799
            $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...
800
            $diff = array_diff($columns, $selectColumns);
801
802
            if (!empty($diff)) {
803
                throw new \InvalidArgumentException(sprintf(
804
                    'The specified columns doen\'t exist: ' . implode(', ', $diff)
805
                ));
806
            }
807
808
            return $newState + $state;
809
        });
810 43
811
        $instructions->addPostStep(function ($state) use ($columns) {
812
            $sql = '';
813 43
814 42
            foreach ($columns as $columnName) {
815
                $search = sprintf(
816 43
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
817
                    $this->quoteColumnName($columnName)
818
                );
819 43
                $sql = preg_replace($search, '', $state['createSQL'], 1);
820 1
            }
821
822 43
            if ($sql) {
823 38
                $this->execute($sql);
824
            }
825 43
826 42
            return $state;
827
        });
828 43
829 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
830
    }
831 43
832 1
    /**
833
     * {@inheritdoc}
834 43
     */
835 1
    public function getSqlType($type, $limit = null)
836
    {
837 43
        switch ($type) {
838 42
            case static::PHINX_TYPE_TEXT:
839
            case static::PHINX_TYPE_INTEGER:
840 43
            case static::PHINX_TYPE_FLOAT:
841 1
            case static::PHINX_TYPE_DECIMAL:
842
            case static::PHINX_TYPE_DATETIME:
843 43
            case static::PHINX_TYPE_TIME:
844 1
            case static::PHINX_TYPE_DATE:
845
            case static::PHINX_TYPE_BLOB:
846 43
            case static::PHINX_TYPE_BOOLEAN:
847 43
            case static::PHINX_TYPE_ENUM:
848 1
                return ['name' => $type];
849
            case static::PHINX_TYPE_STRING:
850 43
                return ['name' => 'varchar', 'limit' => 255];
851 42
            case static::PHINX_TYPE_CHAR:
852
                return ['name' => 'char', 'limit' => 255];
853 5
            case static::PHINX_TYPE_BIG_INTEGER:
854
                return ['name' => 'bigint'];
855 5
            case static::PHINX_TYPE_TIMESTAMP:
856 4
                return ['name' => 'datetime'];
857
            case static::PHINX_TYPE_BINARY:
858
                return ['name' => 'blob'];
859
            case static::PHINX_TYPE_UUID:
860 1
                return ['name' => 'char', 'limit' => 36];
861 1
            case static::PHINX_TYPE_JSON:
862
            case static::PHINX_TYPE_JSONB:
863
                return ['name' => 'text'];
864 1
            // Geospatial database types
865
            // No specific data types exist in SQLite, instead all geospatial
866
            // functionality is handled in the client. See also: SpatiaLite.
867 1
            case static::PHINX_TYPE_GEOMETRY:
868
            case static::PHINX_TYPE_POLYGON:
869 1
                return ['name' => 'text'];
870 1
            case static::PHINX_TYPE_LINESTRING:
871 1
                return ['name' => 'varchar', 'limit' => 255];
872
            case static::PHINX_TYPE_POINT:
873
                return ['name' => 'float'];
874
            default:
875
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
876
        }
877
    }
878
879
    /**
880 3
     * Returns Phinx type by SQL type
881
     *
882 3
     * @param string $sqlTypeDef SQL type
883 1
     * @returns string Phinx type
884
     */
885 2
    public function getPhinxType($sqlTypeDef)
886 2
    {
887 2
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*$/', $sqlTypeDef, $matches)) {
888 2
            throw new \RuntimeException('Column type ' . $sqlTypeDef . ' is not supported');
889 1
        } else {
890 1
            $limit = null;
891 2
            $precision = null;
892
            $type = $matches[1];
893
            if (count($matches) > 2) {
894 2
                $limit = $matches[3] ?: null;
895 2
            }
896 1
            if (count($matches) > 4) {
897 1
                $precision = $matches[5];
898
            }
899
            switch ($matches[1]) {
900 1
                case 'varchar':
901 2
                    $type = static::PHINX_TYPE_STRING;
902
                    if ($limit === 255) {
903
                        $limit = null;
904
                    }
905
                    break;
906 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...
907
                    $type = static::PHINX_TYPE_CHAR;
908
                    if ($limit === 255) {
909
                        $limit = null;
910 2
                    }
911
                    if ($limit === 36) {
912
                        $type = static::PHINX_TYPE_UUID;
913
                    }
914
                    break;
915
                case 'int':
916 2
                    $type = static::PHINX_TYPE_INTEGER;
917 1
                    if ($limit === 11) {
918
                        $limit = null;
919
                    }
920 1
                    break;
921 1
                case 'bigint':
922 2
                    if ($limit === 11) {
923 1
                        $limit = null;
924 1
                    }
925 2
                    $type = static::PHINX_TYPE_BIG_INTEGER;
926 2
                    break;
927
                case 'blob':
928
                    $type = static::PHINX_TYPE_BINARY;
929
                    break;
930
            }
931 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...
932
                if ($matches[3] === 1) {
933 2
                    $type = static::PHINX_TYPE_BOOLEAN;
934
                    $limit = null;
935
                }
936 1
            }
937 1
938
            $this->getSqlType($type);
939 1
940
            return [
941
                'name' => $type,
942
                'limit' => $limit,
943
                'precision' => $precision
944
            ];
945
        }
946 48
    }
947
948 48
    /**
949 48
     * {@inheritdoc}
950
     */
951
    public function createDatabase($name, $options = [])
952
    {
953
        touch($name . $this->suffix);
954 2
    }
955
956 2
    /**
957
     * {@inheritdoc}
958
     */
959
    public function hasDatabase($name)
960
    {
961
        return is_file($name . $this->suffix);
962 48
    }
963
964 48
    /**
965 47
     * {@inheritdoc}
966 47
     */
967 48
    public function dropDatabase($name)
968
    {
969
        if (file_exists($name . '.sqlite3')) {
970
            unlink($name . '.sqlite3');
971
        }
972
    }
973
974
    /**
975 42
     * Gets the SQLite Column Definition for a Column object.
976
     *
977 42
     * @param \Phinx\Db\Table\Column $column Column
978 8
     * @return string
979 42
     */
980 42
    protected function getColumnSqlDefinition(Column $column)
981 42
    {
982 42
        $isLiteralType = $column->getType() instanceof Literal;
983
        if ($isLiteralType) {
984
            $def = (string)$column->getType();
985
        } else {
986
            $sqlType = $this->getSqlType($column->getType());
987
            $def = strtoupper($sqlType['name']);
988
989
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
990
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
991 42
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
992
            }
993 42
        }
994 42 View Code Duplication
        if ($column->getPrecision() && $column->getScale()) {
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...
995 42
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
996 42
        }
997 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...
998
            $def .= " CHECK({$column->getName()} IN ('" . implode("', '", $values) . "'))";
999 42
        }
1000 42
1001 42
        $default = $column->getDefault();
1002 42
1003 42
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
1004 4
        $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...
1005 4
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1006
1007 42
        if ($column->getUpdate()) {
1008
            $def .= ' ON UPDATE ' . $column->getUpdate();
1009 42
        }
1010 42
1011 42
        $def .= $this->getCommentDefinition($column);
1012
1013 42
        return $def;
1014
    }
1015
1016
    /**
1017 42
     * Gets the comment Definition for a Column object.
1018
     *
1019 42
     * @param \Phinx\Db\Table\Column $column Column
1020
     * @return string
1021
     */
1022
    protected function getCommentDefinition(Column $column)
1023
    {
1024
        if ($column->getComment()) {
1025
            return ' /* ' . $column->getComment() . ' */ ';
1026
        }
1027
1028 42
        return '';
1029
    }
1030 42
1031 2
    /**
1032
     * Gets the SQLite Index Definition for an Index object.
1033 42
     *
1034
     * @param \Phinx\Db\Table\Table $table Table
1035
     * @param \Phinx\Db\Table\Index $index Index
1036
     * @return string
1037
     */
1038
    protected function getIndexSqlDefinition(Table $table, Index $index)
1039
    {
1040
        if ($index->getType() === Index::UNIQUE) {
1041
            $def = 'UNIQUE INDEX';
1042 8
        } else {
1043
            $def = 'INDEX';
1044 8
        }
1045 2
        if (is_string($index->getName())) {
1046 2
            $indexName = $index->getName();
1047 6
        } else {
1048
            $indexName = $table->getName() . '_';
1049 8
            foreach ($index->getColumns() as $column) {
1050 3
                $indexName .= $column . '_';
1051 3
            }
1052 6
            $indexName .= 'index';
1053 6
        }
1054 6
        $def .= ' `' . $indexName . '`';
1055 6
1056 6
        return $def;
1057
    }
1058 8
1059 8
    /**
1060
     * {@inheritdoc}
1061
     */
1062
    public function getColumnTypes()
1063
    {
1064
        return array_merge(parent::getColumnTypes(), ['enum', 'json', 'jsonb']);
1065 47
    }
1066
1067 47
    /**
1068
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1069
     *
1070
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1071
     * @return string
1072
     */
1073 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...
1074
    {
1075
        $def = '';
1076 5
        if ($foreignKey->getConstraint()) {
1077
            $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...
1078 5
        } else {
1079 5
            $columnNames = [];
1080
            foreach ($foreignKey->getColumns() as $column) {
1081
                $columnNames[] = $this->quoteColumnName($column);
1082 5
            }
1083 5
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1084 5
            $refColumnNames = [];
1085 5
            foreach ($foreignKey->getReferencedColumns() as $column) {
1086 5
                $refColumnNames[] = $this->quoteColumnName($column);
1087 5
            }
1088 5
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1089 5
            if ($foreignKey->getOnDelete()) {
1090 5
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1091 5
            }
1092 5
            if ($foreignKey->getOnUpdate()) {
1093 1
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1094 1
            }
1095 5
        }
1096 1
1097 1
        return $def;
1098
    }
1099 5
1100
    /**
1101
     * {@inheritDoc}
1102
     *
1103
     */
1104
    public function getDecoratedConnection()
1105
    {
1106
        $options = $this->getOptions();
1107
        $options['quoteIdentifiers'] = true;
1108
        $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...
1109
1110
        if (!empty($options['name'])) {
1111
            $options['database'] = $options['name'];
1112
1113
            if (file_exists($options['name'] . $this->suffix)) {
1114
                $options['database'] = $options['name'] . $this->suffix;
1115
            }
1116
        }
1117
1118
        $driver = new SqliteDriver($options);
1119
        if (method_exists($driver, 'setConnection')) {
1120
            $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...
1121
        } else {
1122
            $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...
1123
        }
1124
1125
        return new Connection(['driver' => $driver] + $options);
1126
    }
1127
}
1128