Completed
Push — master ( 152a5f...57e856 )
by José
11s
created

SQLiteAdapter::calculateNewTableColumns()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0187

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 8.439
c 0
b 0
f 0
ccs 20
cts 22
cp 0.9091
cc 5
eloc 24
nc 8
nop 3
crap 5.0187
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 Phinx\Db\Table\Column;
32
use Phinx\Db\Table\ForeignKey;
33
use Phinx\Db\Table\Index;
34
use Phinx\Db\Table\Table;
35
use Phinx\Db\Util\AlterInstructions;
36
use Phinx\Util\Literal;
37
38
/**
39
 * Phinx SQLite Adapter.
40
 *
41
 * @author Rob Morgan <[email protected]>
42
 * @author Richard McIntyre <[email protected]>
43
 */
44
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
45
{
46
    protected $definitionsWithLimits = [
47
        'CHARACTER',
48
        'VARCHAR',
49
        'VARYING CHARACTER',
50
        'NCHAR',
51
        'NATIVE CHARACTER',
52
        'NVARCHAR'
53
    ];
54
55
    /**
56 42
     * {@inheritdoc}
57
     */
58 42
    public function connect()
59 42
    {
60
        if ($this->connection === null) {
61
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
62
                // @codeCoverageIgnoreStart
63
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
64
                // @codeCoverageIgnoreEnd
65 42
            }
66 42
67
            $db = null;
68
            $options = $this->getOptions();
69 42
70
            // if port is specified use it, otherwise use the MySQL default
71
            if (isset($options['memory'])) {
72 42
                $dsn = 'sqlite::memory:';
73 42
            } else {
74 42
                $dsn = 'sqlite:' . $options['name'];
75 42
                if (file_exists($options['name'] . '.sqlite3')) {
76
                    $dsn = 'sqlite:' . $options['name'] . '.sqlite3';
77
                }
78
            }
79 42
80 42
            try {
81
                $db = new \PDO($dsn);
82
            } catch (\PDOException $exception) {
83
                throw new \InvalidArgumentException(sprintf(
84
                    'There was a problem connecting to the database: %s',
85
                    $exception->getMessage()
86
                ));
87 42
            }
88 42
89 42
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
90
            $this->setConnection($db);
91
        }
92
    }
93
94 48
    /**
95
     * {@inheritdoc}
96 48
     */
97 48
    public function disconnect()
98
    {
99
        $this->connection = null;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function hasTransactions()
106
    {
107
        return true;
108
    }
109
110 1
    /**
111
     * {@inheritdoc}
112 1
     */
113 1
    public function beginTransaction()
114
    {
115
        $this->getConnection()->beginTransaction();
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function commitTransaction()
122
    {
123
        $this->getConnection()->commit();
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129
    public function rollbackTransaction()
130
    {
131
        $this->getConnection()->rollBack();
132
    }
133
134 43
    /**
135
     * {@inheritdoc}
136 43
     */
137
    public function quoteTableName($tableName)
138
    {
139
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
140
    }
141
142 44
    /**
143
     * {@inheritdoc}
144 44
     */
145
    public function quoteColumnName($columnName)
146
    {
147
        return '`' . str_replace('`', '``', $columnName) . '`';
148
    }
149
150 42
    /**
151
     * {@inheritdoc}
152 42
     */
153 42
    public function hasTable($tableName)
154 42
    {
155 12
        $tables = [];
156 42
        $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName));
157
        foreach ($rows as $row) {
158 42
            $tables[] = strtolower($row[0]);
159
        }
160
161
        return in_array(strtolower($tableName), $tables);
162
    }
163
164 42
    /**
165
     * {@inheritdoc}
166
     */
167 42
    public function createTable(Table $table, array $columns = [], array $indexes = [])
168 42
    {
169 42
        // Add the default primary key
170 35
        $options = $table->getOptions();
171 35
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
172 35
            $column = new Column();
173 35
            $column->setName('id')
174
                   ->setType('integer')
175 35
                   ->setIdentity(true);
176 42
177
            array_unshift($columns, $column);
178 1
        } elseif (isset($options['id']) && is_string($options['id'])) {
179 1
            // Handle id => "field_name" to support AUTO_INCREMENT
180 1
            $column = new Column();
181 1
            $column->setName($options['id'])
182
                   ->setType('integer')
183 1
                   ->setIdentity(true);
184 1
185
            array_unshift($columns, $column);
186
        }
187 42
188 42
        $sql = 'CREATE TABLE ';
189 42
        $sql .= $this->quoteTableName($table->getName()) . ' (';
190 42
        foreach ($columns as $column) {
191 42
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
192
193
            if (isset($options['primary_key']) && $column->getIdentity()) {
194 42
                //remove column from the primary key array as it is already defined as an autoincrement
195 42
                //primary id
196 42
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
197 42
                if ($identityColumnIndex !== false) {
198 42
                    unset($options['primary_key'][$identityColumnIndex]);
199 42
200
                    if (empty($options['primary_key'])) {
201
                        //The last primary key has been removed
202 1
                        unset($options['primary_key']);
203 1
                    }
204 1
                }
205
            }
206 1
        }
207 1
208 1
        // set the primary key(s)
209 1 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...
210 1
            $sql = rtrim($sql);
211 1
            $sql .= ' PRIMARY KEY (';
212 42
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
213 42
                $sql .= $this->quoteColumnName($options['primary_key']);
214 37
            } 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...
