Completed
Pull Request — master (#1371)
by José
07:16 queued 04:02
created

SQLiteAdapter::setOptions()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.576

Importance

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