215
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
216
            }
217
            $sql .= ')';
218 42
        } else {
219 42
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
220 1
        }
221 1
222 1
        $sql = rtrim($sql) . ');';
223 1
        // execute the sql
224
        $this->execute($sql);
225 42
226
        foreach ($indexes as $index) {
227 42
            $this->addIndex($table, $index);
228
        }
229 42
    }
230 6
231 42
    /**
232 42
     * {@inheritdoc}
233
     */
234 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...
235
    {
236
        $sql = sprintf(
237 1
            'ALTER TABLE %s RENAME TO %s',
238
            $this->quoteTableName($tableName),
239 1
            $this->quoteTableName($newTableName)
240 1
        );
241
242
        return new AlterInstructions([], [$sql]);
243
    }
244
245 1
    /**
246
     * {@inheritdoc}
247 1
     */
248 1
    protected function getDropTableInstructions($tableName)
249
    {
250
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
251
252
        return new AlterInstructions([], [$sql]);
253 1
    }
254
255 1
    /**
256 1
     * {@inheritdoc}
257 1
     */
258 1
    public function truncateTable($tableName)
259
    {
260 1
        $sql = sprintf(
261 1
            'DELETE FROM %s',
262
            $this->quoteTableName($tableName)
263
        );
264
265
        $this->execute($sql);
266 1
    }
267
268 1
    /**
269 1
     * {@inheritdoc}
270
     */
271 1
    public function getColumns($tableName)
272 1
    {
273 1
        $columns = [];
274 1
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
275 1
276 1
        foreach ($rows as $columnInfo) {
277
            $column = new Column();
278 1
            $type = strtolower($columnInfo['type']);
279 1
            $column->setName($columnInfo['name'])
280 1
                   ->setNull($columnInfo['notnull'] !== '1')
281
                   ->setDefault($columnInfo['dflt_value']);
282 1
283 1
            $phinxType = $this->getPhinxType($type);
284 1
            $column->setType($phinxType['name'])
285
                   ->setLimit($phinxType['limit']);
286 1
287 1
            if ($columnInfo['pk'] == 1) {
288
                $column->setIdentity(true);
289 1
            }
290
291
            $columns[] = $column;
292
        }
293
294
        return $columns;
295 8
    }
296
297 8
    /**
298 8
     * {@inheritdoc}
299 8
     */
300 7 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...
301
    {
302 8
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
303
        foreach ($rows as $column) {
304 8
            if (strcasecmp($column['name'], $columnName) === 0) {
305
                return true;
306
            }
307
        }
308
309
        return false;
310 4
    }
311
312 4
    /**
313 4
     * {@inheritdoc}
314 4
     */
315 4 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...
316 4
    {
317 4
        $alter = sprintf(
318
            'ADD COLUMN %s %s',
319 4
            $this->quoteColumnName($column->getName()),
320 4
            $this->getColumnSqlDefinition($column)
321
        );
322
323
        return new AlterInstructions([$alter]);
324
    }
325 2
326
    /**
327 2
     * Returns the original CREATE statement for the give table
328
     *
329 2
     * @param string $tableName The table name to get the create statement for
330
     * @return string
331 2
     */
332 2
    protected function getDeclaringSql($tableName)
333 2
    {
334 2
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
335 2
336 2
        $sql = '';
337
        foreach ($rows as $table) {
338 2
            if ($table['tbl_name'] === $tableName) {
339 2
                $sql = $table['sql'];
340 2
            }
341 2
        }
342 2
343 2
        return $sql;
344 2
    }
345 2
346 2
    /**
347
     * Copies all the data from a tmp table to another table
348 2
     *
349 1
     * @param string $tableName The table name to copy the data to
350
     * @param string $tmpTableName The tmp table name where the data is stored
351 1
     * @param string[] $writeColumns The list of columns in the target table
352
     * @param string[] $selectColumns The list of columns in the tmp table
353
     * @return void
354 1
     */
355
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
356 1
    {
357 1
        $sql = sprintf(
358 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
359
            $this->quoteTableName($tableName),
360 1
            implode(', ', $writeColumns),
361 1
            implode(', ', $selectColumns),
362
            $this->quoteTableName($tmpTableName)
363
        );
364 1
        $this->execute($sql);
365 1
    }
366 1
367 1
    /**
368 1
     * Modifies the passed instructions to copy all data from the tmp table into
369
     * the provided table and then drops the tmp table.
370 1
     *
371
     * @param AlterInstructions $instructions The instructions to modify
372 1
     * @param string $tableName The table name to copy the data to
373
     * @return AlterInstructions
374 1
     */
375 1
    protected function copyAndDropTmpTable($instructions, $tableName)
376
    {
377
        $instructions->addPostStep(function ($state) use ($tableName) {
378
            $this->copyDataToNewTable(
379
                $tableName,
380 6
                $state['tmpTableName'],
381
                $state['writeColumns'],
382
                $state['selectColumns']
383
            );
384 6
385
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
386 6
387
            return $state;
388 6
        });
389 6
390 6
        return $instructions;
391 6
    }
392 6
393 6
    /**
394
     * Returns the columns and type to use when copying a table to another in the process
395 6
     * of altering a table
396 6
     *
397 6
     * @param string $tableName The table to modify
398 6
     * @param string $columnName The column name that is about to change
399 6
     * @param string|false $newColumnName Optionally the new name for the column
400 6
     * @return AlterInstructions
401 6
     */
402 6
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
403 6
    {
404
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
405 6
        $selectColumns = [];
406
        $writeColumns = [];
407
        $columnType = null;
408
        $found = false;
409
410
        foreach ($columns as $column) {
411 6
            $selectName = $column['name'];
412
            $writeName = $selectName;
413 6
414 6
            if ($selectName == $columnName) {
415 6
                $writeName = $newColumnName;
416 6
                $found = true;
417
                $columnType = $column['type'];
418 6
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
419
            }
420 6
421
            $selectColumns[] = $selectName;
422 6
            $writeColumns[] = $writeName;
423 6
        }
424 6
425 6
        $selectColumns = array_filter($selectColumns, 'strlen');
426 6
        $writeColumns = array_filter($writeColumns, 'strlen');
427
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
428 6
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
429
430 6
        if (!$found) {
431 6
            throw new \InvalidArgumentException(sprintf(
432 6
                'The specified column doesn\'t exist: ' . $columnName
433
            ));
434
        }
435
436
        return compact('writeColumns', 'selectColumns', 'columnType');
437 2
    }
438
439
    /**
440 2
     * Returns the initial instructions to alter a table using the
441
     * rename-alter-copy strategy
442 2
     *
443
     * @param string $tableName The table to modify
444 2
     * @return AlterInstructions
445 2
     */
446 2
    protected function beginAlterByCopyTable($tableName)
447 2
    {
448 2
        $instructions = new AlterInstructions();
449 2
        $instructions->addPostStep(function ($state) use ($tableName) {
450
            $createSQL = $this->getDeclaringSql($tableName);
451 2
452 2
            $tmpTableName = 'tmp_' . $tableName;
453 2
            $this->execute(
454 2
                sprintf(
455 2
                    'ALTER TABLE %s RENAME TO %s',
456 2
                    $this->quoteTableName($tableName),
457 2
                    $this->quoteTableName($tmpTableName)
458 2
                )
459 2
            );
460
461 2
            return compact('createSQL', 'tmpTableName') + $state;
462
        });
463 2
464
        return $instructions;
465
    }
466
467
    /**
468
     * {@inheritdoc}
469 2
     */
470
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
471 2
    {
472 2
        $instructions = $this->beginAlterByCopyTable($tableName);
473 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...
474
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
475 2
476
            return $newState + $state;
477 2
        });
478 2
479 2
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
480
            $sql = str_replace(
481 2
                $this->quoteColumnName($columnName),
482
                $this->quoteColumnName($newColumnName),
483 2
                $state['createSQL']
484 2
            );
485 2
            $this->execute($sql);
486 2
487 2
            return $state;
488
        });
489 2
490
        return $this->copyAndDropTmpTable($instructions, $tableName);
491 2
    }
492 2
493 2
    /**
494
     * {@inheritdoc}
495
     */
496
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
497
    {
498
        $instructions = $this->beginAlterByCopyTable($tableName);
499
500
        $newColumnName = $newColumn->getName();
501 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...
502
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
503 9
504 9
            return $newState + $state;
505
        });
506 9
507 9
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
508 9
            $sql = preg_replace(
509 9
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
510 9
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
511 9
                $state['createSQL'],
512 9
                1
513 9
            );
514 9
            $this->execute($sql);
515 9
516
            return $state;
517
        });
518
519
        return $this->copyAndDropTmpTable($instructions, $tableName);
520
    }
521 9
522
    /**
523 9
     * {@inheritdoc}
524 4
     */
525 4
    protected function getDropColumnInstructions($tableName, $columnName)
526
    {
527 9
        $instructions = $this->beginAlterByCopyTable($tableName);
528 9
529 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...
530 9
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
531 9
532 9
            return $newState + $state;
533 9
        });
534
535 8
        $instructions->addPostStep(function ($state) use ($columnName) {
536
            $sql = preg_replace(
537 8
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
538
                "",
539
                $state['createSQL']
540
            );
541
542
            if (substr($sql, -2) === ', ') {
543 1
                $sql = substr($sql, 0, -2) . ')';
544
            }
545 1
546
            $this->execute($sql);
547 1
548 1
            return $state;
549 1
        });
550
551
        return $this->copyAndDropTmpTable($instructions, $tableName);
552
    }
553
554
    /**
555
     * Get an array of indexes from a particular table.
556
     *
557
     * @param string $tableName Table Name
558
     * @return array
559 8
     */
560
    protected function getIndexes($tableName)
561 8
    {
562 8
        $indexes = [];
563 8
        $rows = $this->fetchAll(sprintf('pragma index_list(%s)', $tableName));
564 8
565 8
        foreach ($rows as $row) {
566 8
            $indexData = $this->fetchAll(sprintf('pragma index_info(%s)', $row['name']));
567 8
            if (!isset($indexes[$tableName])) {
568 8
                $indexes[$tableName] = ['index' => $row['name'], 'columns' => []];
569 8
            }
570 8
            foreach ($indexData as $indexItem) {
571
                $indexes[$tableName]['columns'][] = strtolower($indexItem['name']);
572 8
            }
573 8
        }
574 8
575
        return $indexes;
576
    }
577
578
    /**
579 1
     * {@inheritdoc}
580
     */
581 1 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...
582 1
    {
583 1
        if (is_string($columns)) {
584
            $columns = [$columns]; // str to array
585 1
        }
586 1
587
        $columns = array_map('strtolower', $columns);
588 1
        $indexes = $this->getIndexes($tableName);
589 1
590 1
        foreach ($indexes as $index) {
591 1
            $a = array_diff($columns, $index['columns']);
592 1
            if (empty($a)) {
593 1
                return true;
594 1
            }
595 1
        }
596 1
597 1
        return false;
598
    }
599
600
    /**
601
     * {@inheritdoc}
602
     */
603 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...
604
    {
605 1
        $indexes = $this->getIndexes($tableName);
606
607 1
        foreach ($indexes as $index) {
608
            if ($indexName === $index['index']) {
609 1
                return true;
610 1
            }
611 1
        }
612 1
613 1
        return false;
614 1
    }
615 1
616 1
    /**
617 1
     * {@inheritdoc}
618
     */
619
    protected function getAddIndexInstructions(Table $table, Index $index)
620
    {
621
        $indexColumnArray = [];
622
        foreach ($index->getColumns() as $column) {
623
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
624
        }
625 5
        $indexColumns = implode(',', $indexColumnArray);
626
        $sql = sprintf(
627 5
            'CREATE %s ON %s (%s)',
628
            $this->getIndexSqlDefinition($table, $index),
629
            $this->quoteTableName($table->getName()),
630 5
            $indexColumns
631
        );
632 5
633 5
        return new AlterInstructions([], [$sql]);
634 5
    }
635
636 1
    /**
637
     * {@inheritdoc}
638
     */
639
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
640
    {
641
        if (is_string($columns)) {
642
            $columns = [$columns]; // str to array
643
        }
644
645 5
        $indexes = $this->getIndexes($tableName);
646
        $columns = array_map('strtolower', $columns);
647 5
        $instructions = new AlterInstructions();
648 5
649
        foreach ($indexes as $index) {
650
            $a = array_diff($columns, $index['columns']);
651
            if (empty($a)) {
652
                $instructions->addPostStep(sprintf(
653
                    'DROP INDEX %s',
654
                    $this->quoteColumnName($index['index'])
655
                ));
656
            }
657
        }
658
659
        return $instructions;
660
    }
661 5
662
    /**
663 5
     * {@inheritdoc}
664 5
     */
665 5
    protected function getDropIndexByNameInstructions($tableName, $indexName)
666 5
    {
667 5
        $indexes = $this->getIndexes($tableName);
668 5
        $instructions = new AlterInstructions();
669 5
670 5
        foreach ($indexes as $index) {
671 5
            if ($indexName === $index['index']) {
672 5
                $instructions->addPostStep(sprintf(
673 5
                    'DROP INDEX %s',
674
                    $this->quoteColumnName($indexName)
675
                ));
676
            }
677
        }
678
679 4
        return $instructions;
680
    }
681
682 4
    /**
683
     * {@inheritdoc}
684 4
     */
685 4
    public function hasForeignKey($tableName, $columns, $constraint = null)
686
    {
687 4
        if (is_string($columns)) {
688 4
            $columns = [$columns]; // str to array
689 4
        }
690 4
        $foreignKeys = $this->getForeignKeys($tableName);
691 4
692 4
        return !array_diff($columns, $foreignKeys);
693
    }
694 4
695 4
    /**
696 4
     * Get an array of foreign keys from a particular table.
697 4
     *
698 4
     * @param string $tableName Table Name
699
     * @return array
700 4
     */
701
    protected function getForeignKeys($tableName)
702 4
    {
703 4
        $foreignKeys = [];
704
        $rows = $this->fetchAll(
705 4
            "SELECT sql, tbl_name
706 4
              FROM (
707 4
                    SELECT sql sql, type type, tbl_name tbl_name, name name
708 4
                      FROM sqlite_master
709 4
                     UNION ALL
710 4
                    SELECT sql, type, tbl_name, name
711 4
                      FROM sqlite_temp_master
712
                   )
713 4
             WHERE type != 'meta'
714 4
               AND sql NOTNULL
715 4
               AND name NOT LIKE 'sqlite_%'
716
             ORDER BY substr(type, 2, 1), name"
717
        );
718
719
        foreach ($rows as $row) {
720 1
            if ($row['tbl_name'] === $tableName) {
721
                if (strpos($row['sql'], 'REFERENCES') !== false) {
722
                    preg_match_all("/\(`([^`]*)`\) REFERENCES/", $row['sql'], $matches);
723 1
                    foreach ($matches[1] as $match) {
724
                        $foreignKeys[] = $match;
725
                    }
726
                }
727 1
            }
728
        }
729 1
730
        return $foreignKeys;
731 1
    }
732 1
733 1
    /**
734 1
     * {@inheritdoc}
735 1
     */
736 1
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
737
    {
738 1
        $instructions = $this->beginAlterByCopyTable($table->getName());
739 1
740 1
        $tableName = $table->getName();
741 1
        $instructions->addPostStep(function ($state) use ($foreignKey) {
742 1
            $this->execute('pragma foreign_keys = ON');
743 1
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
744 1
            $this->execute($sql);
745
746 1
            return $state;
747
        });
748 1
749
        $instructions->addPostStep(function ($state) {
750
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
751
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
752
            $selectColumns = $writeColumns = $names;
753
754 1
            return compact('selectColumns', 'writeColumns') + $state;
755
        });
756 1
757 1
        return $this->copyAndDropTmpTable($instructions, $tableName);
758 1
    }
759 1
760 1
    /**
761 1
     * {@inheritdoc}
762 1
     */
763
    protected function getDropForeignKeyInstructions($tableName, $constraint)
764 1
    {
765
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
766 1
    }
767 1
768 1
    /**
769 1
     * {@inheritdoc}
770 1
     */
771
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
772 1
    {
773
        $instructions = $this->beginAlterByCopyTable($tableName);
774 1
775 1
        $instructions->addPostStep(function ($state) use ($columns) {
776 1
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
777
778
            $selectColumns = $newState['selectColumns'];
779
            $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...
780
            $diff = array_diff($columns, $selectColumns);
781
782
            if (!empty($diff)) {
783
                throw new \InvalidArgumentException(sprintf(
784
                    'The specified columns doen\'t exist: ' . implode(', ', $diff)
785
                ));
786
            }
787
788
            return $newState + $state;
789
        });
790
791
        $instructions->addPostStep(function ($state) use ($columns) {
792
            $sql = '';
793
794
            foreach ($columns as $columnName) {
795
                $search = sprintf(
796
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
797
                    $this->quoteColumnName($columnName)
798
                );
799
                $sql = preg_replace($search, '', $state['createSQL'], 1);
800
            }
801
802
            if ($sql) {
803
                $this->execute($sql);
804
            }
805
806
            return $state;
807
        });
808
809
        return $this->copyAndDropTmpTable($instructions, $tableName);
810 43
    }
811
812
    /**
813 43
     * {@inheritdoc}
814 42
     */
815
    public function getSqlType($type, $limit = null)
816 43
    {
817
        switch ($type) {
818
            case static::PHINX_TYPE_TEXT:
819 43
            case static::PHINX_TYPE_INTEGER:
820 1
            case static::PHINX_TYPE_FLOAT:
821
            case static::PHINX_TYPE_DECIMAL:
822 43
            case static::PHINX_TYPE_DATETIME:
823 38
            case static::PHINX_TYPE_TIME:
824
            case static::PHINX_TYPE_DATE:
825 43
            case static::PHINX_TYPE_BLOB:
826 42
            case static::PHINX_TYPE_BOOLEAN:
827
            case static::PHINX_TYPE_ENUM:
828 43
                return ['name' => $type];
829 2
            case static::PHINX_TYPE_STRING:
830
                return ['name' => 'varchar', 'limit' => 255];
831 43
            case static::PHINX_TYPE_CHAR:
832 1
                return ['name' => 'char', 'limit' => 255];
833
            case static::PHINX_TYPE_BIG_INTEGER:
834 43
                return ['name' => 'bigint'];
835 1
            case static::PHINX_TYPE_TIMESTAMP:
836
                return ['name' => 'datetime'];
837 43
            case static::PHINX_TYPE_BINARY:
838 42
                return ['name' => 'blob'];
839
            case static::PHINX_TYPE_UUID:
840 43
                return ['name' => 'char', 'limit' => 36];
841 1
            case static::PHINX_TYPE_JSON:
842
            case static::PHINX_TYPE_JSONB:
843 43
                return ['name' => 'text'];
844 1
            // Geospatial database types
845
            // No specific data types exist in SQLite, instead all geospatial
846 43
            // functionality is handled in the client. See also: SpatiaLite.
847 43
            case static::PHINX_TYPE_GEOMETRY:
848 1
            case static::PHINX_TYPE_POLYGON:
849
                return ['name' => 'text'];
850 43
            case static::PHINX_TYPE_LINESTRING:
851 42
                return ['name' => 'varchar', 'limit' => 255];
852
            case static::PHINX_TYPE_POINT:
853 5
                return ['name' => 'float'];
854
            default:
855 5
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
856 4
        }
857
    }
858
859
    /**
860 1
     * Returns Phinx type by SQL type
861 1
     *
862
     * @param string $sqlTypeDef SQL type
863
     * @returns string Phinx type
864 1
     */
865
    public function getPhinxType($sqlTypeDef)
866
    {
867 1
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*$/', $sqlTypeDef, $matches)) {
868
            throw new \RuntimeException('Column type ' . $sqlTypeDef . ' is not supported');
869 1
        } else {
870 1
            $limit = null;
871 1
            $precision = null;
872
            $type = $matches[1];
873
            if (count($matches) > 2) {
874
                $limit = $matches[3] ?: null;
875
            }
876
            if (count($matches) > 4) {
877
                $precision = $matches[5];
878
            }
879
            switch ($matches[1]) {
880 3
                case 'varchar':
881
                    $type = static::PHINX_TYPE_STRING;
882 3
                    if ($limit === 255) {
883 1
                        $limit = null;
884
                    }
885 2
                    break;
886 2 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...
887 2
                    $type = static::PHINX_TYPE_CHAR;
888 2
                    if ($limit === 255) {
889 1
                        $limit = null;
890 1
                    }
891 2
                    if ($limit === 36) {
892
                        $type = static::PHINX_TYPE_UUID;
893
                    }
894 2
                    break;
895 2
                case 'int':
896 1
                    $type = static::PHINX_TYPE_INTEGER;
897 1
                    if ($limit === 11) {
898
                        $limit = null;
899
                    }
900 1
                    break;
901 2
                case 'bigint':
902
                    if ($limit === 11) {
903
                        $limit = null;
904
                    }
905
                    $type = static::PHINX_TYPE_BIG_INTEGER;
906
                    break;
907
                case 'blob':
908
                    $type = static::PHINX_TYPE_BINARY;
909
                    break;
910 2
            }
911 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...
912
                if ($matches[3] === 1) {
913
                    $type = static::PHINX_TYPE_BOOLEAN;
914
                    $limit = null;
915
                }
916 2
            }
917 1
918
            $this->getSqlType($type);
919
920 1
            return [
921 1
                'name' => $type,
922 2
                'limit' => $limit,
923 1
                'precision' => $precision
924 1
            ];
925 2
        }
926 2
    }
927
928
    /**
929
     * {@inheritdoc}
930
     */
931
    public function createDatabase($name, $options = [])
932
    {
933 2
        touch($name . '.sqlite3');
934
    }
935
936 1
    /**
937 1
     * {@inheritdoc}
938
     */
939 1
    public function hasDatabase($name)
940
    {
941
        return is_file($name . '.sqlite3');
942
    }
943
944
    /**
945
     * {@inheritdoc}
946 48
     */
947
    public function dropDatabase($name)
948 48
    {
949 48
        if (file_exists($name . '.sqlite3')) {
950
            unlink($name . '.sqlite3');
951
        }
952
    }
953
954 2
    /**
955
     * Gets the SQLite Column Definition for a Column object.
956 2
     *
957
     * @param \Phinx\Db\Table\Column $column Column
958
     * @return string
959
     */
960
    protected function getColumnSqlDefinition(Column $column)
961
    {
962 48
        $isLiteralType = $column->getType() instanceof Literal;
963
        if ($isLiteralType) {
964 48
            $def = (string)$column->getType();
965 47
        } else {
966 47
            $sqlType = $this->getSqlType($column->getType());
967 48
            $def = strtoupper($sqlType['name']);
968
969
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
970
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
971
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
972
            }
973
        }
974 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...
975 42
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
976
        }
977 42 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...
978 8
            $def .= " CHECK({$column->getName()} IN ('" . implode("', '", $values) . "'))";
979 42
        }
980 42
981 42
        $default = $column->getDefault();
982 42
983
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
984
        $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...
985
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
986
987
        if ($column->getUpdate()) {
988
            $def .= ' ON UPDATE ' . $column->getUpdate();
989
        }
990
991 42
        $def .= $this->getCommentDefinition($column);
992
993 42
        return $def;
994 42
    }
995 42
996 42
    /**
997
     * Gets the comment Definition for a Column object.
998
     *
999 42
     * @param \Phinx\Db\Table\Column $column Column
1000 42
     * @return string
1001 42
     */
1002 42
    protected function getCommentDefinition(Column $column)
1003 42
    {
1004 4
        if ($column->getComment()) {
1005 4
            return ' /* ' . $column->getComment() . ' */ ';
1006
        }
1007 42
1008
        return '';
1009 42
    }
1010 42
1011 42
    /**
1012
     * Gets the SQLite Index Definition for an Index object.
1013 42
     *
1014
     * @param \Phinx\Db\Table\Table $table Table
1015
     * @param \Phinx\Db\Table\Index $index Index
1016
     * @return string
1017 42
     */
1018
    protected function getIndexSqlDefinition(Table $table, Index $index)
1019 42
    {
1020
        if ($index->getType() === Index::UNIQUE) {
1021
            $def = 'UNIQUE INDEX';
1022
        } else {
1023
            $def = 'INDEX';
1024
        }
1025
        if (is_string($index->getName())) {
1026
            $indexName = $index->getName();
1027
        } else {
1028 42
            $indexName = $table->getName() . '_';
1029
            foreach ($index->getColumns() as $column) {
1030 42
                $indexName .= $column . '_';
1031 2
            }
1032
            $indexName .= 'index';
1033 42
        }
1034
        $def .= ' `' . $indexName . '`';
1035
1036
        return $def;
1037
    }
1038
1039
    /**
1040
     * {@inheritdoc}
1041
     */
1042 8
    public function getColumnTypes()
1043
    {
1044 8
        return array_merge(parent::getColumnTypes(), ['enum', 'json', 'jsonb']);
1045 2
    }
1046 2
1047 6
    /**
1048
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1049 8
     *
1050 3
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1051 3
     * @return string
1052 6
     */
1053 6 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...
1054 6
    {
1055 6
        $def = '';
1056 6
        if ($foreignKey->getConstraint()) {
1057
            $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...
1058 8
        } else {
1059 8
            $columnNames = [];
1060
            foreach ($foreignKey->getColumns() as $column) {
1061
                $columnNames[] = $this->quoteColumnName($column);
1062
            }
1063
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1064
            $refColumnNames = [];
1065 47
            foreach ($foreignKey->getReferencedColumns() as $column) {
1066
                $refColumnNames[] = $this->quoteColumnName($column);
1067 47
            }
1068
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1069
            if ($foreignKey->getOnDelete()) {
1070
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1071
            }
1072
            if ($foreignKey->getOnUpdate()) {
1073
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1074
            }
1075
        }
1076 5
1077
        return $def;
1078 5
    }
1079
}
1080